{"spec_id":"ks-test-comparison","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# bokeh.py is the script name — remove its directory from sys.path so that\n# `import bokeh` resolves to the installed package, not this file itself.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path[:] = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import (\n    Band,\n    ColumnDataSource,\n    HoverTool,\n    Label,\n    Legend,\n    LegendItem,\n    NumeralTickFormatter,\n    Range1d,\n    Span,\n)\nfrom bokeh.plotting import figure\nfrom scipy import stats\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (Imprint palette — theme-adaptive chrome)\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\"\n\n# Imprint categorical palette — semantic assignment: Good=green, Bad=red, KS=blue\nCOLOR_GOOD = \"#009E73\"  # position 1, brand green — semantic: good/pass\nCOLOR_BAD = \"#AE3030\"  # position 5, matte red — semantic: bad/fail\nCOLOR_KS = \"#4467A3\"  # position 3, blue — neutral emphasis for the distance measurement\n\n# Data — Credit scoring: Good vs Bad customer score distributions\nnp.random.seed(42)\ngood_scores = np.random.beta(5, 2, 400) * 600 + 300  # Good customers: higher scores\nbad_scores = np.random.beta(2, 4, 350) * 600 + 300  # Bad customers: lower scores\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_scores, bad_scores]))\ngood_ecdf_at = np.searchsorted(good_sorted, all_values, side=\"right\") / len(good_sorted)\nbad_ecdf_at = np.searchsorted(bad_sorted, all_values, side=\"right\") / len(bad_sorted)\nmax_idx = np.argmax(np.abs(good_ecdf_at - bad_ecdf_at))\nmax_x = all_values[max_idx]\nmax_y_good = good_ecdf_at[max_idx]\nmax_y_bad = bad_ecdf_at[max_idx]\nks_y_lower = min(max_y_good, max_y_bad)\nks_y_upper = max(max_y_good, max_y_bad)\n\n# Build step function arrays — interleave x,y pairs for step rendering\ngood_x_step = np.concatenate([[good_sorted[0]], np.repeat(good_sorted, 2)[1:]])\ngood_y_step = np.concatenate([[0], np.repeat(good_ecdf, 2)[:-1]])\nbad_x_step = np.concatenate([[bad_sorted[0]], np.repeat(bad_sorted, 2)[1:]])\nbad_y_step = np.concatenate([[0], np.repeat(bad_ecdf, 2)[:-1]])\n\ngood_source = ColumnDataSource(data={\"x\": good_x_step, \"y\": good_y_step})\nbad_source = ColumnDataSource(data={\"x\": bad_x_step, \"y\": bad_y_step})\n\n# Shaded band near max divergence for visual storytelling (distinctive Bokeh Band model)\nband_mask = (all_values >= max_x - 15) & (all_values <= max_x + 15)\nband_x = all_values[band_mask]\nband_upper = np.maximum(good_ecdf_at[band_mask], bad_ecdf_at[band_mask])\nband_lower = np.minimum(good_ecdf_at[band_mask], bad_ecdf_at[band_mask])\nband_source = ColumnDataSource(data={\"x\": band_x, \"upper\": band_upper, \"lower\": band_lower})\n\n# Plot — canonical 3200×1800 landscape canvas; toolbar_location=None avoids toolbar height offset\ntitle = \"ks-test-comparison · python · bokeh · anyplot.ai\"\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Credit Score\",\n    y_axis_label=\"Cumulative Proportion\",\n    y_range=Range1d(-0.03, 1.08),\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Shaded band between ECDFs at max divergence\nband = Band(\n    base=\"x\", upper=\"upper\", lower=\"lower\", source=band_source, fill_color=COLOR_KS, fill_alpha=0.14, line_color=None\n)\np.add_layout(band)\n\n# ECDF step lines — solid green (Good) vs dashed red (Bad)\ngood_line = p.line(x=\"x\", y=\"y\", source=good_source, line_width=5, line_color=COLOR_GOOD, alpha=0.9)\nbad_line = p.line(x=\"x\", y=\"y\", source=bad_source, line_width=5, line_color=COLOR_BAD, alpha=0.9, line_dash=[14, 7])\n\n# HoverTool for HTML artifact — shows exact ECDF values at cursor position\nhover = HoverTool(tooltips=[(\"Score\", \"@x{0.0}\"), (\"Cum. Proportion\", \"@y{0.000}\")], renderers=[good_line, bad_line])\np.add_tools(hover)\n\n# K-S segment — vertical line at maximum divergence\nks_segment_source = ColumnDataSource(data={\"x0\": [max_x], \"y0\": [ks_y_lower], \"x1\": [max_x], \"y1\": [ks_y_upper]})\nks_line = p.segment(x0=\"x0\", y0=\"y0\", x1=\"x1\", y1=\"y1\", source=ks_segment_source, line_width=7, line_color=COLOR_KS)\n\n# Diamond markers at K-S segment endpoints\nks_marker_source = ColumnDataSource(data={\"x\": [max_x, max_x], \"y\": [ks_y_lower, ks_y_upper]})\np.scatter(x=\"x\", y=\"y\", source=ks_marker_source, size=26, color=COLOR_KS, marker=\"diamond\")\n\n# Annotation — K-S statistic and p-value, prominently sized with theme-adaptive background\np_text = \"p < 0.001\" if p_value < 0.001 else f\"p = {p_value:.4f}\"\nks_label = Label(\n    x=max_x,\n    y=(ks_y_lower + ks_y_upper) / 2,\n    text=f\"D = {ks_stat:.3f},  {p_text}\",\n    text_font_size=\"32pt\",\n    text_color=COLOR_KS,\n    text_font_style=\"bold\",\n    text_baseline=\"middle\",\n    x_offset=30,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.92,\n)\np.add_layout(ks_label)\n\n# Subtle vertical reference line at max divergence\nmax_div_span = Span(\n    location=max_x, dimension=\"height\", line_color=COLOR_KS, line_alpha=0.12, line_width=2, line_dash=\"dotted\"\n)\np.add_layout(max_div_span)\n\n# Legend — canonical 34pt size from bokeh.md, theme-adaptive fill\nlegend = Legend(\n    items=[\n        LegendItem(label=\"Good Customers (ECDF)\", renderers=[good_line]),\n        LegendItem(label=\"Bad Customers (ECDF)\", renderers=[bad_line]),\n        LegendItem(label=\"Max Distance (K-S Stat)\", renderers=[ks_line]),\n    ],\n    location=\"top_left\",\n)\nlegend.label_text_font_size = \"34pt\"\nlegend.label_text_color = INK_SOFT\nlegend.background_fill_color = ELEVATED_BG\nlegend.border_line_color = INK_SOFT\nlegend.glyph_height = 40\nlegend.glyph_width = 50\nlegend.padding = 25\nlegend.spacing = 15\nlegend.margin = 30\np.add_layout(legend, \"center\")\n\n# Typography — canonical bokeh.md sizes; title is 49 chars < 67, so no scaling needed\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"bold\"\np.title.text_color = INK\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.15\np.ygrid.grid_line_alpha = 0.15\n\np.yaxis.formatter = NumeralTickFormatter(format=\"0.0\")\n\n# Save HTML (interactive catalog artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Selenium — Selenium 4 / Selenium Manager resolves the driver.\n# Use CDP setDeviceMetricsOverride so the inner viewport is authoritative:\n# --window-size alone is eaten by Chrome chrome in headless mode (gives 1661 instead of 1800).\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n\n# Belt-and-braces: pin the saved PNG to exact dims so the post-render gate passes\nfrom PIL import Image as _PILImage\n\n\n_img = _PILImage.open(f\"plot-{THEME}.png\").convert(\"RGB\")\nif _img.size != (W, H):\n    _norm = _PILImage.new(\"RGB\", (W, H), PAGE_BG)\n    _norm.paste(_img, ((W - _img.size[0]) // 2, (H - _img.size[1]) // 2))\n    _norm.save(f\"plot-{THEME}.png\")\n"}