{"spec_id":"logistic-regression","library":"d3","language":"javascript","code":"// anyplot.ai\n// logistic-regression: Logistic Regression Curve Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 140, right: 60, bottom: 100, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: synthetic credit-risk scenario (deterministic LCG) --------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\nconst n = 180;\nconst beta0True = -4.2;\nconst beta1True = 0.085;\n\nconst points = [];\nfor (let i = 0; i < n; i++) {\n  const utilization = rand() * 100;\n  const logit = beta0True + beta1True * utilization;\n  const pTrue = 1 / (1 + Math.exp(-logit));\n  const defaulted = rand() < pTrue ? 1 : 0;\n  points.push({ utilization, defaulted });\n}\n\n// --- Fit logistic regression via Newton-Raphson (IRLS) ----------------------\nlet b0 = 0;\nlet b1 = 0;\nfor (let iter = 0; iter < 20; iter++) {\n  let g0 = 0;\n  let g1 = 0;\n  let h00 = 0;\n  let h01 = 0;\n  let h11 = 0;\n  for (const d of points) {\n    const eta = b0 + b1 * d.utilization;\n    const p = 1 / (1 + Math.exp(-eta));\n    const w = p * (1 - p);\n    const err = d.defaulted - p;\n    g0 += err;\n    g1 += err * d.utilization;\n    h00 += w;\n    h01 += w * d.utilization;\n    h11 += w * d.utilization * d.utilization;\n  }\n  const det = h00 * h11 - h01 * h01;\n  b0 += (h11 * g0 - h01 * g1) / det;\n  b1 += (h00 * g1 - h01 * g0) / det;\n}\n\n// Covariance matrix = inverse Fisher information at the MLE — feeds the 95% CI band.\nlet h00f = 0;\nlet h01f = 0;\nlet h11f = 0;\nfor (const d of points) {\n  const eta = b0 + b1 * d.utilization;\n  const p = 1 / (1 + Math.exp(-eta));\n  const w = p * (1 - p);\n  h00f += w;\n  h01f += w * d.utilization;\n  h11f += w * d.utilization * d.utilization;\n}\nconst detF = h00f * h11f - h01f * h01f;\nconst cov00 = h11f / detF;\nconst cov01 = -h01f / detF;\nconst cov11 = h00f / detF;\n\nconst accuracy =\n  points.filter((d) => {\n    const predicted = 1 / (1 + Math.exp(-(b0 + b1 * d.utilization))) >= 0.5 ? 1 : 0;\n    return predicted === d.defaulted;\n  }).length / n;\n\n// --- Fitted curve + 95% confidence band over a grid --------------------------\nconst gridN = 100;\nconst curve = [];\nfor (let i = 0; i <= gridN; i++) {\n  const xi = (i / gridN) * 100;\n  const eta = b0 + b1 * xi;\n  const seEta = Math.sqrt(cov00 + 2 * xi * cov01 + xi * xi * cov11);\n  curve.push({\n    x: xi,\n    p: 1 / (1 + Math.exp(-eta)),\n    lo: 1 / (1 + Math.exp(-(eta - 1.96 * seEta))),\n    hi: 1 / (1 + Math.exp(-(eta + 1.96 * seEta))),\n  });\n}\n\n// Jitter for point display only — class assignment itself stays binary.\nconst jittered = points.map((d) => ({\n  ...d,\n  yJitter: d.defaulted + (rand() - 0.5) * 0.08,\n}));\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, 100]).range([0, iw]);\nconst y = d3.scaleLinear().domain([-0.08, 1.08]).range([ih, 0]);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Gridlines (y-axis only) -------------------------------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickValues([0, 0.25, 0.5, 0.75, 1]).tickSize(-iw).tickFormat(\"\"))\n  .call((gr) => gr.select(\".domain\").remove())\n  .call((gr) => gr.selectAll(\"line\").attr(\"stroke\", t.grid));\n\n// --- Confidence band ----------------------------------------------------------\nconst band = d3\n  .area()\n  .x((d) => x(d.x))\n  .y0((d) => y(d.lo))\n  .y1((d) => y(d.hi))\n  .curve(d3.curveMonotoneX);\ng.append(\"path\").datum(curve).attr(\"d\", band).attr(\"fill\", t.palette[2]).attr(\"opacity\", 0.18);\n\n// --- Decision threshold line ---------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", y(0.5))\n  .attr(\"y2\", y(0.5))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,5\")\n  .attr(\"opacity\", 0.55);\n\ng.append(\"text\")\n  .attr(\"x\", iw)\n  .attr(\"y\", y(0.5) - 12)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"decision threshold (p = 0.5)\");\n\n// --- Fitted logistic curve ------------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.x))\n  .y((d) => y(d.p))\n  .curve(d3.curveMonotoneX);\ng.append(\"path\").datum(curve).attr(\"d\", line).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[2]).attr(\"stroke-width\", 3);\n\n// --- Data points (jittered, colored by class) -----------------------------------\ng.selectAll(\"circle\")\n  .data(jittered)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.utilization))\n  .attr(\"cy\", (d) => y(d.yJitter))\n  .attr(\"r\", 7)\n  .attr(\"fill\", (d) => (d.defaulted ? t.palette[1] : t.palette[0]))\n  .attr(\"fill-opacity\", 0.6)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.8);\n\n// --- Axes -----------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickFormat((d) => `${d}%`));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickValues([0, 0.25, 0.5, 0.75, 1]));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels ------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Credit Utilization Rate (%)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -66)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Probability of Default\");\n\n// --- Header row: legend + model stats (kept clear of the plot area) -------------\nconst legendData = [\n  { label: \"No default (y = 0)\", color: t.palette[0] },\n  { label: \"Default (y = 1)\", color: t.palette[1] },\n];\nconst legendG = svg.append(\"g\").attr(\"transform\", `translate(${margin.left}, 92)`);\nlegendData.forEach((item, i) => {\n  const row = legendG.append(\"g\").attr(\"transform\", `translate(${i * 230}, 0)`);\n  row.append(\"circle\").attr(\"r\", 7).attr(\"cy\", -5).attr(\"fill\", item.color).attr(\"fill-opacity\", 0.8);\n  row.append(\"text\").attr(\"x\", 16).attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\").text(item.label);\n});\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width - margin.right)\n  .attr(\"y\", 92)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(`β₀ = ${b0.toFixed(2)} · β₁ = ${b1.toFixed(3)} · accuracy = ${(accuracy * 100).toFixed(0)}%`);\n\n// --- Title -----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 54)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"logistic-regression · javascript · d3 · anyplot.ai\");\n"}