{"spec_id":"silhouette-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nsilhouette-basic: Silhouette Plot\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-10\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom sklearn.cluster import KMeans\nfrom sklearn.datasets import load_iris\nfrom sklearn.metrics import silhouette_samples, silhouette_score\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\"\n\n# Okabe-Ito palette - first series is always #009E73 (brand green)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data - Clustering iris dataset into 3 groups\nnp.random.seed(42)\niris = load_iris()\nX = iris.data\nn_clusters = 3\n\n# Perform K-means clustering\nkmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)\ncluster_labels = kmeans.fit_predict(X)\n\n# Calculate silhouette scores\nsilhouette_vals = silhouette_samples(X, cluster_labels)\navg_silhouette = silhouette_score(X, cluster_labels)\n\n# Build dataframe for plotting - sort samples within each cluster by silhouette score\ndata_rows = []\ny_position = 0\ncluster_centers = []\ncluster_avg_scores = []\n\nfor cluster_idx in range(n_clusters):\n    # Get samples in this cluster\n    mask = cluster_labels == cluster_idx\n    cluster_silhouettes = silhouette_vals[mask]\n    cluster_silhouettes_sorted = np.sort(cluster_silhouettes)\n\n    # Calculate cluster average\n    cluster_avg = cluster_silhouettes.mean()\n    cluster_avg_scores.append(cluster_avg)\n\n    # Track the center position for annotation\n    cluster_start = y_position\n\n    # Add each sample as a row\n    for sil_val in cluster_silhouettes_sorted:\n        data_rows.append(\n            {\"y\": y_position, \"silhouette\": sil_val, \"cluster\": f\"Cluster {cluster_idx}\", \"cluster_idx\": cluster_idx}\n        )\n        y_position += 1\n\n    cluster_end = y_position - 1\n    cluster_centers.append((cluster_start + cluster_end) / 2)\n\n    # Add small gap between clusters\n    y_position += 5\n\ndf = pd.DataFrame(data_rows)\ndf[\"x_start\"] = 0  # Starting x position for horizontal bars\n\n# Map cluster indices to Okabe-Ito colors\ndf[\"cluster_color\"] = df[\"cluster_idx\"].map({i: IMPRINT[i % len(IMPRINT)] for i in range(n_clusters)})\n\n# Create annotation dataframe for cluster labels\nannotation_df = pd.DataFrame(\n    {\n        \"y\": cluster_centers,\n        \"x\": [-0.12] * n_clusters,\n        \"label\": [f\"Cluster {i}\\n(avg: {cluster_avg_scores[i]:.2f})\" for i in range(n_clusters)],\n    }\n)\n\n# Theme-adaptive theme\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major_x=element_line(color=INK, size=0.3),\n    panel_grid_minor_x=element_line(color=INK, size=0.2),\n    axis_title=element_text(size=20, color=INK),\n    axis_text_x=element_text(size=16, color=INK_SOFT),\n    axis_text_y=element_blank(),\n    axis_ticks_y=element_blank(),\n    panel_grid_major_y=element_blank(),\n    panel_grid_minor_y=element_blank(),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(size=24, color=INK),\n    legend_position=\"right\",\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)\n\n# Create the silhouette plot using horizontal bars\nplot = (\n    ggplot(df, aes(x=\"silhouette\", y=\"y\", color=\"cluster\"))\n    + geom_segment(aes(xend=\"silhouette\", yend=\"y\"), x=0, size=1.5)\n    + geom_vline(xintercept=avg_silhouette, color=INK_SOFT, linetype=\"dashed\", size=1)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=annotation_df, size=14, hjust=1, color=INK_SOFT)\n    + scale_color_manual(values=IMPRINT[:n_clusters])\n    + labs(\n        x=\"Silhouette Coefficient\",\n        y=\"Sample Index (sorted within cluster)\",\n        title=\"silhouette-basic · letsplot · anyplot.ai\",\n    )\n    + xlim(-0.3, 1.0)\n    + theme_minimal()\n    + anyplot_theme\n    + ggsize(1600, 900)\n)\n\n# Add annotation for average silhouette line\navg_label_df = pd.DataFrame(\n    {\"x\": [avg_silhouette + 0.03], \"y\": [max(df[\"y\"]) * 0.95], \"label\": [f\"Avg: {avg_silhouette:.2f}\"]}\n)\nplot = plot + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=avg_label_df, size=14, hjust=0, color=INK_SOFT)\n\n# Save as PNG (scale 3x to get 4800 x 2700 px)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\n\n# Save as HTML for interactive version\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}