{"spec_id":"diagnostic-regression-panel","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\ndiagnostic-regression-panel: Regression Diagnostic Panel (Four-Plot Display)\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 89/100 | Created: 2026-05-13\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\nfrom scipy import stats\nfrom statsmodels.nonparametric.smoothers_lowess import lowess\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nGRID = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\nBRAND = \"#009E73\"  # Okabe-Ito position 1 — scatter points\nC2 = \"#C475FD\"  # Okabe-Ito position 2 — LOWESS smoother lines\nC4 = \"#BD8233\"  # Okabe-Ito position 4 — Cook's distance contours\n\n# Data — apartment rental price regression with synthetic data\nnp.random.seed(42)\nn = 200\np = 3  # intercept + sqft + bedrooms\n\nsqft = np.random.uniform(400, 3000, n)\nbeds = np.random.uniform(1.0, 6.0, n)\n# Inject a few high-leverage observations (extreme predictor values)\nsqft[185], sqft[186] = 5800.0, 6200.0\nbeds[190], beds[191] = 11.0, 12.5\n\nX = np.column_stack([np.ones(n), sqft, beds])\nnoise = np.random.normal(0, 25, n)\n# Inject high-residual outliers that will inflate Cook's distance\nnoise[15] += 110\nnoise[22] -= 95\nnoise[67] += 70\n\nrent = X @ [200.0, 0.18, 120.0] + noise\n\n# OLS fit\nXtX_inv = np.linalg.inv(X.T @ X)\nbeta = XtX_inv @ X.T @ rent\nfitted = X @ beta\nresid = rent - fitted\n\n# Hat matrix diagonal (leverage) via einsum — avoids storing full n×n matrix\nh = np.einsum(\"ij,jk,ik->i\", X, XtX_inv, X)\nh = np.clip(h, 0.0, 1.0 - 1e-10)\n\n# Internally studentized residuals and Cook's distance\nmse = resid @ resid / (n - p)\nsr = resid / (np.sqrt(mse) * np.sqrt(1.0 - h))\ncooks = sr**2 * h / (p * (1.0 - h))\ntop3 = np.argsort(cooks)[-3:][::-1]\n\n# LOWESS smoothers for subplots 1 and 3\nord_ = np.argsort(fitted)\nlo_r = lowess(resid[ord_], fitted[ord_], frac=0.5, return_sorted=True)\nlo_sl = lowess(np.sqrt(np.abs(sr[ord_])), fitted[ord_], frac=0.5, return_sorted=True)\n\n# Normal Q-Q data\nqq = stats.probplot(sr, dist=\"norm\")\nth_q, sa_q = qq[0][0], qq[0][1]\nslope, intercept = qq[1][0], qq[1][1]\nql_x = np.array([th_q[0], th_q[-1]])\nql_y = slope * ql_x + intercept\n# Rank of each observation for labeling in QQ space\nqq_ord = np.argsort(sr)\nqq_rank = np.empty(n, dtype=int)\nqq_rank[qq_ord] = np.arange(n)\n\n# Cook's distance contour lines for subplot 4\nh_c = np.linspace(0.001, h.max() * 1.5, 400)\n\n\ndef cook_contour(D_level):\n    r = np.sqrt(D_level * p * (1.0 - h_c) / h_c)\n    mask = r < 5.0\n    return h_c[mask], r[mask]\n\n\nhc05, rc05 = cook_contour(0.5)\nhc10, rc10 = cook_contour(1.0)\n\n# Build 2×2 subplot figure\nfig = make_subplots(\n    rows=2,\n    cols=2,\n    subplot_titles=[\"Residuals vs Fitted\", \"Normal Q-Q\", \"Scale-Location\", \"Residuals vs Leverage\"],\n    horizontal_spacing=0.11,\n    vertical_spacing=0.17,\n)\n\nmk = dict(color=BRAND, size=9, opacity=0.70, line=dict(color=PAGE_BG, width=0.5))\n\n# ── Subplot 1: Residuals vs Fitted ────────────────────────────────────────\nfig.add_trace(go.Scatter(x=fitted, y=resid, mode=\"markers\", marker=mk, showlegend=False), row=1, col=1)\nfig.add_trace(\n    go.Scatter(\n        x=[fitted.min(), fitted.max()],\n        y=[0, 0],\n        mode=\"lines\",\n        line=dict(color=INK_SOFT, width=1.5, dash=\"dash\"),\n        showlegend=False,\n    ),\n    row=1,\n    col=1,\n)\nfig.add_trace(\n    go.Scatter(x=lo_r[:, 0], y=lo_r[:, 1], mode=\"lines\", line=dict(color=C2, width=2.5), showlegend=False), row=1, col=1\n)\nfig.add_trace(\n    go.Scatter(\n        x=fitted[top3],\n        y=resid[top3],\n        mode=\"text\",\n        text=[str(i) for i in top3],\n        textposition=\"middle right\",\n        textfont=dict(size=16, color=INK_MUTED),\n        showlegend=False,\n    ),\n    row=1,\n    col=1,\n)\n\n# ── Subplot 2: Normal Q-Q ─────────────────────────────────────────────────\nfig.add_trace(go.Scatter(x=th_q, y=sa_q, mode=\"markers\", marker=mk, showlegend=False), row=1, col=2)\nfig.add_trace(\n    go.Scatter(x=ql_x, y=ql_y, mode=\"lines\", line=dict(color=INK_SOFT, width=1.5, dash=\"dash\"), showlegend=False),\n    row=1,\n    col=2,\n)\nfig.add_trace(\n    go.Scatter(\n        x=th_q[qq_rank[top3]],\n        y=sa_q[qq_rank[top3]],\n        mode=\"text\",\n        text=[str(i) for i in top3],\n        textposition=\"middle right\",\n        textfont=dict(size=16, color=INK_MUTED),\n        showlegend=False,\n    ),\n    row=1,\n    col=2,\n)\n\n# ── Subplot 3: Scale-Location ─────────────────────────────────────────────\nfig.add_trace(go.Scatter(x=fitted, y=np.sqrt(np.abs(sr)), mode=\"markers\", marker=mk, showlegend=False), row=2, col=1)\nfig.add_trace(\n    go.Scatter(x=lo_sl[:, 0], y=lo_sl[:, 1], mode=\"lines\", line=dict(color=C2, width=2.5), showlegend=False),\n    row=2,\n    col=1,\n)\nfig.add_trace(\n    go.Scatter(\n        x=fitted[top3],\n        y=np.sqrt(np.abs(sr[top3])),\n        mode=\"text\",\n        text=[str(i) for i in top3],\n        textposition=\"middle right\",\n        textfont=dict(size=16, color=INK_MUTED),\n        showlegend=False,\n    ),\n    row=2,\n    col=1,\n)\n\n# ── Subplot 4: Residuals vs Leverage ─────────────────────────────────────\nfig.add_trace(go.Scatter(x=h, y=sr, mode=\"markers\", marker=mk, showlegend=False), row=2, col=2)\nfig.add_trace(\n    go.Scatter(\n        x=[0.0, h.max() * 1.5],\n        y=[0, 0],\n        mode=\"lines\",\n        line=dict(color=INK_SOFT, width=1.0, dash=\"dash\"),\n        showlegend=False,\n    ),\n    row=2,\n    col=2,\n)\nif len(hc05):\n    fig.add_trace(\n        go.Scatter(x=hc05, y=rc05, mode=\"lines\", line=dict(color=C4, width=1.5, dash=\"dot\"), showlegend=False),\n        row=2,\n        col=2,\n    )\n    fig.add_trace(\n        go.Scatter(x=hc05, y=-rc05, mode=\"lines\", line=dict(color=C4, width=1.5, dash=\"dot\"), showlegend=False),\n        row=2,\n        col=2,\n    )\nif len(hc10):\n    fig.add_trace(\n        go.Scatter(x=hc10, y=rc10, mode=\"lines\", line=dict(color=C4, width=2.0, dash=\"dot\"), showlegend=False),\n        row=2,\n        col=2,\n    )\n    fig.add_trace(\n        go.Scatter(x=hc10, y=-rc10, mode=\"lines\", line=dict(color=C4, width=2.0, dash=\"dot\"), showlegend=False),\n        row=2,\n        col=2,\n    )\nfig.add_trace(\n    go.Scatter(\n        x=h[top3],\n        y=sr[top3],\n        mode=\"text\",\n        text=[str(i) for i in top3],\n        textposition=\"middle right\",\n        textfont=dict(size=16, color=INK_MUTED),\n        showlegend=False,\n    ),\n    row=2,\n    col=2,\n)\n\n# Global layout\nfig.update_layout(\n    title=dict(\n        text=\"diagnostic-regression-panel · plotly · anyplot.ai\", font=dict(size=28, color=INK), x=0.5, xanchor=\"center\"\n    ),\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font=dict(color=INK, size=18),\n    showlegend=False,\n    margin=dict(t=95, l=70, r=50, b=70),\n)\n\n# Subplot title fonts (make_subplots stores them as layout annotations)\nfig.update_annotations(font=dict(size=20, color=INK))\n\n# Axis styles\nax_kw = dict(\n    title_font=dict(size=22, color=INK),\n    tickfont=dict(size=18, color=INK_SOFT),\n    gridcolor=GRID,\n    linecolor=INK_SOFT,\n    zerolinecolor=GRID,\n    showgrid=True,\n    showline=True,\n    mirror=False,\n)\nfig.update_xaxes(title_text=\"Fitted Values\", row=1, col=1, **ax_kw)\nfig.update_xaxes(title_text=\"Theoretical Quantiles\", row=1, col=2, **ax_kw)\nfig.update_xaxes(title_text=\"Fitted Values\", row=2, col=1, **ax_kw)\nfig.update_xaxes(title_text=\"Leverage\", row=2, col=2, **ax_kw)\nfig.update_yaxes(title_text=\"Residuals\", row=1, col=1, **ax_kw)\nfig.update_yaxes(title_text=\"Standardized Residuals\", row=1, col=2, **ax_kw)\nfig.update_yaxes(title_text=\"√|Standardized Residuals|\", row=2, col=1, **ax_kw)\nfig.update_yaxes(title_text=\"Standardized Residuals\", row=2, col=2, **ax_kw)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=1200, height=1200, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}