{"spec_id":"ks-test-comparison","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-29\n\"\"\"\n\nimport importlib.util\nimport os\nimport sys\n\nimport numpy as np\nfrom scipy import stats\n\n\n# Prevent this file (pygal.py) from shadowing the installed pygal package\npygal_spec = importlib.util.find_spec(\"pygal\")\nif pygal_spec and pygal_spec.origin != __file__:\n    import pygal\n    from pygal.style import Style\nelse:\n    _here = os.path.dirname(os.path.abspath(__file__))\n    sys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n    try:\n        import pygal\n        from pygal.style import Style\n    finally:\n        sys.path.insert(0, _here)\n\n# Theme-adaptive chrome — Imprint palette\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# Imprint palette — semantic mapping: green=good, matte red=bad\nGOOD_COLOR = \"#009E73\"  # Imprint position 1 — brand green / \"good\"\nBAD_COLOR = \"#AE3030\"  # Imprint matte red — semantic anchor for \"bad/loss/error\"\nKS_COLOR = INK_SOFT  # theme-adaptive neutral for annotation reference line\n\n# Data — credit scoring: Good vs Bad customer score distributions\n# 100 samples keeps the step-function nature clearly visible\nnp.random.seed(42)\nn_samples = 100\ngood_scores = np.random.normal(loc=650, scale=80, size=n_samples)\nbad_scores = np.random.normal(loc=500, scale=90, size=n_samples)\n\n# Compute ECDFs\ngood_sorted = np.sort(good_scores)\nbad_sorted = np.sort(bad_scores)\nn_good, n_bad = len(good_sorted), len(bad_sorted)\ngood_ecdf_y = np.arange(1, n_good + 1) / n_good\nbad_ecdf_y = np.arange(1, n_bad + 1) / n_bad\n\n# KS test\nks_stat, p_value = stats.ks_2samp(good_scores, bad_scores)\n\n# Find point of maximum divergence on a combined grid\nall_values = np.sort(np.concatenate([good_sorted, bad_sorted]))\ngood_ecdf_on_grid = np.searchsorted(good_sorted, all_values, side=\"right\") / n_good\nbad_ecdf_on_grid = np.searchsorted(bad_sorted, all_values, side=\"right\") / n_bad\ndiffs = np.abs(good_ecdf_on_grid - bad_ecdf_on_grid)\nmax_idx = np.argmax(diffs)\nmax_x = all_values[max_idx]\nmax_y_good = good_ecdf_on_grid[max_idx]\nmax_y_bad = bad_ecdf_on_grid[max_idx]\n\n# Build step-function data using vectorized numpy (no loops)\ngood_x_steps = np.repeat(good_sorted, 2)\ngood_y_steps = np.empty_like(good_x_steps)\ngood_y_steps[0::2] = np.concatenate([[0], good_ecdf_y[:-1]])\ngood_y_steps[1::2] = good_ecdf_y\ngood_xy = list(zip(good_x_steps.tolist(), good_y_steps.tolist(), strict=True))\n\nbad_x_steps = np.repeat(bad_sorted, 2)\nbad_y_steps = np.empty_like(bad_x_steps)\nbad_y_steps[0::2] = np.concatenate([[0], bad_ecdf_y[:-1]])\nbad_y_steps[1::2] = bad_ecdf_y\nbad_xy = list(zip(bad_x_steps.tolist(), bad_y_steps.tolist(), strict=True))\n\n_font = \"Helvetica, Arial, sans-serif\"\n\n# Imprint palette — canonical order; first series always #009E73\n# Series slots: Good, Bad, KS line, KS dot bottom, KS dot top\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=(GOOD_COLOR, BAD_COLOR, KS_COLOR, KS_COLOR, KS_COLOR),\n    opacity=\"0.95\",\n    opacity_hover=\"1\",\n    stroke_opacity=\"1\",\n    stroke_opacity_hover=\"1\",\n    stroke_width=2.5,\n    guide_stroke_color=INK_MUTED,\n    guide_stroke_dasharray=\"6, 8\",\n    major_guide_stroke_color=INK_SOFT,\n    major_guide_stroke_dasharray=\"0\",\n    # Font sizes for 3200×1800 canvas (pygal unitless = source pixels)\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=42,\n    value_label_font_size=42,\n    tooltip_font_size=32,\n    font_family=_font,\n    label_font_family=_font,\n    major_label_font_family=_font,\n    legend_font_family=_font,\n    title_font_family=_font,\n    value_font_family=_font,\n    value_label_font_family=_font,\n)\n\nchart = pygal.XY(\n    style=custom_style,\n    width=3200,\n    height=1800,\n    title=\"ks-test-comparison · python · pygal · anyplot.ai\",\n    x_title=\"Credit Score (points)\",\n    y_title=\"Cumulative Proportion\",\n    show_dots=False,\n    fill=False,\n    show_x_guides=False,\n    show_y_guides=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=2,\n    legend_box_size=30,\n    truncate_legend=-1,\n    range=(0, 1.05),\n    print_values=False,\n    print_labels=True,\n    print_zeroes=False,\n    margin=60,\n    margin_top=80,\n    margin_bottom=160,\n    margin_left=150,\n    margin_right=80,\n    x_value_formatter=lambda x: f\"{x:.0f}\",\n    value_formatter=lambda y: f\"{y:.2f}\",\n    y_labels_major_count=6,\n    show_minor_y_labels=False,\n    js=[],\n)\n\n# Good Customers ECDF — bold green line\nchart.add(\"Good Customers\", good_xy, stroke_style={\"width\": 5})\n\n# Bad Customers ECDF — bold red line\nchart.add(\"Bad Customers\", bad_xy, stroke_style={\"width\": 5})\n\n# KS divergence line — dashed neutral annotation\nks_line_points = [(max_x, min(max_y_good, max_y_bad)), (max_x, max(max_y_good, max_y_bad))]\nchart.add(None, ks_line_points, stroke_style={\"width\": 4, \"dasharray\": \"16, 10\"}, show_dots=False)\n\n# KS annotation dot at bottom with D statistic\nchart.add(\n    None,\n    [{\"value\": (max_x, min(max_y_good, max_y_bad)), \"label\": f\"D = {ks_stat:.3f}\"}],\n    stroke_style={\"width\": 0},\n    show_dots=True,\n    dots_size=12,\n)\n\n# KS annotation dot at top with p-value\nchart.add(\n    None,\n    [{\"value\": (max_x, max(max_y_good, max_y_bad)), \"label\": f\"p = {p_value:.2e}\"}],\n    stroke_style={\"width\": 0},\n    show_dots=True,\n    dots_size=12,\n)\n\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}