{"spec_id":"biplot-pca","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nbiplot-pca: PCA Biplot with Scores and Loading Vectors\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 83/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport seaborn as sns\nfrom sklearn.datasets import load_wine\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Okabe-Ito palette\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\n# Load Wine dataset\nwine = load_wine()\nfeature_names = wine.feature_names\nX = wine.data\ntarget = wine.target\n\n# Standardize features (z-score normalization)\nX_mean = X.mean(axis=0)\nX_std = X.std(axis=0)\nX_scaled = (X - X_mean) / X_std\n\n# Perform PCA using numpy (eigenvalue decomposition of covariance matrix)\ncov_matrix = np.cov(X_scaled.T)\neigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)\n\n# Sort eigenvectors by eigenvalues in descending order\nidx = np.argsort(eigenvalues)[::-1]\neigenvalues = eigenvalues[idx]\neigenvectors = eigenvectors[:, idx]\n\n# Get first 2 principal components\npc_vectors = eigenvectors[:, :2]\n\n# Project data onto principal components (scores)\nscores = X_scaled @ pc_vectors\n\n# Loadings are the eigenvectors\nloadings = pc_vectors\n\n# Calculate variance explained\nvar_explained = eigenvalues[:2] / eigenvalues.sum() * 100\n\n# Create figure with seaborn theme\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.10,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Prepare data for seaborn\nscore_df = {\"PC1\": scores[:, 0], \"PC2\": scores[:, 1], \"Wine Class\": target}\n\n# Map target labels to class names\nclass_names = [\"Class 0\", \"Class 1\", \"Class 2\"]\nscore_df[\"Wine Class\"] = [class_names[int(t)] for t in score_df[\"Wine Class\"]]\n\n# Plot observation scores with seaborn using Okabe-Ito colors\nsns.scatterplot(\n    x=\"PC1\",\n    y=\"PC2\",\n    hue=\"Wine Class\",\n    data=score_df,\n    palette=IMPRINT[:3],\n    s=200,\n    alpha=0.7,\n    edgecolor=PAGE_BG,\n    linewidth=1,\n    ax=ax,\n)\n\n# Scale loadings to be visible but not overwhelming\nscore_max = np.abs(scores).max()\nloading_scale = score_max * 1.2\n\n# Draw loading arrows and labels\narrow_color = INK_SOFT\nfeature_labels = [f.replace(\" \", \"\\n\") for f in feature_names[:13]]\n\n# Store arrow endpoints for label positioning\narrow_ends = []\nfor i, feature in enumerate(feature_labels):\n    x_load = loadings[i, 0] * loading_scale\n    y_load = loadings[i, 1] * loading_scale\n    arrow_ends.append((x_load, y_load, feature))\n\n    # Draw arrow from origin to loading position\n    ax.annotate(\n        \"\",\n        xy=(x_load, y_load),\n        xytext=(0, 0),\n        arrowprops={\"arrowstyle\": \"->\", \"color\": arrow_color, \"lw\": 2.0, \"mutation_scale\": 18},\n    )\n\n# Add labels with smart positioning to avoid overlap\nfor _i, (x_load, y_load, feature) in enumerate(arrow_ends):\n    # Offset text beyond arrow tip\n    text_offset = 1.12\n    x_text = x_load * text_offset\n    y_text = y_load * text_offset\n\n    # Adjust horizontal alignment based on position\n    if x_load > 0.3:\n        ha = \"left\"\n    elif x_load < -0.3:\n        ha = \"right\"\n    else:\n        ha = \"center\"\n\n    # Adjust vertical alignment based on position\n    if y_load > 0.5:\n        va = \"bottom\"\n    elif y_load < -0.5:\n        va = \"top\"\n    else:\n        va = \"center\"\n\n    ax.text(\n        x_text,\n        y_text,\n        feature,\n        fontsize=12,\n        fontweight=\"bold\",\n        ha=ha,\n        va=va,\n        color=INK,\n        bbox={\n            \"boxstyle\": \"round,pad=0.3\",\n            \"facecolor\": ELEVATED_BG,\n            \"alpha\": 0.85,\n            \"edgecolor\": INK_SOFT,\n            \"linewidth\": 0.5,\n        },\n    )\n\n# Draw reference lines at origin\nax.axhline(y=0, color=INK_SOFT, linestyle=\"--\", linewidth=1, alpha=0.3)\nax.axvline(x=0, color=INK_SOFT, linestyle=\"--\", linewidth=1, alpha=0.3)\n\n# Styling\nax.set_xlabel(f\"PC1 ({var_explained[0]:.1f}%)\", fontsize=20, color=INK)\nax.set_ylabel(f\"PC2 ({var_explained[1]:.1f}%)\", fontsize=20, color=INK)\nax.set_title(\"biplot-pca · seaborn · anyplot.ai\", fontsize=24, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=16, colors=INK_SOFT)\n\n# Remove top and right spines\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor spine in [\"left\", \"bottom\"]:\n    ax.spines[spine].set_color(INK_SOFT)\n\n# Add subtle grid\nax.yaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK)\n\n# Legend styling\nlegend = ax.legend(title=\"Wine Class\", fontsize=14, title_fontsize=16, loc=\"lower right\")\nlegend.get_frame().set_facecolor(ELEVATED_BG)\nlegend.get_frame().set_edgecolor(INK_SOFT)\nlegend.get_frame().set_alpha(0.95)\nlegend.get_title().set_color(INK)\nfor text in legend.get_texts():\n    text.set_color(INK)\n\n# Set balanced axis limits\nmax_range = max(np.abs(scores).max(), loading_scale) * 1.25\nax.set_xlim(-max_range, max_range)\nax.set_ylim(-max_range, max_range)\nax.set_aspect(\"equal\")\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}