The consequences of ignoring diagnostics are severe: heteroskedasticity can inflate Type I error rates from 5% to 40%, influential outliers can reverse coefficient signs, and multicollinearity can make estimates unstable. This skill enforces rigorous diagnostic practices that protect against these failures.
FUNDAMENTAL TRUTH:
Statistical software will happily compute invalid estimates. It's YOUR responsibility to verify assumptions hold. No diagnostic → no inference.
@app.cell
def complete_regression_diagnostics(model, df, cluster_var=None):
"""
Comprehensive diagnostics are mandatory for valid inference.
Every assumption violation has specific consequences and remediation.
This function enforces systematic checking and clear reporting.
"""
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.stats.diagnostic import het_breuschpagan, het_white
from statsmodels.stats.outliers_influence import variance_inflation_factor, OLSInfluence
import os
print("=" * 70)
print("COMPREHENSIVE REGRESSION DIAGNOSTICS")
print("=" * 70)
# Extract residuals and fitted values
fitted = model.fittedvalues
residuals = model.resid
standardized_resid = residuals / residuals.std()
# 1. DIAGNOSTIC PLOTS (mandatory visualization)
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
# Residuals vs Fitted (linearity + homoskedasticity)
axes[0, 0].scatter(fitted, residuals, alpha=0.5, s=30)
axes[0, 0].axhline(0, color='red', linestyle='--', linewidth=1.5)
# Add loess smoother to detect patterns
from scipy.interpolate import interp1d
sorted_idx = np.argsort(fitted)
window = max(10, len(fitted) // 20)
smoothed = pd.Series(residuals[sorted_idx]).rolling(window, center=True).mean()
axes[0, 0].plot(fitted[sorted_idx], smoothed, 'g-', linewidth=2, label='Loess smoother')
axes[0, 0].set_xlabel("Fitted Values", fontsize=11)
axes[0, 0].set_ylabel("Residuals", fontsize=11)
axes[0, 0].set_title("Residuals vs Fitted\n(Check: Random scatter around zero)", fontsize=12)
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].legend()
# Q-Q Plot (normality)
stats.probplot(residuals, dist="norm", plot=axes[0, 1])
axes[0, 1].set_title("Normal Q-Q Plot\n(Check: Points follow diagonal)", fontsize=12)
axes[0, 1].grid(True, alpha=0.3)
# Scale-Location (homoskedasticity)
sqrt_abs_resid = np.sqrt(np.abs(standardized_resid))
axes[1, 0].scatter(fitted, sqrt_abs_resid, alpha=0.5, s=30)
# Add trend line
z = np.polyfit(fitted, sqrt_abs_resid, 1)
p = np.poly1d(z)
axes[1, 0].plot(fitted, p(fitted), "r-", linewidth=2, label=f'Trend: slope={z[0]:.3f}')
axes[1, 0].set_xlabel("Fitted Values", fontsize=11)
axes[1, 0].set_ylabel("√|Standardized Residuals|", fontsize=11)
axes[1, 0].set_title("Scale-Location\n(Check: Horizontal band, no trend)", fontsize=12)
axes[1, 0].grid(True, alpha=0.3)
axes[1, 0].legend()
# Residuals vs Leverage (influence)
influence = OLSInfluence(model)
leverage = influence.hat_matrix_diag
cooks_d = influence.cooks_distance[0]
axes[1, 1].scatter(leverage, standardized_resid, alpha=0.5, s=30)
axes[1, 1].axhline(0, color='red', linestyle='--', linewidth=1.5)
# Mark influential points
n = len(residuals)
threshold = 4 / n
influential_mask = cooks_d > threshold
if influential_mask.any():
axes[1, 1].scatter(leverage[influential_mask],
standardized_resid[influential_mask],
color='red', s=100, alpha=0.7,
label=f'Influential (Cook's D > {threshold:.3f})')
# Add contour lines for Cook's distance
x_leverage = np.linspace(0, max(leverage) * 1.1, 50)
for cooks_level in [0.5, 1.0]:
y_resid = np.sqrt(cooks_level * n / (x_leverage * (1 - x_leverage)))
axes[1, 1].plot(x_leverage, y_resid, '--', color='gray', alpha=0.5)
axes[1, 1].plot(x_leverage, -y_resid, '--', color='gray', alpha=0.5)
axes[1, 1].set_xlabel("Leverage", fontsize=11)
axes[1, 1].set_ylabel("Standardized Residuals", fontsize=11)
axes[1, 1].set_title("Residuals vs Leverage\n(Check: No influential outliers)", fontsize=12)
axes[1, 1].grid(True, alpha=0.3)
axes[1, 1].legend()
plt.suptitle("Regression Diagnostic Plots - ALL FOUR REQUIRED", fontsize=14, y=1.02)
plt.tight_layout()
os.makedirs("./images", exist_ok=True)
plt.savefig("./images/regression_diagnostics_complete.png", dpi=144, bbox_inches="tight")
# Save figure reference for inline display
diagnostic_fig = plt.gcf()
# 2. HETEROSKEDASTICITY TESTS (mandatory formal testing)
print("\n" + "=" * 70)
print("HETEROSKEDASTICITY TESTS")
print("-" * 70)
bp_stat, bp_pval, _, _ = het_breuschpagan(residuals, model.model.exog)
white_stat, white_pval, _, _ = het_white(residuals, model.model.exog)
print(f"Breusch-Pagan Test:")
print(f" Statistic: {bp_stat:.4f}, p-value: {bp_pval:.4f}")
print(f" Interpretation: {'VIOLATION - Heteroskedasticity detected' if bp_pval < 0.05 else 'No violation detected'}")
print(f"\nWhite Test:")
print(f" Statistic: {white_stat:.4f}, p-value: {white_pval:.4f}")
print(f" Interpretation: {'VIOLATION - Heteroskedasticity detected' if white_pval < 0.05 else 'No violation detected'}")
if bp_pval < 0.05 or white_pval < 0.05:
print("\nWARNING: REQUIRED ACTION:")
print(" 1. Use robust standard errors: model.get_robustcov_results(cov_type='HC3')")
if cluster_var:
print(f" 2. Or cluster-robust SE: cov_type='cluster', cov_kwds={{'groups': df['{cluster_var}']}}")
print(" 3. Consider variance-stabilizing transformation (log, sqrt)")
print(" 4. Check for omitted variables or incorrect functional form")
# 3. MULTICOLLINEARITY (VIF mandatory for all predictors)
print("\n" + "=" * 70)
print("MULTICOLLINEARITY ASSESSMENT (VIF)")
print("-" * 70)
X = model.model.exog
vif_data = []
for i in range(X.shape[1]):
if model.model.exog_names[i] == 'const':
continue # Skip constant term
vif = variance_inflation_factor(X, i)
vif_data.append({
'Variable': model.model.exog_names[i],
'VIF': vif,
'Status': 'SEVERE' if vif > 10 else 'High' if vif > 5 else 'OK'
})
vif_df = pd.DataFrame(vif_data).sort_values('VIF', ascending=False)
print(vif_df.to_string(index=False))
severe_collinear = vif_df[vif_df['VIF'] > 10]
high_collinear = vif_df[vif_df['VIF'] > 5]
if len(severe_collinear) > 0:
print("\nWARNING: SEVERE MULTICOLLINEARITY DETECTED:")
for _, row in severe_collinear.iterrows():
print(f" {row['Variable']}: VIF = {row['VIF']:.2f}")
print("\nREQUIRED ACTIONS:")
print(" 1. Remove one of the correlated variables")
print(" 2. Combine into index/principal component")
print(" 3. Use ridge regression for regularization")
elif len(high_collinear) > 0:
print("\nWARNING: Moderate multicollinearity detected. Monitor but may be acceptable.")
# 4. INFLUENCE DIAGNOSTICS (identify problematic observations)
print("\n" + "=" * 70)
print("INFLUENTIAL OBSERVATIONS")
print("-" * 70)
n_influential = influential_mask.sum()
high_leverage = leverage > (2 * (X.shape[1]) / n)
n_high_leverage = high_leverage.sum()
print(f"Cook's Distance threshold (4/n): {threshold:.4f}")
print(f"Influential observations: {n_influential} ({100*n_influential/n:.1f}% of data)")
print(f"High leverage observations: {n_high_leverage} ({100*n_high_leverage/n:.1f}% of data)")
if n_influential > 0:
influential_idx = np.where(influential_mask)[0]
print(f"\nInfluential observation indices: {influential_idx[:10]}") # Show first 10
print("\nWARNING: REQUIRED ACTIONS:")
print(" 1. Investigate these observations for data errors")
print(" 2. Check if they represent valid but unusual cases")
print(" 3. Report results with AND without influential points")
print(" 4. Consider robust regression if many influential points")
# 5. NORMALITY TESTS (for valid inference)
print("\n" + "=" * 70)
print("NORMALITY OF RESIDUALS")
print("-" * 70)
shapiro_stat, shapiro_pval = stats.shapiro(residuals[:5000]) # Shapiro-Wilk (limit 5000)
jb_stat, jb_pval = stats.jarque_bera(residuals)
print(f"Shapiro-Wilk Test: p-value = {shapiro_pval:.4f}")
print(f"Jarque-Bera Test: p-value = {jb_pval:.4f}")
if shapiro_pval < 0.05 or jb_pval < 0.05:
print("\nWARNING: Non-normality detected in residuals")
print("NOTES:")
print(" - With n > 30, CLT often ensures valid inference despite non-normality")
print(" - For small samples, consider bootstrap or transformation")
print(" - Check for outliers causing non-normality")
# 6. SUMMARY RECOMMENDATIONS
print("\n" + "=" * 70)
print("DIAGNOSTIC SUMMARY & REQUIRED ACTIONS")
print("=" * 70)
violations = []
if bp_pval < 0.05 or white_pval < 0.05:
violations.append("Heteroskedasticity → Use robust/clustered SE")
if len(severe_collinear) > 0:
violations.append("Severe multicollinearity → Remove/combine variables")
if n_influential > 0:
violations.append("Influential observations → Report sensitivity")
if (shapiro_pval < 0.05 or jb_pval < 0.05) and n < 30:
violations.append("Non-normal residuals (small sample) → Bootstrap/transform")
if violations:
print("WARNING: VIOLATIONS REQUIRING ACTION:")
for i, v in enumerate(violations, 1):
print(f" {i}. {v}")
else:
print("SUCCESS: No major violations detected. Standard inference is valid.")
print("\nDiagnostic plots saved to: ./images/regression_diagnostics_complete.png")
# Return figure for inline display along with diagnostic results
return vif_df, influential_mask, cooks_d, diagnostic_fig,
</code_template>
</implementation_pattern>
<examples>
<example context="clean_regression" difficulty="basic">
<description>Well-behaved regression with no major violations</description>
<code>
```python
@app.cell
def diagnose_clean_model():
#Even with clean data, diagnostics are mandatory.
# We verify assumptions hold before trusting inference.
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
# Simulated clean data for demonstration
np.random.seed(42)
n = 500
df = pd.DataFrame({
'x1': np.random.normal(0, 1, n),
'x2': np.random.normal(0, 1, n),
'x3': np.random.normal(0, 1, n),
})
# Well-specified model with homoskedastic errors
df['y'] = 2 + 3*df['x1'] - 1.5*df['x2'] + 0.8*df['x3'] + np.random.normal(0, 2, n)
# Estimate model
model = smf.ols('y ~ x1 + x2 + x3', data=df).fit()
print("MODEL SUMMARY")
print("=" * 70)
print(model.summary())
# Run complete diagnostics
vif_df, influential, cooks = complete_regression_diagnostics(model, df)
# INTERPRETATION
print("\n" + "=" * 70)
print("INTERPRETATION FOR CLEAN MODEL")
print("=" * 70)
print("""
This is an example of a well-behaved regression:
1. Residuals vs Fitted: Random scatter around zero [OK]
→ Linear specification is appropriate
2. Q-Q Plot: Points follow diagonal line [OK]
→ Residuals are approximately normal
3. Scale-Location: Horizontal band, no trend [OK]
→ Variance is constant (homoskedastic)
4. Residuals vs Leverage: No points outside Cook's distance contours [OK]
→ No influential outliers distorting results
5. VIF values all < 5 [OK]
→ No problematic multicollinearity
6. Heteroskedasticity tests p > 0.05 [OK]
→ Can use standard errors as computed
CONCLUSION: Standard inference is valid. Report results as-is.
""")
return model, vif_df,
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
np.random.seed(123)
n = 500
# Generate data with heteroskedastic errors
df = pd.DataFrame({
'income': np.random.lognormal(10, 1, n), # Log-normal income
'education': np.random.normal(12, 3, n),
'experience': np.random.uniform(0, 40, n),
})
# Error variance increases with income (heteroskedasticity)
error_sd = 0.1 * df['income'] # Variance proportional to income
df['consumption'] = (
1000 +
0.7 * df['income'] +
50 * df['education'] +
20 * df['experience'] +
np.random.normal(0, error_sd)
)
# Naive model (ignoring heteroskedasticity)
model_naive = smf.ols('consumption ~ income + education + experience', data=df).fit()
print("NAIVE MODEL (Standard Errors)")
print("=" * 70)
print(model_naive.summary())
# Run diagnostics - will detect heteroskedasticity
vif_df, influential, cooks = complete_regression_diagnostics(model_naive, df)
# CORRECTED MODEL with robust standard errors
print("\n" + "=" * 70)
print("CORRECTED MODEL (HC3 Robust Standard Errors)")
print("=" * 70)
# HC3 is preferred for finite samples (MacKinnon & White 1985)
model_robust = model_naive.get_robustcov_results(cov_type='HC3')
print(model_robust.summary())
# Compare standard errors
comparison = pd.DataFrame({
'Variable': model_naive.params.index,
'Coef': model_naive.params.values,
'SE (Naive)': model_naive.bse.values,
'SE (Robust)': model_robust.bse.values,
'SE Ratio': model_robust.bse.values / model_naive.bse.values,
'p-val (Naive)': model_naive.pvalues.values,
'p-val (Robust)': model_robust.pvalues.values,
})
print("\n" + "=" * 70)
print("STANDARD ERROR COMPARISON")
print("=" * 70)
print(comparison.to_string(index=False))
print("\n" + "=" * 70)
print("KEY INSIGHTS")
print("=" * 70)
print("""
1. Heteroskedasticity detected (p < 0.001) in both tests
2. Robust SEs are larger for variables correlated with variance
3. Some coefficients lose significance with correct SEs
4. Ignoring heteroskedasticity gave false precision
LESSON: Always test for heteroskedasticity. When detected,
robust standard errors are MANDATORY for valid inference.
""")
return model_robust, comparison,
</code>
<best_practice>
ALWAYS compare naive vs robust standard errors when heteroskedasticity is detected. The differences can be substantial and change conclusions about statistical significance.
</best_practice>
</example>
<example context="influential_outliers" difficulty="advanced">
<description>Regression dominated by influential outliers requiring sensitivity analysis</description>
<code>
```python
@app.cell
def diagnose_influential_outliers():
#Influential observations can completely dominate results.
# We identify them, assess impact, and report sensitivity.
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
import matplotlib.pyplot as plt
import os
np.random.seed(789)
n = 200
# Generate mostly normal data
df = pd.DataFrame({
'x': np.random.normal(50, 10, n),
'z': np.random.normal(0, 1, n),
})
df['y'] = 10 + 2*df['x'] + 5*df['z'] + np.random.normal(0, 10, n)
# Add influential outliers
outliers = pd.DataFrame({
'x': [100, 105, 110], # High leverage points
'z': [0, 0, 0],
'y': [50, 45, 40], # Don't follow pattern
})
df = pd.concat([df, outliers], ignore_index=True)
# Full model (with outliers)
model_full = smf.ols('y ~ x + z', data=df).fit()
print("FULL MODEL (Including Outliers)")
print("=" * 70)
print(model_full.summary())
# Run diagnostics - will identify influential points
vif_df, influential_mask, cooks_d = complete_regression_diagnostics(model_full, df)
# Model without influential observations
print("\n" + "=" * 70)
print("ROBUST MODEL (Excluding Influential Points)")
print("=" * 70)
df_clean = df[~influential_mask].copy()
model_robust = smf.ols('y ~ x + z', data=df_clean).fit()
print(model_robust.summary())
# Sensitivity comparison
print("\n" + "=" * 70)
print("SENSITIVITY ANALYSIS")
print("=" * 70)
comparison = pd.DataFrame({
'Model': ['With Outliers', 'Without Outliers'],
'n': [len(df), len(df_clean)],
'β_x': [model_full.params['x'], model_robust.params['x']],
'SE_x': [model_full.bse['x'], model_robust.bse['x']],
'p_x': [model_full.pvalues['x'], model_robust.pvalues['x']],
'R²': [model_full.rsquared, model_robust.rsquared],
})
print(comparison.to_string(index=False))
# Visualize influence
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Plot 1: Data with regression lines
axes[0].scatter(df[~influential_mask]['x'],
df[~influential_mask]['y'],
alpha=0.5, label='Normal observations')
axes[0].scatter(df[influential_mask]['x'],
df[influential_mask]['y'],
color='red', s=100, label='Influential outliers')
x_range = np.linspace(df['x'].min(), df['x'].max(), 100)
axes[0].plot(x_range,
model_full.params['const'] + model_full.params['x']*x_range,
'r--', label='With outliers', linewidth=2)
axes[0].plot(x_range,
model_robust.params['const'] + model_robust.params['x']*x_range,
'g-', label='Without outliers', linewidth=2)
axes[0].set_xlabel("X")
axes[0].set_ylabel("Y")
axes[0].set_title("Outlier Impact on Regression Line")
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Plot 2: Cook's distance
axes[1].stem(range(len(cooks_d)), cooks_d, markerfmt=",", basefmt=" ")
axes[1].axhline(4/len(df), color='red', linestyle='--',
label=f'Threshold = {4/len(df):.3f}')
axes[1].set_xlabel("Observation Index")
axes[1].set_ylabel("Cook's Distance")
axes[1].set_title("Influence Measure (Cook's D)")
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
os.makedirs("./images", exist_ok=True)
plt.savefig("./images/outlier_influence_analysis.png", dpi=144, bbox_inches="tight")
# Save figure reference for inline display
influence_fig = plt.gcf()
print("\n" + "=" * 70)
print("CRITICAL FINDINGS")
print("=" * 70)
print(f"""
1. {influential_mask.sum()} influential observations detected
2. Coefficient on x changes by {abs(model_full.params['x'] - model_robust.params['x']):.3f}
3. R² drops from {model_full.rsquared:.3f} to {model_robust.rsquared:.3f}
4. Statistical significance {'changes' if (model_full.pvalues['x'] < 0.05) != (model_robust.pvalues['x'] < 0.05) else 'unchanged'}
REQUIRED REPORTING:
- Present BOTH models (with and without influential points)
- Investigate influential observations for data quality
- If outliers are valid, consider robust regression methods
- State clearly which results are sensitive to outliers
This demonstrates why influence diagnostics are MANDATORY.
""")
# Return figure for inline display along with models
return model_full, model_robust, influential_mask, influence_fig,
Q-Q Plot:
- GOOD: Points follow diagonal reference line
- BAD: S-shapes indicate skewness, heavy tails indicate outliers
Scale-Location:
- GOOD: Horizontal band with no trend
- BAD: Increasing/decreasing spread indicates heteroskedasticity
Residuals vs Leverage:
- GOOD: Points within Cook's distance contours
- BAD: Points in top-right or bottom-right corners are influential
Multicollinearity detected (VIF > 10)?
→ YES: Must drop variables or use regularization
→ NO but VIF > 5: Monitor, may be acceptable
Influential outliers detected?
→ YES: Report results with AND without
→ NO: Single model sufficient
Non-normal residuals?
→ n > 30: Usually OK (CLT)
→ n < 30: Consider bootstrap or transformation
1---2name: regression-diagnostics3description: Comprehensive regression model diagnostics and assumption checking. Use when validating regression models, checking assumptions (linearity, homoskedasticity, normality, independence), detecting outliers/influence, testing multicollinearity, or when user mentions residuals, heteroskedasticity, VIF, Cook's distance, or diagnostic plots.4---56<skill_content>78<overview>9Regression diagnostics are NOT optional add-ons—they are fundamental to valid inference. A regression model is a hypothesis about the data-generating process, and diagnostics test whether that hypothesis holds. Without diagnostics, you're flying blind: your p-values may be wrong, your confidence intervals misleading, and your conclusions invalid. Every regression analysis MUST include diagnostics.1011The consequences of ignoring diagnostics are severe: heteroskedasticity can inflate Type I error rates from 5% to 40%, influential outliers can reverse coefficient signs, and multicollinearity can make estimates unstable. This skill enforces rigorous diagnostic practices that protect against these failures.12</overview>1314<philosophy>15<core_principle>16"All models are wrong, but some are useful" - George Box. Diagnostics tell us HOW wrong our model is and whether it's still useful despite its wrongness.1718FUNDAMENTAL TRUTH:19Statistical software will happily compute invalid estimates. It's YOUR responsibility to verify assumptions hold. No diagnostic → no inference.20</core_principle>21</philosophy>2223<mandatory_requirements>2425<requirement priority="critical">26 <name>Complete Diagnostic Suite</name>27 <description>MUST generate all four core diagnostic plots: Residuals vs Fitted, Q-Q plot, Scale-Location, Residuals vs Leverage</description>28 <rationale>Each plot tests different assumptions. Missing any plot means missing potential violations (Belsley et al. 1980)</rationale>29 <consequence>Undetected violations lead to invalid inference, wrong p-values, misleading conclusions</consequence>30</requirement>3132<requirement priority="critical">33 <name>Heteroskedasticity Testing</name>34 <description>MUST run both Breusch-Pagan and White tests, use robust SE if p < 0.05</description>35 <rationale>MacKinnon & White (1985) show HC3 robust SE correct size distortions better than classical SE under heteroskedasticity</rationale>36 <consequence>Type I error rates can reach 40% instead of nominal 5% with uncorrected heteroskedasticity</consequence>37</requirement>3839<requirement priority="critical">40 <name>Multicollinearity Assessment</name>41 <description>MUST compute VIF for all predictors, flag VIF > 10, recommend action if VIF > 5</description>42 <rationale>Kutner et al. (2004) demonstrate VIF > 10 indicates serious multicollinearity requiring intervention</rationale>43 <consequence>Unstable estimates, inflated standard errors, sign reversals with minor data changes</consequence>44</requirement>4546<requirement priority="high">47 <name>Influence Diagnostics</name>48 <description>MUST compute Cook's distance, identify points > 4/n threshold, investigate high-leverage observations</description>49 <rationale>Cook & Weisberg (1982) show single influential points can dominate entire regression</rationale>50 <consequence>Results driven by outliers rather than general patterns, non-robust conclusions</consequence>51</requirement>5253<requirement priority="high">54 <name>Actionable Recommendations</name>55 <description>For EVERY violation detected, provide specific remediation (e.g., "use robust SE", "log transform", "remove variable X")</description>56 <rationale>Diagnostics without remediation are useless—practitioners need actionable guidance</rationale>57 <consequence>Known problems persist, analysis remains flawed despite awareness</consequence>58</requirement>5960</mandatory_requirements>6162<thinking_process>63When running regression diagnostics:641. Generate diagnostic plots FIRST (visual inspection reveals patterns)652. Run formal tests for each assumption663. Compute influence measures for all observations674. Check multicollinearity among predictors685. Document ALL violations found696. Provide specific remediation for each violation707. Re-run diagnostics after any remediation718. Report sensitivity to different specifications72</thinking_process>7374<implementation_pattern>7576<code_template>77```python78# CRITICAL: Complete regression diagnostics template7980@app.cell81def complete_regression_diagnostics(model, df, cluster_var=None):82 """83 Comprehensive diagnostics are mandatory for valid inference.84 Every assumption violation has specific consequences and remediation.85 This function enforces systematic checking and clear reporting.86 """87 import matplotlib.pyplot as plt88 import numpy as np89 import pandas as pd90 from scipy import stats91 from statsmodels.stats.diagnostic import het_breuschpagan, het_white92 from statsmodels.stats.outliers_influence import variance_inflation_factor, OLSInfluence93 import os9495 print("=" * 70)96 print("COMPREHENSIVE REGRESSION DIAGNOSTICS")97 print("=" * 70)9899 # Extract residuals and fitted values100 fitted = model.fittedvalues101 residuals = model.resid102 standardized_resid = residuals / residuals.std()103104 # 1. DIAGNOSTIC PLOTS (mandatory visualization)105 fig, axes = plt.subplots(2, 2, figsize=(14, 12))106107 # Residuals vs Fitted (linearity + homoskedasticity)108 axes[0, 0].scatter(fitted, residuals, alpha=0.5, s=30)109 axes[0, 0].axhline(0, color='red', linestyle='--', linewidth=1.5)110111 # Add loess smoother to detect patterns112 from scipy.interpolate import interp1d113 sorted_idx = np.argsort(fitted)114 window = max(10, len(fitted) // 20)115 smoothed = pd.Series(residuals[sorted_idx]).rolling(window, center=True).mean()116 axes[0, 0].plot(fitted[sorted_idx], smoothed, 'g-', linewidth=2, label='Loess smoother')117118 axes[0, 0].set_xlabel("Fitted Values", fontsize=11)119 axes[0, 0].set_ylabel("Residuals", fontsize=11)120 axes[0, 0].set_title("Residuals vs Fitted\n(Check: Random scatter around zero)", fontsize=12)121 axes[0, 0].grid(True, alpha=0.3)122 axes[0, 0].legend()123124 # Q-Q Plot (normality)125 stats.probplot(residuals, dist="norm", plot=axes[0, 1])126 axes[0, 1].set_title("Normal Q-Q Plot\n(Check: Points follow diagonal)", fontsize=12)127 axes[0, 1].grid(True, alpha=0.3)128129 # Scale-Location (homoskedasticity)130 sqrt_abs_resid = np.sqrt(np.abs(standardized_resid))131 axes[1, 0].scatter(fitted, sqrt_abs_resid, alpha=0.5, s=30)132133 # Add trend line134 z = np.polyfit(fitted, sqrt_abs_resid, 1)135 p = np.poly1d(z)136 axes[1, 0].plot(fitted, p(fitted), "r-", linewidth=2, label=f'Trend: slope={z[0]:.3f}')137138 axes[1, 0].set_xlabel("Fitted Values", fontsize=11)139 axes[1, 0].set_ylabel("√|Standardized Residuals|", fontsize=11)140 axes[1, 0].set_title("Scale-Location\n(Check: Horizontal band, no trend)", fontsize=12)141 axes[1, 0].grid(True, alpha=0.3)142 axes[1, 0].legend()143144 # Residuals vs Leverage (influence)145 influence = OLSInfluence(model)146 leverage = influence.hat_matrix_diag147 cooks_d = influence.cooks_distance[0]148149 axes[1, 1].scatter(leverage, standardized_resid, alpha=0.5, s=30)150 axes[1, 1].axhline(0, color='red', linestyle='--', linewidth=1.5)151152 # Mark influential points153 n = len(residuals)154 threshold = 4 / n155 influential_mask = cooks_d > threshold156 if influential_mask.any():157 axes[1, 1].scatter(leverage[influential_mask],158 standardized_resid[influential_mask],159 color='red', s=100, alpha=0.7,160 label=f'Influential (Cook's D > {threshold:.3f})')161162 # Add contour lines for Cook's distance163 x_leverage = np.linspace(0, max(leverage) * 1.1, 50)164 for cooks_level in [0.5, 1.0]:165 y_resid = np.sqrt(cooks_level * n / (x_leverage * (1 - x_leverage)))166 axes[1, 1].plot(x_leverage, y_resid, '--', color='gray', alpha=0.5)167 axes[1, 1].plot(x_leverage, -y_resid, '--', color='gray', alpha=0.5)168169 axes[1, 1].set_xlabel("Leverage", fontsize=11)170 axes[1, 1].set_ylabel("Standardized Residuals", fontsize=11)171 axes[1, 1].set_title("Residuals vs Leverage\n(Check: No influential outliers)", fontsize=12)172 axes[1, 1].grid(True, alpha=0.3)173 axes[1, 1].legend()174175 plt.suptitle("Regression Diagnostic Plots - ALL FOUR REQUIRED", fontsize=14, y=1.02)176 plt.tight_layout()177178 os.makedirs("./images", exist_ok=True)179 plt.savefig("./images/regression_diagnostics_complete.png", dpi=144, bbox_inches="tight")180181 # Save figure reference for inline display182 diagnostic_fig = plt.gcf()183184 # 2. HETEROSKEDASTICITY TESTS (mandatory formal testing)185 print("\n" + "=" * 70)186 print("HETEROSKEDASTICITY TESTS")187 print("-" * 70)188189 bp_stat, bp_pval, _, _ = het_breuschpagan(residuals, model.model.exog)190 white_stat, white_pval, _, _ = het_white(residuals, model.model.exog)191192 print(f"Breusch-Pagan Test:")193 print(f" Statistic: {bp_stat:.4f}, p-value: {bp_pval:.4f}")194 print(f" Interpretation: {'VIOLATION - Heteroskedasticity detected' if bp_pval < 0.05 else 'No violation detected'}")195196 print(f"\nWhite Test:")197 print(f" Statistic: {white_stat:.4f}, p-value: {white_pval:.4f}")198 print(f" Interpretation: {'VIOLATION - Heteroskedasticity detected' if white_pval < 0.05 else 'No violation detected'}")199200 if bp_pval < 0.05 or white_pval < 0.05:201 print("\nWARNING: REQUIRED ACTION:")202 print(" 1. Use robust standard errors: model.get_robustcov_results(cov_type='HC3')")203 if cluster_var:204 print(f" 2. Or cluster-robust SE: cov_type='cluster', cov_kwds={{'groups': df['{cluster_var}']}}")205 print(" 3. Consider variance-stabilizing transformation (log, sqrt)")206 print(" 4. Check for omitted variables or incorrect functional form")207208 # 3. MULTICOLLINEARITY (VIF mandatory for all predictors)209 print("\n" + "=" * 70)210 print("MULTICOLLINEARITY ASSESSMENT (VIF)")211 print("-" * 70)212213 X = model.model.exog214 vif_data = []215216 for i in range(X.shape[1]):217 if model.model.exog_names[i] == 'const':218 continue # Skip constant term219 vif = variance_inflation_factor(X, i)220 vif_data.append({221 'Variable': model.model.exog_names[i],222 'VIF': vif,223 'Status': 'SEVERE' if vif > 10 else 'High' if vif > 5 else 'OK'224 })225226 vif_df = pd.DataFrame(vif_data).sort_values('VIF', ascending=False)227 print(vif_df.to_string(index=False))228229 severe_collinear = vif_df[vif_df['VIF'] > 10]230 high_collinear = vif_df[vif_df['VIF'] > 5]231232 if len(severe_collinear) > 0:233 print("\nWARNING: SEVERE MULTICOLLINEARITY DETECTED:")234 for _, row in severe_collinear.iterrows():235 print(f" {row['Variable']}: VIF = {row['VIF']:.2f}")236 print("\nREQUIRED ACTIONS:")237 print(" 1. Remove one of the correlated variables")238 print(" 2. Combine into index/principal component")239 print(" 3. Use ridge regression for regularization")240 elif len(high_collinear) > 0:241 print("\nWARNING: Moderate multicollinearity detected. Monitor but may be acceptable.")242243 # 4. INFLUENCE DIAGNOSTICS (identify problematic observations)244 print("\n" + "=" * 70)245 print("INFLUENTIAL OBSERVATIONS")246 print("-" * 70)247248 n_influential = influential_mask.sum()249 high_leverage = leverage > (2 * (X.shape[1]) / n)250 n_high_leverage = high_leverage.sum()251252 print(f"Cook's Distance threshold (4/n): {threshold:.4f}")253 print(f"Influential observations: {n_influential} ({100*n_influential/n:.1f}% of data)")254 print(f"High leverage observations: {n_high_leverage} ({100*n_high_leverage/n:.1f}% of data)")255256 if n_influential > 0:257 influential_idx = np.where(influential_mask)[0]258 print(f"\nInfluential observation indices: {influential_idx[:10]}") # Show first 10259 print("\nWARNING: REQUIRED ACTIONS:")260 print(" 1. Investigate these observations for data errors")261 print(" 2. Check if they represent valid but unusual cases")262 print(" 3. Report results with AND without influential points")263 print(" 4. Consider robust regression if many influential points")264265 # 5. NORMALITY TESTS (for valid inference)266 print("\n" + "=" * 70)267 print("NORMALITY OF RESIDUALS")268 print("-" * 70)269270 shapiro_stat, shapiro_pval = stats.shapiro(residuals[:5000]) # Shapiro-Wilk (limit 5000)271 jb_stat, jb_pval = stats.jarque_bera(residuals)272273 print(f"Shapiro-Wilk Test: p-value = {shapiro_pval:.4f}")274 print(f"Jarque-Bera Test: p-value = {jb_pval:.4f}")275276 if shapiro_pval < 0.05 or jb_pval < 0.05:277 print("\nWARNING: Non-normality detected in residuals")278 print("NOTES:")279 print(" - With n > 30, CLT often ensures valid inference despite non-normality")280 print(" - For small samples, consider bootstrap or transformation")281 print(" - Check for outliers causing non-normality")282283 # 6. SUMMARY RECOMMENDATIONS284 print("\n" + "=" * 70)285 print("DIAGNOSTIC SUMMARY & REQUIRED ACTIONS")286 print("=" * 70)287288 violations = []289 if bp_pval < 0.05 or white_pval < 0.05:290 violations.append("Heteroskedasticity → Use robust/clustered SE")291 if len(severe_collinear) > 0:292 violations.append("Severe multicollinearity → Remove/combine variables")293 if n_influential > 0:294 violations.append("Influential observations → Report sensitivity")295 if (shapiro_pval < 0.05 or jb_pval < 0.05) and n < 30:296 violations.append("Non-normal residuals (small sample) → Bootstrap/transform")297298 if violations:299 print("WARNING: VIOLATIONS REQUIRING ACTION:")300 for i, v in enumerate(violations, 1):301 print(f" {i}. {v}")302 else:303 print("SUCCESS: No major violations detected. Standard inference is valid.")304305 print("\nDiagnostic plots saved to: ./images/regression_diagnostics_complete.png")306307 # Return figure for inline display along with diagnostic results308 return vif_df, influential_mask, cooks_d, diagnostic_fig,309```310</code_template>311312</implementation_pattern>313314<examples>315316<example context="clean_regression" difficulty="basic">317<description>Well-behaved regression with no major violations</description>318<code>319```python320@app.cell321def diagnose_clean_model():322 #Even with clean data, diagnostics are mandatory.323 # We verify assumptions hold before trusting inference.324325 import pandas as pd326 import numpy as np327 import statsmodels.formula.api as smf328329 # Simulated clean data for demonstration330 np.random.seed(42)331 n = 500332 df = pd.DataFrame({333 'x1': np.random.normal(0, 1, n),334 'x2': np.random.normal(0, 1, n),335 'x3': np.random.normal(0, 1, n),336 })337338 # Well-specified model with homoskedastic errors339 df['y'] = 2 + 3*df['x1'] - 1.5*df['x2'] + 0.8*df['x3'] + np.random.normal(0, 2, n)340341 # Estimate model342 model = smf.ols('y ~ x1 + x2 + x3', data=df).fit()343344 print("MODEL SUMMARY")345 print("=" * 70)346 print(model.summary())347348 # Run complete diagnostics349 vif_df, influential, cooks = complete_regression_diagnostics(model, df)350351 # INTERPRETATION352 print("\n" + "=" * 70)353 print("INTERPRETATION FOR CLEAN MODEL")354 print("=" * 70)355 print("""356 This is an example of a well-behaved regression:357358 1. Residuals vs Fitted: Random scatter around zero [OK]359 → Linear specification is appropriate360361 2. Q-Q Plot: Points follow diagonal line [OK]362 → Residuals are approximately normal363364 3. Scale-Location: Horizontal band, no trend [OK]365 → Variance is constant (homoskedastic)366367 4. Residuals vs Leverage: No points outside Cook's distance contours [OK]368 → No influential outliers distorting results369370 5. VIF values all < 5 [OK]371 → No problematic multicollinearity372373 6. Heteroskedasticity tests p > 0.05 [OK]374 → Can use standard errors as computed375376 CONCLUSION: Standard inference is valid. Report results as-is.377 """)378379 return model, vif_df,380```381</code>382<lesson>383Even with "clean" data, running diagnostics is non-negotiable. This example shows what good diagnostics look like—use this as a reference for comparison.384</lesson>385</example>386387<example context="heteroskedastic_regression" difficulty="intermediate">388<description>Regression with heteroskedasticity requiring robust standard errors</description>389<code>390```python391@app.cell392def diagnose_heteroskedastic_model():393 #Heteroskedasticity is common in cross-sectional data.394 # We detect it, apply robust SE, and show the difference in inference.395396 import pandas as pd397 import numpy as np398 import statsmodels.formula.api as smf399400 np.random.seed(123)401 n = 500402403 # Generate data with heteroskedastic errors404 df = pd.DataFrame({405 'income': np.random.lognormal(10, 1, n), # Log-normal income406 'education': np.random.normal(12, 3, n),407 'experience': np.random.uniform(0, 40, n),408 })409410 # Error variance increases with income (heteroskedasticity)411 error_sd = 0.1 * df['income'] # Variance proportional to income412 df['consumption'] = (413 1000 +414 0.7 * df['income'] +415 50 * df['education'] +416 20 * df['experience'] +417 np.random.normal(0, error_sd)418 )419420 # Naive model (ignoring heteroskedasticity)421 model_naive = smf.ols('consumption ~ income + education + experience', data=df).fit()422423 print("NAIVE MODEL (Standard Errors)")424 print("=" * 70)425 print(model_naive.summary())426427 # Run diagnostics - will detect heteroskedasticity428 vif_df, influential, cooks = complete_regression_diagnostics(model_naive, df)429430 # CORRECTED MODEL with robust standard errors431 print("\n" + "=" * 70)432 print("CORRECTED MODEL (HC3 Robust Standard Errors)")433 print("=" * 70)434435 # HC3 is preferred for finite samples (MacKinnon & White 1985)436 model_robust = model_naive.get_robustcov_results(cov_type='HC3')437 print(model_robust.summary())438439 # Compare standard errors440 comparison = pd.DataFrame({441 'Variable': model_naive.params.index,442 'Coef': model_naive.params.values,443 'SE (Naive)': model_naive.bse.values,444 'SE (Robust)': model_robust.bse.values,445 'SE Ratio': model_robust.bse.values / model_naive.bse.values,446 'p-val (Naive)': model_naive.pvalues.values,447 'p-val (Robust)': model_robust.pvalues.values,448 })449450 print("\n" + "=" * 70)451 print("STANDARD ERROR COMPARISON")452 print("=" * 70)453 print(comparison.to_string(index=False))454455 print("\n" + "=" * 70)456 print("KEY INSIGHTS")457 print("=" * 70)458 print("""459 1. Heteroskedasticity detected (p < 0.001) in both tests460 2. Robust SEs are larger for variables correlated with variance461 3. Some coefficients lose significance with correct SEs462 4. Ignoring heteroskedasticity gave false precision463464 LESSON: Always test for heteroskedasticity. When detected,465 robust standard errors are MANDATORY for valid inference.466 """)467468 return model_robust, comparison,469```470</code>471<best_practice>472ALWAYS compare naive vs robust standard errors when heteroskedasticity is detected. The differences can be substantial and change conclusions about statistical significance.473</best_practice>474</example>475476<example context="influential_outliers" difficulty="advanced">477<description>Regression dominated by influential outliers requiring sensitivity analysis</description>478<code>479```python480@app.cell481def diagnose_influential_outliers():482 #Influential observations can completely dominate results.483 # We identify them, assess impact, and report sensitivity.484485 import pandas as pd486 import numpy as np487 import statsmodels.formula.api as smf488 import matplotlib.pyplot as plt489 import os490491 np.random.seed(789)492 n = 200493494 # Generate mostly normal data495 df = pd.DataFrame({496 'x': np.random.normal(50, 10, n),497 'z': np.random.normal(0, 1, n),498 })499 df['y'] = 10 + 2*df['x'] + 5*df['z'] + np.random.normal(0, 10, n)500501 # Add influential outliers502 outliers = pd.DataFrame({503 'x': [100, 105, 110], # High leverage points504 'z': [0, 0, 0],505 'y': [50, 45, 40], # Don't follow pattern506 })507 df = pd.concat([df, outliers], ignore_index=True)508509 # Full model (with outliers)510 model_full = smf.ols('y ~ x + z', data=df).fit()511512 print("FULL MODEL (Including Outliers)")513 print("=" * 70)514 print(model_full.summary())515516 # Run diagnostics - will identify influential points517 vif_df, influential_mask, cooks_d = complete_regression_diagnostics(model_full, df)518519 # Model without influential observations520 print("\n" + "=" * 70)521 print("ROBUST MODEL (Excluding Influential Points)")522 print("=" * 70)523524 df_clean = df[~influential_mask].copy()525 model_robust = smf.ols('y ~ x + z', data=df_clean).fit()526 print(model_robust.summary())527528 # Sensitivity comparison529 print("\n" + "=" * 70)530 print("SENSITIVITY ANALYSIS")531 print("=" * 70)532533 comparison = pd.DataFrame({534 'Model': ['With Outliers', 'Without Outliers'],535 'n': [len(df), len(df_clean)],536 'β_x': [model_full.params['x'], model_robust.params['x']],537 'SE_x': [model_full.bse['x'], model_robust.bse['x']],538 'p_x': [model_full.pvalues['x'], model_robust.pvalues['x']],539 'R²': [model_full.rsquared, model_robust.rsquared],540 })541 print(comparison.to_string(index=False))542543 # Visualize influence544 fig, axes = plt.subplots(1, 2, figsize=(14, 6))545546 # Plot 1: Data with regression lines547 axes[0].scatter(df[~influential_mask]['x'],548 df[~influential_mask]['y'],549 alpha=0.5, label='Normal observations')550 axes[0].scatter(df[influential_mask]['x'],551 df[influential_mask]['y'],552 color='red', s=100, label='Influential outliers')553554 x_range = np.linspace(df['x'].min(), df['x'].max(), 100)555 axes[0].plot(x_range,556 model_full.params['const'] + model_full.params['x']*x_range,557 'r--', label='With outliers', linewidth=2)558 axes[0].plot(x_range,559 model_robust.params['const'] + model_robust.params['x']*x_range,560 'g-', label='Without outliers', linewidth=2)561562 axes[0].set_xlabel("X")563 axes[0].set_ylabel("Y")564 axes[0].set_title("Outlier Impact on Regression Line")565 axes[0].legend()566 axes[0].grid(True, alpha=0.3)567568 # Plot 2: Cook's distance569 axes[1].stem(range(len(cooks_d)), cooks_d, markerfmt=",", basefmt=" ")570 axes[1].axhline(4/len(df), color='red', linestyle='--',571 label=f'Threshold = {4/len(df):.3f}')572 axes[1].set_xlabel("Observation Index")573 axes[1].set_ylabel("Cook's Distance")574 axes[1].set_title("Influence Measure (Cook's D)")575 axes[1].legend()576 axes[1].grid(True, alpha=0.3)577578 plt.tight_layout()579 os.makedirs("./images", exist_ok=True)580 plt.savefig("./images/outlier_influence_analysis.png", dpi=144, bbox_inches="tight")581582 # Save figure reference for inline display583 influence_fig = plt.gcf()584585 print("\n" + "=" * 70)586 print("CRITICAL FINDINGS")587 print("=" * 70)588 print(f"""589 1. {influential_mask.sum()} influential observations detected590 2. Coefficient on x changes by {abs(model_full.params['x'] - model_robust.params['x']):.3f}591 3. R² drops from {model_full.rsquared:.3f} to {model_robust.rsquared:.3f}592 4. Statistical significance {'changes' if (model_full.pvalues['x'] < 0.05) != (model_robust.pvalues['x'] < 0.05) else 'unchanged'}593594 REQUIRED REPORTING:595 - Present BOTH models (with and without influential points)596 - Investigate influential observations for data quality597 - If outliers are valid, consider robust regression methods598 - State clearly which results are sensitive to outliers599600 This demonstrates why influence diagnostics are MANDATORY.601 """)602603 # Return figure for inline display along with models604 return model_full, model_robust, influential_mask, influence_fig,605```606</code>607<power_user_tip>608When influential observations are detected:6091. NEVER simply delete them without investigation6102. Check if they're data errors or valid extreme cases6113. Report results both with AND without6124. Consider robust methods (M-estimation, quantile regression)6135. Document which conclusions are robust to their inclusion614</power_user_tip>615</example>616617</examples>618619<common_mistakes>620621<mistake severity="critical">622 <what>Running regression without any diagnostics</what>623 <consequence>Invalid p-values, wrong confidence intervals, false discoveries</consequence>624 <prevention>ALWAYS run complete_regression_diagnostics() after EVERY regression</prevention>625</mistake>626627<mistake severity="critical">628 <what>Ignoring heteroskedasticity in cross-sectional data</what>629 <consequence>Type I error rates up to 40% instead of 5%</consequence>630 <prevention>Always test with Breusch-Pagan/White, use robust SE if detected</prevention>631</mistake>632633<mistake severity="critical">634 <what>Not checking for influential observations</what>635 <consequence>Results driven by 1-2 outliers rather than general pattern</consequence>636 <prevention>Compute Cook's distance, report sensitivity to outlier exclusion</prevention>637</mistake>638639<mistake severity="high">640 <what>Ignoring severe multicollinearity (VIF > 10)</what>641 <consequence>Unstable estimates, coefficients flip signs with minor changes</consequence>642 <prevention>Check VIF for all variables, remove/combine if VIF > 10</prevention>643</mistake>644645<mistake severity="high">646 <what>Using classical SE when robust SE required</what>647 <consequence>Overstated precision, false statistical significance</consequence>648 <prevention>Default to HC3 robust SE when heteroskedasticity detected</prevention>649</mistake>650651<mistake severity="medium">652 <what>Over-interpreting normality tests with large samples</what>653 <consequence>Unnecessary transformations when CLT ensures valid inference</consequence>654 <prevention>With n > 30, normality violations rarely affect inference materially</prevention>655</mistake>656657</common_mistakes>658659<interpretation_guide>660661<reading_diagnostic_plots>662**Residuals vs Fitted**:663- GOOD: Random scatter around horizontal line at zero664- BAD: Patterns (curves, fans, clusters) indicate misspecification665666**Q-Q Plot**:667- GOOD: Points follow diagonal reference line668- BAD: S-shapes indicate skewness, heavy tails indicate outliers669670**Scale-Location**:671- GOOD: Horizontal band with no trend672- BAD: Increasing/decreasing spread indicates heteroskedasticity673674**Residuals vs Leverage**:675- GOOD: Points within Cook's distance contours676- BAD: Points in top-right or bottom-right corners are influential677</reading_diagnostic_plots>678679<decision_tree>680Heteroskedasticity detected?681 → YES: Use HC3 robust SE or cluster-robust if panel data682 → NO: Classical SE are valid683684Multicollinearity detected (VIF > 10)?685 → YES: Must drop variables or use regularization686 → NO but VIF > 5: Monitor, may be acceptable687688Influential outliers detected?689 → YES: Report results with AND without690 → NO: Single model sufficient691692Non-normal residuals?693 → n > 30: Usually OK (CLT)694 → n < 30: Consider bootstrap or transformation695</decision_tree>696697<honest_limitations>698- Diagnostics can't detect all problems (e.g., omitted variables)699- Some violations may not matter for specific research questions700- Trade-offs exist (robust SE have lower power)701- Perfect models don't exist—document deviations honestly702</honest_limitations>703704</interpretation_guide>705706<references>707<paper>Belsley, D.A., Kuh, E., & Welsch, R.E. (1980). Regression Diagnostics: Identifying Influential Data and Sources of Collinearity. The foundational text on regression diagnostics.</paper>708<paper>Cook, R.D. & Weisberg, S. (1982). Residuals and Influence in Regression. Introduces Cook's distance for influence detection.</paper>709<paper>MacKinnon, J.G. & White, H. (1985). "Some heteroskedasticity-consistent covariance matrix estimators with improved finite sample properties." HC3 robust standard errors.</paper>710<paper>Kutner, M.H., Nachtsheim, C.J., & Neter, J. (2004). Applied Linear Regression Models. Comprehensive treatment of multicollinearity and VIF.</paper>711<paper>Long, J.S. & Ervin, L.H. (2000). "Using heteroscedasticity consistent standard errors in the linear regression model." Practical guide to robust inference.</paper>712</references>713714</skill_content>