{"spec_id":"ks-test-comparison","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// ks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: credit-scoring model validation (Good vs Bad customers) ---------\n// Tiny fixed-seed LCG for deterministic pseudo-random sampling in the browser.\nlet lcgState = 20260826;\nfunction nextRandom() {\n  lcgState = (lcgState * 1664525 + 1013904223) % 4294967296;\n  return lcgState / 4294967296;\n}\nfunction randomNormal(mean, stdDev) {\n  const u1 = Math.max(nextRandom(), 1e-9);\n  const u2 = nextRandom();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * stdDev;\n}\n\nconst SAMPLE_SIZE = 400;\nconst goodScores = [];\nconst badScores = [];\nfor (let i = 0; i < SAMPLE_SIZE; i += 1) {\n  goodScores.push(Math.min(850, Math.max(300, randomNormal(680, 65))));\n  badScores.push(Math.min(850, Math.max(300, randomNormal(590, 75))));\n}\ngoodScores.sort((a, b) => a - b);\nbadScores.sort((a, b) => a - b);\n\n// Build an ECDF as a step series over a shared grid of x values.\nfunction buildEcdf(sortedSample, xGrid) {\n  const n = sortedSample.length;\n  let pointer = 0;\n  return xGrid.map((x) => {\n    while (pointer < n && sortedSample[pointer] <= x) pointer += 1;\n    return pointer / n;\n  });\n}\n\nconst minScore = Math.floor(Math.min(goodScores[0], badScores[0]) / 10) * 10;\nconst maxScore = Math.ceil(Math.max(goodScores[SAMPLE_SIZE - 1], badScores[SAMPLE_SIZE - 1]) / 10) * 10;\nconst GRID_STEP = 2;\nconst xGrid = [];\nfor (let x = minScore; x <= maxScore; x += GRID_STEP) xGrid.push(x);\n\nconst goodCdf = buildEcdf(goodScores, xGrid);\nconst badCdf = buildEcdf(badScores, xGrid);\n\n// K-S statistic: max vertical distance between the two ECDFs, and where it occurs.\nlet ksStatistic = 0;\nlet ksIndex = 0;\nfor (let i = 0; i < xGrid.length; i += 1) {\n  const distance = Math.abs(goodCdf[i] - badCdf[i]);\n  if (distance > ksStatistic) {\n    ksStatistic = distance;\n    ksIndex = i;\n  }\n}\nconst ksScore = xGrid[ksIndex];\nconst ksLow = Math.min(goodCdf[ksIndex], badCdf[ksIndex]);\nconst ksHigh = Math.max(goodCdf[ksIndex], badCdf[ksIndex]);\n\n// Two-sample K-S asymptotic p-value (Kolmogorov distribution survival function).\nconst nEff = Math.sqrt((SAMPLE_SIZE * SAMPLE_SIZE) / (2 * SAMPLE_SIZE));\nconst lambda = (nEff + 0.12 + 0.11 / nEff) * ksStatistic;\nlet pValue = 0;\nfor (let k = 1; k <= 100; k += 1) {\n  pValue += 2 * (-1) ** (k - 1) * Math.exp(-2 * k * k * lambda * lambda);\n}\npValue = Math.min(1, Math.max(0, pValue));\nconst pValueLabel = pValue < 0.001 ? \"p < 0.001\" : `p = ${pValue.toFixed(3)}`;\n\nconst goodSeriesData = xGrid.map((x, i) => [x, goodCdf[i]]);\nconst badSeriesData = xGrid.map((x, i) => [x, badCdf[i]]);\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"ks-test-comparison · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: `D = ${ksStatistic.toFixed(3)} at score ${ksScore} · ${pValueLabel}`,\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    title: { text: \"Credit Score\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 0,\n    min: minScore,\n    max: maxScore,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: \"Cumulative Proportion\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    min: 0,\n    max: 1,\n    tickInterval: 0.2,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    align: \"right\",\n    verticalAlign: \"top\",\n    layout: \"vertical\",\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    borderWidth: 1,\n    borderRadius: 6,\n  },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: { animation: false, marker: { enabled: false } },\n  },\n  series: [\n    {\n      name: \"Good Customers (ECDF)\",\n      data: goodSeriesData,\n      color: t.palette[0],\n      step: \"left\",\n      lineWidth: 2.5,\n    },\n    {\n      name: \"Bad Customers (ECDF)\",\n      data: badSeriesData,\n      color: t.palette[4],\n      step: \"left\",\n      lineWidth: 2.5,\n      dashStyle: \"ShortDash\",\n    },\n    {\n      name: `Max Divergence (D = ${ksStatistic.toFixed(3)})`,\n      type: \"line\",\n      data: [\n        [ksScore, ksLow],\n        [ksScore, ksHigh],\n      ],\n      color: t.ink,\n      lineWidth: 3,\n      dashStyle: \"ShortDot\",\n      marker: {\n        enabled: true,\n        radius: 7,\n        symbol: \"circle\",\n        fillColor: t.ink,\n        lineColor: t.pageBg,\n        lineWidth: 2,\n      },\n      dataLabels: {\n        enabled: true,\n        formatter() {\n          return this.point.index === 1 ? `Score ${ksScore}` : null;\n        },\n        y: -12,\n        backgroundColor: t.elevatedBg,\n        borderColor: t.ink,\n        borderWidth: 1,\n        borderRadius: 4,\n        padding: 4,\n        style: { color: t.ink, fontSize: \"12px\", fontWeight: \"600\", textOutline: \"none\" },\n      },\n      enableMouseTracking: false,\n    },\n  ],\n});\n"}