{"spec_id":"logistic-regression","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nlogistic-regression: Logistic Regression Curve Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\n\n\n# Work around filename shadowing the altair library\nsys.path.pop(0)\nimport altair as alt\n\n\n# Theme configuration\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# Okabe-Ito palette\nBRAND = \"#009E73\"  # First series (Fail)\nSECONDARY = \"#C475FD\"  # Second series (Pass)\n\n# Data - Study hours vs exam pass/fail\nnp.random.seed(42)\nn_samples = 150\n\n# Generate study hours with different distributions for pass/fail\nhours_fail = np.random.normal(3, 1.5, 60)\nhours_pass = np.random.normal(7, 1.5, 90)\nhours = np.concatenate([hours_fail, hours_pass])\nhours = np.clip(hours, 0.5, 10)\n\noutcome = np.concatenate([np.zeros(60), np.ones(90)])\n\n# Fit logistic regression using gradient descent\nX_b = np.column_stack([np.ones(n_samples), hours])\nw = np.zeros(2)\n\nfor _ in range(1000):\n    z = X_b @ w\n    predictions = 1 / (1 + np.exp(-z))\n    gradient = X_b.T @ (predictions - outcome) / n_samples\n    w -= 0.1 * gradient\n\nb0, b1 = w[0], w[1]\n\n# Generate smooth curve points\nx_curve = np.linspace(0, 10.5, 200)\ny_proba = 1 / (1 + np.exp(-(b0 + b1 * x_curve)))\n\n# Calculate confidence intervals\nse = np.sqrt(y_proba * (1 - y_proba) / n_samples) * 2.5\nci_lower = np.clip(y_proba - 1.96 * se, 0, 1)\nci_upper = np.clip(y_proba + 1.96 * se, 0, 1)\n\n# Create curve DataFrame\ncurve_df = pd.DataFrame({\"Study Hours\": x_curve, \"Probability\": y_proba, \"CI Lower\": ci_lower, \"CI Upper\": ci_upper})\n\n# Add jitter to data points for visibility\njitter = np.random.uniform(-0.03, 0.03, len(outcome))\ny_jittered = outcome + jitter\n\n# Create data points DataFrame\npoints_df = pd.DataFrame(\n    {\n        \"Study Hours\": hours,\n        \"Outcome\": outcome,\n        \"Outcome Jittered\": y_jittered,\n        \"Class\": [\"Fail\" if o == 0 else \"Pass\" for o in outcome],\n    }\n)\n\n# Decision threshold line\nthreshold_df = pd.DataFrame({\"Study Hours\": [0, 10.5], \"Probability\": [0.5, 0.5]})\n\n# Create the confidence interval band\nci_band = (\n    alt.Chart(curve_df)\n    .mark_area(opacity=0.25, color=INK_SOFT)\n    .encode(x=alt.X(\"Study Hours:Q\"), y=alt.Y(\"CI Lower:Q\"), y2=alt.Y2(\"CI Upper:Q\"))\n)\n\n# Create the logistic curve\ncurve = (\n    alt.Chart(curve_df).mark_line(strokeWidth=4, color=INK).encode(x=alt.X(\"Study Hours:Q\"), y=alt.Y(\"Probability:Q\"))\n)\n\n# Create the data points with Okabe-Ito colors\npoints = (\n    alt.Chart(points_df)\n    .mark_circle(size=200, opacity=0.6, strokeWidth=1, stroke=PAGE_BG)\n    .encode(\n        x=alt.X(\"Study Hours:Q\", title=\"Study Hours (hrs)\", scale=alt.Scale(domain=[0, 10.5])),\n        y=alt.Y(\"Outcome Jittered:Q\", title=\"Probability\", scale=alt.Scale(domain=[-0.05, 1.05])),\n        color=alt.Color(\n            \"Class:N\",\n            scale=alt.Scale(domain=[\"Fail\", \"Pass\"], range=[BRAND, SECONDARY]),\n            legend=alt.Legend(\n                title=\"Exam Result\",\n                titleFontSize=20,\n                labelFontSize=18,\n                symbolSize=300,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n            ),\n        ),\n        tooltip=[\"Study Hours\", \"Class\"],\n    )\n)\n\n# Decision threshold line\nthreshold = (\n    alt.Chart(threshold_df)\n    .mark_line(strokeDash=[12, 8], strokeWidth=3, color=INK_SOFT)\n    .encode(x=alt.X(\"Study Hours:Q\"), y=alt.Y(\"Probability:Q\"))\n)\n\n# Threshold label\nthreshold_label = (\n    alt.Chart(pd.DataFrame({\"x\": [9.5], \"y\": [0.54], \"text\": [\"Decision Threshold (p=0.5)\"]}))\n    .mark_text(fontSize=16, color=INK_SOFT, align=\"right\")\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Combine all layers\nchart = (\n    alt.layer(ci_band, curve, threshold, threshold_label, points)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\"logistic-regression · python · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK),\n    )\n    .configure_axis(\n        labelFontSize=18,\n        titleFontSize=22,\n        gridOpacity=0.15,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_title(color=INK)\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}