{"spec_id":"silhouette-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nsilhouette-basic: Silhouette Plot\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-10\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, Label, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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\"\nRULE = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\n# Okabe-Ito colors for clusters\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data - simulating silhouette analysis of customer segmentation (3 clusters)\n# Realistic scenario: clustering customers by purchase behavior\nnp.random.seed(42)\n\nn_clusters = 3\ncluster_sizes = [50, 55, 45]  # Different sized clusters\n\n# Generate realistic silhouette values for each cluster\n# Cluster 0: Well-separated cluster (high silhouette scores)\ncluster0_vals = np.clip(np.random.beta(8, 2, cluster_sizes[0]) * 0.6 + 0.35, 0.1, 0.95)\n# Cluster 1: Good cluster with some overlap (medium-high scores)\ncluster1_vals = np.clip(np.random.beta(5, 2, cluster_sizes[1]) * 0.5 + 0.25, 0.0, 0.85)\n# Cluster 2: Some ambiguous samples (includes negative values)\ncluster2_vals = np.clip(np.random.beta(4, 3, cluster_sizes[2]) * 0.8 - 0.1, -0.15, 0.75)\n\n# Combine all values\nsilhouette_vals = np.concatenate([cluster0_vals, cluster1_vals, cluster2_vals])\ncluster_labels = np.concatenate(\n    [\n        np.zeros(cluster_sizes[0], dtype=int),\n        np.ones(cluster_sizes[1], dtype=int),\n        np.full(cluster_sizes[2], 2, dtype=int),\n    ]\n)\n\n# Calculate average silhouette score\navg_silhouette = float(np.mean(silhouette_vals))\n\n# Prepare data for plotting - sorted silhouette values within each cluster\ny_lower = 15\nbar_data = {\"x\": [], \"y\": [], \"width\": [], \"height\": [], \"color\": []}\ncluster_info = []  # For labels\n\nfor i in range(n_clusters):\n    # Get silhouette values for this cluster\n    cluster_mask = cluster_labels == i\n    cluster_silhouette_vals = silhouette_vals[cluster_mask]\n    cluster_silhouette_vals.sort()\n\n    cluster_size = len(cluster_silhouette_vals)\n    y_upper = y_lower + cluster_size\n\n    # Store center position for cluster label\n    cluster_center = (y_lower + y_upper) / 2\n    cluster_avg = float(np.mean(cluster_silhouette_vals))\n    cluster_info.append((cluster_center, cluster_avg, cluster_size, i))\n\n    # Add bars for each sample in cluster\n    for j, val in enumerate(cluster_silhouette_vals):\n        bar_data[\"x\"].append(val / 2)  # Center of bar\n        bar_data[\"y\"].append(y_lower + j + 0.5)  # Y position\n        bar_data[\"width\"].append(abs(val))  # Width = silhouette value\n        bar_data[\"height\"].append(0.85)  # Slightly less than 1 for gap\n        bar_data[\"color\"].append(IMPRINT[i])\n\n    y_lower = y_upper + 15  # Gap between clusters\n\n# Create figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"silhouette-basic · bokeh · anyplot.ai\",\n    x_axis_label=\"Silhouette Coefficient\",\n    y_axis_label=\"Cluster (samples sorted by silhouette score)\",\n    x_range=(-0.3, 1.25),\n    y_range=(0, y_lower + 5),\n    tools=\"\",\n)\n\n# Style the figure - sized for 4800x2700 canvas\np.title.text_font_size = \"28pt\"\np.xaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_font_size = \"18pt\"\np.xaxis.axis_label_standoff = 25\np.yaxis.axis_label_standoff = 25\n\n# Theme-adaptive colors\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.title.text_color = INK\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\n# Create data source for bars\nsource = ColumnDataSource(data=bar_data)\n\n# Draw horizontal bars\np.rect(x=\"x\", y=\"y\", width=\"width\", height=\"height\", color=\"color\", source=source, line_color=None, alpha=0.85)\n\n# Add vertical line for average silhouette score\navg_line = Span(location=avg_silhouette, dimension=\"height\", line_color=INK_SOFT, line_width=3, line_dash=\"dashed\")\np.add_layout(avg_line)\n\n# Add average silhouette score label at top\navg_label = Label(\n    x=avg_silhouette + 0.03,\n    y=y_lower - 5,\n    text=f\"Average: {avg_silhouette:.3f}\",\n    text_font_size=\"20pt\",\n    text_color=INK_SOFT,\n    text_font_style=\"bold\",\n)\np.add_layout(avg_label)\n\n# Add cluster labels with their average silhouette scores\nfor center_y, cluster_avg, size, cluster_idx in cluster_info:\n    # Position label to the left side, outside the bars\n    cluster_label = Label(\n        x=-0.22,\n        y=center_y,\n        text=f\"Cluster {cluster_idx}\",\n        text_font_size=\"18pt\",\n        text_color=IMPRINT[cluster_idx],\n        text_font_style=\"bold\",\n        text_align=\"left\",\n        text_baseline=\"middle\",\n    )\n    p.add_layout(cluster_label)\n\n    # Add cluster stats on the right side\n    stats_label = Label(\n        x=1.01,\n        y=center_y,\n        text=f\"n={size}, avg={cluster_avg:.2f}\",\n        text_font_size=\"16pt\",\n        text_color=IMPRINT[cluster_idx],\n        text_font_style=\"normal\",\n        text_align=\"left\",\n        text_baseline=\"middle\",\n    )\n    p.add_layout(stats_label)\n\n# Style grid\np.xgrid.grid_line_color = RULE\np.xgrid.grid_line_alpha = 0.10\np.xgrid.grid_line_dash = [6, 4]\np.ygrid.grid_line_alpha = 0.0  # No horizontal grid\n\n# Remove y-axis ticks (sample indices are not meaningful)\np.yaxis.major_tick_line_color = None\np.yaxis.minor_tick_line_color = None\np.yaxis.major_label_text_font_size = \"0pt\"\n\n# Add vertical line at x=0 for reference\nzero_line = Span(location=0, dimension=\"height\", line_color=INK_SOFT, line_width=2, line_alpha=0.5)\np.add_layout(zero_line)\n\n# Save as HTML first\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome\nW, H = 4800, 2700\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}