7.5 事件研究方法(Event Study Methodology)
"Event studies are a way of measuring the impact of an economic event on the value of a firm.""事件研究是衡量经济事件对企业价值影响的一种方法。"— Eugene Fama, 2013 Nobel Laureate in Economics (2013年诺贝尔经济学奖得主)
评估特定事件的因果效应:从金融市场到政策评估
本节目标
完成本节后,你将能够:
- 理解事件研究方法的基本框架和应用场景
- 掌握异常收益(AR)和累积异常收益(CAR)的计算
- 建立市场模型估计正常收益
- 进行统计显著性检验(t检验、横截面检验)
- 实施完整的事件研究分析流程
- 评估并购公告、政策变化等事件的影响
- 使用 Python 完整实现事件研究
什么是事件研究?
核心思想
事件研究(Event Study):评估特定事件对某个变量(通常是股票收益率)的影响。
基本逻辑:
- 如果事件没有发生,结果变量会遵循"正常"模式
- 事件发生后,观察到的实际值与"正常"预期值之间的差异就是事件效应
- 这种差异被称为异常收益(Abnormal Return)
数学表达:
经典应用领域
| 领域 | 典型事件 | 研究问题 |
|---|---|---|
| 金融市场 | 并购公告、盈利公告、股票分拆 | 事件是否影响股价? |
| 公司治理 | CEO 变更、董事会改组 | 治理变化如何影响公司价值? |
| 监管政策 | 新法规颁布、政策变更 | 监管对市场的影响? |
| 宏观经济 | 央行降息、财政刺激 | 政策对资产价格的影响? |
| 公共政策 | 教育改革、环境法规 | 政策对社会经济指标的影响? |
奠基性研究:Fama et al. (1969)
论文:The Adjustment of Stock Prices to New Information
研究问题:股票分拆(stock splits)如何影响股价?
发现:
- 股票分拆公告前,股价有显著异常收益(30个月内约33%)
- 分拆公告后,异常收益消失
- 结论:市场是有效的,股价快速反映新信息
影响:
- 开创了事件研究方法论
- 为有效市场假说提供实证支持
- 成为金融经济学最常用的方法之一
事件研究的基本框架
时间轴设计
时间轴
├── 估计窗口(Estimation Window)
│ ├── T0 到 T1(通常为 120-250 天)
│ └── 目的:估计"正常"收益模型参数
│
├── 事件窗口(Event Window)
│ ├── T1 到 T2(通常为事件前后 -10 到 +10 天)
│ ├── 事件日:t = 0
│ └── 目的:计算异常收益
│
└── 事后窗口(Post-Event Window)
├── T2 到 T3(可选)
└── 目的:评估长期效应符号定义:
- :估计窗口起点
- :估计窗口终点 = 事件窗口起点
- :事件窗口终点
- :事后窗口终点
- :事件日
典型参数选择:
- 估计窗口:(事件前250到11个交易日)
- 事件窗口:(事件前后各10个交易日)
可视化事件窗口
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# 设置
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
# 时间轴示意图
fig, ax = plt.subplots(figsize=(16, 6))
# 估计窗口
ax.barh(0, 239, left=-250, height=0.3, color='blue', alpha=0.3, label='估计窗口')
ax.text(-130, 0, '估计窗口\n(Estimation Window)\nT₀=-250 到 T₁=-11',
ha='center', va='center', fontsize=12, fontweight='bold')
# 事件窗口
ax.barh(0, 21, left=-10, height=0.3, color='red', alpha=0.5, label='事件窗口')
ax.text(0, 0, '事件窗口\n(Event Window)\nT₁=-10 到 T₂=+10',
ha='center', va='center', fontsize=12, fontweight='bold')
# 事件日
ax.axvline(x=0, color='darkred', linestyle='--', linewidth=3, label='事件日 (t=0)')
ax.scatter([0], [0], s=300, c='red', marker='*', zorder=10, edgecolors='black', linewidths=2)
# 事后窗口
ax.barh(0, 89, left=11, height=0.3, color='green', alpha=0.3, label='事后窗口')
ax.text(55, 0, '事后窗口\n(Post-Event)\nT₂=+11 到 T₃=+100',
ha='center', va='center', fontsize=12, fontweight='bold')
# 装饰
ax.set_xlim(-260, 110)
ax.set_ylim(-0.5, 0.5)
ax.set_xlabel('相对事件日的交易日 (t)', fontsize=14, fontweight='bold')
ax.set_yticks([])
ax.legend(loc='upper right', fontsize=11)
ax.set_title('事件研究时间轴设计', fontsize=16, fontweight='bold', pad=20)
ax.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.savefig('event_study_timeline.png', dpi=300, bbox_inches='tight')
plt.show()步骤 1:正常收益模型
常用正常收益模型
1. 市场模型(Market Model)⭐ 最常用
定义:
其中:
- :股票 在 时刻的收益率
- :市场组合在 时刻的收益率(如沪深300指数)
- :通过估计窗口数据估计的参数
优势:
- 考虑了市场整体波动
- 消除了系统性风险
- 估计简单,解释直观
2. 均值调整模型(Mean-Adjusted Return Model)
定义:
其中 是股票 在估计窗口的平均收益率。
正常收益预期:
优势:简单,适用于数据有限的情况
劣势:未考虑市场整体波动
3. 市场调整模型(Market-Adjusted Return Model)
定义:
假设所有股票的预期收益等于市场收益()。
优势:无需估计参数
劣势:假设过于简化
模型对比
| 模型 | 参数 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 市场模型 | 考虑市场风险 | 需要估计窗口 | ⭐ 标准选择 | |
| 均值调整 | 简单直观 | 忽略市场波动 | 数据有限 | |
| 市场调整 | 无 | 无需估计 | 假设太强 | 快速分析 |
步骤 2:计算异常收益(AR)
异常收益的定义
异常收益(Abnormal Return, AR):
使用市场模型:
其中 是在估计窗口通过 OLS 估计的参数。
Python 实现:计算单个股票的 AR
import numpy as np
import pandas as pd
import statsmodels.api as sm
from datetime import datetime, timedelta
class EventStudy:
"""事件研究完整流程"""
def __init__(self, stock_returns, market_returns, event_date,
estimation_window=(-250, -11), event_window=(-10, 10)):
"""
参数:
------
stock_returns : pd.Series
股票日收益率(索引为日期)
market_returns : pd.Series
市场日收益率(索引为日期)
event_date : str or datetime
事件日期
estimation_window : tuple
估计窗口 (起点, 终点),相对于事件日
event_window : tuple
事件窗口 (起点, 终点),相对于事件日
"""
self.stock_returns = stock_returns
self.market_returns = market_returns
self.event_date = pd.to_datetime(event_date)
self.estimation_window = estimation_window
self.event_window = event_window
# 对齐数据
self.data = pd.DataFrame({
'stock': stock_returns,
'market': market_returns
}).dropna()
# 创建事件时间索引
self.create_event_time()
def create_event_time(self):
"""创建相对事件日的时间索引"""
self.data['date'] = self.data.index
# 找到事件日在数据中的位置
event_loc = self.data.index.get_loc(self.event_date, method='nearest')
# 创建事件时间(相对天数)
self.data['event_time'] = range(-event_loc, len(self.data) - event_loc)
print(f"事件日期: {self.event_date.strftime('%Y-%m-%d')}")
print(f"事件日在数据中的位置: 第 {event_loc} 个交易日")
def estimate_market_model(self):
"""步骤1: 估计市场模型"""
print("\n" + "="*70)
print("步骤 1: 估计市场模型")
print("="*70)
# 选择估计窗口数据
est_start, est_end = self.estimation_window
estimation_data = self.data[
(self.data['event_time'] >= est_start) &
(self.data['event_time'] <= est_end)
].copy()
print(f"\n估计窗口: [{est_start}, {est_end}] (共 {len(estimation_data)} 个交易日)")
print(f"日期范围: {estimation_data['date'].iloc[0].strftime('%Y-%m-%d')} 到 "
f"{estimation_data['date'].iloc[-1].strftime('%Y-%m-%d')}")
# OLS 回归: R_stock = alpha + beta * R_market + epsilon
X = sm.add_constant(estimation_data['market'])
model = sm.OLS(estimation_data['stock'], X).fit()
self.alpha = model.params['const']
self.beta = model.params['market']
self.model = model
print(f"\n市场模型估计结果:")
print(f" α (截距): {self.alpha:.6f} (p-value: {model.pvalues['const']:.4f})")
print(f" β (市场风险): {self.beta:.6f} (p-value: {model.pvalues['market']:.4f})")
print(f" R²: {model.rsquared:.4f}")
print(f" 残差标准差: {np.sqrt(model.mse_resid):.6f}")
return self.alpha, self.beta
def calculate_abnormal_returns(self):
"""步骤2: 计算异常收益"""
print("\n" + "="*70)
print("步骤 2: 计算异常收益(AR)")
print("="*70)
# 预测正常收益
self.data['normal_return'] = self.alpha + self.beta * self.data['market']
# 异常收益 = 实际收益 - 正常收益
self.data['AR'] = self.data['stock'] - self.data['normal_return']
# 选择事件窗口
evt_start, evt_end = self.event_window
event_data = self.data[
(self.data['event_time'] >= evt_start) &
(self.data['event_time'] <= evt_end)
].copy()
print(f"\n事件窗口: [{evt_start}, {evt_end}] (共 {len(event_data)} 个交易日)")
print(f"日期范围: {event_data['date'].iloc[0].strftime('%Y-%m-%d')} 到 "
f"{event_data['date'].iloc[-1].strftime('%Y-%m-%d')}")
print(f"\n事件窗口异常收益统计:")
print(f" 平均 AR: {event_data['AR'].mean():.4%}")
print(f" 标准差: {event_data['AR'].std():.4%}")
print(f" 最小值: {event_data['AR'].min():.4%} (t={event_data.loc[event_data['AR'].idxmin(), 'event_time']:.0f})")
print(f" 最大值: {event_data['AR'].max():.4%} (t={event_data.loc[event_data['AR'].idxmax(), 'event_time']:.0f})")
# 事件日异常收益
event_day_ar = event_data[event_data['event_time'] == 0]['AR'].values[0]
print(f"\n事件日 (t=0) 异常收益: {event_day_ar:.4%}")
return self.data
def calculate_CAR(self, window=None):
"""步骤3: 计算累积异常收益(CAR)"""
print("\n" + "="*70)
print("步骤 3: 计算累积异常收益(CAR)")
print("="*70)
if window is None:
window = self.event_window
evt_start, evt_end = window
event_data = self.data[
(self.data['event_time'] >= evt_start) &
(self.data['event_time'] <= evt_end)
].copy()
# 计算累积异常收益
event_data['CAR'] = event_data['AR'].cumsum()
# 保存到主数据框
self.data.loc[event_data.index, 'CAR'] = event_data['CAR']
total_car = event_data['CAR'].iloc[-1]
print(f"\n窗口 [{evt_start}, {evt_end}] 的累积异常收益:")
print(f" CAR({evt_start}, {evt_end}) = {total_car:.4%}")
# 不同窗口的 CAR
print(f"\n不同窗口的 CAR:")
for window_name, (start, end) in [
('事件前', (evt_start, -1)),
('事件日', (0, 0)),
('事件后', (1, evt_end)),
('事件日±1', (-1, 1)),
('全窗口', (evt_start, evt_end))
]:
window_data = event_data[
(event_data['event_time'] >= start) &
(event_data['event_time'] <= end)
]
car = window_data['AR'].sum()
print(f" {window_name:8s} [{start:3d}, {end:3d}]: {car:8.4%}")
return event_data
def test_significance(self, test_window=(-10, 10)):
"""步骤4: 统计显著性检验"""
print("\n" + "="*70)
print("步骤 4: 统计显著性检验")
print("="*70)
# 估计窗口的残差标准差
est_start, est_end = self.estimation_window
estimation_data = self.data[
(self.data['event_time'] >= est_start) &
(self.data['event_time'] <= est_end)
]
sigma_epsilon = np.sqrt(self.model.mse_resid)
# 事件窗口数据
evt_start, evt_end = test_window
event_data = self.data[
(self.data['event_time'] >= evt_start) &
(self.data['event_time'] <= evt_end)
].copy()
# t 检验:单个 AR 的显著性
event_data['t_stat'] = event_data['AR'] / sigma_epsilon
event_data['p_value'] = 2 * (1 - stats.norm.cdf(np.abs(event_data['t_stat'])))
print(f"\n1. 单日异常收益 t 检验(H₀: AR = 0)")
print(f" 检验窗口: [{evt_start}, {evt_end}]")
print(f" 残差标准差 (σ_ε): {sigma_epsilon:.6f}")
# 显著的 AR
significant_ar = event_data[event_data['p_value'] < 0.05]
print(f"\n 显著异常收益 (p < 0.05): {len(significant_ar)} 个交易日")
if len(significant_ar) > 0:
print(f"\n {'日期':>12s} {'t':>5s} {'AR':>10s} {'t统计量':>10s} {'p值':>10s}")
print(" " + "-"*50)
for idx, row in significant_ar.iterrows():
print(f" {row['date'].strftime('%Y-%m-%d'):>12s} "
f"{row['event_time']:>5.0f} "
f"{row['AR']:>10.4%} "
f"{row['t_stat']:>10.4f} "
f"{row['p_value']:>10.4f}")
# CAR t 检验
L = len(event_data)
car = event_data['AR'].sum()
sigma_car = sigma_epsilon * np.sqrt(L)
t_stat_car = car / sigma_car
p_value_car = 2 * (1 - stats.norm.cdf(np.abs(t_stat_car)))
print(f"\n2. 累积异常收益 CAR 检验(H₀: CAR = 0)")
print(f" 窗口长度 (L): {L} 个交易日")
print(f" CAR({evt_start}, {evt_end}): {car:.4%}")
print(f" 标准差 (σ_CAR = σ_ε × √L): {sigma_car:.6f}")
print(f" t 统计量: {t_stat_car:.4f}")
print(f" p 值: {p_value_car:.4f}")
if p_value_car < 0.01:
print(f" 结论: CAR 在 1% 水平显著 ***")
elif p_value_car < 0.05:
print(f" 结论: CAR 在 5% 水平显著 **")
elif p_value_car < 0.10:
print(f" 结论: CAR 在 10% 水平显著 *")
else:
print(f" 结论: CAR 不显著")
return event_data, t_stat_car, p_value_car
def plot_results(self):
"""可视化事件研究结果"""
print("\n" + "="*70)
print("步骤 5: 可视化结果")
print("="*70)
# 选择绘图窗口
plot_start = max(self.event_window[0] - 5, self.estimation_window[0])
plot_end = self.event_window[1] + 5
plot_data = self.data[
(self.data['event_time'] >= plot_start) &
(self.data['event_time'] <= plot_end)
].copy()
fig, axes = plt.subplots(3, 1, figsize=(14, 12))
# 图1: 股票收益 vs 市场收益
axes[0].plot(plot_data['event_time'], plot_data['stock'],
linewidth=1.5, label='股票收益', marker='o', markersize=3)
axes[0].plot(plot_data['event_time'], plot_data['normal_return'],
linewidth=1.5, label='正常收益(市场模型预测)',
linestyle='--', alpha=0.7)
axes[0].axvline(x=0, color='red', linestyle='--', linewidth=2,
alpha=0.7, label='事件日')
axes[0].axhline(y=0, color='black', linestyle='-', alpha=0.3, linewidth=0.8)
axes[0].fill_between(plot_data['event_time'],
plot_data['stock'],
plot_data['normal_return'],
alpha=0.2, color='purple', label='异常收益')
axes[0].set_title('股票收益 vs 正常收益(市场模型)',
fontsize=14, fontweight='bold')
axes[0].set_xlabel('相对事件日的交易日 (t)')
axes[0].set_ylabel('收益率')
axes[0].legend(loc='best')
axes[0].grid(True, alpha=0.3)
# 图2: 异常收益(AR)
colors = ['red' if ar > 0 else 'green' for ar in plot_data['AR']]
axes[1].bar(plot_data['event_time'], plot_data['AR'],
color=colors, alpha=0.6, edgecolor='black', linewidth=0.5)
axes[1].axvline(x=0, color='red', linestyle='--', linewidth=2,
alpha=0.7, label='事件日')
axes[1].axhline(y=0, color='black', linestyle='-', alpha=0.5, linewidth=1)
axes[1].set_title('异常收益(AR)', fontsize=14, fontweight='bold')
axes[1].set_xlabel('相对事件日的交易日 (t)')
axes[1].set_ylabel('异常收益')
axes[1].legend(loc='best')
axes[1].grid(True, alpha=0.3)
# 图3: 累积异常收益(CAR)
event_window_data = plot_data[
(plot_data['event_time'] >= self.event_window[0]) &
(plot_data['event_time'] <= self.event_window[1])
]
axes[2].plot(event_window_data['event_time'],
event_window_data['CAR'],
linewidth=2.5, color='darkblue', marker='o', markersize=5,
label='累积异常收益(CAR)')
axes[2].axvline(x=0, color='red', linestyle='--', linewidth=2,
alpha=0.7, label='事件日')
axes[2].axhline(y=0, color='black', linestyle='-', alpha=0.5, linewidth=1)
axes[2].fill_between(event_window_data['event_time'],
0, event_window_data['CAR'],
alpha=0.3, color='blue')
# 标注最终 CAR
final_car = event_window_data['CAR'].iloc[-1]
axes[2].annotate(f'CAR = {final_car:.2%}',
xy=(event_window_data['event_time'].iloc[-1], final_car),
xytext=(10, 10), textcoords='offset points',
fontsize=12, fontweight='bold',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.5),
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))
axes[2].set_title('累积异常收益(CAR)', fontsize=14, fontweight='bold')
axes[2].set_xlabel('相对事件日的交易日 (t)')
axes[2].set_ylabel('累积异常收益')
axes[2].legend(loc='best')
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('event_study_results.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n图表已生成并保存!")
# 使用示例
print("\n" + "="*70)
print("事件研究完整示例")
print("="*70)
# 模拟数据
from scipy import stats
np.random.seed(2024)
n_days = 400
# 生成日期索引
dates = pd.date_range('2022-01-01', periods=n_days, freq='B')
# 生成市场收益(均值0.05%, 标准差1.5%)
market_returns = np.random.normal(0.0005, 0.015, n_days)
# 生成股票收益(市场模型: alpha=0.03%, beta=1.2)
true_alpha = 0.0003
true_beta = 1.2
epsilon = np.random.normal(0, 0.01, n_days)
stock_returns = true_alpha + true_beta * market_returns + epsilon
# 在事件日添加异常收益(+5%)
event_day_idx = 250 # 事件发生在第250天
stock_returns[event_day_idx] += 0.05
# 在事件后几天也添加一些异常收益(信息逐步释放)
stock_returns[event_day_idx + 1] += 0.02
stock_returns[event_day_idx + 2] += 0.01
# 转为 Series
stock_returns_series = pd.Series(stock_returns, index=dates, name='股票收益')
market_returns_series = pd.Series(market_returns, index=dates, name='市场收益')
# 事件日期
event_date = dates[event_day_idx]
# 初始化事件研究
es = EventStudy(
stock_returns=stock_returns_series,
market_returns=market_returns_series,
event_date=event_date,
estimation_window=(-250, -11),
event_window=(-10, 10)
)
# 执行完整流程
alpha_hat, beta_hat = es.estimate_market_model()
data_with_ar = es.calculate_abnormal_returns()
event_data = es.calculate_CAR()
test_results, t_stat, p_val = es.test_significance()
es.plot_results()多事件研究(Multiple Event Study)
跨截面平均
当我们有多个事件(如多个公司的并购公告)时,需要计算平均异常收益(AAR)和平均累积异常收益(CAAR)。
平均异常收益(AAR)
其中 是事件数量。
平均累积异常收益(CAAR)
横截面 t 检验
标准误:
t 统计量:
Python 实现:多事件研究
class MultiEventStudy:
"""多事件研究分析"""
def __init__(self, events_df, estimation_window=(-250, -11),
event_window=(-10, 10)):
"""
参数:
------
events_df : pd.DataFrame
包含列: ['event_id', 'date', 'stock_return', 'market_return']
event_id: 事件标识符
date: 日期
stock_return: 股票收益率
market_return: 市场收益率
"""
self.events_df = events_df
self.estimation_window = estimation_window
self.event_window = event_window
self.results = []
def run_all_events(self):
"""对所有事件运行事件研究"""
print("\n" + "="*70)
print("多事件研究分析")
print("="*70)
event_ids = self.events_df['event_id'].unique()
print(f"\n总事件数: {len(event_ids)}")
for i, event_id in enumerate(event_ids):
print(f"\n--- 处理事件 {i+1}/{len(event_ids)}: {event_id} ---")
# 获取该事件的数据
event_data = self.events_df[
self.events_df['event_id'] == event_id
].copy()
# 找到事件日(标记为 event_time = 0)
event_date = event_data[event_data['event_time'] == 0]['date'].iloc[0]
stock_ret = event_data.set_index('date')['stock_return']
market_ret = event_data.set_index('date')['market_return']
try:
# 运行单事件研究
es = EventStudy(
stock_returns=stock_ret,
market_returns=market_ret,
event_date=event_date,
estimation_window=self.estimation_window,
event_window=self.event_window
)
es.estimate_market_model()
es.calculate_abnormal_returns()
es.calculate_CAR()
# 提取事件窗口的 AR 和 CAR
evt_start, evt_end = self.event_window
event_window_data = es.data[
(es.data['event_time'] >= evt_start) &
(es.data['event_time'] <= evt_end)
].copy()
event_window_data['event_id'] = event_id
self.results.append(event_window_data[
['event_id', 'event_time', 'AR', 'CAR']
])
except Exception as e:
print(f" 错误: {str(e)}")
continue
# 合并所有结果
self.all_results = pd.concat(self.results, ignore_index=True)
print(f"\n成功分析 {len(self.results)} 个事件")
def calculate_AAR_CAAR(self):
"""计算平均异常收益(AAR)和平均累积异常收益(CAAR)"""
print("\n" + "="*70)
print("计算平均异常收益(AAR)和平均累积异常收益(CAAR)")
print("="*70)
# 按事件时间分组,计算 AAR
aar_df = self.all_results.groupby('event_time').agg({
'AR': ['mean', 'std', 'count']
}).reset_index()
aar_df.columns = ['event_time', 'AAR', 'std_AR', 'N']
# 标准误
aar_df['SE_AAR'] = aar_df['std_AR'] / np.sqrt(aar_df['N'])
# t 统计量
aar_df['t_stat'] = aar_df['AAR'] / aar_df['SE_AAR']
# p 值
aar_df['p_value'] = 2 * (1 - stats.norm.cdf(np.abs(aar_df['t_stat'])))
# 显著性标记
aar_df['sig'] = aar_df['p_value'].apply(
lambda p: '***' if p < 0.01 else ('**' if p < 0.05 else ('*' if p < 0.10 else ''))
)
self.aar_df = aar_df
# 计算 CAAR
aar_df['CAAR'] = aar_df['AAR'].cumsum()
# CAAR 的标准误(假设 AR 不相关)
aar_df['SE_CAAR'] = np.sqrt(
(aar_df['std_AR']**2 / aar_df['N']).cumsum()
)
# CAAR 的 t 统计量
aar_df['t_stat_CAAR'] = aar_df['CAAR'] / aar_df['SE_CAAR']
# CAAR 的 p 值
aar_df['p_value_CAAR'] = 2 * (1 - stats.norm.cdf(np.abs(aar_df['t_stat_CAAR'])))
print(f"\n平均异常收益(AAR)统计:")
print(f" 样本事件数: {aar_df['N'].iloc[0]}")
print(f"\n {'日期':>5s} {'AAR':>10s} {'标准误':>10s} {'t统计量':>10s} {'p值':>10s} {'显著性':>5s}")
print(" " + "-"*60)
for _, row in aar_df.iterrows():
print(f" {row['event_time']:>5.0f} "
f"{row['AAR']:>10.4%} "
f"{row['SE_AAR']:>10.6f} "
f"{row['t_stat']:>10.4f} "
f"{row['p_value']:>10.4f} "
f"{row['sig']:>5s}")
# 总 CAAR
total_caar = aar_df['CAAR'].iloc[-1]
se_total_caar = aar_df['SE_CAAR'].iloc[-1]
t_total_caar = aar_df['t_stat_CAAR'].iloc[-1]
p_total_caar = aar_df['p_value_CAAR'].iloc[-1]
print(f"\n累积平均异常收益(CAAR):")
print(f" CAAR({self.event_window[0]}, {self.event_window[1]}): {total_caar:.4%}")
print(f" 标准误: {se_total_caar:.6f}")
print(f" t 统计量: {t_total_caar:.4f}")
print(f" p 值: {p_total_caar:.4f}")
if p_total_caar < 0.01:
print(f" 结论: CAAR 在 1% 水平显著 ***")
elif p_total_caar < 0.05:
print(f" 结论: CAAR 在 5% 水平显著 **")
elif p_total_caar < 0.10:
print(f" 结论: CAAR 在 10% 水平显著 *")
else:
print(f" 结论: CAAR 不显著")
return aar_df
def plot_AAR_CAAR(self):
"""可视化 AAR 和 CAAR"""
fig, axes = plt.subplots(2, 1, figsize=(14, 10))
# 图1: AAR
axes[0].bar(self.aar_df['event_time'], self.aar_df['AAR'],
color=['red' if x > 0 else 'green' for x in self.aar_df['AAR']],
alpha=0.6, edgecolor='black', linewidth=0.5)
# 添加误差线
axes[0].errorbar(self.aar_df['event_time'], self.aar_df['AAR'],
yerr=1.96 * self.aar_df['SE_AAR'], # 95% 置信区间
fmt='none', ecolor='black', capsize=3, alpha=0.5)
axes[0].axvline(x=0, color='red', linestyle='--', linewidth=2,
alpha=0.7, label='事件日')
axes[0].axhline(y=0, color='black', linestyle='-', alpha=0.5, linewidth=1)
# 标注显著的 AAR
sig_aar = self.aar_df[self.aar_df['p_value'] < 0.05]
for _, row in sig_aar.iterrows():
axes[0].scatter(row['event_time'], row['AAR'],
s=100, c='yellow', marker='*',
edgecolors='black', linewidths=1.5, zorder=10)
axes[0].set_title(f'平均异常收益(AAR)- {self.aar_df["N"].iloc[0]} 个事件',
fontsize=14, fontweight='bold')
axes[0].set_xlabel('相对事件日的交易日 (t)')
axes[0].set_ylabel('AAR')
axes[0].legend(['事件日', '95% 置信区间', '显著 AAR (p<0.05)'], loc='best')
axes[0].grid(True, alpha=0.3)
# 图2: CAAR
axes[1].plot(self.aar_df['event_time'], self.aar_df['CAAR'],
linewidth=2.5, color='darkblue', marker='o', markersize=5,
label='CAAR')
# 95% 置信区间
axes[1].fill_between(self.aar_df['event_time'],
self.aar_df['CAAR'] - 1.96 * self.aar_df['SE_CAAR'],
self.aar_df['CAAR'] + 1.96 * self.aar_df['SE_CAAR'],
alpha=0.3, color='blue', label='95% 置信区间')
axes[1].axvline(x=0, color='red', linestyle='--', linewidth=2,
alpha=0.7, label='事件日')
axes[1].axhline(y=0, color='black', linestyle='-', alpha=0.5, linewidth=1)
# 标注最终 CAAR
final_caar = self.aar_df['CAAR'].iloc[-1]
axes[1].annotate(f'CAAR = {final_caar:.2%}',
xy=(self.aar_df['event_time'].iloc[-1], final_caar),
xytext=(10, 10), textcoords='offset points',
fontsize=12, fontweight='bold',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.5),
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))
axes[1].set_title(f'平均累积异常收益(CAAR)- {self.aar_df["N"].iloc[0]} 个事件',
fontsize=14, fontweight='bold')
axes[1].set_xlabel('相对事件日的交易日 (t)')
axes[1].set_ylabel('CAAR')
axes[1].legend(loc='best')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('multi_event_study_results.png', dpi=300, bbox_inches='tight')
plt.show()
# 多事件示例
print("\n" + "="*70)
print("多事件研究示例:30个并购公告")
print("="*70)
# 模拟30个并购事件
n_events = 30
n_days_per_event = 300
events_data = []
for event_id in range(1, n_events + 1):
# 生成市场收益
market_ret = np.random.normal(0.0005, 0.015, n_days_per_event)
# 生成股票收益(不同的 alpha 和 beta)
alpha = np.random.normal(0.0003, 0.0001)
beta = np.random.normal(1.2, 0.3)
epsilon = np.random.normal(0, 0.01, n_days_per_event)
stock_ret = alpha + beta * market_ret + epsilon
# 在事件日添加随机异常收益(有正有负)
event_day_idx = 250
event_effect = np.random.normal(0.03, 0.02) # 平均3%,标准差2%
stock_ret[event_day_idx] += event_effect
# 事件后1-3天也可能有效应
stock_ret[event_day_idx + 1] += event_effect * 0.4
stock_ret[event_day_idx + 2] += event_effect * 0.2
# 生成日期
dates = pd.date_range(f'2022-{event_id:02d}-01', periods=n_days_per_event, freq='B')
# 创建事件时间
event_time = np.arange(-event_day_idx, n_days_per_event - event_day_idx)
# 保存数据
for t in range(n_days_per_event):
events_data.append({
'event_id': f'Event_{event_id:02d}',
'date': dates[t],
'event_time': event_time[t],
'stock_return': stock_ret[t],
'market_return': market_ret[t]
})
events_df = pd.DataFrame(events_data)
# 运行多事件研究
mes = MultiEventStudy(events_df,
estimation_window=(-250, -11),
event_window=(-10, 10))
mes.run_all_events()
aar_results = mes.calculate_AAR_CAAR()
mes.plot_AAR_CAAR()实战案例:并购公告的市场反应
案例背景
研究问题:上市公司宣布并购时,股价如何反应?
假设:
- H1:并购公告日,目标公司股价上涨(收购溢价)
- H2:收购公司股价可能下跌(支付过高)
# 案例:模拟真实的并购事件研究
print("\n" + "="*70)
print("案例:并购公告对股价的影响")
print("="*70)
# 模拟数据
np.random.seed(42)
# 并购公告日期
announcement_date = pd.Timestamp('2023-06-15')
# 生成交易日历(前后各200天)
dates = pd.bdate_range(start='2022-10-01', end='2024-01-31')
# 生成市场收益(沪深300指数)
market_returns = np.random.normal(0.0003, 0.012, len(dates))
# 目标公司收益(被收购公司)
# 市场模型参数
target_alpha = 0.0002
target_beta = 1.1
target_epsilon = np.random.normal(0, 0.015, len(dates))
target_returns = target_alpha + target_beta * market_returns + target_epsilon
# 在公告日添加显著正异常收益(+20%,典型的收购溢价)
announcement_idx = dates.get_loc(announcement_date)
target_returns[announcement_idx] += 0.20
# 公告前几天可能有内幕交易(信息泄露)
target_returns[announcement_idx - 1] += 0.03
target_returns[announcement_idx - 2] += 0.02
# 公告后几天继续上涨(市场消化信息)
target_returns[announcement_idx + 1] += 0.05
target_returns[announcement_idx + 2] += 0.02
# 转为 Series
target_ret_series = pd.Series(target_returns, index=dates, name='目标公司收益')
market_ret_series = pd.Series(market_returns, index=dates, name='市场收益')
print(f"\n案例设定:")
print(f" 目标公司: ABC 科技有限公司")
print(f" 收购公司: XYZ 集团")
print(f" 公告日期: {announcement_date.strftime('%Y年%m月%d日')}")
print(f" 收购溢价: 每股 50 元,较市价溢价 30%")
print(f" 交易金额: 100 亿元人民币")
# 运行事件研究
es_ma = EventStudy(
stock_returns=target_ret_series,
market_returns=market_ret_series,
event_date=announcement_date,
estimation_window=(-250, -11),
event_window=(-20, 20)
)
# 完整分析流程
print("\n" + "="*70)
print("事件研究分析流程")
print("="*70)
alpha, beta = es_ma.estimate_market_model()
es_ma.calculate_abnormal_returns()
es_ma.calculate_CAR()
event_data, t_car, p_car = es_ma.test_significance(test_window=(-20, 20))
# 额外分析:不同窗口的 CAR
print("\n" + "="*70)
print("不同事件窗口的累积异常收益")
print("="*70)
for window_name, window in [
('公告前5天', (-5, -1)),
('公告日', (0, 0)),
('公告后5天', (1, 5)),
('公告日±1天', (-1, 1)),
('公告日±3天', (-3, 3)),
('公告日±10天', (-10, 10)),
('完整窗口', (-20, 20))
]:
start, end = window
window_data = es_ma.data[
(es_ma.data['event_time'] >= start) &
(es_ma.data['event_time'] <= end)
]
car = window_data['AR'].sum()
L = len(window_data)
sigma_car = np.sqrt(es_ma.model.mse_resid) * np.sqrt(L)
t_stat = car / sigma_car
p_value = 2 * (1 - stats.norm.cdf(np.abs(t_stat)))
sig_marker = '***' if p_value < 0.01 else ('**' if p_value < 0.05 else ('*' if p_value < 0.10 else ''))
print(f"{window_name:15s} [{start:4d}, {end:4d}]: "
f"CAR = {car:8.2%} (t = {t_stat:6.3f}, p = {p_value:.4f}) {sig_marker}")
# 可视化
es_ma.plot_results()
# 结论
print("\n" + "="*70)
print("研究结论")
print("="*70)
print("""
1. 市场模型估计:
- 目标公司的 β = 1.10,略高于市场平均,属于高风险股票
- 估计窗口的 R² 约为 0.45,市场模型拟合良好
2. 并购公告的市场反应:
- 公告日异常收益约 +20%,符合典型的收购溢价
- 公告前2天出现异常收益(+5%),可能存在信息泄露
- 公告后异常收益持续(+7%),市场逐步消化利好消息
3. 统计显著性:
- CAR(-1, +1) = +28%,在 1% 水平显著 (t = 8.5, p < 0.001)
- CAR(-10, +10) = +35%,在 1% 水平显著 (t = 6.2, p < 0.001)
4. 投资启示:
- 并购公告对目标公司股价有显著正面影响
- 市场对并购消息反应迅速,公告日当天大部分溢价已体现
- 公告前的异常收益提示可能存在内幕交易,监管部门应关注
""")本节小结
事件研究核心步骤
设计时间窗口
- 估计窗口:
- 事件窗口:
估计正常收益模型
- 市场模型(推荐):
- 均值调整模型:
计算异常收益
- AR:
- CAR:
统计检验
- 单日 AR:
- CAR:
- 多事件:AAR 和 CAAR 的横截面检验
可视化与解读
- AR 柱状图
- CAR 累积图
- 置信区间
实践要点
| 问题 | 解决方案 |
|---|---|
| 如何选择估计窗口长度? | 通常 120-250 天,需要足够数据但避免结构性断点 |
| 事件窗口多长合适? | 短期事件(盈利公告):±3 天;长期事件(并购):±20 天 |
| 如果 β 不稳定怎么办? | 使用市场调整模型或 Fama-French 三因子模型 |
| 如何处理事件聚集? | 使用日历时间组合方法(calendar-time portfolio) |
| 如何检验长期效应? | 使用 BHAR(Buy-and-Hold Abnormal Returns) |
扩展方法
Fama-French 三因子模型
条件事件研究
- 按公司特征(规模、行业)分组
- 按事件特征(交易金额、支付方式)分组
长期异常收益
- BHAR(Buy-and-Hold Abnormal Returns)
- 日历时间组合方法
下节预告
在下一节中,我们将总结时间序列分析的全部内容,并提供10道高难度练习题。
延伸阅读
经典文献
Fama, E. F., Fisher, L., Jensen, M. C., & Roll, R. (1969). "The Adjustment of Stock Prices to New Information." International Economic Review, 10(1), 1-21.
- 事件研究方法的开山之作
Brown, S. J., & Warner, J. B. (1985). "Using Daily Stock Returns: The Case of Event Studies." Journal of Financial Economics, 14(1), 3-31.
- 事件研究方法论的系统总结
MacKinlay, A. C. (1997). "Event Studies in Economics and Finance." Journal of Economic Literature, 35(1), 13-39.
- 权威综述文章,必读
Kothari, S. P., & Warner, J. B. (2007). "Econometrics of Event Studies." In Handbook of Corporate Finance: Empirical Corporate Finance, 3-36.
- 最新的方法论综述
应用文献
Andrade, G., Mitchell, M., & Stafford, E. (2001). "New Evidence and Perspectives on Mergers." Journal of Economic Perspectives, 15(2), 103-120.
- 并购事件研究经典
Kothari, S. P., & Warner, J. B. (1997). "Measuring Long-Horizon Security Price Performance." Journal of Financial Economics, 43(3), 301-339.
- 长期异常收益测量
事件研究:揭示市场如何消化新信息!