Skip to content

5.4 回归诊断(Regression Diagnostics)

"All models are wrong, some are useful, and a few are insightful.""所有模型都是错的,有些是有用的,少数是有洞察力的。"— George Box, Statistician (统计学家)

检验假设,诊断问题,确保推断有效

难度重要性


本节目标

完成本节后,你将能够:

  • 理解 Gauss-Markov 假设的含义
  • 检验异方差性(Heteroskedasticity)
  • 检验自相关(Autocorrelation)
  • 进行残差分析(Residual Analysis)
  • 识别影响点和异常值(Outliers & Influential Points)
  • 使用稳健标准误(Robust Standard Errors)

Gauss-Markov 假设(Classical Linear Model Assumptions)

完整假设列表

假设数学表达含义违反后果
MLR.1 线性性总体模型是线性的系数有偏
MLR.2 随机抽样 是 i.i.d.样本独立同分布推断失效
MLR.3 无完全共线性 矩阵满秩自变量不存在完全线性关系无法估计
MLR.4 零条件均值$E[\varepsilonX] = 0$误差项与 不相关(外生性)
MLR.5 同方差性$Var(\varepsilonX) = \sigma^2$误差方差为常数
MLR.6 正态性误差服从正态分布影响小样本推断

Gauss-Markov 定理

定理:在假设 MLR.1-MLR.5 下,OLS 估计量是 BLUE(Best Linear Unbiased Estimator)

实践意义

  • 假设 1-4 保证无偏性:
  • 假设 5 保证标准误正确
  • 假设 6 不是必需的(但对小样本推断有用)

诊断工具箱

1. 残差分析(Residual Analysis)

残差类型

类型公式用途
原始残差基本诊断
标准化残差识别大残差
学生化残差识别异常值

其中 是杠杆值(Leverage)

Python 实现

python
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats

# 生成示例数据
np.random.seed(42)
n = 200
education = np.random.normal(13, 3, n)
experience = np.random.uniform(0, 30, n)
log_wage = 1.5 + 0.08*education + 0.03*experience - 0.0005*experience**2 + np.random.normal(0, 0.3, n)

df = pd.DataFrame({
    'log_wage': log_wage,
    'education': education,
    'experience': experience,
    'experience_sq': experience**2
})

# 回归
X = sm.add_constant(df[['education', 'experience', 'experience_sq']])
y = df['log_wage']
model = sm.OLS(y, X).fit()

# 获取残差和诊断统计量
influence = model.get_influence()
standardized_resid = influence.resid_studentized_internal
leverage = influence.hat_matrix_diag
cooks_d = influence.cooks_distance[0]

df['resid'] = model.resid
df['fitted'] = model.fittedvalues
df['std_resid'] = standardized_resid
df['leverage'] = leverage
df['cooks_d'] = cooks_d

print("前 10 个观测的诊断统计量:")
print(df[['resid', 'std_resid', 'leverage', 'cooks_d']].head(10))

2. 残差可视化(四合一诊断图)

python
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 图 1:残差 vs 拟合值(检验线性性和同方差性)
axes[0, 0].scatter(df['fitted'], df['resid'], alpha=0.5)
axes[0, 0].axhline(y=0, color='r', linestyle='--', linewidth=2)
axes[0, 0].set_xlabel('拟合值')
axes[0, 0].set_ylabel('残差')
axes[0, 0].set_title('残差 vs 拟合值')
axes[0, 0].grid(True, alpha=0.3)

# 添加局部回归曲线(LOWESS)
from statsmodels.nonparametric.smoothers_lowess import lowess
lowess_result = lowess(df['resid'], df['fitted'], frac=0.3)
axes[0, 0].plot(lowess_result[:, 0], lowess_result[:, 1], 'b-', linewidth=2, label='LOWESS')
axes[0, 0].legend()

# 图 2:Q-Q 图(检验正态性)
stats.probplot(df['resid'], dist="norm", plot=axes[0, 1])
axes[0, 1].set_title('正态 Q-Q 图')
axes[0, 1].grid(True, alpha=0.3)

# 图 3:Scale-Location 图(检验同方差性)
axes[1, 0].scatter(df['fitted'], np.sqrt(np.abs(df['std_resid'])), alpha=0.5)
axes[1, 0].set_xlabel('拟合值')
axes[1, 0].set_ylabel('√|标准化残差|')
axes[1, 0].set_title('Scale-Location 图')
axes[1, 0].grid(True, alpha=0.3)

# 添加 LOWESS 曲线
lowess_result2 = lowess(np.sqrt(np.abs(df['std_resid'])), df['fitted'], frac=0.3)
axes[1, 0].plot(lowess_result2[:, 0], lowess_result2[:, 1], 'r-', linewidth=2)

# 图 4:残差 vs 杠杆值(识别影响点)
axes[1, 1].scatter(df['leverage'], df['std_resid'], alpha=0.5)
axes[1, 1].set_xlabel('杠杆值')
axes[1, 1].set_ylabel('标准化残差')
axes[1, 1].set_title('残差 vs 杠杆值')
axes[1, 1].grid(True, alpha=0.3)

# 标记高影响力点(Cook's D > 0.5)
high_influence = df['cooks_d'] > 0.5
if high_influence.any():
    axes[1, 1].scatter(df.loc[high_influence, 'leverage'], 
                      df.loc[high_influence, 'std_resid'], 
                      color='red', s=100, label="高影响力点")
    axes[1, 1].legend()

plt.tight_layout()
plt.show()

解读指南

好的模式问题模式
残差 vs 拟合值随机分布,水平带状漏斗形(异方差)、曲线(非线性)
Q-Q 图点在直线上系统性偏离(非正态)
Scale-Location水平线向上或向下趋势(异方差)
残差 vs 杠杆值无极端点右上/右下角有点(高影响力)

异方差性(Heteroskedasticity)

定义

同方差性(Homoskedasticity)(常数)

异方差性(Heteroskedasticity)(随 变化)

后果

好消息:OLS 估计量仍然无偏
坏消息:标准误有偏 统计量和 不可靠

检验方法

1. Breusch-Pagan 检验

原假设(同方差)

python
from statsmodels.stats.diagnostic import het_breuschpagan

# BP 检验
bp_test = het_breuschpagan(model.resid, model.model.exog)
bp_stat, bp_pvalue, bp_f, bp_f_pvalue = bp_test

print("Breusch-Pagan 检验:")
print(f"  LM 统计量 = {bp_stat:.3f}")
print(f"  p 值 = {bp_pvalue:.4f}")

if bp_pvalue < 0.05:
    print("  结论:拒绝同方差假设,存在异方差性")
else:
    print("  结论:不能拒绝同方差假设")

2. White 检验

优势:不假设异方差的具体形式

python
from statsmodels.stats.diagnostic import het_white

# White 检验
white_test = het_white(model.resid, model.model.exog)
white_stat, white_pvalue, white_f, white_f_pvalue = white_test

print("\nWhite 检验:")
print(f"  LM 统计量 = {white_stat:.3f}")
print(f"  p 值 = {white_pvalue:.4f}")

3. 图形诊断

python
# 绘制残差平方与拟合值
plt.figure(figsize=(10, 6))
plt.scatter(df['fitted'], df['resid']**2, alpha=0.5)
plt.xlabel('拟合值')
plt.ylabel('残差平方')
plt.title('残差平方 vs 拟合值(异方差诊断)')
plt.grid(True, alpha=0.3)

# 添加趋势线
z = np.polyfit(df['fitted'], df['resid']**2, 2)
p = np.poly1d(z)
x_line = np.linspace(df['fitted'].min(), df['fitted'].max(), 100)
plt.plot(x_line, p(x_line), "r-", linewidth=2, label='二次拟合')
plt.legend()
plt.show()

处理方法

方法 1:稳健标准误(Heteroskedasticity-Robust Standard Errors)

python
# 使用 HC3 稳健标准误(推荐)
model_robust = sm.OLS(y, X).fit(cov_type='HC3')

print("普通 OLS:")
print(model.summary().tables[1])

print("\n稳健标准误(HC3):")
print(model_robust.summary().tables[1])

# 对比标准误
comparison = pd.DataFrame({
    'Variable': model.params.index,
    'Coefficient': model.params.values,
    'SE (OLS)': model.bse.values,
    'SE (Robust)': model_robust.bse.values,
    'Ratio': model_robust.bse.values / model.bse.values
})
print("\n标准误对比:")
print(comparison)

稳健标准误类型

类型适用场景Python 参数
HC0基本版cov_type='HC0'
HC1小样本调整cov_type='HC1'
HC2杠杆值调整cov_type='HC2'
HC3推荐(最保守)cov_type='HC3'

方法 2:加权最小二乘法(WLS)

python
# 如果知道方差结构:Var(ε) ∝ X²
# 使用 WLS
weights = 1 / df['education']**2
model_wls = sm.WLS(y, X, weights=weights).fit()

print("WLS 回归结果:")
print(model_wls.summary())

方法 3:变量变换

python
# 对因变量取对数(常用于工资数据)
# 可以稳定方差

自相关(Autocorrelation)

定义

无自相关假设 for

自相关:误差项之间存在相关性(常见于时间序列数据)

Durbin-Watson 检验

python
from statsmodels.stats.stattools import durbin_watson

# DW 统计量
dw_stat = durbin_watson(model.resid)
print(f"Durbin-Watson 统计量: {dw_stat:.3f}")

# 判断标准
if dw_stat < 1.5:
    print("可能存在正自相关")
elif dw_stat > 2.5:
    print("可能存在负自相关")
else:
    print("无明显自相关")

DW 统计量解释

  • DW ≈ 2:无自相关
  • DW < 2:正自相关
  • DW > 2:负自相关
  • 范围:[0, 4]

处理方法

python
# 方法 1:Newey-West 标准误(HAC)
model_hac = sm.OLS(y, X).fit(cov_type='HAC', cov_kwds={'maxlags': 4})
print("Newey-West 标准误:")
print(model_hac.bse)

# 方法 2:加入滞后因变量(仅时间序列)
# y_t = β₀ + β₁ x_t + ρ y_{t-1} + ε_t

影响力分析(Influence Analysis)

关键指标

1. 杠杆值(Leverage)

含义:观测值 值与平均值的距离

判断标准 为高杠杆值

python
# 计算杠杆值阈值
k = X.shape[1] - 1  # 自变量个数
n = len(df)
leverage_threshold = 2 * (k + 1) / n

print(f"杠杆值阈值: {leverage_threshold:.4f}")
print(f"高杠杆值点数量: {(df['leverage'] > leverage_threshold).sum()}")

# 可视化
plt.figure(figsize=(10, 6))
plt.stem(df.index, df['leverage'], linefmt='b-', markerfmt='bo', basefmt=' ')
plt.axhline(y=leverage_threshold, color='r', linestyle='--', linewidth=2, label=f'阈值 = {leverage_threshold:.3f}')
plt.xlabel('观测序号')
plt.ylabel('杠杆值')
plt.title('杠杆值诊断')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

2. Cook's Distance

含义:删除观测值 后,所有拟合值的变化程度

判断标准 为高影响力点

python
# Cook's D 阈值
cooks_threshold = 4 / n

print(f"Cook's D 阈值: {cooks_threshold:.4f}")
high_influence = df['cooks_d'] > cooks_threshold
print(f"高影响力点数量: {high_influence.sum()}")

if high_influence.any():
    print("\n高影响力点:")
    print(df.loc[high_influence, ['education', 'experience', 'log_wage', 'cooks_d']])

# 可视化
plt.figure(figsize=(10, 6))
plt.stem(df.index, df['cooks_d'], linefmt='b-', markerfmt='bo', basefmt=' ')
plt.axhline(y=cooks_threshold, color='r', linestyle='--', linewidth=2, label=f'阈值 = {cooks_threshold:.3f}')
plt.xlabel('观测序号')
plt.ylabel("Cook's Distance")
plt.title('影响力诊断')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

3. DFBETAS

含义:删除观测值 后,每个系数的变化

python
# DFBETAS
dfbetas = influence.dfbetas
dfbetas_df = pd.DataFrame(dfbetas, columns=X.columns)

print("DFBETAS 描述统计:")
print(dfbetas_df.describe())

# 可视化(针对教育系数)
plt.figure(figsize=(10, 6))
plt.stem(df.index, dfbetas_df['education'], linefmt='b-', markerfmt='bo', basefmt=' ')
plt.axhline(y=2/np.sqrt(n), color='r', linestyle='--', linewidth=2, label='阈值')
plt.axhline(y=-2/np.sqrt(n), color='r', linestyle='--', linewidth=2)
plt.xlabel('观测序号')
plt.ylabel('DFBETAS (education)')
plt.title('教育系数的影响力分析')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

处理影响点

python
# 方法 1:删除高影响力点后重新估计
df_no_outliers = df[~high_influence].copy()
X_no = sm.add_constant(df_no_outliers[['education', 'experience', 'experience_sq']])
y_no = df_no_outliers['log_wage']
model_no_outliers = sm.OLS(y_no, X_no).fit()

print("原模型(包含异常值):")
print(model.params)

print("\n删除异常值后:")
print(model_no_outliers.params)

# 方法 2:稳健回归(Robust Regression)
from statsmodels.robust.robust_linear_model import RLM

model_rlm = RLM(y, X, M=sm.robust.norms.HuberT()).fit()
print("\n稳健回归:")
print(model_rlm.params)

正态性检验(Normality Tests)

1. Shapiro-Wilk 检验

python
from scipy.stats import shapiro

# Shapiro-Wilk 检验
stat, p_value = shapiro(model.resid)
print(f"Shapiro-Wilk 统计量: {stat:.4f}")
print(f"p 值: {p_value:.4f}")

if p_value < 0.05:
    print("结论:拒绝正态性假设")
else:
    print("结论:不能拒绝正态性假设")

2. Jarque-Bera 检验

python
from statsmodels.stats.stattools import jarque_bera

# Jarque-Bera 检验
jb_stat, jb_pvalue, jb_skew, jb_kurtosis = jarque_bera(model.resid)
print(f"\nJarque-Bera 统计量: {jb_stat:.3f}")
print(f"p 值: {jb_pvalue:.4f}")
print(f"偏度: {jb_skew:.3f}")
print(f"峰度: {jb_kurtosis:.3f}")

3. 可视化

python
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 直方图
axes[0].hist(model.resid, bins=30, density=True, alpha=0.7, edgecolor='black')
# 叠加正态分布
mu, sigma = model.resid.mean(), model.resid.std()
x = np.linspace(model.resid.min(), model.resid.max(), 100)
axes[0].plot(x, stats.norm.pdf(x, mu, sigma), 'r-', linewidth=2, label='理论正态分布')
axes[0].set_xlabel('残差')
axes[0].set_ylabel('密度')
axes[0].set_title('残差分布')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Q-Q 图
stats.probplot(model.resid, dist="norm", plot=axes[1])
axes[1].set_title('正态 Q-Q 图')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

注意

  • 正态性假设不是必需的(Gauss-Markov 定理不需要)
  • 大样本下,CLT 保证渐近正态性
  • 小样本下,非正态性影响 检验和置信区间

完整诊断流程

诊断检查清单

python
def comprehensive_diagnostics(model, X, y):
    """
    完整的回归诊断
    """
    print("="*70)
    print("回归诊断报告")
    print("="*70)
    
    # 1. 基本信息
    print(f"\n1. 样本量: {len(y)}")
    print(f"   自变量数: {X.shape[1] - 1}")
    print(f"   R²: {model.rsquared:.4f}")
    print(f"   Adjusted R²: {model.rsquared_adj:.4f}")
    
    # 2. 异方差检验
    print("\n2. 异方差检验:")
    bp_test = het_breuschpagan(model.resid, model.model.exog)
    print(f"   Breusch-Pagan: LM = {bp_test[0]:.3f}, p = {bp_test[1]:.4f}")
    
    white_test = het_white(model.resid, model.model.exog)
    print(f"   White: LM = {white_test[0]:.3f}, p = {white_test[1]:.4f}")
    
    if bp_test[1] < 0.05 or white_test[1] < 0.05:
        print("   ️  建议:使用稳健标准误(HC3)")
    else:
        print("    无明显异方差")
    
    # 3. 自相关检验
    print("\n3. 自相关检验:")
    dw_stat = durbin_watson(model.resid)
    print(f"   Durbin-Watson: {dw_stat:.3f}")
    if 1.5 < dw_stat < 2.5:
        print("    无明显自相关")
    else:
        print("   ️  可能存在自相关")
    
    # 4. 正态性检验
    print("\n4. 正态性检验:")
    jb_stat, jb_pvalue, _, _ = jarque_bera(model.resid)
    print(f"   Jarque-Bera: JB = {jb_stat:.3f}, p = {jb_pvalue:.4f}")
    if jb_pvalue < 0.05:
        print("   ️  残差可能不服从正态分布(大样本下通常不影响推断)")
    else:
        print("    残差近似正态分布")
    
    # 5. 影响力分析
    print("\n5. 影响力分析:")
    influence = model.get_influence()
    cooks_d = influence.cooks_distance[0]
    high_influence = cooks_d > 4/len(y)
    print(f"   高影响力点数量: {high_influence.sum()} ({high_influence.sum()/len(y)*100:.1f}%)")
    if high_influence.sum() > 0:
        print("   ️  建议:检查高影响力点,考虑稳健回归")
    else:
        print("    无明显高影响力点")
    
    # 6. VIF
    print("\n6. 多重共线性(VIF):")
    from statsmodels.stats.outliers_influence import variance_inflation_factor
    vif_data = pd.DataFrame()
    vif_data["Variable"] = X.columns
    vif_data["VIF"] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
    vif_data = vif_data[vif_data['Variable'] != 'const']
    print(vif_data.to_string(index=False))
    if (vif_data['VIF'] > 10).any():
        print("   ️  部分变量 VIF > 10,存在严重多重共线性")
    else:
        print("    VIF 在可接受范围")
    
    print("\n" + "="*70)
    print("诊断完成")
    print("="*70)

# 运行诊断
comprehensive_diagnostics(model, X, y)

练习题

练习 1:识别异方差

给定以下残差图,判断是否存在异方差:

python
# 模拟异方差数据
np.random.seed(123)
X_het = np.random.uniform(0, 10, 200)
y_het = 2 + 3*X_het + X_het * np.random.normal(0, 1, 200)  # 方差随 X 增加

df_het = pd.DataFrame({'X': X_het, 'y': y_het})
X_het_model = sm.add_constant(df_het['X'])
model_het = sm.OLS(df_het['y'], X_het_model).fit()

plt.scatter(model_het.fittedvalues, model_het.resid)
plt.xlabel('拟合值')
plt.ylabel('残差')
plt.title('残差 vs 拟合值')
plt.axhline(y=0, color='r', linestyle='--')
plt.show()

任务

  1. 进行 BP 检验
  2. 使用稳健标准误重新估计
  3. 对比标准误的变化

练习 2:影响力分析

在上面的工资数据中:

  1. 人为添加一个异常值
  2. 计算 Cook's D
  3. 对比删除前后的回归系数

本节小结

诊断流程

数据准备

OLS 回归

检验假设 ——→ 异方差? ——→ 是 → 使用稳健 SE
   ↓          ↓ 否
   ↓          ↓
   ↓      自相关? ——→ 是 → 使用 HAC SE
   ↓          ↓ 否
   ↓          ↓
   ↓      非正态? ——→ 是 → 大样本 OK,小样本谨慎
   ↓          ↓ 否
   ↓          ↓
   ↓      影响点? ——→ 是 → 稳健回归或删除
   ↓          ↓ 否
   ↓          ↓
   ↓        通过所有检验

 最终模型

关键要点

问题检验后果解决方案
异方差BP, WhiteSE 有偏HC3 稳健 SE
自相关DWSE 有偏HAC SE
非正态JB, Shapiro小样本推断失效大样本下无妨
影响点Cook's D系数不稳定稳健回归

下节预告

下一节中,我们将学习:

  • 分类变量的处理(Dummy Variables)
  • 虚拟变量陷阱(Dummy Variable Trap)
  • 交互效应(Interaction Effects)
  • 非线性关系建模

扩展回归模型的表达能力!


延伸阅读

  1. Wooldridge (2020): Chapter 8 "Heteroskedasticity"
  2. White, H. (1980). "A Heteroskedasticity-Consistent Covariance Matrix Estimator"
  3. Cook, R. D. (1977). "Detection of Influential Observation in Linear Regression"
  4. Belsley, Kuh, & Welsch (1980). Regression Diagnostics

继续探索分类变量与交互效应!

基于 MIT 许可证发布。内容版权归作者所有。