{"spec_id":"ks-test-comparison","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom scipy.stats import ecdf, ks_2samp\n\n\n# Theme-adaptive chrome — Imprint palette\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint categorical colors — semantic mapping: green=good/pass, red=bad/fail\nGREEN = \"#009E73\"  # Imprint slot 1 — Good Customers\nRED = \"#AE3030\"  # Imprint slot 5 — Bad Customers (semantic bad/fail anchor)\nBLUE = \"#4467A3\"  # Imprint slot 3 — max divergence marker\n\n# Data — credit scoring; symmetric betas ensure ECDFs cross near score 50\nnp.random.seed(42)\ngood_customers = np.random.beta(5, 3, size=200) * 100\nbad_customers = np.random.beta(3, 5, size=200) * 100\n\n# ECDFs via scipy\ngood_ecdf_result = ecdf(good_customers)\nbad_ecdf_result = ecdf(bad_customers)\ngood_sorted = good_ecdf_result.cdf.quantiles\ngood_cdf = good_ecdf_result.cdf.probabilities\nbad_sorted = bad_ecdf_result.cdf.quantiles\nbad_cdf = bad_ecdf_result.cdf.probabilities\n\n# K-S test\nks_stat, p_value = ks_2samp(good_customers, bad_customers)\n\n# Find point of maximum divergence\nall_values = np.sort(np.concatenate([good_sorted, bad_sorted]))\ngood_cdf_at_all = good_ecdf_result.cdf.evaluate(all_values)\nbad_cdf_at_all = bad_ecdf_result.cdf.evaluate(all_values)\ndiff = np.abs(good_cdf_at_all - bad_cdf_at_all)\nmax_idx = np.argmax(diff)\nmax_x = all_values[max_idx]\nmax_good_y = good_cdf_at_all[max_idx]\nmax_bad_y = bad_cdf_at_all[max_idx]\ny_lo = min(max_good_y, max_bad_y)\ny_hi = max(max_good_y, max_bad_y)\n\np_text = f\"p = {p_value:.2e}\" if p_value >= 0.001 else \"p < 0.001\"\n\n# Figure\nfig = go.Figure()\n\n# Good Customers ECDF — Imprint green (semantic \"good/pass\")\nfig.add_trace(\n    go.Scatter(\n        x=good_sorted,\n        y=good_cdf,\n        mode=\"lines\",\n        name=\"Good Customers\",\n        line={\"color\": GREEN, \"width\": 3, \"shape\": \"hv\"},\n        hovertemplate=\"<b>Good Customers</b><br>Credit Score: %{x:.1f}<br>Cumulative: %{y:.3f}<extra></extra>\",\n    )\n)\n\n# Bad Customers ECDF — Imprint matte red (semantic \"bad/fail\")\nfig.add_trace(\n    go.Scatter(\n        x=bad_sorted,\n        y=bad_cdf,\n        mode=\"lines\",\n        name=\"Bad Customers\",\n        line={\"color\": RED, \"width\": 3, \"shape\": \"hv\"},\n        hovertemplate=\"<b>Bad Customers</b><br>Credit Score: %{x:.1f}<br>Cumulative: %{y:.3f}<extra></extra>\",\n    )\n)\n\n# Shaded region between ECDFs around point of maximum divergence\nregion_width = 5\nregion_mask = (all_values >= max_x - region_width) & (all_values <= max_x + region_width)\nregion_x = all_values[region_mask]\nregion_upper = np.maximum(good_cdf_at_all[region_mask], bad_cdf_at_all[region_mask])\nregion_lower = np.minimum(good_cdf_at_all[region_mask], bad_cdf_at_all[region_mask])\n\nfig.add_trace(\n    go.Scatter(\n        x=np.concatenate([region_x, region_x[::-1]]),\n        y=np.concatenate([region_upper, region_lower[::-1]]),\n        fill=\"toself\",\n        fillcolor=\"rgba(68,103,163,0.12)\",\n        line={\"color\": \"rgba(68,103,163,0.25)\", \"width\": 1},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\n# Maximum divergence line with diamond endpoint markers\nfig.add_trace(\n    go.Scatter(\n        x=[max_x, max_x],\n        y=[y_lo, y_hi],\n        mode=\"lines+markers\",\n        name=f\"Max Divergence (D = {ks_stat:.3f})\",\n        line={\"color\": BLUE, \"width\": 3, \"dash\": \"dash\"},\n        marker={\"color\": BLUE, \"size\": 11, \"symbol\": \"diamond\", \"line\": {\"color\": PAGE_BG, \"width\": 1.5}},\n        hovertemplate=f\"<b>Max Divergence</b><br>Score: {max_x:.1f}<br>D = {ks_stat:.3f}<extra></extra>\",\n    )\n)\n\n# K-S statistic annotation with arrow\nfig.add_annotation(\n    x=max_x,\n    y=(y_lo + y_hi) / 2,\n    text=f\"<b>K-S Statistic</b><br>D = {ks_stat:.3f}<br>{p_text}\",\n    showarrow=True,\n    arrowhead=2,\n    arrowsize=1.0,\n    arrowwidth=2,\n    arrowcolor=BLUE,\n    ax=90,\n    ay=-40,\n    font={\"size\": 12, \"color\": INK, \"family\": \"Arial, sans-serif\"},\n    bordercolor=BLUE,\n    borderwidth=1.5,\n    borderpad=8,\n    bgcolor=ELEVATED_BG,\n)\n\n# Subtle dotted quartile reference lines\nfor y_ref in [0.25, 0.50, 0.75]:\n    fig.add_shape(type=\"line\", x0=0, x1=100, y0=y_ref, y1=y_ref, line={\"color\": GRID, \"width\": 1, \"dash\": \"dot\"})\n\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": (\n            \"ks-test-comparison · python · plotly · anyplot.ai\"\n            f'<br><span style=\"font-size:10px;color:{INK_MUTED}\">'\n            f\"Good vs. Bad customers — distributions differ significantly \"\n            f\"(D = {ks_stat:.3f}, {p_text})</span>\"\n        ),\n        \"font\": {\"size\": 16, \"color\": INK, \"family\": \"Arial, sans-serif\"},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Credit Score (0–100)\", \"font\": {\"size\": 12, \"color\": INK}, \"standoff\": 12},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showline\": False,\n        \"range\": [-2, 102],\n        \"dtick\": 20,\n    },\n    yaxis={\n        \"title\": {\"text\": \"Cumulative Proportion\", \"font\": {\"size\": 12, \"color\": INK}, \"standoff\": 8},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"range\": [-0.02, 1.05],\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showline\": False,\n        \"dtick\": 0.25,\n        \"tickformat\": \".2f\",\n    },\n    legend={\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"x\": 0.02,\n        \"y\": 0.98,\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    margin={\"l\": 80, \"r\": 40, \"t\": 80, \"b\": 60},\n    hoverlabel={\"font\": {\"size\": 12}, \"bgcolor\": ELEVATED_BG, \"bordercolor\": INK_SOFT},\n    hovermode=\"x unified\",\n    xaxis_spikemode=\"across\",\n    xaxis_spikesnap=\"cursor\",\n    xaxis_spikethickness=1,\n    xaxis_spikecolor=GRID,\n    xaxis_spikedash=\"dot\",\n    yaxis_spikemode=\"across\",\n    yaxis_spikesnap=\"cursor\",\n    yaxis_spikethickness=1,\n    yaxis_spikecolor=GRID,\n    yaxis_spikedash=\"dot\",\n)\n\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(\n    f\"plot-{THEME}.html\",\n    include_plotlyjs=\"cdn\",\n    config={\"displayModeBar\": True, \"modeBarButtonsToAdd\": [\"drawline\", \"eraseshape\"]},\n)\n"}