{"spec_id":"biplot-pca","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nbiplot-pca: PCA Biplot with Scores and Loading Vectors\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot.export import ggsave as export_ggsave\nfrom sklearn.datasets import load_iris\nfrom sklearn.decomposition import PCA\nfrom sklearn.preprocessing import StandardScaler\n\n\nLetsPlot.setup_html()\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"  # Okabe-Ito position 1\n\n# Okabe-Ito palette\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Load Iris dataset\niris = load_iris()\nX = iris.data\ny = iris.target\nfeature_names = iris.feature_names\ntarget_names = iris.target_names\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  # Variables x Components\n\n# Variance explained\nvar_explained = pca.explained_variance_ratio_ * 100\n\n# Create dataframe for scores\nscores_df = pd.DataFrame({\"PC1\": scores[:, 0], \"PC2\": scores[:, 1], \"Species\": [target_names[i] for i in y]})\n\n# Scale loadings for visibility alongside scores\nscore_range = max(np.abs(scores).max(), 1)\nloading_scale = score_range * 1.5\n\n# Create dataframe for loading arrows\nclean_names = [\"Sepal Length\", \"Sepal Width\", \"Petal Length\", \"Petal Width\"]\nloadings_df = pd.DataFrame(\n    {\n        \"x_start\": [0] * len(feature_names),\n        \"y_start\": [0] * len(feature_names),\n        \"x_end\": loadings[:, 0] * loading_scale,\n        \"y_end\": loadings[:, 1] * loading_scale,\n        \"variable\": clean_names,\n    }\n)\n\n# Label positions with smart offset to avoid overlap\nlabel_offsets = []\nfor i, name in enumerate(clean_names):\n    x_end = loadings_df[\"x_end\"].iloc[i]\n    y_end = loadings_df[\"y_end\"].iloc[i]\n    if name == \"Petal Width\":\n        label_offsets.append((x_end * 1.15, y_end * 1.15 + 0.4))\n    elif name == \"Petal Length\":\n        label_offsets.append((x_end * 1.15, y_end * 1.15 - 0.3))\n    else:\n        label_offsets.append((x_end * 1.15, y_end * 1.15))\n\nloadings_df[\"label_x\"] = [offset[0] for offset in label_offsets]\nloadings_df[\"label_y\"] = [offset[1] for offset in label_offsets]\n\n# Unit circle for reference\ncircle_theta = np.linspace(0, 2 * np.pi, 100)\ncircle_df = pd.DataFrame({\"x\": np.cos(circle_theta), \"y\": np.sin(circle_theta)})\n\n# Build the plot\nplot = (\n    ggplot()\n    + geom_point(\n        data=scores_df,\n        mapping=aes(x=\"PC1\", y=\"PC2\", color=\"Species\"),\n        size=5,\n        alpha=0.8,\n    )\n    + geom_segment(\n        data=loadings_df,\n        mapping=aes(x=\"x_start\", y=\"y_start\", xend=\"x_end\", yend=\"y_end\"),\n        color=INK_SOFT,\n        size=1.8,\n        arrow=arrow(length=15, type=\"open\"),\n    )\n    + geom_text(\n        data=loadings_df,\n        mapping=aes(x=\"label_x\", y=\"label_y\", label=\"variable\"),\n        size=14,\n        color=INK_SOFT,\n    )\n    + geom_path(\n        data=circle_df,\n        mapping=aes(x=\"x\", y=\"y\"),\n        color=INK_MUTED,\n        size=0.5,\n    )\n    + geom_hline(yintercept=0, color=INK_MUTED, size=0.5, linetype=\"dashed\")\n    + geom_vline(xintercept=0, color=INK_MUTED, size=0.5, linetype=\"dashed\")\n    + labs(\n        x=f\"PC1 ({var_explained[0]:.1f}%)\",\n        y=f\"PC2 ({var_explained[1]:.1f}%)\",\n        title=\"biplot-pca · letsplot · anyplot.ai\",\n        color=\"Species\",\n    )\n    + scale_color_manual(values=IMPRINT)\n    + scale_x_continuous(expand=[0.15, 0.15])\n    + scale_y_continuous(expand=[0.15, 0.15])\n    + theme_minimal()\n    + theme(\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(\n            color=INK, size=0.3\n        ),\n        panel_grid_minor=element_blank(),\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, size=0.5),\n        plot_title=element_text(size=24, color=INK),\n        legend_background=element_rect(\n            fill=ELEVATED_BG, color=INK_SOFT\n        ),\n        legend_text=element_text(size=16, color=INK_SOFT),\n        legend_title=element_text(size=18, color=INK),\n    )\n    + ggsize(1600, 900)\n)\n\n# Save PNG (scale 3x for 4800x2700)\nexport_ggsave(plot, filename=f\"plot-{THEME}.png\", path=\".\", scale=3)\n\n# Save HTML for interactivity\nexport_ggsave(plot, filename=f\"plot-{THEME}.html\", path=\".\")\n"}