Source code for sctrial.stats.diagnostics
from __future__ import annotations
from typing import Any
import numpy as np
from statsmodels.regression.linear_model import RegressionResultsWrapper
from statsmodels.stats.diagnostic import het_breuschpagan
from statsmodels.stats.stattools import jarque_bera
try: # optional plotting
import matplotlib.pyplot as plt # type: ignore[assignment]
except Exception: # pragma: no cover
plt = None # type: ignore[assignment]
[docs]
def check_did_assumptions(
fit: RegressionResultsWrapper,
*,
return_figures: bool = False,
) -> dict[str, Any]:
"""Run lightweight diagnostics for a fitted DiD model.
Parameters
----------
fit
Fitted statsmodels RegressionResultsWrapper.
return_figures
If True, include matplotlib figures for residual diagnostics.
Returns
-------
dict
Dictionary with diagnostic statistics:
- bp_pvalue: Breusch-Pagan p-value for heteroscedasticity
- jb_pvalue: Jarque-Bera p-value for normality
- resid_mean, resid_std
- figures (optional): dict of matplotlib Figures
"""
resid = np.asarray(fit.resid)
exog = fit.model.exog
bp = het_breuschpagan(resid, exog)
jb = jarque_bera(resid)
out: dict[str, Any] = {
"bp_pvalue": float(bp[1]),
"jb_pvalue": float(jb[1]),
"resid_mean": float(np.mean(resid)),
"resid_std": float(np.std(resid, ddof=1)),
}
if return_figures:
if plt is None:
raise ImportError("matplotlib is required for return_figures=True")
figs: dict[str, Any] = {}
fig1, ax1 = plt.subplots(figsize=(4, 3))
ax1.scatter(fit.fittedvalues, resid, alpha=0.5)
ax1.axhline(0, color="black", linewidth=0.8)
ax1.set_xlabel("Fitted values")
ax1.set_ylabel("Residuals")
ax1.set_title("Residuals vs Fitted")
figs["residuals_vs_fitted"] = fig1
fig2, ax2 = plt.subplots(figsize=(4, 3))
ax2.hist(resid, bins=30, color="steelblue", alpha=0.7)
ax2.set_title("Residuals Histogram")
ax2.set_xlabel("Residual")
figs["residuals_hist"] = fig2
out["figures"] = figs
return out