{"spec_id":"diagnostic-regression-panel","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ndiagnostic-regression-panel: Regression Diagnostic Panel (Four-Plot Display)\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 86/100 | Created: 2026-05-13\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom scipy import stats\nfrom statsmodels.nonparametric.smoothers_lowess import lowess\n\n\nLetsPlot.setup_html()\n\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\"\nGRID_COLOR = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\nBRAND = \"#009E73\"  # Okabe-Ito position 1\nSMOOTHER = \"#C475FD\"  # Okabe-Ito position 2\nREFERENCE = \"#4467A3\"  # Okabe-Ito position 3\nCONTOUR = \"#BD8233\"  # Okabe-Ito position 4\n\nCREDIT = \"diagnostic-regression-panel · letsplot · anyplot.ai\"\n\n# Data: salary prediction regression with heteroscedasticity and influential points\nnp.random.seed(42)\nn = 150\n# x1: years of experience (standardised), x2: education index\nx1 = np.random.normal(0, 1, n)\nx2 = np.random.uniform(-2, 2, n)\nnoise = np.random.normal(0, 0.8 + 0.4 * np.abs(x1), n)\ny = 3.0 + 2.0 * x1 - 1.2 * x2 + noise  # log(Salary / median)\n\n# Inject influential outliers (extreme experience / education combinations)\nx1[[0, 1, 2]] = [4.0, -3.8, 4.5]\nx2[[0, 1, 2]] = [-3.0, 2.8, -2.5]\ny[[0, 1, 2]] = [16.0, -11.0, 19.0]\n\nX = np.column_stack([np.ones(n), x1, x2])\np_params = 3\n\nXtX_inv = np.linalg.inv(X.T @ X)\nbeta = XtX_inv @ X.T @ y\nfitted = X @ beta\nresiduals = y - fitted\nmse = np.sum(residuals**2) / (n - p_params)\n\nH = X @ XtX_inv @ X.T\nleverage = np.diag(H)\nstd_residuals = residuals / np.sqrt(mse * (1 - leverage))\ncooks_d = (std_residuals**2 * leverage) / (p_params * (1 - leverage))\n\ntop3 = np.argsort(cooks_d)[-3:][::-1]\n\n# Shared theme — font sizes at required minimums, panel borders removed\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_border=element_blank(),\n    panel_grid_major=element_line(color=GRID_COLOR, size=0.5),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=20),\n    axis_text=element_text(color=INK_SOFT, size=16),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=24, face=\"bold\"),\n    plot_subtitle=element_text(color=INK_MUTED, size=14),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=16),\n    legend_title=element_text(color=INK, size=18),\n)\n\n# ── Plot 1: Residuals vs Fitted ──────────────────────────────────────────────\nlw1 = lowess(residuals, fitted, frac=0.4, return_sorted=True)\ndf1 = pd.DataFrame({\"fitted\": fitted, \"residuals\": residuals})\ndf1_smooth = pd.DataFrame({\"x\": lw1[:, 0], \"y\": lw1[:, 1]})\ndf1_labels = pd.DataFrame({\"fitted\": fitted[top3], \"residuals\": residuals[top3], \"label\": [str(i) for i in top3]})\n\np1 = (\n    ggplot(df1, aes(\"fitted\", \"residuals\"))\n    + geom_hline(yintercept=0, color=INK_SOFT, size=0.8, linetype=\"dashed\")\n    + geom_point(color=BRAND, size=3, alpha=0.65)\n    + geom_line(data=df1_smooth, mapping=aes(\"x\", \"y\"), color=SMOOTHER, size=1.5)\n    + geom_text(data=df1_labels, mapping=aes(\"fitted\", \"residuals\", label=\"label\"), color=INK, size=12, nudge_y=0.6)\n    + labs(x=\"Fitted log(Salary)\", y=\"Residuals\", title=\"Residuals vs Fitted\")\n    + anyplot_theme\n    + ggsize(800, 450)\n)\n\n# ── Plot 2: Normal Q-Q ───────────────────────────────────────────────────────\nsorted_idx = np.argsort(std_residuals)\nsorted_std = std_residuals[sorted_idx]\ntheoretical_q = stats.norm.ppf(np.linspace(0.5 / n, 1 - 0.5 / n, n))\n\nq25, q75 = np.percentile(sorted_std, [25, 75])\nt25, t75 = stats.norm.ppf([0.25, 0.75])\nslope_qq = (q75 - q25) / (t75 - t25)\nintercept_qq = q25 - slope_qq * t25\nt_range = np.linspace(theoretical_q.min() * 1.1, theoretical_q.max() * 1.1, 80)\ndf2_ref = pd.DataFrame({\"x\": t_range, \"y\": intercept_qq + slope_qq * t_range})\n\nabs_rank = np.argsort(np.abs(sorted_std))[-3:]\ndf2 = pd.DataFrame({\"theoretical\": theoretical_q, \"std_res\": sorted_std})\n\n# Compute staggered label positions to avoid overlap in the top-right cluster\nlab_theo = theoretical_q[abs_rank]\nlab_std = sorted_std[abs_rank]\nlab_labels = [str(sorted_idx[k]) for k in abs_rank]\nsort_order = np.argsort(lab_theo)\nlab_theo = lab_theo[sort_order]\nlab_std = lab_std[sort_order]\nlab_labels = [lab_labels[i] for i in sort_order]\n# Three extreme points: leftmost nudged left, next two separated up/down\nnudge_x = np.array([-0.50, 0.20, 0.20])\nnudge_y = np.array([0.0, 0.65, -0.55])\ndf2_labels = pd.DataFrame({\"lx\": lab_theo + nudge_x, \"ly\": lab_std + nudge_y, \"label\": lab_labels})\n\np2 = (\n    ggplot(df2, aes(\"theoretical\", \"std_res\"))\n    + geom_line(data=df2_ref, mapping=aes(\"x\", \"y\"), color=REFERENCE, size=1.2)\n    + geom_point(color=BRAND, size=3, alpha=0.65)\n    + geom_text(data=df2_labels, mapping=aes(\"lx\", \"ly\", label=\"label\"), color=INK, size=12)\n    + labs(x=\"Theoretical Quantiles\", y=\"Standardized Residuals\", title=\"Normal Q-Q\")\n    + anyplot_theme\n    + ggsize(800, 450)\n)\n\n# ── Plot 3: Scale-Location ───────────────────────────────────────────────────\nsqrt_abs = np.sqrt(np.abs(std_residuals))\nlw3 = lowess(sqrt_abs, fitted, frac=0.4, return_sorted=True)\ndf3 = pd.DataFrame({\"fitted\": fitted, \"sqrt_abs\": sqrt_abs})\ndf3_smooth = pd.DataFrame({\"x\": lw3[:, 0], \"y\": lw3[:, 1]})\ndf3_labels = pd.DataFrame({\"fitted\": fitted[top3], \"sqrt_abs\": sqrt_abs[top3], \"label\": [str(i) for i in top3]})\n\np3 = (\n    ggplot(df3, aes(\"fitted\", \"sqrt_abs\"))\n    + geom_point(color=BRAND, size=3, alpha=0.65)\n    + geom_line(data=df3_smooth, mapping=aes(\"x\", \"y\"), color=SMOOTHER, size=1.5)\n    + geom_text(data=df3_labels, mapping=aes(\"fitted\", \"sqrt_abs\", label=\"label\"), color=INK, size=12, nudge_y=0.07)\n    + labs(x=\"Fitted log(Salary)\", y=\"√|Standardized Residuals|\", title=\"Scale-Location\")\n    + anyplot_theme\n    + ggsize(800, 450)\n)\n\n# ── Plot 4: Residuals vs Leverage ────────────────────────────────────────────\nh_seq = np.linspace(0.001, leverage.max() * 1.25, 400)\nstd_lim = max(abs(std_residuals)) * 1.15\ncontour_rows = []\nfor D in [0.5, 1.0]:\n    r_vals = np.sqrt(D * p_params * (1 - h_seq) / h_seq)\n    for sign, tag in [(1, \"pos\"), (-1, \"neg\")]:\n        mask = r_vals <= std_lim\n        contour_rows.append(pd.DataFrame({\"h\": h_seq[mask], \"r\": sign * r_vals[mask], \"grp\": f\"D={D}_{tag}\"}))\ndf_contours = pd.concat(contour_rows, ignore_index=True)\n\ndf4 = pd.DataFrame({\"leverage\": leverage, \"std_res\": std_residuals})\ndf4_labels = pd.DataFrame({\"leverage\": leverage[top3], \"std_res\": std_residuals[top3], \"label\": [str(i) for i in top3]})\n\np4 = (\n    ggplot(df4, aes(\"leverage\", \"std_res\"))\n    + geom_hline(yintercept=0, color=INK_SOFT, size=0.8, linetype=\"dashed\")\n    + geom_line(data=df_contours, mapping=aes(\"h\", \"r\", group=\"grp\"), color=CONTOUR, size=0.9, linetype=\"dashed\")\n    + geom_point(color=BRAND, size=3, alpha=0.65)\n    + geom_text(data=df4_labels, mapping=aes(\"leverage\", \"std_res\", label=\"label\"), color=INK, size=12, nudge_y=0.35)\n    + labs(x=\"Leverage (Hat Value)\", y=\"Standardized Residuals\", title=\"Residuals vs Leverage\")\n    + anyplot_theme\n    + ggsize(800, 450)\n)\n\n# ── Assemble 2×2 panel with figure-level credit ──────────────────────────────\npanel = (\n    gggrid([p1, p2, p3, p4], ncol=2)\n    + labs(subtitle=CREDIT)\n    + theme(plot_subtitle=element_text(color=INK_MUTED, size=14))\n)\n\nggsave(panel, f\"plot-{THEME}.png\", path=\".\", scale=3)\nggsave(panel, f\"plot-{THEME}.html\", path=\".\")\n"}