{"spec_id":"ks-test-comparison","library":"muix","language":"javascript","code":"// anyplot.ai\n// ks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Box-Muller normal sampler --------------------\nlet seed = 42;\nfunction nextUniform(): number {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction nextNormal(mean: number, std: number): number {\n  const u1 = nextUniform() || 1e-9;\n  const u2 = nextUniform();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + std * z;\n}\n\n// --- Data: credit-scoring samples for a model-discrimination check -----------\n// Good-standing vs. bad-standing customers should show separated score\n// distributions if the scoring model discriminates well.\nconst SAMPLE_SIZE = 400;\nconst goodScores = Array.from({ length: SAMPLE_SIZE }, () => nextNormal(680, 55));\nconst badScores = Array.from({ length: SAMPLE_SIZE }, () => nextNormal(590, 70));\n\nconst sortedGood = [...goodScores].sort((a, b) => a - b);\nconst sortedBad = [...badScores].sort((a, b) => a - b);\n\n// Fraction of a sorted sample that is ≤ x (binary search)\nfunction ecdfAt(sorted: number[], x: number): number {\n  let lo = 0;\n  let hi = sorted.length;\n  while (lo < hi) {\n    const mid = (lo + hi) >> 1;\n    if (sorted[mid] <= x) lo = mid + 1;\n    else hi = mid;\n  }\n  return lo / sorted.length;\n}\n\n// Evaluate both ECDFs at every observed score so the step jumps land exactly\n// where either sample has a data point.\nconst xGrid = [...goodScores, ...badScores].sort((a, b) => a - b);\nconst ecdfGood = xGrid.map((x) => ecdfAt(sortedGood, x));\nconst ecdfBad = xGrid.map((x) => ecdfAt(sortedBad, x));\n\nlet ksStatistic = 0;\nlet ksLocation = xGrid[0];\nxGrid.forEach((x, i) => {\n  const distance = Math.abs(ecdfGood[i] - ecdfBad[i]);\n  if (distance > ksStatistic) {\n    ksStatistic = distance;\n    ksLocation = x;\n  }\n});\n\n// Asymptotic two-sample K-S p-value (Kolmogorov distribution tail probability)\nconst effectiveN = (SAMPLE_SIZE * SAMPLE_SIZE) / (SAMPLE_SIZE + SAMPLE_SIZE);\nconst lambda = (Math.sqrt(effectiveN) + 0.12 + 0.11 / Math.sqrt(effectiveN)) * 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 pLabel = pValue < 0.001 ? \"p < 0.001\" : `p = ${pValue.toFixed(3)}`;\n\nconst HEADER_HEIGHT = 96;\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n\n  return (\n    <Box sx={{ width, height, display: \"flex\", flexDirection: \"column\", paddingTop: \"20px\" }}>\n      <Typography\n        sx={{ color: t.ink, fontSize: 22, fontWeight: 500, textAlign: \"center\", lineHeight: 1.2 }}\n      >\n        ks-test-comparison · javascript · muix · anyplot.ai\n      </Typography>\n      <Typography\n        sx={{ color: t.inkSoft, fontSize: 16, textAlign: \"center\", lineHeight: 1.4, marginTop: \"6px\" }}\n      >\n        K-S statistic D = {ksStatistic.toFixed(3)} · {pLabel}\n      </Typography>\n      <LineChart\n        width={width}\n        height={height - HEADER_HEIGHT}\n        skipAnimation\n        series={[\n          {\n            data: ecdfGood,\n            label: \"Good-standing customers (n=400)\",\n            color: t.palette[0],\n            curve: \"stepAfter\" as const,\n            showMark: false,\n          },\n          {\n            data: ecdfBad,\n            label: \"Bad-standing customers (n=400)\",\n            color: t.palette[4],\n            curve: \"stepAfter\" as const,\n            showMark: false,\n          },\n        ]}\n        xAxis={[\n          {\n            data: xGrid,\n            scaleType: \"linear\",\n            label: \"Credit Score\",\n            valueFormatter: (v: number) => `${Math.round(v)}`,\n          },\n        ]}\n        yAxis={[\n          {\n            label: \"Cumulative Proportion\",\n            min: 0,\n            max: 1,\n          },\n        ]}\n        grid={{ horizontal: true }}\n        margin={{ left: 90, right: 60, top: 20, bottom: 80 }}\n        sx={{\n          \"& .MuiChartsAxis-tickLabel\": { fontSize: \"14px\" },\n          \"& .MuiChartsAxis-label\": { fontSize: \"16px\" },\n          \"& .MuiChartsLegend-label\": { fontSize: \"14px\" },\n          \"& .MuiLineElement-root\": { strokeWidth: 2.5 },\n        }}\n      >\n        <ChartsReferenceLine\n          x={ksLocation}\n          label={`max |ΔF| at ${Math.round(ksLocation)}`}\n          labelAlign=\"end\"\n          lineStyle={{ stroke: t.ink, strokeDasharray: \"6 4\", strokeWidth: 1.5 }}\n          labelStyle={{ fill: t.ink, fontSize: 13 }}\n        />\n      </LineChart>\n    </Box>\n  );\n}\n"}