{"spec_id":"diagnostic-regression-panel","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ndiagnostic-regression-panel: Regression Diagnostic Panel (Four-Plot Display)\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 83/100 | Created: 2026-05-13\n\"\"\"\n\nimport os\nimport sys\n\n\n# Workaround for import conflict: remove script directory from path\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nif _script_dir in sys.path:\n    sys.path.remove(_script_dir)\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom scipy.stats import probplot\nfrom statsmodels.nonparametric.smoothers_lowess import lowess\nfrom statsmodels.regression.linear_model import OLS\nfrom statsmodels.stats.outliers_influence import OLSInfluence\nfrom statsmodels.tools import add_constant\n\n\n# Theme tokens\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"\nACCENT = \"#C475FD\"\n\n# Data — housing price prediction scenario\nnp.random.seed(42)\nn = 150\nsqft = np.random.uniform(800, 3500, n)\nbedrooms = np.random.randint(1, 6, n).astype(float)\nage_yr = np.random.uniform(0, 50, n)\nX = add_constant(np.column_stack([sqft, bedrooms, age_yr]))\nprice = X @ np.array([50000.0, 120.0, 8000.0, -800.0]) + np.random.normal(0, 25000, n)\n\n# Introduce a few influential outliers\nprice[12] += 180000\nprice[47] -= 150000\nprice[88] += 120000\n\nmodel = OLS(price, X).fit()\ninfluence = OLSInfluence(model)\n\nfitted = np.asarray(model.fittedvalues)\nresiduals = np.asarray(model.resid)\nstd_resid = np.asarray(influence.resid_studentized_internal)\nleverage_vals = np.asarray(influence.hat_matrix_diag)\ncooks_d_vals = np.asarray(influence.cooks_distance[0])\np_params = X.shape[1]\n\ntop3 = set(np.argsort(cooks_d_vals)[-3:])\n\n# LOWESS smoothers for subplot 1 and 3\nsmooth1 = lowess(residuals, fitted, frac=0.4)\nsmooth3 = lowess(np.sqrt(np.abs(std_resid)), fitted, frac=0.4)\n\n# Main DataFrame\ndf_main = pd.DataFrame(\n    {\n        \"fitted\": fitted,\n        \"residuals\": residuals,\n        \"std_resid\": std_resid,\n        \"sqrt_abs_sr\": np.sqrt(np.abs(std_resid)),\n        \"leverage\": leverage_vals,\n        \"cooks_d\": cooks_d_vals,\n        \"obs\": np.arange(n),\n        \"label\": [str(i) if i in top3 else \"\" for i in range(n)],\n    }\n)\n\n# Q-Q plot data\nsort_order = np.argsort(std_resid)\n(theoretical_q, sample_q), (slope, intercept, _) = probplot(std_resid, fit=True)\nextreme_qq_pos = set(np.argsort(np.abs(sample_q))[-3:])\nqq_df = pd.DataFrame(\n    {\n        \"theoretical\": theoretical_q,\n        \"sample\": sample_q,\n        \"label\": [str(sort_order[i]) if i in extreme_qq_pos else \"\" for i in range(n)],\n    }\n)\nqq_ref_df = pd.DataFrame(\n    {\n        \"theoretical\": [theoretical_q[0], theoretical_q[-1]],\n        \"sample\": [intercept + slope * theoretical_q[0], intercept + slope * theoretical_q[-1]],\n    }\n)\n\n# LOWESS DataFrames\nloess1_df = pd.DataFrame({\"fitted\": smooth1[:, 0], \"smoothed\": smooth1[:, 1]})\nloess3_df = pd.DataFrame({\"fitted\": smooth3[:, 0], \"smoothed\": smooth3[:, 1]})\n\n# Cook's distance contour data for subplot 4\nh_vals = np.linspace(0.002, max(leverage_vals) * 1.5, 500)\nsr_max = max(np.abs(std_resid)) * 1.15\ncontour_rows = []\nfor D in [0.5, 1.0]:\n    sr_curve = np.sqrt(D * p_params * (1 - h_vals) / h_vals)\n    for branch, sr_vals in [(\"pos\", sr_curve), (\"neg\", -sr_curve)]:\n        for h, sr in zip(h_vals, sr_vals, strict=True):\n            if abs(sr) <= sr_max:\n                contour_rows.append({\"leverage\": h, \"std_resid\": sr, \"level\": f\"D={D}\", \"branch\": f\"{D}_{branch}\"})\ncontour_df = (\n    pd.DataFrame(contour_rows) if contour_rows else pd.DataFrame(columns=[\"leverage\", \"std_resid\", \"level\", \"branch\"])\n)\n\nzero_df = pd.DataFrame({\"y\": [0.0]})\nW, H = 740, 395\n\n# ── Chart 1: Residuals vs Fitted ──\nc1_pts = (\n    alt.Chart(df_main)\n    .mark_circle(color=BRAND, opacity=0.6, size=70)\n    .encode(\n        x=alt.X(\"fitted:Q\", title=\"Fitted Values\"),\n        y=alt.Y(\"residuals:Q\", title=\"Residuals\"),\n        tooltip=[\"obs:Q\", alt.Tooltip(\"residuals:Q\", format=\".0f\")],\n    )\n)\nc1_ref = alt.Chart(zero_df).mark_rule(color=INK_SOFT, strokeDash=[6, 3], strokeWidth=1.5).encode(y=\"y:Q\")\nc1_loess = alt.Chart(loess1_df).mark_line(color=ACCENT, strokeWidth=2.5).encode(x=\"fitted:Q\", y=\"smoothed:Q\")\nc1_lbl = (\n    alt.Chart(df_main[df_main[\"label\"] != \"\"])\n    .mark_text(color=INK_MUTED, fontSize=14, dx=8, dy=-8)\n    .encode(x=\"fitted:Q\", y=\"residuals:Q\", text=\"label:N\")\n)\nchart1 = (c1_pts + c1_ref + c1_loess + c1_lbl).properties(\n    width=W, height=H, title=alt.TitleParams(text=\"Residuals vs Fitted\", fontSize=20, color=INK, anchor=\"start\")\n)\n\n# ── Chart 2: Normal Q-Q ──\nc2_pts = (\n    alt.Chart(qq_df)\n    .mark_circle(color=BRAND, opacity=0.6, size=70)\n    .encode(\n        x=alt.X(\"theoretical:Q\", title=\"Theoretical Quantiles\"), y=alt.Y(\"sample:Q\", title=\"Standardized Residuals\")\n    )\n)\nc2_ref = (\n    alt.Chart(qq_ref_df)\n    .mark_line(color=INK_SOFT, strokeDash=[6, 3], strokeWidth=1.5)\n    .encode(x=\"theoretical:Q\", y=\"sample:Q\")\n)\nc2_lbl = (\n    alt.Chart(qq_df[qq_df[\"label\"] != \"\"])\n    .mark_text(color=INK_MUTED, fontSize=14, dx=8, dy=-8)\n    .encode(x=\"theoretical:Q\", y=\"sample:Q\", text=\"label:N\")\n)\nchart2 = (c2_pts + c2_ref + c2_lbl).properties(\n    width=W, height=H, title=alt.TitleParams(text=\"Normal Q–Q\", fontSize=20, color=INK, anchor=\"start\")\n)\n\n# ── Chart 3: Scale-Location ──\nc3_pts = (\n    alt.Chart(df_main)\n    .mark_circle(color=BRAND, opacity=0.6, size=70)\n    .encode(\n        x=alt.X(\"fitted:Q\", title=\"Fitted Values\"),\n        y=alt.Y(\"sqrt_abs_sr:Q\", title=\"√|Standardized Residuals|\"),\n        tooltip=[\"obs:Q\", alt.Tooltip(\"sqrt_abs_sr:Q\", format=\".3f\")],\n    )\n)\nc3_loess = alt.Chart(loess3_df).mark_line(color=ACCENT, strokeWidth=2.5).encode(x=\"fitted:Q\", y=\"smoothed:Q\")\nc3_lbl = (\n    alt.Chart(df_main[df_main[\"label\"] != \"\"])\n    .mark_text(color=INK_MUTED, fontSize=14, dx=8, dy=-8)\n    .encode(x=\"fitted:Q\", y=\"sqrt_abs_sr:Q\", text=\"label:N\")\n)\nchart3 = (c3_pts + c3_loess + c3_lbl).properties(\n    width=W, height=H, title=alt.TitleParams(text=\"Scale–Location\", fontSize=20, color=INK, anchor=\"start\")\n)\n\n# ── Chart 4: Residuals vs Leverage ──\nc4_pts = (\n    alt.Chart(df_main)\n    .mark_circle(color=BRAND, opacity=0.6, size=70)\n    .encode(\n        x=alt.X(\"leverage:Q\", title=\"Leverage\"),\n        y=alt.Y(\"std_resid:Q\", title=\"Standardized Residuals\"),\n        tooltip=[\n            \"obs:Q\",\n            alt.Tooltip(\"leverage:Q\", format=\".3f\"),\n            alt.Tooltip(\"cooks_d:Q\", format=\".3f\", title=\"Cook's D\"),\n        ],\n    )\n)\nc4_ref = alt.Chart(zero_df).mark_rule(color=INK_SOFT, strokeDash=[6, 3], strokeWidth=1.5).encode(y=\"y:Q\")\nc4_lbl = (\n    alt.Chart(df_main[df_main[\"label\"] != \"\"])\n    .mark_text(color=INK_MUTED, fontSize=14, dx=8, dy=-8)\n    .encode(x=\"leverage:Q\", y=\"std_resid:Q\", text=\"label:N\")\n)\n\nif len(contour_df) > 0:\n    contour_05_df = contour_df[contour_df[\"level\"] == \"D=0.5\"]\n    contour_10_df = contour_df[contour_df[\"level\"] == \"D=1.0\"]\n    c4_c05 = (\n        alt.Chart(contour_05_df)\n        .mark_line(color=INK_SOFT, strokeWidth=1.5, strokeDash=[6, 3], opacity=0.8)\n        .encode(x=\"leverage:Q\", y=\"std_resid:Q\", detail=\"branch:N\")\n    )\n    c4_c10 = (\n        alt.Chart(contour_10_df)\n        .mark_line(color=INK_MUTED, strokeWidth=1.5, strokeDash=[3, 2], opacity=0.8)\n        .encode(x=\"leverage:Q\", y=\"std_resid:Q\", detail=\"branch:N\")\n    )\n    # Text labels at the end of positive contour branches\n    contour_label_rows = []\n    for D, df_c in [(0.5, contour_05_df), (1.0, contour_10_df)]:\n        pos = df_c[df_c[\"branch\"] == f\"{D}_pos\"]\n        if len(pos) > 0:\n            row = pos.iloc[-1]\n            contour_label_rows.append({\"leverage\": row[\"leverage\"], \"std_resid\": row[\"std_resid\"], \"label\": f\"D={D}\"})\n    if contour_label_rows:\n        c4_clabels = (\n            alt.Chart(pd.DataFrame(contour_label_rows))\n            .mark_text(color=INK_SOFT, fontSize=13, dx=4, dy=-6)\n            .encode(x=\"leverage:Q\", y=\"std_resid:Q\", text=\"label:N\")\n        )\n        chart4_layers = c4_pts + c4_ref + c4_c05 + c4_c10 + c4_clabels + c4_lbl\n    else:\n        chart4_layers = c4_pts + c4_ref + c4_c05 + c4_c10 + c4_lbl\nelse:\n    chart4_layers = c4_pts + c4_ref + c4_lbl\n\nchart4 = chart4_layers.properties(\n    width=W, height=H, title=alt.TitleParams(text=\"Residuals vs Leverage\", fontSize=20, color=INK, anchor=\"start\")\n)\n\n# ── Compose 2×2 panel ──\npanel = (\n    alt.concat(chart1, chart2, chart3, chart4, columns=2)\n    .properties(\n        title=alt.TitleParams(\n            text=\"diagnostic-regression-panel · altair · anyplot.ai\", fontSize=26, color=INK, anchor=\"middle\", offset=14\n        ),\n        background=PAGE_BG,\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0.5)\n    .configure_concat(spacing=50)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=16,\n        titleFontSize=18,\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=14,\n        titleFontSize=15,\n    )\n)\n\npanel.save(f\"plot-{THEME}.png\", scale_factor=3.0)\npanel.save(f\"plot-{THEME}.html\")\n"}