{"spec_id":"ks-test-comparison","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_hline,\n    geom_segment,\n    geom_step,\n    ggplot,\n    labs,\n    scale_color_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom scipy import stats\n\n\n# Theme tokens — 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\"\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Semantic exception: green for Good, red for Bad customers\nCOLOR_GOOD = IMPRINT_PALETTE[0]  # #009E73 — brand green\nCOLOR_BAD = IMPRINT_PALETTE[4]  # #AE3030 — matte red (bad/loss semantic anchor)\n\n# Data - credit scoring distributions\nnp.random.seed(42)\ngood_scores = np.random.beta(5, 2, 300) * 800 + 200\nbad_scores = np.random.beta(2, 4, 300) * 800 + 200\n\n# K-S test\nks_stat, p_value = stats.ks_2samp(good_scores, bad_scores)\n\n# Compute ECDFs\ngood_sorted = np.sort(good_scores)\nbad_sorted = np.sort(bad_scores)\ngood_ecdf = np.arange(1, len(good_sorted) + 1) / len(good_sorted)\nbad_ecdf = np.arange(1, len(bad_sorted) + 1) / len(bad_sorted)\n\n# Find the point of maximum divergence\nall_values = np.sort(np.concatenate([good_sorted, bad_sorted]))\ngood_cdf_at_all = np.searchsorted(good_sorted, all_values, side=\"right\") / len(good_sorted)\nbad_cdf_at_all = np.searchsorted(bad_sorted, all_values, side=\"right\") / len(bad_sorted)\nmax_idx = np.argmax(np.abs(good_cdf_at_all - bad_cdf_at_all))\nmax_x = all_values[max_idx]\nmax_y_good = good_cdf_at_all[max_idx]\nmax_y_bad = bad_cdf_at_all[max_idx]\ny_lo, y_hi = min(max_y_good, max_y_bad), max(max_y_good, max_y_bad)\n\n# Build ECDF DataFrame\ndf = pd.concat(\n    [\n        pd.DataFrame({\"score\": good_sorted, \"ecdf\": good_ecdf, \"group\": \"Good Customers\"}),\n        pd.DataFrame({\"score\": bad_sorted, \"ecdf\": bad_ecdf, \"group\": \"Bad Customers\"}),\n    ],\n    ignore_index=True,\n)\n\n# Axis range — balanced padding on both sides\nx_min, x_max = df[\"score\"].min(), df[\"score\"].max()\nx_pad = (x_max - x_min) * 0.04\n\n# Divergence segment DataFrame\ndf_seg = pd.DataFrame({\"x\": [max_x], \"xend\": [max_x], \"y\": [y_lo], \"yend\": [y_hi]})\n\n# Title with required language token\ntitle = \"ks-test-comparison · python · plotnine · anyplot.ai\"\nn = len(title)\nratio = 67 / n if n > 67 else 1.0\ntitle_fontsize = max(8, round(12 * ratio))\n\n# Color mapping — semantic exception (good→green, bad→red)\ncolors = {\"Good Customers\": COLOR_GOOD, \"Bad Customers\": COLOR_BAD}\n\n# Plot\nplot = (\n    ggplot(df, aes(x=\"score\", y=\"ecdf\", color=\"group\"))\n    # Midpoint reference line at 0.5 for structural anchor\n    + geom_hline(yintercept=0.5, color=INK_MUTED, size=0.4, linetype=\"dotted\", inherit_aes=False)\n    # Max divergence vertical line\n    + geom_segment(\n        aes(x=\"x\", xend=\"xend\", y=\"y\", yend=\"yend\"),\n        data=df_seg,\n        color=COLOR_BAD,\n        size=1.5,\n        linetype=\"dashed\",\n        inherit_aes=False,\n    )\n    # ECDF step functions\n    + geom_step(size=1.2)\n    # D label next to max divergence\n    + annotate(\n        \"text\",\n        x=max_x + 18,\n        y=(max_y_good + max_y_bad) / 2,\n        label=f\"D = {ks_stat:.3f}\",\n        size=3.5,\n        ha=\"left\",\n        va=\"center\",\n        color=COLOR_BAD,\n        fontweight=\"bold\",\n    )\n    # Statistical summary box\n    + annotate(\n        \"label\",\n        x=x_min + 0.50 * (x_max - x_min),\n        y=0.03,\n        label=f\"K-S Statistic = {ks_stat:.3f}  |  p-value = {p_value:.2e}\",\n        size=3.4,\n        ha=\"center\",\n        va=\"center\",\n        color=COLOR_BAD,\n        fontweight=\"bold\",\n        fill=ELEVATED_BG,\n        alpha=0.92,\n        label_size=0.5,\n        boxstyle=\"round\",\n    )\n    # Interpretive subtitle above the ECDF area\n    + annotate(\n        \"label\",\n        x=x_min + 0.50 * (x_max - x_min),\n        y=1.06,\n        label=\"Distributions are highly distinct (D > 0.5) — strong evidence the groups differ\",\n        size=3.2,\n        ha=\"center\",\n        va=\"center\",\n        color=INK_MUTED,\n        fontstyle=\"italic\",\n        fill=ELEVATED_BG,\n        alpha=1.0,\n        label_size=0,\n        boxstyle=\"round\",\n    )\n    + scale_color_manual(name=\"Distribution\", values=colors)\n    + scale_y_continuous(limits=(0, 1.10), breaks=np.arange(0, 1.1, 0.2))\n    + scale_x_continuous(limits=(x_min - x_pad, x_max + x_pad))\n    + labs(x=\"Credit Score (points)\", y=\"Cumulative Proportion (0–1)\", title=title)\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        text=element_text(size=7, family=\"sans-serif\"),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        plot_title=element_text(size=title_fontsize, color=INK),\n        legend_title=element_text(size=8, color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_position=(0.15, 0.80),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, alpha=0.9, size=0.4),\n        legend_key=element_rect(fill=PAGE_BG, color=\"none\"),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_y=element_line(color=INK, alpha=0.15, size=0.3),\n        axis_line_x=element_line(color=INK_SOFT, size=0.5),\n        axis_line_y=element_line(color=INK_SOFT, size=0.5),\n        panel_border=element_blank(),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n    )\n)\n\n# Save — canonical plotnine landscape: 8×4.5 in at 400 dpi → 3200×1800 px\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}