{"spec_id":"biplot-pca","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nbiplot-pca: PCA Biplot with Scores and Loading Vectors\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    arrow,\n    element_line,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    labs,\n    scale_color_manual,\n    theme,\n    theme_minimal,\n)\nfrom sklearn.datasets import load_iris\nfrom sklearn.decomposition import PCA\nfrom sklearn.preprocessing import StandardScaler\n\n\n# Theme configuration\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 (first 3 for species)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Load Iris dataset\niris = load_iris()\nX = iris.data\ny = iris.target\nfeature_names = iris.feature_names\nspecies_names = [iris.target_names[i] for i in y]\n\n# Standardize features\nscaler = StandardScaler()\nX_scaled = scaler.fit_transform(X)\n\n# Perform PCA\npca = PCA(n_components=2)\nscores = pca.fit_transform(X_scaled)\nloadings = pca.components_.T  # Shape: (n_features, n_components)\nvar_explained = pca.explained_variance_ratio_ * 100\n\n# Create scores DataFrame\ndf_scores = pd.DataFrame({\"PC1\": scores[:, 0], \"PC2\": scores[:, 1], \"Species\": species_names})\n\n# Scale loadings to be visible alongside scores\nscore_scale = np.max(np.abs(scores)) * 0.8\nloading_scale = np.max(np.abs(loadings))\nscale_factor = score_scale / loading_scale\n\n# Create loadings DataFrame for arrows\nxend = loadings[:, 0] * scale_factor\nyend = loadings[:, 1] * scale_factor\n\ndf_loadings = pd.DataFrame(\n    {\n        \"x\": [0] * len(feature_names),\n        \"y\": [0] * len(feature_names),\n        \"xend\": xend,\n        \"yend\": yend,\n        \"variable\": [name.replace(\" (cm)\", \"\") for name in feature_names],\n    }\n)\n\n# Create label positions with smart offsets to avoid overlap\nlabel_offset = 0.25\ndf_labels = pd.DataFrame(\n    {\n        \"x\": xend + np.sign(xend) * label_offset,\n        \"y\": yend + np.sign(yend) * label_offset * 0.8,\n        \"variable\": [name.replace(\" (cm)\", \"\") for name in feature_names],\n    }\n)\n# Manually adjust overlapping labels (petal length and petal width)\ndf_labels.loc[2, \"y\"] -= 0.15  # petal length - move down\ndf_labels.loc[3, \"y\"] += 0.15  # petal width - move up\n\n# Create unit circle reference (scaled)\ntheta = np.linspace(0, 2 * np.pi, 100)\ndf_circle = pd.DataFrame({\"x\": np.cos(theta) * scale_factor, \"y\": np.sin(theta) * scale_factor})\n\n# Theme-adaptive theme\nanyplot_theme = theme(\n    figure_size=(16, 9),\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major=element_line(color=INK, size=0.3, alpha=0.10),\n    panel_grid_minor=element_line(color=INK, size=0.2, alpha=0.05),\n    panel_border=element_rect(color=INK_SOFT, fill=None),\n    axis_title=element_text(size=20, color=INK),\n    axis_text=element_text(size=16, color=INK_SOFT),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(size=24, color=INK),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(size=16, color=INK_SOFT),\n    legend_title=element_text(size=18, color=INK),\n    text=element_text(size=14),\n)\n\n# Create biplot\nplot = (\n    ggplot()\n    # Unit circle reference\n    + geom_path(df_circle, aes(x=\"x\", y=\"y\"), color=INK_SOFT, linetype=\"dashed\", size=0.8, alpha=0.5)\n    # Observation scores as points\n    + geom_point(df_scores, aes(x=\"PC1\", y=\"PC2\", color=\"Species\"), size=4, alpha=0.7)\n    # Loading arrows\n    + geom_segment(\n        df_loadings,\n        aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        color=INK,\n        size=1.2,\n        arrow=arrow(length=0.15, type=\"closed\"),\n    )\n    # Loading labels\n    + geom_text(df_labels, aes(x=\"x\", y=\"y\", label=\"variable\"), size=12, color=INK, fontweight=\"bold\")\n    # Labels\n    + labs(\n        x=f\"PC1 ({var_explained[0]:.1f}%)\",\n        y=f\"PC2 ({var_explained[1]:.1f}%)\",\n        title=\"biplot-pca · plotnine · anyplot.ai\",\n        color=\"Species\",\n    )\n    # Theme\n    + theme_minimal()\n    + anyplot_theme\n    + scale_color_manual(values=IMPRINT)\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=300, verbose=False)\n"}