Skip to content

8.5 面板数据高级专题

掌握前沿技术:双向固定效应、聚类标准误、动态面板与 DID

难度前沿性


本节目标

  • 深入理解双向固定效应(Two-Way FE)的识别逻辑
  • 正确使用聚类标准误(Clustered SE)
  • 初步了解动态面板模型(Arellano-Bond)
  • 将面板方法应用于 DID 研究
  • 处理不平衡面板的技巧
  • 应对面板数据的常见陷阱

双向固定效应(Two-Way Fixed Effects)

模型定义

单向固定效应

  • 控制:个体异质性

双向固定效应

  • 控制:个体异质性 + 时间效应

时间效应 的含义

  • 影响所有个体的时间特定冲击
  • 例如:宏观经济周期、政策变化、技术进步、自然灾害

为什么需要双向 FE?

场景 1:存在共同时间趋势

问题:如果 都随时间增长,可能是因为共同的时间因素

例子:研究广告支出对销售额的影响

  • 广告支出逐年增加(技术进步,媒体成本下降)
  • 销售额也逐年增加(经济增长,消费者收入提高)
  • 如果不控制时间趋势,可能错误归因为广告效应

解决方案:双向 FE

python
model_twoway = PanelOLS(sales, ads,
                        entity_effects=True,  # 控制公司固定效应
                        time_effects=True).fit()  # 控制年份固定效应

场景 2:DID 研究的标准做法

DID 模型

  • :控制处理组和对照组的固定差异
  • :控制共同的时间趋势(平行趋势假设的体现)

Python 实现

python
# DID 的标准实现就是双向 FE + 交互项
model_did = PanelOLS(y, treated_post,
                     entity_effects=True,
                     time_effects=True).fit()

双向 FE 的识别逻辑

去均值变换(两次):

  1. 个体去均值
  2. 时间去均值

最终估计

直觉

  • 第一步:消除个体异质性(组间差异)
  • 第二步:消除时间趋势(共同的宏观冲击)
  • 剩余变异:个体特定的时间变化(individual-specific time variation)

Python 完整示例

python
import numpy as np
import pandas as pd
from linearmodels.panel import PanelOLS
import matplotlib.pyplot as plt
import seaborn as sns

plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
sns.set_style("whitegrid")

# 模拟数据:加入时间趋势
np.random.seed(42)

N = 100  # 100 家公司
T = 10   # 10 年

data = []
for i in range(N):
    alpha_i = np.random.normal(0, 1)  # 公司固定效应

    for t in range(T):
        year = 2010 + t
        lambda_t = 0.05 * t  # 时间固定效应(共同增长趋势)

        x = 10 + 0.3 * t + np.random.normal(0, 2)
        y = 5 + 2 * x + alpha_i + lambda_t + np.random.normal(0, 1)

        data.append({'company': i, 'year': year, 'y': y, 'x': x})

df = pd.DataFrame(data)
df_panel = df.set_index(['company', 'year'])

# 模型 1:混合 OLS(有偏)
import statsmodels.api as sm
X_pooled = sm.add_constant(df[['x']])
model_pooled = sm.OLS(df['y'], X_pooled).fit()

# 模型 2:单向 FE(控制公司)
model_oneway = PanelOLS(df_panel['y'], df_panel[['x']],
                        entity_effects=True).fit(cov_type='clustered',
                                                 cluster_entity=True)

# 模型 3:双向 FE(控制公司 + 年份)
model_twoway = PanelOLS(df_panel['y'], df_panel[['x']],
                        entity_effects=True,
                        time_effects=True).fit(cov_type='clustered',
                                               cluster_entity=True)

print("=" * 70)
print("单向 FE vs 双向 FE")
print("=" * 70)
print(f"真实参数:     2.0000")
print(f"混合 OLS:     {model_pooled.params['x']:.4f}")
print(f"单向 FE:      {model_oneway.params['x']:.4f}")
print(f"双向 FE:      {model_twoway.params['x']:.4f} (最接近真实值)")

# 可视化时间效应
# 提取估计的时间固定效应
time_effects = model_twoway.estimated_effects.time_effects
print("\n" + "=" * 70)
print("估计的时间固定效应")
print("=" * 70)
print(time_effects.head(10))

# 绘制时间效应
plt.figure(figsize=(12, 6))
plt.plot(time_effects.index, time_effects.values, 'o-',
         linewidth=2, markersize=8, color='darkblue')
plt.xlabel('年份', fontweight='bold', fontsize=12)
plt.ylabel('时间固定效应', fontweight='bold', fontsize=12)
plt.title('估计的时间固定效应(共同时间趋势)', fontweight='bold', fontsize=14)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()

输出解读

  • 单向 FE:如果存在时间趋势但未控制,估计可能有偏
  • 双向 FE:同时控制个体和时间,估计更准确

聚类标准误(Clustered Standard Errors)

为什么需要聚类标准误?

问题:面板数据中,同一个体不同时间的误差通常相关(序列相关)

后果

  • 经典 OLS 标准误假设误差独立
  • 如果误差相关,OLS 标准误低估真实不确定性
  • 导致 统计量虚高,假阳性增加(Type I Error)

例子

  • 个体 在 2015 年受到正向冲击(
  • 这个冲击可能持续到 2016 年(
  • 因此

聚类标准误的原理

核心思想:允许同一聚类(cluster)内的观测相关,不同聚类间独立

面板数据的标准做法:在个体层面聚类

  • 允许个体 的所有时间观测相关
  • 假设不同个体间独立

Python 实现

python
model = PanelOLS(y, X, entity_effects=True).fit(
    cov_type='clustered',
    cluster_entity=True  # 在个体(entity)层面聚类
)

聚类的选择

聚类层面何时使用Python 实现
个体(Entity)面板数据的标准做法cluster_entity=True
时间(Time)同一时间不同个体可能相关(罕见)cluster_time=True
双向聚类同时允许个体和时间聚类cluster_entity=True, cluster_time=True
自定义聚类例如:按州聚类、按行业聚类clusters=df['state']

推荐

  • 面板数据 → cluster_entity=True(最常用)
  • DID 研究 → 在处理单位层面聚类(如州、城市)

聚类标准误 vs 稳健标准误

类型允许的误差模式何时使用
经典 OLS SE同方差 + 独立几乎从不(假设太强)
稳健 SE异方差 + 独立横截面数据
聚类 SE异方差 + 组内相关面板数据 ⭐

重要规则

  • 面板数据必须使用聚类 SE
  • 不使用聚类 SE 会导致标准误严重低估(可能低估 50%)

Python 对比示例

python
import numpy as np
import pandas as pd
from linearmodels.panel import PanelOLS

# 模拟数据:强序列相关
np.random.seed(123)
data = []
for i in range(100):
    shock = np.random.normal(0, 2)  # 个体特定的持久冲击
    for t in range(10):
        x = 10 + np.random.normal(0, 1)
        # 误差项有持久成分(序列相关)
        epsilon = shock + np.random.normal(0, 0.5)
        y = 5 + 2 * x + epsilon
        data.append({'id': i, 'year': 2010 + t, 'y': y, 'x': x})

df = pd.DataFrame(data)
df_panel = df.set_index(['id', 'year'])

# 三种标准误
model_unadjusted = PanelOLS(df_panel['y'], df_panel[['x']]).fit(
    cov_type='unadjusted'  # 经典 OLS SE
)

model_robust = PanelOLS(df_panel['y'], df_panel[['x']]).fit(
    cov_type='robust'  # 稳健 SE(仅异方差)
)

model_clustered = PanelOLS(df_panel['y'], df_panel[['x']]).fit(
    cov_type='clustered',
    cluster_entity=True  # 聚类 SE(异方差 + 序列相关)
)

print("=" * 70)
print("标准误对比")
print("=" * 70)
print(f"系数估计:          {model_clustered.params['x']:.4f} (三种方法相同)")
print(f"经典 SE:           {model_unadjusted.std_errors['x']:.4f} (低估!)")
print(f"稳健 SE:           {model_robust.std_errors['x']:.4f} (仍低估)")
print(f"聚类 SE:           {model_clustered.std_errors['x']:.4f} (正确)")
print(f"\n聚类 SE / 经典 SE: {model_clustered.std_errors['x'] / model_unadjusted.std_errors['x']:.2f}x")

关键发现

  • 聚类 SE 通常是经典 SE 的 1.5-3 倍
  • 如果不使用聚类 SE, 统计量会虚高,导致错误拒绝原假设

动态面板模型(Dynamic Panel Models)

什么是动态面板?

模型

特征:因变量的滞后值 作为自变量

应用场景

  • 持续性(Persistence):收入、GDP、健康状态
  • 调整成本(Adjustment Costs):公司投资、就业
  • 习惯形成(Habit Formation):消费、储蓄

为什么普通 FE 不适用?

问题 内生

原因

  • 依赖于
  • 组内变换后, 依赖于 (包含
  • 导致

后果:FE 估计有偏且不一致(即使


Arellano-Bond 估计量

核心思想:使用工具变量(IV)+ 一阶差分

步骤 1:一阶差分消除固定效应

步骤 2:使用更早期的 作为工具变量

工具变量

  • 相关(相关性条件)
  • 不相关(外生性条件)

估计方法:GMM(广义矩估计)


Python 实现(简化版)

python
from linearmodels.panel import PanelOLS
import pandas as pd
import numpy as np

# 模拟动态面板数据
np.random.seed(42)
data = []
for i in range(100):
    alpha_i = np.random.normal(0, 1)
    y_lag = 5  # 初始值

    for t in range(10):
        x = 10 + np.random.normal(0, 2)
        epsilon = np.random.normal(0, 1)
        y = 0.5 * y_lag + 1.5 * x + alpha_i + epsilon  # 真实参数:beta1=0.5, beta2=1.5

        data.append({'id': i, 'year': 2010 + t, 'y': y, 'x': x})
        y_lag = y  # 更新滞后值

df = pd.DataFrame(data)

# 创建滞后变量
df = df.sort_values(['id', 'year'])
df['y_lag'] = df.groupby('id')['y'].shift(1)
df = df.dropna()

df_panel = df.set_index(['id', 'year'])

# 错误方法:普通 FE(有偏!)
model_fe_wrong = PanelOLS(df_panel['y'],
                          df_panel[['y_lag', 'x']],
                          entity_effects=True).fit()

print("=" * 70)
print("动态面板模型")
print("=" * 70)
print(f"真实参数: y_lag=0.5, x=1.5")
print(f"\nFE 估计(有偏):")
print(f"  y_lag: {model_fe_wrong.params['y_lag']:.4f}")
print(f"  x:     {model_fe_wrong.params['x']:.4f}")
print("\n注意:FE 估计有偏!应使用 Arellano-Bond GMM")

注意

  • Python 的 linearmodels 目前不支持 Arellano-Bond
  • 需要使用 Stata 的 xtabond 或 R 的 plm
  • 这是动态面板的高级主题,超出本课程范围

面板数据与 DID

DID 就是双向固定效应 + 交互项

标准 DID 模型

等价于

python
model_did = PanelOLS(y, treated_post,
                     entity_effects=True,   # 控制 α_i
                     time_effects=True).fit()  # 控制 λ_t

Python 完整 DID 示例

python
import numpy as np
import pandas as pd
from linearmodels.panel import PanelOLS
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']

# 模拟 DID 数据
np.random.seed(2024)

data = []
# 处理组:ID 1-50,2018 年接受处理
# 对照组:ID 51-100,不接受处理

for i in range(1, 101):
    treated = 1 if i <= 50 else 0
    alpha_i = np.random.normal(0, 1)

    for t in range(2015, 2021):
        year = t
        post = 1 if year >= 2018 else 0
        treated_post = treated * post

        # DID 效应 = 10
        y = 50 + 10 * treated_post + alpha_i + 0.5 * year + np.random.normal(0, 2)

        data.append({
            'id': i,
            'year': year,
            'y': y,
            'treated': treated,
            'post': post,
            'treated_post': treated_post
        })

df = pd.DataFrame(data)
df_panel = df.set_index(['id', 'year'])

# DID 回归
model_did = PanelOLS(df_panel['y'],
                     df_panel[['treated_post']],
                     entity_effects=True,
                     time_effects=True).fit(cov_type='clustered',
                                            cluster_entity=True)

print("=" * 70)
print("DID 估计结果")
print("=" * 70)
print(model_did)
print(f"\nDID 效应: {model_did.params['treated_post']:.2f} (真实值: 10.00)")

# 事件研究图(Event Study Plot)
# 创建年份虚拟变量
for year in range(2015, 2021):
    df[f'treated_x_{year}'] = df['treated'] * (df['year'] == year)

# 以 2017 年为基准(处理前最后一年)
event_vars = [f'treated_x_{y}' for y in [2015, 2016, 2018, 2019, 2020]]
df_panel_event = df.set_index(['id', 'year'])

model_event = PanelOLS(df_panel_event['y'],
                       df_panel_event[event_vars],
                       entity_effects=True,
                       time_effects=True).fit(cov_type='clustered',
                                              cluster_entity=True)

# 提取系数
years = [2015, 2016, 2017, 2018, 2019, 2020]
coefs = [model_event.params[f'treated_x_{y}'] if y != 2017 else 0 for y in years]
se = [model_event.std_errors[f'treated_x_{y}'] if y != 2017 else 0 for y in years]

# 绘制事件研究图
plt.figure(figsize=(12, 6))
plt.errorbar(years, coefs, yerr=1.96*np.array(se), marker='o',
             markersize=8, linewidth=2, capsize=5, color='darkblue')
plt.axhline(0, color='red', linestyle='--', linewidth=1)
plt.axvline(2017.5, color='green', linestyle='--', linewidth=1.5, alpha=0.7)
plt.text(2017.5, max(coefs) * 0.8, '政策实施', fontsize=12, color='green')
plt.xlabel('年份', fontweight='bold', fontsize=12)
plt.ylabel('处理效应', fontweight='bold', fontsize=12)
plt.title('事件研究图(Event Study)', fontweight='bold', fontsize=14)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()

解读

  • 2015-2017:系数接近 0(平行趋势成立)
  • 2018-2020:系数显著为正(处理效应)

️ 处理不平衡面板

不平衡面板的类型

  1. 自然流失(Attrition):个体退出样本

    • 例如:公司破产、个人退出调查
  2. 进入和退出(Entry and Exit):新个体加入样本

    • 例如:新公司上市、新医院建立
  3. 随机缺失:某些时点数据缺失

    • 例如:调查未完成、数据录入错误

不平衡面板的问题

问题 1:选择性偏差(Selection Bias)

  • 如果退出与结果变量相关,估计会有偏
  • 例如:业绩差的公司更可能退市

问题 2:效率损失

  • 缺失数据导致样本量减少

处理方法

方法 1:保持不平衡(推荐)⭐

linearmodels 自动处理不平衡面板

python
# 不需要特殊操作,linearmodels 会自动处理
model = PanelOLS(y, X, entity_effects=True).fit()

优点

  • 保留所有可用信息
  • 避免人为删除数据

前提

  • 缺失是随机的(Missing at Random, MAR)
  • 或缺失与自变量相关,但与误差项无关

方法 2:使用平衡子样本

构造平衡面板

python
# 只保留在所有时期都有观测的个体
complete_ids = df.groupby('id')['year'].count()
complete_ids = complete_ids[complete_ids == T].index
df_balanced = df[df['id'].isin(complete_ids)]

优点

  • 避免选择性偏差(如果担心流失非随机)

缺点

  • 损失大量数据
  • 效率低

方法 3:样本选择模型(Heckman)

适用于:非随机流失(如公司破产)

方法

  1. 第一阶段:估计流失概率(Probit)
  2. 第二阶段:加入 Inverse Mills Ratio 作为控制变量

超出本课程范围,参考 Wooldridge (2010) 第 19 章


本节小结

核心要点

  1. 双向固定效应

    • 控制个体 + 时间效应
    • DID 的标准做法
    • 消除共同时间趋势
  2. 聚类标准误

    • 面板数据的必备工具
    • 在个体层面聚类(标准做法)
    • 避免低估标准误
  3. 动态面板

    • 包含滞后因变量
    • 普通 FE 有偏
    • 需要 Arellano-Bond GMM
  4. 面板数据 + DID

    • DID = 双向 FE + 交互项
    • 事件研究图检验平行趋势
    • 聚类在处理单位层面
  5. 不平衡面板

    • linearmodels 自动处理
    • 优先保持不平衡(如果 MAR)
    • 担心选择性偏差时使用平衡子样本

实战建议

标准面板回归的检查清单

  • 使用双向 FE(如果存在时间趋势)
  • 使用聚类标准误(cluster_entity=True
  • 检查组内变异是否足够
  • 进行 Hausman 检验(FE vs RE)
  • 报告 、总观测值
  • 检查是否有坏控制(中介变量)

DID 研究的检查清单

  • 使用双向 FE
  • 聚类在处理单位层面
  • 绘制事件研究图
  • 检验平行趋势
  • 进行安慰剂检验

下一步

第6节:小结和本章复习 中,我们将:

  • 总结面板数据方法的决策树
  • 提供 10 道练习题
  • 推荐经典文献

掌握高级技术,成为面板数据专家!

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