{"spec_id":"silhouette-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nsilhouette-basic: Silhouette Plot\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-10\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent importing local pygal.py file\nsys.path = [p for p in sys.path if not p.endswith(\"/implementations/python\")]\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\nfrom sklearn.cluster import KMeans\nfrom sklearn.metrics import silhouette_samples, silhouette_score\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette (first series is always #009E73)\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data - Synthetic clustering data designed to show both positive and negative silhouettes\nnp.random.seed(42)\n\n# Create 3 clusters with deliberate overlap to generate negative silhouette values\n# Cluster 0: tight cluster at origin\n# Cluster 1: well-separated cluster\n# Cluster 2: overlaps significantly with cluster 0 to create misclassified samples\nn_samples_per_cluster = 50\ncluster0 = np.random.randn(n_samples_per_cluster, 2) * 0.6 + np.array([0, 0])\ncluster1 = np.random.randn(n_samples_per_cluster, 2) * 0.7 + np.array([4, 4])\ncluster2 = np.random.randn(n_samples_per_cluster, 2) * 1.0 + np.array([0.8, 0.3])\nX = np.vstack([cluster0, cluster1, cluster2])\n\n# Cluster the data\nkmeans = KMeans(n_clusters=3, random_state=42, n_init=10)\ncluster_labels = kmeans.fit_predict(X)\n\n# Compute silhouette scores\nsilhouette_vals = silhouette_samples(X, cluster_labels)\navg_silhouette = silhouette_score(X, cluster_labels)\nn_clusters = 3\n\n# Custom style with theme-adaptive colors and prominent reference line\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT,\n    title_font_size=28,\n    label_font_size=22,\n    major_label_font_size=18,\n    legend_font_size=16,\n    value_font_size=14,\n    stroke_width=3,\n)\n\n# Process and sort silhouette values within each cluster\noriginal_cluster_avgs = {}\nfor i in range(n_clusters):\n    cluster_silhouette_vals = silhouette_vals[cluster_labels == i]\n    original_cluster_avgs[i] = np.mean(cluster_silhouette_vals)\n\n# Build cluster data with sorted values (descending for visual appeal)\ncluster_data = {}\nsample_idx = 0\nfor i in range(n_clusters):\n    cluster_silhouette_vals = silhouette_vals[cluster_labels == i]\n    cluster_silhouette_vals = np.sort(cluster_silhouette_vals)[::-1]\n    # Subsample for thicker bars while maintaining pattern\n    reduced_vals = cluster_silhouette_vals[::2] if len(cluster_silhouette_vals) > 30 else cluster_silhouette_vals\n    cluster_data[i] = {\n        \"values\": reduced_vals,\n        \"avg\": original_cluster_avgs[i],\n        \"start_idx\": sample_idx,\n        \"size\": len(reduced_vals),\n    }\n    sample_idx += len(reduced_vals)\n\ntotal_samples = sample_idx\n\n# Build all bars list for chart data with separator gaps between clusters\nall_bars = []\nseparator_count = 3\nfor i in range(n_clusters):\n    for val in cluster_data[i][\"values\"]:\n        all_bars.append((i, val))\n    if i < n_clusters - 1:\n        for _ in range(separator_count):\n            all_bars.append((-1, None))\n\n# Create chart with prominent average silhouette line\nchart = pygal.HorizontalBar(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"silhouette-basic · pygal · anyplot.ai\",\n    x_title=f\"Silhouette Coefficient (avg: {avg_silhouette:.3f})\",\n    y_title=\"Samples (grouped by cluster)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    show_y_guides=False,\n    show_x_guides=True,\n    print_values=False,\n    range=(-0.2, 1.0),\n    spacing=4,\n    margin=50,\n    margin_bottom=150,\n    show_y_labels=False,\n    x_labels=[-0.2, 0.0, 0.2, round(avg_silhouette, 2), 0.4, 0.6, 0.8, 1.0],\n    x_labels_major=[round(avg_silhouette, 2)],\n)\n\n# Build data series for each cluster\ncluster_positions = {}\npos = 0\nfor i in range(n_clusters):\n    cluster_positions[i] = {\"start\": pos, \"size\": cluster_data[i][\"size\"]}\n    pos += cluster_data[i][\"size\"]\n    if i < n_clusters - 1:\n        pos += separator_count\n\nfor cluster_idx in range(n_clusters):\n    cluster_avg = cluster_data[cluster_idx][\"avg\"]\n    cluster_size = cluster_positions[cluster_idx][\"size\"]\n    start_pos = cluster_positions[cluster_idx][\"start\"]\n    mid_point = start_pos + cluster_size // 2\n\n    series_data = []\n    for bar_idx, (c, val) in enumerate(all_bars):\n        if c == cluster_idx:\n            if bar_idx == mid_point:\n                series_data.append({\"value\": val, \"label\": f\"Cluster {cluster_idx} avg: {cluster_avg:.3f}\"})\n            else:\n                series_data.append(val)\n        else:\n            series_data.append(None)\n\n    chart.add(f\"Cluster {cluster_idx} (avg: {cluster_avg:.3f})\", series_data)\n\n# Save outputs\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}