{"spec_id":"logistic-regression","library":"echarts","language":"javascript","code":"// anyplot.ai\n// logistic-regression: Logistic Regression Curve Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// A tiny fixed-seed LCG stands in for a seeded RNG (the browser has none).\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\nconst N = 180;\nconst X_DOMAIN_MAX = 30;\nconst TRUE_BETA0 = -3.5;\nconst TRUE_BETA1 = 0.35; // ad exposures per week -> conversion probability\n\nconst sigmoid = (z) => 1 / (1 + Math.exp(-z));\n\nconst adExposures = Array.from({ length: N }, () => rand() * X_DOMAIN_MAX);\nconst converted = adExposures.map((x) => (rand() < sigmoid(TRUE_BETA0 + TRUE_BETA1 * x) ? 1 : 0));\n\n// Jitter the binary outcome around 0/1 so overlapping points stay visible.\nconst notConvertedPoints = [];\nconst convertedPoints = [];\nadExposures.forEach((x, i) => {\n  const jitter = (rand() - 0.5) * 0.08;\n  const point = [x, converted[i] + jitter];\n  if (converted[i] === 1) convertedPoints.push(point);\n  else notConvertedPoints.push(point);\n});\n\n// --- Fit a logistic regression via full-batch gradient descent -------------\nconst xMean = adExposures.reduce((a, b) => a + b, 0) / N;\nconst xStd = Math.sqrt(adExposures.reduce((a, x) => a + (x - xMean) ** 2, 0) / N);\nconst xNorm = adExposures.map((x) => (x - xMean) / xStd);\n\nlet w0 = 0;\nlet w1 = 0;\nconst LEARNING_RATE = 0.5;\nfor (let iter = 0; iter < 4000; iter++) {\n  let grad0 = 0;\n  let grad1 = 0;\n  for (let i = 0; i < N; i++) {\n    const p = sigmoid(w0 + w1 * xNorm[i]);\n    const err = p - converted[i];\n    grad0 += err;\n    grad1 += err * xNorm[i];\n  }\n  w0 -= (LEARNING_RATE * grad0) / N;\n  w1 -= (LEARNING_RATE * grad1) / N;\n}\n\n// Fisher information (Hessian of the log-likelihood) at the fitted weights,\n// inverted analytically to get the asymptotic covariance of (w0, w1).\nlet h00 = 0;\nlet h01 = 0;\nlet h11 = 0;\nfor (let i = 0; i < N; i++) {\n  const p = sigmoid(w0 + w1 * xNorm[i]);\n  const wgt = p * (1 - p);\n  h00 += wgt;\n  h01 += wgt * xNorm[i];\n  h11 += wgt * xNorm[i] * xNorm[i];\n}\nconst det = h00 * h11 - h01 * h01;\nconst cov00 = h11 / det;\nconst cov01 = -h01 / det;\nconst cov11 = h00 / det;\n\n// Real-scale coefficients (undo the x-normalization) for the annotation.\nconst realBeta1 = w1 / xStd;\nconst realBeta0 = w0 - (w1 * xMean) / xStd;\nconst accuracy =\n  adExposures.reduce((correct, x, i) => {\n    const predicted = sigmoid(realBeta0 + realBeta1 * x) > 0.5 ? 1 : 0;\n    return correct + (predicted === converted[i] ? 1 : 0);\n  }, 0) / N;\n\n// --- Fitted curve + 95% confidence band (logit-scale, mapped back to prob) --\nconst CURVE_POINTS = 100;\nconst xMin = Math.min(...adExposures);\nconst xMax = Math.max(...adExposures);\nconst curveFit = [];\nconst curveLower = [];\nconst curveBandHeight = [];\nfor (let i = 0; i < CURVE_POINTS; i++) {\n  const x = xMin + ((xMax - xMin) * i) / (CURVE_POINTS - 1);\n  const xn = (x - xMean) / xStd;\n  const eta = w0 + w1 * xn;\n  const varEta = cov00 + 2 * xn * cov01 + xn * xn * cov11;\n  const se = Math.sqrt(Math.max(varEta, 0));\n  const pLo = sigmoid(eta - 1.96 * se);\n  const pHi = sigmoid(eta + 1.96 * se);\n  curveFit.push([x, sigmoid(eta)]);\n  curveLower.push([x, pLo]);\n  curveBandHeight.push([x, pHi - pLo]);\n}\n\n// x-value where the fitted curve crosses p = 0.5 (the decision threshold).\nconst xThreshold = -realBeta0 / realBeta1;\n\n// --- Title (mandated format, fontsize scaled to the descriptive prefix) ----\nconst titleText = \"Marketing Conversion · logistic-regression · javascript · echarts · anyplot.ai\";\nconst titleRatio = titleText.length > 67 ? 67 / titleText.length : 1.0;\nconst titleFontSize = Math.max(14, Math.round(22 * titleRatio));\nconst subtext = `Fitted: p = σ(${realBeta0.toFixed(2)} + ${realBeta1.toFixed(2)}·x) · Accuracy: ${Math.round(accuracy * 100)}%`;\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  color: t.palette,\n  title: {\n    text: titleText,\n    subtext,\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 500 },\n    subtextStyle: { color: t.inkSoft, fontSize: 16 },\n  },\n  legend: {\n    data: [\"Not Converted\", \"Converted\", \"Fitted Probability\"],\n    top: 96,\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 16 },\n    itemWidth: 22,\n    itemHeight: 14,\n  },\n  grid: { left: 110, right: 90, top: 190, bottom: 110 },\n  xAxis: {\n    type: \"value\",\n    name: \"Ad Exposures per Week\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    min: 0,\n    max: X_DOMAIN_MAX,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Probability\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    min: -0.1,\n    max: 1.1,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"ci-lower\",\n      type: \"line\",\n      data: curveLower,\n      stack: \"confidence\",\n      symbol: \"none\",\n      lineStyle: { opacity: 0 },\n      areaStyle: { opacity: 0 },\n      silent: true,\n      tooltip: { show: false },\n      z: 1,\n    },\n    {\n      name: \"ci-band\",\n      type: \"line\",\n      data: curveBandHeight,\n      stack: \"confidence\",\n      symbol: \"none\",\n      lineStyle: { opacity: 0 },\n      areaStyle: { color: t.palette[2], opacity: 0.24 },\n      silent: true,\n      tooltip: { show: false },\n      z: 1,\n    },\n    {\n      name: \"Not Converted\",\n      type: \"scatter\",\n      data: notConvertedPoints,\n      symbolSize: 10,\n      itemStyle: { color: t.palette[0], opacity: 0.6, borderColor: t.pageBg, borderWidth: 1 },\n      z: 3,\n    },\n    {\n      name: \"Converted\",\n      type: \"scatter\",\n      data: convertedPoints,\n      symbolSize: 10,\n      itemStyle: { color: t.palette[1], opacity: 0.6, borderColor: t.pageBg, borderWidth: 1 },\n      z: 3,\n    },\n    {\n      name: \"Fitted Probability\",\n      type: \"line\",\n      data: curveFit,\n      symbol: \"none\",\n      lineStyle: { color: t.palette[2], width: 3 },\n      z: 2,\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        lineStyle: { type: \"dashed\", color: t.inkSoft, width: 2 },\n        label: { formatter: \"p = 0.5\", color: t.inkSoft, fontSize: 14, position: \"insideEndTop\" },\n        data: [{ yAxis: 0.5 }],\n      },\n      markPoint: {\n        silent: true,\n        symbol: \"circle\",\n        symbolSize: 14,\n        itemStyle: { color: t.palette[2], borderColor: t.pageBg, borderWidth: 2 },\n        label: {\n          formatter: `x ≈ ${xThreshold.toFixed(1)}`,\n          color: t.ink,\n          fontSize: 13,\n          position: \"top\",\n          distance: 10,\n        },\n        data: [{ coord: [xThreshold, 0.5] }],\n      },\n    },\n  ],\n});\n"}