{"spec_id":"ks-test-comparison","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    arrow,\n    coord_cartesian,\n    element_blank,\n    element_line,\n    element_markdown,\n    element_rect,\n    element_text,\n    geom_ribbon,\n    geom_segment,\n    geom_step,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom scipy import stats\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — Imprint palette chrome (theme-adaptive)\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 categorical palette — hybrid-v3 sort\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"  # warning / caution semantic anchor (outside categorical pool)\n\n# Data — credit scoring: Good vs Bad customer score distributions\nnp.random.seed(42)\ngood_scores = np.clip(np.random.normal(loc=620, scale=80, size=500), 300, 850)\nbad_scores = np.clip(np.random.normal(loc=520, scale=90, size=300), 300, 850)\n\n# Compute ECDFs\ngood_sorted = np.sort(good_scores)\ngood_ecdf = np.arange(1, len(good_sorted) + 1) / len(good_sorted)\nbad_sorted = np.sort(bad_scores)\nbad_ecdf = np.arange(1, len(bad_sorted) + 1) / len(bad_sorted)\n\n# K-S test\nks_stat, p_value = stats.ks_2samp(good_scores, bad_scores)\n\n# Find point of maximum divergence\nall_values = np.sort(np.concatenate([good_sorted, bad_sorted]))\ngood_ecdf_all = np.searchsorted(good_sorted, all_values, side=\"right\") / len(good_sorted)\nbad_ecdf_all = np.searchsorted(bad_sorted, all_values, side=\"right\") / len(bad_sorted)\nmax_idx = np.argmax(np.abs(good_ecdf_all - bad_ecdf_all))\nmax_x = all_values[max_idx]\nmax_good_y = good_ecdf_all[max_idx]\nmax_bad_y = bad_ecdf_all[max_idx]\nks_mid_y = (max_good_y + max_bad_y) / 2\n\n# DataFrames for ggplot layers\ndf_good = pd.DataFrame({\"score\": good_sorted, \"ecdf\": good_ecdf, \"group\": \"Good Customers\"})\ndf_bad = pd.DataFrame({\"score\": bad_sorted, \"ecdf\": bad_ecdf, \"group\": \"Bad Customers\"})\ndf_ecdf = pd.concat([df_good, df_bad], ignore_index=True)\n\nribbon_ymin = np.minimum(good_ecdf_all, bad_ecdf_all)\nribbon_ymax = np.maximum(good_ecdf_all, bad_ecdf_all)\ndf_ribbon = pd.DataFrame({\"score\": all_values, \"ymin\": ribbon_ymin, \"ymax\": ribbon_ymax})\n\ndf_ks_seg = pd.DataFrame(\n    {\"x\": [max_x], \"y\": [min(max_good_y, max_bad_y)], \"xend\": [max_x], \"yend\": [max(max_good_y, max_bad_y)]}\n)\ndf_ks_arrow = pd.DataFrame({\"x\": [max_x + 40], \"y\": [ks_mid_y + 0.06], \"xend\": [max_x + 3], \"yend\": [ks_mid_y]})\ndf_ks_label = pd.DataFrame({\"score\": [max_x + 42], \"ecdf\": [ks_mid_y + 0.08], \"label\": [f\"D = {ks_stat:.3f}\"]})\n\n# Subtitle with formatted K-S stats (element_markdown renders the bold/span tags)\nsubtitle_text = (\n    f\"K-S Statistic: **{ks_stat:.3f}** | \"\n    f\"p-value: **{p_value:.2e}** \"\n    f\"<span style='color:{INK_MUTED}'>(highly significant)</span>\"\n)\n\ntitle = \"ks-test-comparison · python · letsplot · anyplot.ai\"\n\n# Ribbon: amber on light (caution semantic), neutral-muted on dark (amber+dark = olive)\nribbon_color = ANYPLOT_AMBER if THEME == \"light\" else INK_MUTED\nribbon_alpha = 0.20 if THEME == \"light\" else 0.30\n\n# Plot\nplot = (\n    ggplot(df_ecdf, aes(x=\"score\", y=\"ecdf\"))\n    # Ribbon shows total divergence area (amber on light = caution; muted on dark = neutral)\n    + geom_ribbon(\n        data=df_ribbon,\n        mapping=aes(x=\"score\", ymin=\"ymin\", ymax=\"ymax\"),\n        fill=ribbon_color,\n        alpha=ribbon_alpha,\n        tooltips=\"none\",\n    )\n    # Both ECDFs as step functions — Imprint positions 1 (green) and 3 (blue)\n    + geom_step(\n        mapping=aes(color=\"group\"),\n        size=2.2,\n        tooltips=layer_tooltips()\n        .line(\"@group\")\n        .line(\"Score|@score\")\n        .line(\"ECDF|@ecdf\")\n        .format(\"@score\", \".0f\")\n        .format(\"@ecdf\", \".3f\"),\n    )\n    # Dashed red segment at maximum divergence\n    + geom_segment(\n        data=df_ks_seg,\n        mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        color=IMPRINT_PALETTE[4],\n        size=2.5,\n        linetype=\"dashed\",\n        tooltips=\"none\",\n    )\n    # Arrow pointing from annotation label to K-S midpoint\n    + geom_segment(\n        data=df_ks_arrow,\n        mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        color=IMPRINT_PALETTE[4],\n        size=1.2,\n        arrow=arrow(length=8, type=\"closed\"),\n        tooltips=\"none\",\n    )\n    # Bold annotation of K-S statistic value — ink color for readability on both themes\n    + geom_text(\n        data=df_ks_label,\n        mapping=aes(x=\"score\", y=\"ecdf\", label=\"label\"),\n        color=INK,\n        size=5,\n        fontface=\"bold\",\n        tooltips=\"none\",\n    )\n    + scale_color_manual(\n        values={\n            \"Good Customers\": IMPRINT_PALETTE[0],  # brand green\n            \"Bad Customers\": IMPRINT_PALETTE[2],  # blue\n        }\n    )\n    + labs(x=\"Credit Score (points)\", y=\"Cumulative Proportion\", title=title, subtitle=subtitle_text, color=\"\")\n    + coord_cartesian(xlim=[280, 860])\n    + scale_x_continuous(breaks=list(range(300, 851, 100)))\n    + scale_y_continuous(limits=[0, 1.05], breaks=[0, 0.25, 0.5, 0.75, 1.0])\n    + ggsize(800, 450)\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_border=element_blank(),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        plot_subtitle=element_markdown(size=12, color=INK_SOFT),\n        axis_title=element_text(size=12, face=\"bold\", color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"top\",\n        panel_grid_major_x=element_blank(),\n        panel_grid_major_y=element_line(color=INK_SOFT, size=0.3, linetype=\"dashed\"),\n        panel_grid_minor=element_blank(),\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        plot_margin=[40, 20, 20, 20],\n    )\n)\n\n# Save PNG (3200×1800 via scale=4) and HTML with interactive tooltips\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}