{"spec_id":"ks-test-comparison","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as mticker\nimport numpy as np\nimport seaborn as sns\nfrom matplotlib.lines import Line2D\nfrom scipy import stats\n\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — position 1 (green) for Good Customers, position 2 (lavender) for Bad\nGOOD_COLOR = \"#009E73\"\nBAD_COLOR = \"#C475FD\"\n\n# Data — credit scoring: Good vs Bad customer score distributions\nnp.random.seed(42)\ngood_scores = np.random.beta(5, 3, size=300) * 500 + 350\nbad_scores = np.random.beta(3, 4, size=200) * 500 + 350\n\n# Compute K-S statistic\nks_stat, p_value = stats.ks_2samp(good_scores, bad_scores)\n\n# Compute ECDFs for identifying max-distance point\ngood_sorted = np.sort(good_scores)\nbad_sorted = np.sort(bad_scores)\nall_values = np.sort(np.concatenate([good_sorted, bad_sorted]))\ngood_ecdf = np.searchsorted(good_sorted, all_values, side=\"right\") / len(good_sorted)\nbad_ecdf = np.searchsorted(bad_sorted, all_values, side=\"right\") / len(bad_sorted)\ndistances = np.abs(good_ecdf - bad_ecdf)\nmax_idx = np.argmax(distances)\nmax_x = all_values[max_idx]\nmax_y_good = good_ecdf[max_idx]\nmax_y_bad = bad_ecdf[max_idx]\ny_lo, y_hi = min(max_y_good, max_y_bad), max(max_y_good, max_y_bad)\n\n# Seaborn theme — theme-adaptive chrome\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Figure — landscape 3200×1800 px; no bbox_inches=\"tight\" (trims canvas)\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400)\nfig.patch.set_facecolor(PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# ECDFs — separate calls give direct linestyle control (avoids fragile hue-color matching)\nsns.ecdfplot(x=good_scores, color=GOOD_COLOR, linewidth=2.5, linestyle=\"-\", ax=ax)\nsns.ecdfplot(x=bad_scores, color=BAD_COLOR, linewidth=2.5, linestyle=\"--\", ax=ax)\n\n# Shaded band between ECDFs around the max-divergence region\nband_half = 25\nband_mask = (all_values >= max_x - band_half) & (all_values <= max_x + band_half)\nax.fill_between(all_values[band_mask], good_ecdf[band_mask], bad_ecdf[band_mask], alpha=0.25, color=BAD_COLOR, zorder=2)\n\n# K-S statistic: dotted vertical line at max divergence\nax.plot([max_x, max_x], [y_lo, y_hi], color=INK, linewidth=1.8, linestyle=\":\", zorder=5)\n\n# Dots at max-divergence intersection points\nax.scatter([max_x, max_x], [max_y_good, max_y_bad], color=INK, s=60, zorder=6, edgecolors=PAGE_BG, linewidth=1.2)\n\n# x limits for tight framing\nx_min = min(good_scores.min(), bad_scores.min()) - 10\nx_max = max(good_scores.max(), bad_scores.max()) + 10\n\n# Annotation in lower-left dead space (where both CDFs are near zero) — arrow to KS line\nax.annotate(\n    f\"K-S = {ks_stat:.3f}\\np = {p_value:.2e}\",\n    xy=(max_x, (y_lo + y_hi) / 2),\n    xytext=(x_min + 30, 0.17),\n    fontsize=9,\n    fontweight=\"bold\",\n    color=INK,\n    ha=\"left\",\n    va=\"center\",\n    bbox={\n        \"boxstyle\": \"round,pad=0.4\",\n        \"facecolor\": ELEVATED_BG,\n        \"edgecolor\": INK_SOFT,\n        \"linewidth\": 1.0,\n        \"alpha\": 0.95,\n    },\n    arrowprops={\"arrowstyle\": \"-|>\", \"color\": INK_MUTED, \"linewidth\": 1.0, \"connectionstyle\": \"arc3,rad=0.25\"},\n)\n\n# Axis limits and labels\nax.set_xlim(x_min, x_max)\nax.set_ylim(0, 1.04)\n\ntitle = \"Credit Scoring · ks-test-comparison · python · seaborn · anyplot.ai\"\nn = len(title)\nratio = 67 / n if n > 67 else 1.0\ntitle_fontsize = max(8, round(12 * ratio))\n\nax.set_xlabel(\"Credit Score (points)\", fontsize=10, labelpad=8)\nax.set_ylabel(\"Cumulative Proportion\", fontsize=10, labelpad=8)\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", pad=12)\nax.tick_params(axis=\"both\", labelsize=8)\nax.xaxis.set_major_formatter(mticker.FormatStrFormatter(\"%.0f\"))\n\n# Grid — y-axis only, subtle\nax.yaxis.grid(True, alpha=0.15, linewidth=0.8)\nax.xaxis.grid(False)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\n\n# Legend with explicit handles — robust, no fragile color-matching\nlegend_handles = [\n    Line2D([0], [0], color=GOOD_COLOR, linewidth=2.5, linestyle=\"-\", label=\"Good Customers\"),\n    Line2D([0], [0], color=BAD_COLOR, linewidth=2.5, linestyle=\"--\", label=\"Bad Customers\"),\n]\nax.legend(handles=legend_handles, fontsize=8, loc=\"upper left\")\n\n# Inset: seaborn kdeplot with fill — shows underlying score density to complement the ECDF\nax_inset = ax.inset_axes([0.66, 0.04, 0.30, 0.27])\nax_inset.set_facecolor(ELEVATED_BG)\nsns.kdeplot(x=good_scores, color=GOOD_COLOR, linewidth=1.2, fill=True, alpha=0.25, ax=ax_inset)\nsns.kdeplot(x=bad_scores, color=BAD_COLOR, linewidth=1.2, fill=True, alpha=0.25, linestyle=\"--\", ax=ax_inset)\nax_inset.set_xlabel(\"Score\", fontsize=6, labelpad=2, color=INK_SOFT)\nax_inset.set_ylabel(\"\", fontsize=6)\nax_inset.set_title(\"Score Density\", fontsize=6.5, color=INK_SOFT, pad=3, fontweight=\"medium\")\nax_inset.tick_params(axis=\"x\", labelsize=5, colors=INK_SOFT)\nax_inset.tick_params(axis=\"y\", labelleft=False, left=False)\nax_inset.spines[\"top\"].set_visible(False)\nax_inset.spines[\"right\"].set_visible(False)\nfor spine in [\"left\", \"bottom\"]:\n    ax_inset.spines[spine].set_color(INK_SOFT)\n    ax_inset.spines[spine].set_linewidth(0.6)\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}