{"spec_id":"diagnostic-regression-panel","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ndiagnostic-regression-panel: Regression Diagnostic Panel (Four-Plot Display)\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 80/100 | Created: 2026-05-13\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove this script's directory from sys.path so \"pygal\" resolves to the installed package\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _this_dir]\n\nimport io\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom PIL import Image\nfrom pygal.style import Style\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_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data — two-predictor regression with mild heteroscedasticity\nnp.random.seed(42)\nn = 120\nx1 = np.random.normal(50, 15, n)\nx2 = np.random.normal(30, 10, n)\nnoise = np.random.normal(0, np.abs(x1) * 0.12, n)\ny = 2.5 + 0.8 * x1 + 1.2 * x2 + noise\n\nX_mat = np.column_stack([np.ones(n), x1, x2])\nbeta = np.linalg.lstsq(X_mat, y, rcond=None)[0]\nfitted = X_mat @ beta\nresiduals = y - fitted\n\nH_hat = X_mat @ np.linalg.inv(X_mat.T @ X_mat) @ X_mat.T\nleverage = np.diag(H_hat)\np_params = 3\nmse = np.sum(residuals**2) / (n - p_params)\nstd_residuals = residuals / np.sqrt(mse * (1 - leverage))\nsqrt_abs_std_res = np.sqrt(np.abs(std_residuals))\ncooks_d = (std_residuals**2 * leverage) / (p_params * (1 - leverage))\n\n# Q-Q theoretical quantiles\nsorted_std_res = np.sort(std_residuals)\nprobs = np.linspace(0.5 / n, 1 - 0.5 / n, n)\ntheoretical_q = stats.norm.ppf(probs)\n\n# LOWESS smoothers\nlowess_res_fit = lowess(residuals, fitted, frac=0.4, return_sorted=True)\nlowess_scale = lowess(sqrt_abs_std_res, fitted, frac=0.4, return_sorted=True)\n\n# Cook's distance contour prep\nh_range = np.linspace(1e-4, float(np.max(leverage)) * 1.3, 400)\ny_clip = float(np.max(np.abs(std_residuals))) * 1.4\ncook_levels = [0.5, 1.0]\n\n\ndef make_style():\n    return Style(\n        background=PAGE_BG,\n        plot_background=PAGE_BG,\n        foreground=INK,\n        foreground_strong=INK,\n        foreground_subtle=INK_MUTED,\n        colors=IMPRINT,\n        title_font_size=26,\n        label_font_size=18,\n        major_label_font_size=16,\n        legend_font_size=14,\n        value_font_size=12,\n        stroke_width=2.5,\n    )\n\n\nW, H_SUB = 2400, 1350\n\n# Chart 1: Residuals vs Fitted\nchart1 = pygal.XY(\n    style=make_style(),\n    width=W,\n    height=H_SUB,\n    title=\"Residuals vs Fitted\",\n    x_title=\"Fitted values\",\n    y_title=\"Residuals\",\n    show_legend=False,\n    dots_size=2.5,\n    stroke=False,\n)\nchart1.add(\"Residuals\", [(float(f), float(r)) for f, r in zip(fitted, residuals, strict=True)])\nchart1.add(\"Zero\", [(float(fitted.min()), 0.0), (float(fitted.max()), 0.0)], stroke=True, dots_size=0)\nchart1.add(\"LOWESS\", [(float(row[0]), float(row[1])) for row in lowess_res_fit], stroke=True, dots_size=0)\n\n# Chart 2: Normal Q-Q\nchart2 = pygal.XY(\n    style=make_style(),\n    width=W,\n    height=H_SUB,\n    title=\"Normal Q-Q\",\n    x_title=\"Theoretical Quantiles\",\n    y_title=\"Standardized Residuals\",\n    show_legend=False,\n    dots_size=2.5,\n    stroke=False,\n)\nchart2.add(\"Q-Q\", [(float(t), float(s)) for t, s in zip(theoretical_q, sorted_std_res, strict=True)])\nref_lo = float(min(theoretical_q.min(), sorted_std_res.min()))\nref_hi = float(max(theoretical_q.max(), sorted_std_res.max()))\nchart2.add(\"45° line\", [(ref_lo, ref_lo), (ref_hi, ref_hi)], stroke=True, dots_size=0)\n\n# Chart 3: Scale-Location\nchart3 = pygal.XY(\n    style=make_style(),\n    width=W,\n    height=H_SUB,\n    title=\"Scale-Location\",\n    x_title=\"Fitted values\",\n    y_title=\"√|Standardized Residuals|\",\n    show_legend=False,\n    dots_size=2.5,\n    stroke=False,\n)\nchart3.add(\"Scale-Loc\", [(float(f), float(s)) for f, s in zip(fitted, sqrt_abs_std_res, strict=True)])\nchart3.add(\"LOWESS\", [(float(row[0]), float(row[1])) for row in lowess_scale], stroke=True, dots_size=0)\n\n# Chart 4: Residuals vs Leverage\nchart4 = pygal.XY(\n    style=make_style(),\n    width=W,\n    height=H_SUB,\n    title=\"Residuals vs Leverage\",\n    x_title=\"Leverage\",\n    y_title=\"Standardized Residuals\",\n    show_legend=False,\n    dots_size=2.5,\n    stroke=False,\n)\nchart4.add(\"Residuals\", [(float(lv), float(sr)) for lv, sr in zip(leverage, std_residuals, strict=True)])\nfor lvl in cook_levels:\n    upper = [\n        (float(h), float(np.sqrt(lvl * p_params * (1 - h) / h)))\n        for h in h_range\n        if np.sqrt(lvl * p_params * (1 - h) / h) <= y_clip\n    ]\n    lower = [\n        (float(h), -float(np.sqrt(lvl * p_params * (1 - h) / h)))\n        for h in h_range\n        if np.sqrt(lvl * p_params * (1 - h) / h) <= y_clip\n    ]\n    if upper:\n        chart4.add(f\"Cook {lvl}+\", upper, stroke=True, dots_size=0)\n    if lower:\n        chart4.add(f\"Cook {lvl}-\", lower, stroke=True, dots_size=0)\n\n# Save HTML — four SVGs in a 2x2 grid\ncharts = [chart1, chart2, chart3, chart4]\nsvg_parts = []\nfor c in charts:\n    raw = c.render()\n    svg_parts.append(raw.decode(\"utf-8\") if isinstance(raw, bytes) else raw)\n\nhtml = f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>diagnostic-regression-panel · pygal · anyplot.ai</title>\n<style>\n  body {{ margin: 0; background: {PAGE_BG}; font-family: sans-serif; }}\n  h1 {{ text-align: center; color: {INK}; font-size: 28px; margin: 20px 0 10px; font-weight: 500; }}\n  .grid {{ display: grid; grid-template-columns: 50% 50%; width: 100%; }}\n  .cell svg {{ width: 100%; height: auto; display: block; }}\n</style>\n</head>\n<body>\n<h1>diagnostic-regression-panel · pygal · anyplot.ai</h1>\n<div class=\"grid\">\n  <div class=\"cell\">{svg_parts[0]}</div>\n  <div class=\"cell\">{svg_parts[1]}</div>\n  <div class=\"cell\">{svg_parts[2]}</div>\n  <div class=\"cell\">{svg_parts[3]}</div>\n</div>\n</body>\n</html>\"\"\"\n\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(html)\n\n\n# Save PNG — render each chart and combine into 2x2 grid\ndef chart_to_pil(chart):\n    svg_bytes = chart.render()\n    png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=W, output_height=H_SUB)\n    return Image.open(io.BytesIO(png_bytes)).convert(\"RGB\")\n\n\nimgs = [chart_to_pil(c) for c in charts]\ncombined = Image.new(\"RGB\", (W * 2, H_SUB * 2))\ncombined.paste(imgs[0], (0, 0))\ncombined.paste(imgs[1], (W, 0))\ncombined.paste(imgs[2], (0, H_SUB))\ncombined.paste(imgs[3], (W, H_SUB))\ncombined.save(f\"plot-{THEME}.png\")\n"}