8.3 固定效应模型(Fixed Effects Model)
差分的力量:消除不可观测异质性的终极武器
本节目标
- 深入理解固定效应(FE)模型的数学原理
- 掌握三种 FE 估计方法(组内变换、LSDV、First Difference)
- 区分单向 FE 和双向 FE(Two-Way FE)
- 使用 linearmodels.PanelOLS 实现 FE 回归
- 理解 FE 的识别假设和因果解释
- 处理 FE 的常见陷阱(时不变变量、坏控制)
- 完整案例:工资决定因素(Mincer 方程)
固定效应模型的核心思想
问题的起源:遗漏变量偏差
回顾上一节的例子:研究教育对工资的影响
真实模型:
问题:
- (能力)不可观测,无法直接测量
- 与 相关(聪明人受更多教育)
- 如果忽略 , 会有偏差
传统解决方案:
- 找到"完美"的代理变量(proxy)测量能力 → 几乎不可能
- 通过随机实验消除相关性 → 成本高、不现实
固定效应的解决方案: 利用面板数据的时间维度,通过差分消除
FE 的核心直觉:差分消除固定效应
假设我们观测同一个人两年:
第 1 年:
第 2 年:
差分(第2年 - 第1年):
神奇之处: 被消除了!
直觉:
- (能力)在两年内没变(固定的)
- 差分后,我们只利用个体内部的时间变化
- 工资的变化 = 教育的变化 × 回报率
这就是固定效应的本质!
固定效应模型的数学表达
通用 FE 模型
符号定义:
- :个体索引
- :时间索引
- :因变量
- :第 个自变量
- :个体固定效应(不随时间变化,可以与 相关)
- :随机误差项(idiosyncratic error)
关键假设:
严格外生性(Strict Exogeneity):
固定效应不随时间变化:
允许 与 相关:
固定效应的三种估计方法
方法 1:组内变换(Within Transformation)⭐ 推荐
核心思想:对每个变量做去均值(demeaning)
步骤 1:计算个体均值
注意:(因为 不随时间变化)
步骤 2:原方程减去均值方程
原方程:
均值方程(两边对时间取均值):
差分(去均值):
其中 是去均值后的变量
关键结果: 被消除了!
步骤 3:OLS 估计去均值后的方程
这就是固定效应估计量(Within Estimator)
Python 实现:手动组内变换
import numpy as np
import pandas as pd
import statsmodels.api as sm
# 模拟数据
np.random.seed(42)
data = []
for i in range(1, 101): # 100 个个体
alpha_i = np.random.normal(0, 1) # 个体固定效应
for t in range(2015, 2020): # 5 年
x = 10 + t - 2015 + np.random.normal(0, 2)
y = 5 + 2 * x + alpha_i + np.random.normal(0, 1)
data.append({'id': i, 'year': t, 'y': y, 'x': x})
df = pd.DataFrame(data)
print("=" * 70)
print("原始数据")
print("=" * 70)
print(df.head(10))
# 手动计算组内变换
# 步骤 1:计算个体均值
df['y_mean'] = df.groupby('id')['y'].transform('mean')
df['x_mean'] = df.groupby('id')['x'].transform('mean')
# 步骤 2:去均值
df['y_within'] = df['y'] - df['y_mean']
df['x_within'] = df['x'] - df['x_mean']
print("\n" + "=" * 70)
print("去均值后的数据(前 10 行)")
print("=" * 70)
print(df[['id', 'year', 'y', 'y_within', 'x', 'x_within']].head(10))
# 步骤 3:OLS 回归去均值后的数据(无截距!)
model_within = sm.OLS(df['y_within'], df['x_within']).fit()
print("\n" + "=" * 70)
print("组内变换 FE 回归结果(手动实现)")
print("=" * 70)
print(f"系数: {model_within.params['x_within']:.4f}")
print(f"标准误: {model_within.bse['x_within']:.4f}")
print(f"真实参数: 2.0000")关键观察:
- 去均值后,每个个体的均值变为 0
- 只剩下组内变异(within variation)
- 回归时不需要截距(去均值后截距为 0)
方法 2:最小二乘虚拟变量(LSDV)
核心思想:为每个个体添加一个虚拟变量
其中 如果 ,否则为 0
解释:
- 个虚拟变量(第 1 个个体作为参照组)
- :个体 相对于个体 1 的固定效应差异
- (如果 )或 (参照组)
优点:
- 可以估计出每个个体的固定效应
- 与组内变换等价(系数 完全相同)
缺点:
- 如果 很大,需要估计很多参数()
- 计算慢(但结果与组内变换一致)
Python 实现:
import pandas as pd
import statsmodels.api as sm
# 创建虚拟变量(N-1 个)
dummies = pd.get_dummies(df['id'], prefix='id', drop_first=True)
# 合并数据
X_lsdv = pd.concat([df[['x']], dummies], axis=1)
X_lsdv = sm.add_constant(X_lsdv)
# OLS 回归
model_lsdv = sm.OLS(df['y'], X_lsdv).fit()
print("=" * 70)
print("LSDV 回归结果")
print("=" * 70)
print(f"x 的系数: {model_lsdv.params['x']:.4f}")
print(f"估计的固定效应数量: {len(dummies.columns)}")注意:
- LSDV 和组内变换的 系数完全相同
- 但标准误可能略有不同(自由度调整)
方法 3:一阶差分(First Difference)
核心思想:对相邻时期做差分
简写为:
与组内变换的区别:
- 组内变换:(减去所有时期的均值)
- 一阶差分:(减去上一期的值)
何时等价?
- 如果 (只有两期),两种方法完全等价
- 如果 ,通常组内变换更有效率
何时优先使用一阶差分?
- 如果误差项存在随机游走(random walk)
- 如果自变量存在严重的序列相关
Python 实现:
# 一阶差分
df_sorted = df.sort_values(['id', 'year'])
df_sorted['y_diff'] = df_sorted.groupby('id')['y'].diff()
df_sorted['x_diff'] = df_sorted.groupby('id')['x'].diff()
# 去掉第一期(没有差分)
df_fd = df_sorted.dropna(subset=['y_diff', 'x_diff'])
# OLS 回归(无截距)
model_fd = sm.OLS(df_fd['y_diff'], df_fd['x_diff']).fit()
print("=" * 70)
print("一阶差分 FE 回归结果")
print("=" * 70)
print(f"系数: {model_fd.params['x_diff']:.4f}")️ linearmodels.PanelOLS:专业工具
基本语法
from linearmodels.panel import PanelOLS
# 设置面板索引
df_panel = df.set_index(['id', 'year'])
# 固定效应回归
model = PanelOLS(
dependent=df_panel['y'],
exog=df_panel[['x1', 'x2']],
entity_effects=True, # 个体固定效应
time_effects=False # 时间固定效应(可选)
).fit(
cov_type='clustered', # 标准误类型
cluster_entity=True # 在个体层面聚类
)
print(model)参数详解
1. entity_effects:个体固定效应
# 开启个体固定效应(推荐)
entity_effects=True等价于对每个变量做组内变换
2. time_effects:时间固定效应
# 开启时间固定效应
time_effects=True控制所有个体共同的时间趋势(宏观冲击、政策变化等)
3. cov_type:标准误类型
| 选项 | 含义 | 何时使用 |
|---|---|---|
'unadjusted' | 经典 OLS 标准误 | 仅用于教学 |
'robust' | 异方差稳健标准误 | 有异方差时 |
'clustered' | 聚类标准误 | 面板数据的标准选择 ⭐ |
'kernel' | Newey-West 标准误 | 时间序列相关严重时 |
推荐:
cov_type='clustered',
cluster_entity=True # 在个体层面聚类完整示例:工资决定因素
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(123)
N = 500 # 500 名工人
T = 7 # 7 年(2015-2021)
data = []
for i in range(N):
# 个体固定效应(能力、家庭背景等)
ability = np.random.normal(0, 0.3)
# 初始教育水平
education_0 = np.random.choice([10, 12, 14, 16])
for t in range(T):
year = 2015 + t
# 教育(可能增加,如读夜校)
education = education_0 + (0.1 * t if np.random.rand() < 0.1 else 0)
# 工作经验
experience = t
# 是否工会成员(随时间可能变化)
union = 1 if np.random.rand() < 0.3 else 0
# 对数工资
# 真实参数:education=0.08, experience=0.05, union=0.10
log_wage = (1.5 + 0.08 * education + 0.05 * experience +
0.10 * union + ability + np.random.normal(0, 0.1))
data.append({
'id': i,
'year': year,
'log_wage': log_wage,
'education': education,
'experience': experience,
'union': union,
'ability': ability # 实际不可观测
})
df = pd.DataFrame(data)
print("=" * 70)
print("数据摘要")
print("=" * 70)
print(df[['log_wage', 'education', 'experience', 'union']].describe())
print(f"\n样本量: {len(df)}")
print(f"个体数: {df['id'].nunique()}")
print(f"时间期数: {df['year'].nunique()}")
# 设置面板索引
df_panel = df.set_index(['id', 'year'])
# 方法 1:混合 OLS(有偏)
import statsmodels.api as sm
X_pooled = sm.add_constant(df[['education', 'experience', 'union']])
model_pooled = sm.OLS(df['log_wage'], X_pooled).fit()
print("\n" + "=" * 70)
print("方法 1:混合 OLS(遗漏能力 → 有偏)")
print("=" * 70)
print(model_pooled.summary().tables[1])
# 方法 2:固定效应(无偏)
model_fe = PanelOLS(
df_panel['log_wage'],
df_panel[['education', 'experience', 'union']],
entity_effects=True
).fit(cov_type='clustered', cluster_entity=True)
print("\n" + "=" * 70)
print("方法 2:固定效应(控制能力 → 无偏)")
print("=" * 70)
print(model_fe)
# 对比估计结果
print("\n" + "=" * 70)
print("估计对比")
print("=" * 70)
results_table = pd.DataFrame({
'真实参数': [0.08, 0.05, 0.10],
'混合 OLS': [model_pooled.params['education'],
model_pooled.params['experience'],
model_pooled.params['union']],
'固定效应': [model_fe.params['education'],
model_fe.params['experience'],
model_fe.params['union']]
}, index=['education', 'experience', 'union'])
print(results_table.round(4))
# 计算偏差
print("\n偏差(估计值 - 真实值):")
print((results_table[['混合 OLS', '固定效应']] - results_table['真实参数'].values[:, None]).round(4))输出解读:
- 混合 OLS:高估教育系数(因为遗漏了能力)
- 固定效应:接近真实值(差分消除了能力)
单向 FE vs 双向 FE(Two-Way FE)
单向固定效应(One-Way FE)
模型:
控制:
- 个体固定效应 (个体异质性)
Python 实现:
model_oneway = PanelOLS(y, X, entity_effects=True).fit()双向固定效应(Two-Way FE)
模型:
控制:
- 个体固定效应 (个体异质性)
- 时间固定效应 (宏观趋势)
Python 实现:
model_twoway = PanelOLS(y, X,
entity_effects=True,
time_effects=True).fit()何时使用双向 FE?
场景 1:存在宏观时间趋势
- 例如:经济周期、通货膨胀、技术进步
- 这些因素影响所有个体,但与你的自变量无关
场景 2:DID 研究
- 双向 FE 是 DID 的标准做法
- 控制共同的时间趋势
场景 3:避免虚假相关
- 如果 和 都有上升趋势,可能是因为共同的时间因素
- 时间 FE 消除这种虚假相关
Python 对比:单向 vs 双向 FE
import numpy as np
import pandas as pd
from linearmodels.panel import PanelOLS
# 模拟数据:加入时间趋势
np.random.seed(42)
data = []
for i in range(1, 101):
alpha_i = np.random.normal(0, 1)
for t in range(2010, 2020):
# 时间趋势(影响所有个体)
lambda_t = 0.1 * (t - 2010)
x = 10 + np.random.normal(0, 2)
y = 5 + 2 * x + alpha_i + lambda_t + np.random.normal(0, 1)
data.append({'id': i, 'year': t, 'y': y, 'x': x})
df = pd.DataFrame(data)
df_panel = df.set_index(['id', 'year'])
# 单向 FE(只控制个体)
model_oneway = PanelOLS(df_panel['y'], df_panel[['x']],
entity_effects=True).fit()
# 双向 FE(控制个体 + 时间)
model_twoway = PanelOLS(df_panel['y'], df_panel[['x']],
entity_effects=True,
time_effects=True).fit()
print("=" * 70)
print("单向 FE vs 双向 FE")
print("=" * 70)
print(f"真实参数: 2.0000")
print(f"单向 FE: {model_oneway.params['x']:.4f}")
print(f"双向 FE: {model_twoway.params['x']:.4f}")结论:
- 如果存在时间趋势且未控制,单向 FE 可能有偏
- 双向 FE 同时消除个体和时间效应,更稳健
️ 固定效应的局限性
局限 1:无法估计时不变变量
问题:FE 差分消除了所有不随时间变化的变量
例子:
- 性别(gender)
- 种族(race)
- 出生地(birthplace)
- 行业(industry,如果不换工作)
为什么?
- 差分后:(恒为 0)
- 无法估计其系数
解决方案:
- 使用随机效应(RE)模型(如果 RE 假设成立)
- 研究时变变量与时不变变量的交互效应
- 使用 Mundlak 方法(在 RE 中加入时变变量的均值)
局限 2:只利用组内变异
问题:FE 丢弃了组间变异(between variation)
后果:
- 如果自变量在组内变异很小,FE 估计效率低
- 例如:教育水平在短期内很少变化
示例:
# 计算变异比例
total_var = df['education'].var()
within_var = df.groupby('id')['education'].apply(lambda x: (x - x.mean()).var()).mean()
between_var = df.groupby('id')['education'].mean().var()
print(f"总变异: {total_var:.2f}")
print(f"组内变异: {within_var:.2f} ({within_var/total_var*100:.1f}%)")
print(f"组间变异: {between_var:.2f} ({between_var/total_var*100:.1f}%)")如果组内变异 < 10%:
- FE 估计的标准误会很大(不精确)
- 考虑使用 RE(如果 RE 假设成立)
局限 3:严格外生性假设
假设:
含义:
- 误差项与所有时期的自变量都不相关
- 不仅包括当期 ,还包括过去和未来的
违反的场景:
- 反馈效应(Feedback Effect): 影响
- 同期内生性(Simultaneity): 和 互相影响
- 测量误差(Measurement Error): 被错误测量
解决方案:
- 使用工具变量(IV-FE)
- 使用动态面板模型(Arellano-Bond)
局限 4:坏控制问题(Bad Control)
问题:加入被处理影响的变量作为控制变量
例子:研究教育对工资的影响
# 错误:职业是教育的结果(中介变量)
model = PanelOLS(log_wage, pd.concat([education, occupation]), entity_effects=True).fit()为什么错误?
- 教育 → 职业 → 工资(因果链)
- 控制职业后,阻断了教育的部分效应
- 估计的是教育的直接效应,而非总效应
决策规则:
- 只控制混杂因素(confounders):同时影响 和
- 不控制中介变量(mediators):被 影响,再影响
- 不控制结果变量(colliders):被 和 共同影响
完整案例:Mincer 工资方程
背景
Mincer 方程(1974)是劳动经济学最经典的模型:
解释:
- :教育回报率(每多一年教育,工资增加 )
- :经验的非线性效应(先增后减)
Python 完整实现
import numpy as np
import pandas as pd
from linearmodels.panel import PanelOLS
from statsmodels.iolib.summary2 import summary_col
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
# 模拟真实的 Mincer 数据
np.random.seed(2024)
N = 1000 # 1000 名工人
T = 10 # 10 年
data = []
for i in range(N):
# 个体固定效应(能力)
ability = np.random.normal(0, 0.4)
# 初始特征
education_0 = np.random.choice([10, 12, 14, 16], p=[0.2, 0.3, 0.3, 0.2])
experience_0 = np.random.randint(0, 10)
for t in range(T):
year = 2010 + t
# 教育(可能增加)
education = education_0 + (0.5 if (t > 3 and np.random.rand() < 0.05) else 0)
# 经验
experience = experience_0 + t
# 对数工资(Mincer 方程)
log_wage = (1.8 + 0.08 * education + 0.05 * experience -
0.001 * experience**2 + ability + np.random.normal(0, 0.15))
data.append({
'id': i,
'year': year,
'log_wage': log_wage,
'education': education,
'experience': experience,
'ability': ability
})
df = pd.DataFrame(data)
df['experience_sq'] = df['experience'] ** 2
print("=" * 70)
print("Mincer 工资方程:面板数据分析")
print("=" * 70)
print(f"样本量: {len(df):,}")
print(f"个体数: {df['id'].nunique()}")
print(f"时间跨度: {df['year'].min()} - {df['year'].max()}")
# 设置面板索引
df_panel = df.set_index(['id', 'year'])
# 模型 1:混合 OLS
import statsmodels.api as sm
X1 = sm.add_constant(df[['education', 'experience', 'experience_sq']])
model1 = sm.OLS(df['log_wage'], X1).fit()
# 模型 2:固定效应(单向)
model2 = PanelOLS(df_panel['log_wage'],
df_panel[['education', 'experience', 'experience_sq']],
entity_effects=True).fit(cov_type='clustered',
cluster_entity=True)
# 模型 3:固定效应(双向)
model3 = PanelOLS(df_panel['log_wage'],
df_panel[['education', 'experience', 'experience_sq']],
entity_effects=True,
time_effects=True).fit(cov_type='clustered',
cluster_entity=True)
# 对比结果
print("\n" + "=" * 70)
print("回归结果对比")
print("=" * 70)
results = summary_col([model1, model2, model3],
stars=True,
float_format='%.4f',
model_names=['混合 OLS', '单向 FE', '双向 FE'],
info_dict={
'N': lambda x: f"{int(x.nobs):,}",
'R²': lambda x: f"{x.rsquared:.3f}" if hasattr(x, 'rsquared') else 'N/A'
})
print(results)
# 计算教育回报率
print("\n" + "=" * 70)
print("教育回报率估计")
print("=" * 70)
print(f"真实参数: 8.0%")
print(f"混合 OLS: {model1.params['education']*100:.2f}% (高估)")
print(f"单向 FE: {model2.params['education']*100:.2f}%")
print(f"双向 FE: {model3.params['education']*100:.2f}%")
# 可视化:经验-工资曲线
experience_range = np.linspace(0, 30, 100)
wage_curve = (model2.params['experience'] * experience_range +
model2.params['experience_sq'] * experience_range**2)
plt.figure(figsize=(10, 6))
plt.plot(experience_range, wage_curve * 100, linewidth=3, color='darkblue')
plt.xlabel('工作经验(年)', fontweight='bold', fontsize=12)
plt.ylabel('对数工资变化(%)', fontweight='bold', fontsize=12)
plt.title('经验对工资的边际效应(Mincer 方程)', fontweight='bold', fontsize=14)
plt.grid(alpha=0.3)
plt.axhline(0, color='black', linewidth=0.8, linestyle='--')
plt.tight_layout()
plt.show()
# 计算经验的最优值
optimal_exp = -model2.params['experience'] / (2 * model2.params['experience_sq'])
print(f"\n工资达到峰值的经验年数: {optimal_exp:.1f} 年")输出解读:
- 混合 OLS:高估教育回报率(遗漏能力)
- 单向 FE:控制个体异质性,接近真实值
- 双向 FE:进一步控制时间趋势,最稳健
- 经验曲线:先上升后下降(倒 U 型)
本节小结
核心要点
FE 的本质:通过差分消除不可观测的个体异质性
三种估计方法:
- 组内变换(Within)⭐ 最常用
- LSDV(虚拟变量):等价于组内变换
- 一阶差分: 时等价,否则组内变换更优
单向 vs 双向 FE:
- 单向:控制个体异质性
- 双向:同时控制个体 + 时间趋势
FE 的优势:
- 控制不可观测异质性
- 允许 与 相关
- 因果识别的利器
FE 的局限:
- 无法估计时不变变量
- 只利用组内变异(效率损失)
- 需要严格外生性
实战工具:
linearmodels.PanelOLS- 聚类标准误(必须!)
下一步
在 第4节:随机效应模型 中,我们将学习:
- RE 模型的理论和 GLS 估计
- FE vs RE 的选择(Hausman 检验)
- 何时 RE 比 FE 更优
差分的力量,因果推断的基石!