13  多重检验(Multiple Testing)

13.1 导读

在前面的章节中,我们主要关注估计和预测。本章转向同时检验多个假设时的错误控制。以量化因子和行业动量为背景,我们比较 FWER 与 FDR,推导 Bonferroni、Holm、BH 与 BY 方法,并说明依赖结构、效应量和独立复现为何与校正后的 p 值同样重要。

13.2 学习目标

完成本章后,学生应能:

  1. 给定真值与拒绝集合计算 \(V,R\)、FWER、FDR 与功效,并准确区分概率、期望与单次实现。
  2. 手工执行 Bonferroni、Holm、BH 与 BY,并说明各自控制目标和依赖条件。
  3. 预注册检验族、效应估计与信息时点,记录计划/实际检验数及所有跳过理由。
  4. 为面板或重叠时序选择合适的依赖稳健推断;公司/年份双聚类、HAC 与时间块重抽样的完整推导列为拓展。
  5. 完成行业动量检验的稳健标准误、多重校正和独立时期复现,不把显著性等同于可交易性。
先修自检

20 个独立真零假设各按 \(\alpha=0.05\) 检验,期望假阳性数为 1;“至少一次假阳性”的概率为 \(1-0.95^{20}\approx0.642\),并不等于 1。若混淆期望计数与事件概率,可复习 式 13.1

多重检验问题

假设你要测试 1000 个量化因子是否能预测股票收益率。如果每个检验使用显著性水平\(\alpha = 0.05\),即使所有因子实际上都没有预测能力,我们仍然期望约\(1000 \times 0.05 = 50\) 个因子被错误地认定为有效。

这就是多重检验问题(multiple testing problem):当我们同时进行大量假设检验时,犯第一类错误的概率会显著增加。

13.3 假设检验回顾

13.3.1 基本概念

假设检验通常包含四个步骤:

  1. 设定假设

    • 零假设\(H_0\)(默认状态)
    • 备择假设 \(H_a\)(与零假设对照的事前研究方向)
  2. 构造检验统计量:总结反对 \(H_0\) 的证据强度

  3. 计算 p 值:在 \(H_0\) 成立的条件下,获得至少与当前观察一样极端的结果的概率

  4. 做出决策:基于p 值决定是否拒绝\(H_0\)

统计检验只作“拒绝 \(H_0\)”或“不拒绝 \(H_0\)”的决定;前者表示当前设计下存在反对 \(H_0\) 的证据,后者表示证据不足,二者都不给任何命题判定真值。

13.3.2 两类错误

\(H_0\) 为真 \(H_0\) 为假
拒绝 \(H_0\) 第一类错误(Type I Error) 正确决策
不拒绝\(H_0\) 正确决策 第二类错误(Type II Error)
  • 第一类错误率:\(\alpha = \Pr(\text{拒绝 } H_0 | H_0 \text{ 为真})\)
  • 第二类错误率:\(\beta = \Pr(\text{不拒绝} H_0 | H_0 \text{ 为假})\)
  • 检验的功效(power):\(1 - \beta\)

13.4 多重检验的问题

13.4.1 问题阐述

假设我们要同时检验\(m\) 个零假设 \(H_{01}, H_{02}, \ldots, H_{0m}\)

\(V\) 为错误拒绝的零假设的数量(假阳性,false positives),\(R\) 为总的拒绝数量。

\(m_0\) 个真零假设的 p 值相互独立,且每个检验都有精确的第一类错误率 \(\alpha\),则

\[ \Pr(\text{至少犯一次第一类错误}) = 1 - (1 - \alpha)^{m_0}. \tag{13.1}\]

在“全部零假设都为真”的例子中 \(m_0=m\)。不独立时上式不一定成立;不需要独立性的通用上界是并集界 \(\mathrm{FWER}\le m_0\alpha\le m\alpha\)

直观例子

假设你测试 100 个相互独立且全部为真的零假设,每个使用 \(\alpha = 0.05\)

  • 单个检验的犯第一类错误概率:5%
  • 至少一次犯错的概率\(1 - (1 - 0.05)^{100} \approx 99.4\%\)

这意味着几乎肯定会有假阳性发现!

13.4.2 中国案例:股票因子检验

在量化金融中,研究者经常测试大量潜在的交易因子。让我们模拟这个问题。

在量化投资的因子挖掘(Alpha Research)过程中,反复筛选海量股票基本面和量价特征会提高偶然出现亮眼历史回测的机会。一次研究可能找到若干候选,也可能一个都没有;关键问题是这些结果能否在预注册的独立样本中复现。 下面的机制实验令 1000 个零假设全部成立,并从标准正态分布生成检验统计量。它用于展示在单项显著性水平为 0.05 时,多次检验为何会产生偶然拒绝;这些随机数不是交易因子数据,也不能用来评价真实策略收益。 当 1000 个真零假设各按 \(\alpha=0.05\) 检验时,假阳性数的期望是 50,但单次模拟的实现值会波动。把这些偶然拒绝当作候选会提高实盘失效风险;它本身既不决定具体策略的损益,也不替代交易成本和独立样本评价。

表 13.1: 多重检验问题演示:股票因子测试
import numpy as np  # 生成检验统计量并计算家族错误指标
import pandas as pd  # 组织后续校正结果的表格结构
import matplotlib.pyplot as plt  # 支持本章错误率与拒绝边界可视化
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'Arial Unicode MS']  # 保证图中中文方法名可读
plt.rcParams['axes.unicode_minus'] = False  # 避免收益与统计量负号显示为方框
from scipy import stats  # 将检验统计量转换为双侧或单侧p值

# 固定机制演示的随机流以便学生复算假阳性数量
np.random.seed(42)  # 固定随机种子以保证结果可复现

# 模拟测试 1000 个股票因子
test_count = 1000  # 检验数量
significance_level = 0.05  # 显著性水平

# 情况1:所有零假设都为真(没有真实因子)
# 生成检验统计量(标准正态分布)
null_test_statistics = np.random.normal(0, 1, test_count)  # 从标准正态分布抽取1000个检验统计量
null_p_values = 2 * (1 - stats.norm.cdf(np.abs(null_test_statistics)))  # 计算双侧检验的p值

# 统计显著的结果
significant_null_count = np.sum(null_p_values < significance_level)  # 统计p值低于阈值的假阳性数

print('多重检验问题演示')  # 标明输出对应全零假设机制实验
print('='*60)  # 分隔实验设定与错误率结果
print(f'总检验数量: {test_count}')  # 报告决定家族风险累积的检验规模
print(f'显著性水平: {significance_level}')  # 报告每项检验采用的未校正阈值
print(f'预期的假阳性数量: {test_count * significance_level:.1f}')  # 给出全零家族的理论误拒期望
print(f'实际观察到的假阳性数量: {significant_null_count}')  # 对照单次模拟实现值与理论期望
print(f'假阳性率: {significant_null_count / test_count:.2%}')  # 报告单次误拒占家族比例
print(f'至少一次犯错的概率: {1 - (1-significance_level)**test_count:.2%}')  # 报告独立全零设定下的理论FWER
多重检验问题演示
============================================================
总检验数量: 1000
显著性水平: 0.05
预期的假阳性数量: 50.0
实际观察到的假阳性数量: 43
假阳性率: 4.30%
至少一次犯错的概率: 100.00%

重复模拟在下一块估计长期错误率;它复用上块已经定义的检验数与显著性水平。

# 用重复实验估计长期错误率;单次实验中的 0/1 指示量不是“错误率”本身
simulation_repetitions = 2000  # 用足够重复次数稳定估计长期错误率
simulation_rng = np.random.default_rng(2025)  # 使用独立随机流保证重复模拟可复现
minimum_p_values = np.empty(simulation_repetitions)  # 保存每次检验族的最小p值以估计FWER
false_discovery_proportions = np.empty(simulation_repetitions)  # 保存全零家族每次实现的FDP
for repetition in range(simulation_repetitions):  # 重复生成相互独立的全零检验族
    simulated_p_values = simulation_rng.uniform(size=test_count)  # 全部零假设为真
    rejected = simulated_p_values < significance_level  # 按未校正阈值形成单次拒绝集合
    minimum_p_values[repetition] = simulated_p_values.min()  # 记录是否至少发生一次误拒
    false_discovery_proportions[repetition] = 1.0 if rejected.any() else 0.0  # 全零家族有拒绝时FDP等于1

empirical_fwer = np.mean(minimum_p_values < significance_level)  # 用至少一次误拒的频率估计FWER
empirical_fdr = np.mean(false_discovery_proportions)  # 用重复实验FDP均值估计FDR
print(f'{simulation_repetitions} 次重复模拟的经验 FWER: {empirical_fwer:.3f}')  # 报告家族层面误拒风险
print(f'全零假设下的经验 FDR: {empirical_fdr:.3f}')  # 核对全零情形FDR与FWER相等
2000 次重复模拟的经验 FWER: 1.000
全零假设下的经验 FDR: 1.000

单次运行中的假阳性个数会随随机种子变化;其期望为 \(m\alpha=50\)。重复模拟估计的是长期 FWER,结果应接近理论值 \(1-(1-\alpha)^m\)。在全零假设下,只要发生拒绝就有 \(V/R=1\),因此经验 FDR 与经验 FWER 相等;这并不表示任意混合真值情形下二者都相等。

13.4.3 错误度量

在多重检验中,我们需要定义新的错误度量:

符号 描述 频期
\(m\) 总检验数量 固定
\(m_0\) 真实零假设的数量 未知
\(V\) 假阳性数量(Type I error) 随机
\(S\) 真阳性数量(正确拒绝) 随机
\(R\) 总拒绝数量= \(V + S\) 随机
\(T\) 假阴性数量(Type II error) 随机
\(U\) 真阴性数量(正确不拒绝) 随机

13.4.4 FWER 与FDR

13.4.4.1 族错误率(Family-Wise Error Rate, FWER)

\[ \text{FWER} = \Pr(V \geq 1) \tag{13.2}\]

这是至少发生一次误拒的概率。FWER 控制的是家族层面的“至少一次”事件,目标与 FDR 不同,不能脱离误拒代价和研究目的笼统评价其保守程度。

13.4.4.2 错误发现率(False Discovery Rate, FDR)

\[ \operatorname{FDP}=\frac{V}{\max(R,1)},\qquad \operatorname{FDR}=E(\operatorname{FDP}) =E\left[\frac{V}{\max(R,1)}\right]. \tag{13.3}\]

分母使用 \(\max(R,1)\),因此没有拒绝时 \(\operatorname{FDP}=0\)

FWER vs FDR

FWER(族错误率)

  • 控制任何假阳性的概率
  • 当研究目标要求限制“至少一次误拒”的概率时使用;具体程序还需结合依赖结构与功效要求
  • Bonferroni 校正控制 FWER

FDR(错误发现率)

  • 定义为 \(\operatorname{FDR}=E[V/\max(R,1)]\),即单次错误发现比例 FDP 的重复抽样期望
  • 只有输入 p 值本身有效且程序的依赖条件成立时,才具有相应控制保证
  • BH 在独立或 PRDS 等正依赖条件下控制 FDR;任意依赖时可考虑 BY
  • 研究规模不能替代错误控制目标和依赖条件的判断

选择建议

  • 需要严格控制错误:使用 FWER
  • 关注拒绝集合中错误发现风险:在核对单项 p 值与依赖条件后选择相应 FDR 程序

13.5 Bonferroni 校正

Bonferroni 是最简单的单步 FWER 校正之一;它允许任意依赖,但相对逐步程序可能损失功效,不能脱离候选方法和检验结构笼统称为“最保守”。

13.5.1 方法原理

为了控制 FWER 在水平\(\alpha\),我们使用调整后的显著性水平:

\[ \alpha_{\text{adjusted}} = \frac{\alpha}{m} \tag{13.4}\]

或者等价地,将 p 值乘以\(m\)

\[ p_{\text{adjusted}} = \min(m \times p_{\text{original}}, 1) \tag{13.5}\]

13.5.2 中国案例:基于多重检验的强势股筛选(Bonferroni 校正)

下面用 Bonferroni 演示“检验数量增加会抬高至少一次误拒风险”。estimand 明确定义为 2020 年个股绝对日均收益率;它没有减市场、行业或无风险基准,因此不称为相对或超额收益。每只股票先用 HAC 标准误处理日序列相关和异方差,再对这些单项 p 值实施 Bonferroni。 右图把每个检验的阈值改为 \(0.05/m\),所以拒绝数通常下降。真实市场样本中不知道哪些零假设为真,点的颜色不能称作“真 Alpha”或“假阳性”;Bonferroni 的功效代价必须在预先注入真值的重复模拟中评估。

import pandas as pd  # 整理逐股票HAC检验与校正证据
import numpy as np  # 抽样股票并构造家族p值向量
import matplotlib.pyplot as plt  # 对照未校正与Bonferroni边界
from scipy import stats  # 科学计算统计模块
import statsmodels.api as sm  # 计算个股均值的HAC标准误
import os  # 将在线教材的固定数据根同步给本章后续独立代码块

np.random.seed(123)  # 设置随机种子以保证抽样结果可复现

from pathlib import Path  # 使用跨平台路径对象解析显式数据根
BOOK_DATA_DIR = Path('/home/ubuntu/r2_data_mount/data').resolve()  # 明文定义在线教材的BOOK_DATA_DIR绝对路径
DATA_DIR = BOOK_DATA_DIR  # 保留本章后续代码使用的数据根名称
os.environ['BOOK_DATA_DIR'] = str(BOOK_DATA_DIR)  # 为本章后续独立代码块登记同一数据根
if not DATA_DIR.is_dir():  # 在读取前验证数据根
    raise FileNotFoundError(f'BOOK_DATA_DIR 不存在或不是目录: {DATA_DIR}')  # 失败即停止,不回退固定路径
path_price = DATA_DIR / 'stock' / 'stock_price_post_adjusted.h5'  # 构建后复权股价文件路径
if not path_price.is_file():  # 在HDF读取前核验本案例的精确输入
    raise FileNotFoundError(f'缺少后复权行情文件: {path_price};请检查 BOOK_DATA_DIR')  # 报告缺失路径便于修复挂载

确认数据位置后,只读取案例所需的年份与字段,再在公司内部按日期构造收益率。

stock_price_history = pd.read_hdf(  # 在 HDF 存储层只读取 2020 年真实行情
    path_price,  # 指定后复权股价文件
    where="date>='2020-01-01' & date<'2021-01-01'",  # 固定案例估计年份并下推日期过滤
    columns=['date', 'order_book_id', 'close'],  # 只读取 HAC 均值检验所需字段
).reset_index()  # 恢复日期与证券索引列

# HDF 查询已固定为 2020 年;保留独立副本供收益构造
daily_stock_data = stock_price_history.copy()  # 使用存储层筛选后的2020年全年数据

# 计算每只股票的日收益率
daily_stock_data = daily_stock_data.sort_values(['order_book_id', 'date'])  # 按股票代码和日期排序
daily_stock_data['ret'] = daily_stock_data.groupby('order_book_id')['close'].pct_change()  # 按股票分组计算日收益率

# 获取至少 100天交易数据的有效股票
valid_stock_codes = daily_stock_data.groupby('order_book_id')['ret'].count()  # 统计每只股票的有效交易日数
valid_stock_codes = valid_stock_codes[valid_stock_codes >= 100].index.tolist()  # 筛选交易日不少于100天的股票

target_stock_count = 500  # 设定抽样目标数量为500只
assert len(valid_stock_codes) >= target_stock_count, f'有效股票仅{len(valid_stock_codes)}只,不足500只'  # 检查抽样支持域
sampled_stock_codes = np.random.choice(valid_stock_codes, target_stock_count, replace=False)  # 无放回随机抽样500只股票

对每只抽样股票进行单样本 t 检验,检验其2020年日均收益率是否显著大于零。

# 进行HAC稳健均值检验(H0: mean <= 0, H1: mean > 0)
calculated_p_values = []  # 保留完整抽样股票族的单项p值
strong_stock_records = []  # 同步保留效应、SE、带宽和样本量

for stock_code in sampled_stock_codes:  # 遍历每只抽样股票
    stock_returns = daily_stock_data.loc[daily_stock_data['order_book_id'] == stock_code, 'ret'].dropna()  # 获取该股票的日收益率序列
    hac_lags = max(1, int(np.sqrt(len(stock_returns))))  # 事前经验带宽规则
    mean_model = sm.OLS(stock_returns.to_numpy(), np.ones((len(stock_returns), 1))).fit(cov_type='HAC', cov_kwds={'maxlags': hac_lags})  # 稳健均值模型
    t_statistic = mean_model.params[0] / mean_model.bse[0]  # HAC t统计量
    p_value = stats.norm.sf(t_statistic)  # 正向单尾渐近p值
    calculated_p_values.append(p_value)  # 存储该股票的p值
    strong_stock_records.append({'order_book_id': stock_code, 'estimand': 'absolute mean daily return', 'estimate': mean_model.params[0], 'se': mean_model.bse[0], 'hac_lags': hac_lags, 'n': len(stock_returns), 'p_value': p_value})  # 保存单项推断证据

calculated_p_values = np.array(calculated_p_values)  # 统一为向量以便执行家族校正
# 颜色只标出原始 p 值较小的观测,不把同一 p 值反过来当作“真实效应”标签
small_p_highlight = calculated_p_values < 0.01  # 固定描述性高亮规则而不构造真值标签

# 未校正的结果
significance_level = 0.05  # 设定显著性水平为5%
uncorrected_significant_flags = calculated_p_values < significance_level  # 未校正下的显著标记

# Bonferroni 校正
bonferroni_significance_level = significance_level / target_stock_count  # Bonferroni校正后的阈值 = α/m
bonferroni_significant_flags = calculated_p_values < bonferroni_significance_level  # Bonferroni校正后的显著标记
strong_stock_table = pd.DataFrame(strong_stock_records)  # 形成planned family完整单项证据
strong_stock_table['bonferroni_adjusted_p'] = np.minimum(strong_stock_table['p_value'] * target_stock_count, 1.0)  # 任意依赖FWER校正
print(strong_stock_table.to_string(index=False))  # 输出基准、estimand、HAC SE、带宽与校正p值
order_book_id                   estimand      estimate       se  hac_lags   n  p_value  bonferroni_adjusted_p
  000401.XSHE absolute mean daily return -3.591155e-04 0.001488        15 242 0.595372               1.000000
  300128.XSHE absolute mean daily return  1.203349e-04 0.002125        15 242 0.477425               1.000000
  600713.XSHG absolute mean daily return  4.033885e-05 0.000835        15 242 0.480741               1.000000
  600963.XSHG absolute mean daily return  1.080124e-03 0.001352        15 242 0.212102               1.000000
  600681.XSHG absolute mean daily return -7.974885e-04 0.000868        15 242 0.820875               1.000000
  002542.XSHE absolute mean daily return -6.461254e-04 0.001071        15 242 0.726800               1.000000
  600114.XSHG absolute mean daily return  1.584375e-04 0.002408        15 242 0.473768               1.000000
  300648.XSHE absolute mean daily return  5.024693e-03 0.002737        15 242 0.033166               1.000000
  300010.XSHE absolute mean daily return -5.262363e-04 0.002651        15 242 0.578669               1.000000
  601229.XSHG absolute mean daily return -5.628369e-04 0.000695        15 242 0.790869               1.000000
  600210.XSHG absolute mean daily return  1.029225e-03 0.001633        15 242 0.264203               1.000000
  300409.XSHE absolute mean daily return  1.006353e-03 0.001595        15 242 0.264052               1.000000
  300627.XSHE absolute mean daily return  2.141917e-03 0.001790        15 242 0.115781               1.000000
  300328.XSHE absolute mean daily return  3.823571e-06 0.001949        15 242 0.499218               1.000000
  600738.XSHG absolute mean daily return  1.445969e-03 0.001945        15 242 0.228625               1.000000
  300191.XSHE absolute mean daily return -6.784942e-04 0.001490        15 242 0.675523               1.000000
  601588.XSHG absolute mean daily return -1.210050e-03 0.001061        15 242 0.872923               1.000000
  300399.XSHE absolute mean daily return  1.483913e-03 0.002416        15 242 0.269572               1.000000
  603607.XSHG absolute mean daily return  2.729615e-05 0.001288        15 242 0.491549               1.000000
  600744.XSHG absolute mean daily return  1.082832e-03 0.001339        15 242 0.209427               1.000000
  000761.XSHE absolute mean daily return -7.916615e-04 0.000889        15 242 0.813354               1.000000
  600704.XSHG absolute mean daily return -4.014900e-04 0.000829        15 242 0.686010               1.000000
  600097.XSHG absolute mean daily return  4.161159e-04 0.001357        15 242 0.379534               1.000000
  002090.XSHE absolute mean daily return -7.688312e-04 0.001392        15 242 0.709676               1.000000
  002778.XSHE absolute mean daily return  8.003395e-04 0.001690        15 242 0.317865               1.000000
  002986.XSHE absolute mean daily return -2.830167e-03 0.001532        12 144 0.967640               1.000000
  600463.XSHG absolute mean daily return -3.331151e-04 0.001420        15 242 0.592719               1.000000
  603768.XSHG absolute mean daily return  1.841817e-03 0.001839        15 242 0.158305               1.000000
  603335.XSHG absolute mean daily return  2.826230e-05 0.001193        15 242 0.490554               1.000000
  300379.XSHE absolute mean daily return  5.721544e-04 0.001822        15 242 0.376733               1.000000
  000981.XSHE absolute mean daily return  6.711423e-04 0.002015        15 242 0.369515               1.000000
  300757.XSHE absolute mean daily return  1.171916e-03 0.002036        15 242 0.282475               1.000000
  603110.XSHG absolute mean daily return  2.743017e-03 0.001959        15 242 0.080724               1.000000
  600956.XSHG absolute mean daily return  7.383563e-03 0.007163        11 127 0.151318               1.000000
  000156.XSHE absolute mean daily return -3.110583e-04 0.001419        15 242 0.586758               1.000000
  601615.XSHG absolute mean daily return  2.171258e-03 0.001632        15 242 0.091649               1.000000
  300713.XSHE absolute mean daily return  2.597997e-03 0.003293        15 242 0.215077               1.000000
  000417.XSHE absolute mean daily return  2.770345e-04 0.001500        15 242 0.426718               1.000000
  601177.XSHG absolute mean daily return -4.272292e-04 0.001040        15 242 0.659348               1.000000
  600580.XSHG absolute mean daily return  1.597313e-03 0.001579        15 242 0.155933               1.000000
  300106.XSHE absolute mean daily return  3.976395e-03 0.003282        15 242 0.112829               1.000000
  002342.XSHE absolute mean daily return  1.600270e-03 0.001457        15 242 0.135977               1.000000
  600566.XSHG absolute mean daily return -3.210113e-04 0.001156        15 242 0.609416               1.000000
  002017.XSHE absolute mean daily return -1.805874e-03 0.001425        15 242 0.897440               1.000000
  300342.XSHE absolute mean daily return  2.063899e-03 0.003438        15 242 0.274132               1.000000
  002405.XSHE absolute mean daily return -1.369082e-04 0.001876        15 242 0.529087               1.000000
  300330.XSHE absolute mean daily return  4.542328e-04 0.001573        15 242 0.386412               1.000000
  600615.XSHG absolute mean daily return -1.840772e-03 0.001535        15 242 0.884805               1.000000
  601598.XSHG absolute mean daily return  5.293066e-04 0.001382        15 242 0.350891               1.000000
  002980.XSHE absolute mean daily return  5.320838e-03 0.005142        13 175 0.150380               1.000000
  603656.XSHG absolute mean daily return  1.793979e-04 0.000983        15 242 0.427600               1.000000
  002338.XSHE absolute mean daily return  2.234847e-03 0.002847        15 242 0.216227               1.000000
  603199.XSHG absolute mean daily return -5.484987e-04 0.000948        15 242 0.718569               1.000000
  300413.XSHE absolute mean daily return  3.167188e-03 0.001774        15 242 0.037131               1.000000
  002133.XSHE absolute mean daily return -8.227104e-05 0.000852        15 242 0.538463               1.000000
  300278.XSHE absolute mean daily return  1.415982e-03 0.003097        15 242 0.323770               1.000000
  600790.XSHG absolute mean daily return  2.768308e-04 0.000845        15 242 0.371640               1.000000
  000060.XSHE absolute mean daily return  7.630235e-04 0.001397        15 242 0.292421               1.000000
  603606.XSHG absolute mean daily return  3.822702e-03 0.001980        15 242 0.026770               1.000000
  600635.XSHG absolute mean daily return -4.715427e-04 0.001201        15 242 0.652664               1.000000
  002927.XSHE absolute mean daily return -6.157136e-04 0.001352        15 242 0.675549               1.000000
  300358.XSHE absolute mean daily return  2.873515e-03 0.002437        15 242 0.119201               1.000000
  300160.XSHE absolute mean daily return  4.709579e-03 0.004593        15 242 0.152603               1.000000
  002071.XSHE absolute mean daily return -4.673999e-03 0.003277        15 242 0.923112               1.000000
  603131.XSHG absolute mean daily return  3.902909e-03 0.002744        15 242 0.077492               1.000000
  603566.XSHG absolute mean daily return  9.275016e-04 0.001750        15 242 0.298029               1.000000
  600613.XSHG absolute mean daily return -1.275584e-03 0.001561        15 242 0.793021               1.000000
  603027.XSHG absolute mean daily return  4.236308e-03 0.001839        15 242 0.010612               1.000000
  603890.XSHG absolute mean daily return  2.337644e-03 0.002230        15 242 0.147234               1.000000
  002137.XSHE absolute mean daily return -2.644342e-04 0.001541        15 242 0.568126               1.000000
  300588.XSHE absolute mean daily return  6.382411e-04 0.002155        15 242 0.383533               1.000000
  600309.XSHG absolute mean daily return  2.469454e-03 0.001588        15 242 0.059936               1.000000
  002685.XSHE absolute mean daily return -1.718488e-03 0.002072        15 242 0.796555               1.000000
  688157.XSHG absolute mean daily return  7.581701e-05 0.002714        11 139 0.488857               1.000000
  601866.XSHG absolute mean daily return  9.532978e-04 0.001801        15 242 0.298250               1.000000
  000563.XSHE absolute mean daily return -3.809174e-04 0.001386        15 242 0.608253               1.000000
  300707.XSHE absolute mean daily return  2.390066e-04 0.002193        15 242 0.456604               1.000000
  603533.XSHG absolute mean daily return  3.717542e-03 0.003524        15 242 0.145711               1.000000
  300019.XSHE absolute mean daily return  3.172761e-03 0.002499        15 242 0.102150               1.000000
  300559.XSHE absolute mean daily return -1.078230e-03 0.001498        15 242 0.764139               1.000000
  300442.XSHE absolute mean daily return  4.055414e-03 0.002154        15 242 0.029897               1.000000
  601127.XSHG absolute mean daily return  2.513832e-03 0.003286        15 242 0.222104               1.000000
  002004.XSHE absolute mean daily return  8.409403e-04 0.001007        15 242 0.201855               1.000000
  002621.XSHE absolute mean daily return -1.160093e-03 0.001477        15 242 0.783942               1.000000
  603390.XSHG absolute mean daily return -1.962459e-03 0.001587        15 242 0.891909               1.000000
  002531.XSHE absolute mean daily return  1.534227e-03 0.001683        15 242 0.180942               1.000000
  603725.XSHG absolute mean daily return  4.354255e-05 0.001347        15 242 0.487105               1.000000
  002318.XSHE absolute mean daily return  1.063753e-03 0.001511        15 242 0.240694               1.000000
  600649.XSHG absolute mean daily return -7.140233e-05 0.001128        15 242 0.525246               1.000000
  600600.XSHG absolute mean daily return  3.145115e-03 0.001680        15 242 0.030635               1.000000
  300375.XSHE absolute mean daily return  2.183475e-03 0.002207        15 242 0.161290               1.000000
  300319.XSHE absolute mean daily return -8.764320e-04 0.001985        15 242 0.670576               1.000000
  000780.XSHE absolute mean daily return  9.644144e-04 0.001538        15 242 0.265340               1.000000
  600348.XSHG absolute mean daily return  5.573836e-04 0.001140        15 242 0.312503               1.000000
  600356.XSHG absolute mean daily return  4.756505e-04 0.001167        15 242 0.341804               1.000000
  000816.XSHE absolute mean daily return  4.627934e-03 0.004112        15 242 0.130179               1.000000
  300301.XSHE absolute mean daily return  2.022099e-03 0.004740        15 242 0.334838               1.000000
  002959.XSHE absolute mean daily return  3.711148e-03 0.002336        15 242 0.056104               1.000000
  300653.XSHE absolute mean daily return  1.158832e-03 0.001658        15 242 0.242353               1.000000
  002016.XSHE absolute mean daily return -8.524508e-04 0.001114        15 242 0.777978               1.000000
  002102.XSHE absolute mean daily return -9.901874e-04 0.000882        15 242 0.869092               1.000000
  688086.XSHG absolute mean daily return -2.638415e-03 0.002197        14 209 0.885089               1.000000
  002975.XSHE absolute mean daily return  7.809489e-03 0.004338        14 224 0.035898               1.000000
  601997.XSHG absolute mean daily return -4.704582e-04 0.000899        15 242 0.699704               1.000000
  600784.XSHG absolute mean daily return  5.825333e-04 0.001482        15 242 0.347147               1.000000
  002190.XSHE absolute mean daily return  1.692540e-03 0.001810        15 242 0.174813               1.000000
  600316.XSHG absolute mean daily return  6.909304e-03 0.003092        15 242 0.012722               1.000000
  002114.XSHE absolute mean daily return  6.798133e-04 0.001417        15 242 0.315751               1.000000
  603356.XSHG absolute mean daily return -3.918206e-04 0.001575        15 242 0.598207               1.000000
  605318.XSHG absolute mean daily return -1.963640e-03 0.003699        10 102 0.702245               1.000000
  002983.XSHE absolute mean daily return  3.848220e-03 0.005566        12 166 0.244680               1.000000
  002320.XSHE absolute mean daily return  7.966614e-04 0.001598        15 242 0.309055               1.000000
  603098.XSHG absolute mean daily return -9.487794e-04 0.001195        15 242 0.786314               1.000000
  002454.XSHE absolute mean daily return  1.127999e-03 0.001293        15 242 0.191422               1.000000
  002614.XSHE absolute mean daily return  1.526526e-03 0.002407        15 242 0.262991               1.000000
  002145.XSHE absolute mean daily return  1.281302e-03 0.001673        15 242 0.221819               1.000000
  603095.XSHG absolute mean daily return -2.463074e-03 0.001681        13 175 0.928601               1.000000
  002293.XSHE absolute mean daily return  1.671599e-03 0.001277        15 242 0.095228               1.000000
  600021.XSHG absolute mean daily return -3.606905e-04 0.000729        15 242 0.689604               1.000000
  002641.XSHE absolute mean daily return  1.848600e-03 0.002174        15 242 0.197613               1.000000
  300583.XSHE absolute mean daily return -1.288367e-03 0.001028        15 242 0.895033               1.000000
  300395.XSHE absolute mean daily return  4.527228e-03 0.002072        15 242 0.014450               1.000000
  603500.XSHG absolute mean daily return  1.929157e-04 0.003044        15 242 0.474733               1.000000
  002129.XSHE absolute mean daily return  3.799591e-03 0.002018        15 242 0.029865               1.000000
  002579.XSHE absolute mean daily return -1.609182e-04 0.001336        15 242 0.547924               1.000000
  605158.XSHG absolute mean daily return -6.329150e-04 0.002692        10 100 0.592939               1.000000
  002442.XSHE absolute mean daily return -6.788720e-04 0.001641        15 242 0.660427               1.000000
  000909.XSHE absolute mean daily return  5.259317e-04 0.001294        15 242 0.342223               1.000000
  600903.XSHG absolute mean daily return -3.827172e-04 0.001277        15 242 0.617782               1.000000
  603895.XSHG absolute mean daily return -1.052985e-03 0.001208        15 242 0.808381               1.000000
  603897.XSHG absolute mean daily return  2.807087e-05 0.001260        15 242 0.491111               1.000000
  002932.XSHE absolute mean daily return  3.108222e-03 0.003041        15 242 0.153338               1.000000
  300545.XSHE absolute mean daily return  6.137387e-04 0.001940        15 242 0.375892               1.000000
  000629.XSHE absolute mean daily return -9.900697e-04 0.001475        15 242 0.749005               1.000000
  002155.XSHE absolute mean daily return  6.518047e-04 0.001312        15 242 0.309672               1.000000
  601319.XSHG absolute mean daily return -3.692296e-04 0.001219        15 242 0.619032               1.000000
  300637.XSHE absolute mean daily return -1.065281e-03 0.001815        15 242 0.721328               1.000000
  300179.XSHE absolute mean daily return  1.124476e-03 0.002292        15 242 0.311844               1.000000
  600766.XSHG absolute mean daily return -9.392741e-04 0.001414        15 242 0.746797               1.000000
  300097.XSHE absolute mean daily return  1.128808e-03 0.002956        15 242 0.351300               1.000000
  600486.XSHG absolute mean daily return  3.039957e-03 0.001438        15 242 0.017282               1.000000
  603218.XSHG absolute mean daily return  3.277318e-03 0.001438        15 242 0.011326               1.000000
  603214.XSHG absolute mean daily return -8.099453e-04 0.001604        15 242 0.693169               1.000000
  002850.XSHE absolute mean daily return  3.801103e-03 0.002526        15 242 0.066190               1.000000
  600163.XSHG absolute mean daily return  8.344094e-04 0.001206        15 242 0.244497               1.000000
  600698.XSHG absolute mean daily return -5.263785e-04 0.001321        15 242 0.654831               1.000000
  600380.XSHG absolute mean daily return  1.631498e-03 0.001753        15 242 0.175993               1.000000
  300609.XSHE absolute mean daily return -2.552256e-03 0.002146        15 242 0.882825               1.000000
  688033.XSHG absolute mean daily return -2.197210e-03 0.001416        15 242 0.939594               1.000000
  300065.XSHE absolute mean daily return  1.173725e-03 0.002109        15 242 0.288916               1.000000
  600500.XSHG absolute mean daily return  2.797834e-04 0.001192        15 242 0.407190               1.000000
  002543.XSHE absolute mean daily return -2.984059e-04 0.001022        15 242 0.614804               1.000000
  300650.XSHE absolute mean daily return  3.334738e-04 0.002081        15 242 0.436349               1.000000
  600760.XSHG absolute mean daily return  4.375583e-03 0.003004        15 242 0.072629               1.000000
  603716.XSHG absolute mean daily return  1.080429e-04 0.001331        15 242 0.467657               1.000000
  002838.XSHE absolute mean daily return  4.720755e-03 0.004518        15 242 0.148019               1.000000
  002582.XSHE absolute mean daily return  2.198230e-03 0.001795        15 242 0.110392               1.000000
  002176.XSHE absolute mean daily return  3.713117e-04 0.003160        15 242 0.453226               1.000000
  603308.XSHG absolute mean daily return  3.537728e-03 0.002284        15 242 0.060707               1.000000
  002265.XSHE absolute mean daily return -5.699602e-04 0.001348        15 242 0.663826               1.000000
  300464.XSHE absolute mean daily return  1.697838e-03 0.004025        15 242 0.336585               1.000000
  000617.XSHE absolute mean daily return -7.023418e-04 0.001099        15 242 0.738681               1.000000
  300150.XSHE absolute mean daily return  8.341800e-04 0.001416        15 242 0.277830               1.000000
  600829.XSHG absolute mean daily return  7.443260e-04 0.001676        15 242 0.328488               1.000000
  600416.XSHG absolute mean daily return  5.276542e-03 0.003480        15 242 0.064748               1.000000
  002117.XSHE absolute mean daily return -9.996945e-04 0.001778        15 242 0.713040               1.000000
  002401.XSHE absolute mean daily return  9.736708e-04 0.001590        15 242 0.270128               1.000000
  600699.XSHG absolute mean daily return  2.051635e-03 0.002226        15 242 0.178322               1.000000
  600610.XSHG absolute mean daily return  2.363076e-03 0.002740        15 242 0.194231               1.000000
  002367.XSHE absolute mean daily return  1.516416e-03 0.001453        15 242 0.148363               1.000000
  600847.XSHG absolute mean daily return -2.797209e-04 0.001283        15 242 0.586312               1.000000
  002879.XSHE absolute mean daily return -1.410450e-04 0.001314        15 242 0.542755               1.000000
  603006.XSHG absolute mean daily return  8.176930e-04 0.001610        15 242 0.305774               1.000000
  002306.XSHE absolute mean daily return  4.326268e-04 0.001661        15 242 0.397266               1.000000
  300205.XSHE absolute mean daily return  2.837935e-04 0.001930        15 242 0.441546               1.000000
  600329.XSHG absolute mean daily return  1.272636e-03 0.001154        15 242 0.134982               1.000000
  600978.XSHG absolute mean daily return -4.771053e-03 0.002808        15 242 0.955331               1.000000
  300448.XSHE absolute mean daily return  1.242002e-03 0.002581        15 242 0.315204               1.000000
  603063.XSHG absolute mean daily return  3.436078e-03 0.001964        15 242 0.040138               1.000000
  002611.XSHE absolute mean daily return  6.162600e-04 0.001256        15 242 0.311876               1.000000
  688202.XSHG absolute mean daily return  4.788265e-03 0.001957        15 242 0.007217               1.000000
  600898.XSHG absolute mean daily return -6.061365e-04 0.002406        15 242 0.599459               1.000000
  300701.XSHE absolute mean daily return -9.785880e-05 0.001621        15 242 0.524066               1.000000
  600238.XSHG absolute mean daily return  1.891487e-03 0.002345        15 242 0.209901               1.000000
  600885.XSHG absolute mean daily return  2.384574e-03 0.001643        15 242 0.073358               1.000000
  600536.XSHG absolute mean daily return  9.419405e-04 0.002278        15 242 0.339652               1.000000
  603600.XSHG absolute mean daily return  1.022326e-03 0.002422        15 242 0.336463               1.000000
  300163.XSHE absolute mean daily return  3.315771e-04 0.001971        15 242 0.433204               1.000000
  002177.XSHE absolute mean daily return  4.022795e-04 0.001986        15 242 0.419728               1.000000
  688258.XSHG absolute mean daily return -4.657447e-04 0.002797        15 242 0.566129               1.000000
  601566.XSHG absolute mean daily return  5.081171e-04 0.001324        15 242 0.350557               1.000000
  300431.XSHE absolute mean daily return -1.116508e-02 0.006448        14 204 0.958317               1.000000
  000750.XSHE absolute mean daily return  1.169396e-03 0.001634        15 242 0.237150               1.000000
  300624.XSHE absolute mean daily return  1.998287e-03 0.002448        15 242 0.207149               1.000000
  002411.XSHE absolute mean daily return -3.953076e-03 0.002142        15 242 0.967502               1.000000
  002214.XSHE absolute mean daily return  4.526604e-03 0.002949        15 242 0.062370               1.000000
  002856.XSHE absolute mean daily return -8.364983e-04 0.001649        15 242 0.694034               1.000000
  002106.XSHE absolute mean daily return  1.162778e-03 0.001892        15 242 0.269467               1.000000
  300132.XSHE absolute mean daily return  2.667470e-03 0.002064        15 242 0.098121               1.000000
  688266.XSHG absolute mean daily return  3.281905e-04 0.002975        15 227 0.456073               1.000000
  002053.XSHE absolute mean daily return  3.033558e-04 0.001014        15 242 0.382416               1.000000
  601100.XSHG absolute mean daily return  5.417944e-03 0.001247        15 242 0.000007               0.003489
  300391.XSHE absolute mean daily return  2.978248e-03 0.003001        15 242 0.160515               1.000000
  300599.XSHE absolute mean daily return  1.200916e-03 0.001459        15 242 0.205179               1.000000
  603086.XSHG absolute mean daily return -3.111989e-04 0.001256        15 242 0.597814               1.000000
  600968.XSHG absolute mean daily return -6.926707e-04 0.001023        15 242 0.750755               1.000000
  000882.XSHE absolute mean daily return -6.265352e-04 0.000892        15 242 0.758865               1.000000
  000807.XSHE absolute mean daily return  2.048342e-03 0.002665        15 242 0.221034               1.000000
  002504.XSHE absolute mean daily return -7.999163e-04 0.001434        15 242 0.711514               1.000000
  600800.XSHG absolute mean daily return -4.801354e-04 0.001153        15 242 0.661479               1.000000
  002674.XSHE absolute mean daily return  1.429584e-03 0.002913        15 242 0.311800               1.000000
  603136.XSHG absolute mean daily return  3.014957e-04 0.001642        15 242 0.427142               1.000000
  603195.XSHG absolute mean daily return  4.520932e-03 0.003438        14 223 0.094240               1.000000
  002792.XSHE absolute mean daily return -1.297372e-03 0.001618        15 242 0.788654               1.000000
  002241.XSHE absolute mean daily return  3.206129e-03 0.002350        15 242 0.086196               1.000000
  000591.XSHE absolute mean daily return  3.491999e-03 0.002356        15 242 0.069178               1.000000
  603668.XSHG absolute mean daily return  2.191824e-04 0.001874        15 242 0.453455               1.000000
  300152.XSHE absolute mean daily return -2.458992e-05 0.003093        15 242 0.503172               1.000000
  600889.XSHG absolute mean daily return  1.951629e-04 0.002384        15 242 0.467380               1.000000
  300336.XSHE absolute mean daily return -3.646142e-04 0.002574        15 242 0.556316               1.000000
  002215.XSHE absolute mean daily return -1.928209e-04 0.001049        15 242 0.572935               1.000000
  002780.XSHE absolute mean daily return  7.709633e-04 0.001669        15 242 0.322102               1.000000
  600336.XSHG absolute mean daily return  4.194451e-03 0.002314        15 242 0.034936               1.000000
  300252.XSHE absolute mean daily return  6.591179e-04 0.001649        15 242 0.344706               1.000000
  000430.XSHE absolute mean daily return -7.656229e-05 0.001287        15 242 0.523712               1.000000
  603993.XSHG absolute mean daily return  2.026975e-03 0.002015        15 242 0.157189               1.000000
  601188.XSHG absolute mean daily return -8.912903e-05 0.000893        15 242 0.539730               1.000000
  300233.XSHE absolute mean daily return  4.798053e-04 0.002406        15 242 0.420978               1.000000
  300555.XSHE absolute mean daily return  8.968848e-04 0.002642        15 242 0.367130               1.000000
  600346.XSHG absolute mean daily return  2.735940e-03 0.001691        15 242 0.052887               1.000000
  600063.XSHG absolute mean daily return  9.422309e-05 0.001564        15 242 0.475983               1.000000
  300256.XSHE absolute mean daily return  5.382615e-04 0.001593        15 242 0.367752               1.000000
  688278.XSHG absolute mean daily return  8.662091e-04 0.003434        15 231 0.400421               1.000000
  002253.XSHE absolute mean daily return  8.186059e-05 0.001252        15 242 0.473934               1.000000
  002084.XSHE absolute mean daily return  1.277348e-03 0.002932        15 242 0.331567               1.000000
  300386.XSHE absolute mean daily return  1.790812e-03 0.002135        15 242 0.200829               1.000000
  603927.XSHG absolute mean daily return -1.817749e-03 0.001725        15 242 0.853971               1.000000
  002066.XSHE absolute mean daily return  7.546310e-04 0.001626        15 242 0.321280               1.000000
  002097.XSHE absolute mean daily return  1.644241e-03 0.001565        15 242 0.146700               1.000000
  600859.XSHG absolute mean daily return  4.350482e-03 0.004335        15 242 0.157797               1.000000
  601886.XSHG absolute mean daily return -5.671430e-04 0.000925        15 242 0.730095               1.000000
  600696.XSHG absolute mean daily return -2.616000e-05 0.002064        15 242 0.505056               1.000000
  688981.XSHG absolute mean daily return -2.660059e-03 0.002739        10 114 0.834309               1.000000
  002817.XSHE absolute mean daily return  2.062087e-04 0.001176        15 242 0.430378               1.000000
  603288.XSHG absolute mean daily return  3.619411e-03 0.001428        15 242 0.005642               1.000000
  300474.XSHE absolute mean daily return  1.150029e-03 0.001699        15 242 0.249264               1.000000
  002833.XSHE absolute mean daily return  2.706683e-03 0.001954        15 242 0.083034               1.000000
  002360.XSHE absolute mean daily return  1.955691e-03 0.001805        15 242 0.139363               1.000000
  300217.XSHE absolute mean daily return  2.876101e-03 0.003123        15 242 0.178535               1.000000
  002654.XSHE absolute mean daily return -1.022751e-03 0.001549        15 242 0.745454               1.000000
  600975.XSHG absolute mean daily return  3.516934e-04 0.001856        15 242 0.424848               1.000000
  300345.XSHE absolute mean daily return -2.299184e-03 0.002535        15 242 0.817748               1.000000
  600148.XSHG absolute mean daily return  6.982777e-04 0.001534        15 242 0.324478               1.000000
  300287.XSHE absolute mean daily return  1.082480e-03 0.002195        15 242 0.310930               1.000000
  688108.XSHG absolute mean daily return  3.499419e-05 0.003153        15 242 0.495572               1.000000
  000958.XSHE absolute mean daily return -4.199389e-04 0.001362        15 242 0.621059               1.000000
  000529.XSHE absolute mean daily return  2.454984e-04 0.001355        15 242 0.428128               1.000000
  300480.XSHE absolute mean daily return  6.859365e-04 0.001891        15 242 0.358435               1.000000
  688008.XSHG absolute mean daily return  1.362864e-03 0.002585        15 242 0.299001               1.000000
  603023.XSHG absolute mean daily return  8.754522e-04 0.001842        15 242 0.317317               1.000000
  600802.XSHG absolute mean daily return  8.483297e-04 0.001764        15 242 0.315302               1.000000
  600162.XSHG absolute mean daily return -3.457797e-04 0.001127        15 242 0.620465               1.000000
  600552.XSHG absolute mean daily return  1.034222e-03 0.001692        15 242 0.270511               1.000000
  601965.XSHG absolute mean daily return  2.956346e-03 0.001534        15 242 0.026999               1.000000
  002368.XSHE absolute mean daily return  5.112336e-05 0.001655        15 242 0.487675               1.000000
  300392.XSHE absolute mean daily return  1.892644e-03 0.002014        15 242 0.173725               1.000000
  002481.XSHE absolute mean daily return  2.922433e-03 0.002681        15 242 0.137804               1.000000
  600143.XSHG absolute mean daily return  4.130399e-03 0.002089        15 242 0.023983               1.000000
  002938.XSHE absolute mean daily return  9.378225e-04 0.001967        15 242 0.316729               1.000000
  603885.XSHG absolute mean daily return -8.091253e-04 0.001484        15 242 0.707146               1.000000
  300051.XSHE absolute mean daily return  1.134752e-04 0.003257        15 242 0.486104               1.000000
  600644.XSHG absolute mean daily return  4.830381e-04 0.001174        15 242 0.340345               1.000000
  002992.XSHE absolute mean daily return -1.112005e-03 0.004174        10 102 0.605050               1.000000
  601636.XSHG absolute mean daily return  4.294304e-03 0.002442        15 242 0.039355               1.000000
  300436.XSHE absolute mean daily return  9.607528e-04 0.001466        15 242 0.256127               1.000000
  603778.XSHG absolute mean daily return  2.155736e-04 0.001265        15 242 0.432331               1.000000
  600400.XSHG absolute mean daily return  6.829155e-05 0.001300        15 242 0.479055               1.000000
  002228.XSHE absolute mean daily return  5.991343e-04 0.001711        15 242 0.363131               1.000000
  002096.XSHE absolute mean daily return  1.038736e-03 0.003303        15 242 0.376584               1.000000
  002738.XSHE absolute mean daily return  2.393034e-03 0.001739        15 242 0.084372               1.000000
  300521.XSHE absolute mean daily return  3.495568e-03 0.003782        15 242 0.177684               1.000000
  002637.XSHE absolute mean daily return  1.251101e-03 0.001598        15 242 0.216826               1.000000
  002613.XSHE absolute mean daily return  2.770715e-03 0.003769        15 242 0.231159               1.000000
  000153.XSHE absolute mean daily return  1.718992e-03 0.002173        15 242 0.214413               1.000000
  300625.XSHE absolute mean daily return  3.462851e-04 0.001392        15 242 0.401792               1.000000
  688098.XSHG absolute mean daily return  6.150750e-04 0.002285        15 242 0.393912               1.000000
  688558.XSHG absolute mean daily return -3.722078e-03 0.001987        11 126 0.969513               1.000000
  603798.XSHG absolute mean daily return -1.289860e-03 0.001185        15 242 0.861751               1.000000
  300760.XSHE absolute mean daily return  3.907244e-03 0.001439        15 242 0.003306               1.000000
  002101.XSHE absolute mean daily return  1.841757e-04 0.001450        15 242 0.449471               1.000000
  600039.XSHG absolute mean daily return  1.397592e-03 0.001151        15 242 0.112379               1.000000
  600861.XSHG absolute mean daily return  3.654742e-03 0.002187        15 242 0.047353               1.000000
  688198.XSHG absolute mean daily return  3.308386e-03 0.003096        15 242 0.142599               1.000000
  002424.XSHE absolute mean daily return -1.340510e-04 0.001188        15 242 0.544914               1.000000
  688580.XSHG absolute mean daily return -4.364507e-03 0.003884        10 111 0.869444               1.000000
  605288.XSHG absolute mean daily return -1.145467e-04 0.001623        12 145 0.528126               1.000000
  600077.XSHG absolute mean daily return  4.188779e-04 0.001668        15 242 0.400865               1.000000
  002846.XSHE absolute mean daily return -7.341443e-04 0.002514        15 242 0.614886               1.000000
  601021.XSHG absolute mean daily return  1.238778e-03 0.001418        15 242 0.191160               1.000000
  600578.XSHG absolute mean daily return  1.270439e-04 0.000761        15 242 0.433745               1.000000
  601399.XSHG absolute mean daily return -2.786342e-03 0.002595        11 140 0.858503               1.000000
  002146.XSHE absolute mean daily return -1.277518e-03 0.001220        15 242 0.852495               1.000000
  688566.XSHG absolute mean daily return -3.183040e-03 0.001503        12 155 0.982931               1.000000
  600022.XSHG absolute mean daily return  1.405656e-04 0.000860        15 242 0.435053               1.000000
  600909.XSHG absolute mean daily return  9.733751e-04 0.001751        15 242 0.289140               1.000000
  002692.XSHE absolute mean daily return -1.092079e-03 0.000990        15 242 0.864902               1.000000
  002076.XSHE absolute mean daily return -5.775094e-04 0.003025        15 242 0.575696               1.000000
  600690.XSHG absolute mean daily return  2.010671e-03 0.001796        15 242 0.131517               1.000000
  600108.XSHG absolute mean daily return  1.357518e-03 0.001371        15 242 0.161111               1.000000
  002987.XSHE absolute mean daily return  2.926879e-03 0.003958        12 162 0.229805               1.000000
  600710.XSHG absolute mean daily return  3.687691e-04 0.001442        15 242 0.399079               1.000000
  600777.XSHG absolute mean daily return -9.885851e-04 0.001299        15 242 0.776733               1.000000
  000877.XSHE absolute mean daily return  1.595384e-03 0.002130        15 242 0.226884               1.000000
  000980.XSHE absolute mean daily return -2.843303e-03 0.002083        15 242 0.913925               1.000000
  000715.XSHE absolute mean daily return -3.753050e-04 0.001616        15 242 0.591824               1.000000
  600363.XSHG absolute mean daily return  2.520930e-03 0.002167        15 242 0.122317               1.000000
  600029.XSHG absolute mean daily return -6.231804e-04 0.001234        15 242 0.693250               1.000000
  002596.XSHE absolute mean daily return -4.797195e-04 0.001664        15 242 0.613456               1.000000
  002383.XSHE absolute mean daily return -8.911586e-04 0.001623        15 242 0.708554               1.000000
  601226.XSHG absolute mean daily return  1.413238e-04 0.000934        15 242 0.439842               1.000000
  002312.XSHE absolute mean daily return  1.649870e-03 0.001502        15 242 0.135988               1.000000
  300591.XSHE absolute mean daily return  1.687122e-03 0.002470        15 242 0.247255               1.000000
  300360.XSHE absolute mean daily return  7.651259e-05 0.001589        15 242 0.480800               1.000000
  600624.XSHG absolute mean daily return -6.517459e-04 0.001374        15 242 0.682388               1.000000
  300745.XSHE absolute mean daily return  6.499401e-05 0.001969        15 242 0.486832               1.000000
  300244.XSHE absolute mean daily return  2.179489e-03 0.001863        15 242 0.121017               1.000000
  300036.XSHE absolute mean daily return -3.531074e-05 0.001686        15 242 0.508357               1.000000
  300727.XSHE absolute mean daily return  4.140136e-03 0.003063        15 242 0.088260               1.000000
  300153.XSHE absolute mean daily return  6.545177e-04 0.002181        15 242 0.382026               1.000000
  601158.XSHG absolute mean daily return -1.255491e-04 0.000496        15 242 0.599977               1.000000
  000419.XSHE absolute mean daily return  1.747794e-04 0.001096        15 242 0.436641               1.000000
  002937.XSHE absolute mean daily return -4.585046e-04 0.001247        15 242 0.643422               1.000000
  002677.XSHE absolute mean daily return  1.149360e-03 0.001802        15 242 0.261823               1.000000
  000028.XSHE absolute mean daily return  3.762358e-04 0.001361        15 242 0.391087               1.000000
  300145.XSHE absolute mean daily return -2.630298e-05 0.001618        15 242 0.506486               1.000000
  605222.XSHG absolute mean daily return -1.753750e-03 0.001877        10 103 0.824971               1.000000
  601099.XSHG absolute mean daily return  7.241874e-04 0.001883        15 242 0.350268               1.000000
  600809.XSHG absolute mean daily return  6.413168e-03 0.001513        15 242 0.000011               0.005592
  002563.XSHE absolute mean daily return  5.883318e-04 0.001501        15 242 0.347587               1.000000
  002703.XSHE absolute mean daily return  1.333343e-03 0.001503        15 242 0.187470               1.000000
  600081.XSHG absolute mean daily return  2.166609e-03 0.001769        15 242 0.110331               1.000000
  002574.XSHE absolute mean daily return -2.607159e-04 0.000868        15 242 0.618040               1.000000
  002086.XSHE absolute mean daily return -2.614492e-03 0.001980        15 242 0.906678               1.000000
  300002.XSHE absolute mean daily return  2.656855e-03 0.002332        15 242 0.127262               1.000000
  002019.XSHE absolute mean daily return  1.230905e-03 0.001910        15 242 0.259634               1.000000
  603959.XSHG absolute mean daily return  1.444660e-03 0.002415        15 242 0.274828               1.000000
  600748.XSHG absolute mean daily return -3.753614e-04 0.001498        15 242 0.598924               1.000000
  600461.XSHG absolute mean daily return  7.466729e-04 0.000847        15 242 0.188911               1.000000
  300268.XSHE absolute mean daily return -2.581828e-04 0.001683        15 242 0.560955               1.000000
  000700.XSHE absolute mean daily return  2.244458e-03 0.004765        15 242 0.318799               1.000000
  002824.XSHE absolute mean daily return  2.546527e-03 0.004135        15 242 0.268989               1.000000
  300159.XSHE absolute mean daily return  1.313342e-03 0.002582        15 242 0.305521               1.000000
  300853.XSHE absolute mean daily return  4.523682e-03 0.006077        10 108 0.228313               1.000000
  002108.XSHE absolute mean daily return  1.917566e-03 0.002528        15 242 0.224022               1.000000
  002826.XSHE absolute mean daily return  1.957396e-04 0.001388        15 242 0.443913               1.000000
  600716.XSHG absolute mean daily return  1.131047e-04 0.000997        15 242 0.454834               1.000000
  002565.XSHE absolute mean daily return -1.236032e-03 0.001930        15 242 0.739104               1.000000
  603808.XSHG absolute mean daily return -9.653874e-05 0.001708        15 242 0.522540               1.000000
  603117.XSHG absolute mean daily return -3.821088e-05 0.000950        15 242 0.516046               1.000000
  000902.XSHE absolute mean daily return  3.244692e-03 0.001503        15 242 0.015460               1.000000
  600120.XSHG absolute mean daily return  1.272816e-04 0.001535        15 242 0.466947               1.000000
  600169.XSHG absolute mean daily return -1.562383e-04 0.001116        15 242 0.555680               1.000000
  002012.XSHE absolute mean daily return  1.633520e-04 0.001381        15 242 0.452918               1.000000
  002813.XSHE absolute mean daily return  1.168645e-04 0.001966        15 242 0.476305               1.000000
  601012.XSHG absolute mean daily return  5.735573e-03 0.002303        15 242 0.006380               1.000000
  002721.XSHE absolute mean daily return -2.186018e-03 0.001497        15 242 0.927836               1.000000
  002751.XSHE absolute mean daily return -1.047648e-03 0.001462        15 242 0.763116               1.000000
  002918.XSHE absolute mean daily return  2.559955e-03 0.002242        15 242 0.126760               1.000000
  600592.XSHG absolute mean daily return -1.168587e-03 0.001715        15 242 0.752172               1.000000
  688277.XSHG absolute mean daily return -4.948929e-03 0.004499        11 121 0.864326               1.000000
  300511.XSHE absolute mean daily return  3.346462e-03 0.002094        15 242 0.054989               1.000000
  002668.XSHE absolute mean daily return -1.184056e-03 0.001520        15 242 0.781979               1.000000
  300304.XSHE absolute mean daily return  1.288262e-03 0.002164        15 242 0.275808               1.000000
  002604.XSHE absolute mean daily return -1.555826e-02 0.009208        11 126 0.954447               1.000000
  000032.XSHE absolute mean daily return  1.420467e-03 0.002396        15 242 0.276665               1.000000
  300167.XSHE absolute mean daily return  1.220856e-04 0.001915        15 242 0.474589               1.000000
  300074.XSHE absolute mean daily return  2.960883e-04 0.001890        15 242 0.437760               1.000000
  002470.XSHE absolute mean daily return -2.702591e-03 0.001960        15 242 0.916037               1.000000
  600676.XSHG absolute mean daily return  7.707466e-04 0.001410        15 242 0.292258               1.000000
  002010.XSHE absolute mean daily return -1.435271e-03 0.000957        15 242 0.933220               1.000000
  603181.XSHG absolute mean daily return  2.360237e-03 0.001548        15 242 0.063635               1.000000
  000669.XSHE absolute mean daily return -1.907312e-03 0.002601        15 242 0.768272               1.000000
  603986.XSHG absolute mean daily return  1.699516e-03 0.002510        15 242 0.249154               1.000000
  002812.XSHE absolute mean daily return  4.761013e-03 0.002004        15 242 0.008769               1.000000
  002438.XSHE absolute mean daily return  2.432229e-03 0.001844        15 242 0.093526               1.000000
  600559.XSHG absolute mean daily return  4.936624e-03 0.002941        15 242 0.046601               1.000000
  002757.XSHE absolute mean daily return  1.913696e-03 0.002141        15 242 0.185680               1.000000
  603538.XSHG absolute mean daily return  2.398694e-03 0.002282        15 242 0.146595               1.000000
  002080.XSHE absolute mean daily return  3.215197e-03 0.001896        15 242 0.044946               1.000000
  002051.XSHE absolute mean daily return -1.076722e-03 0.001192        15 242 0.816868               1.000000
  600863.XSHG absolute mean daily return  5.994050e-06 0.000679        15 242 0.496478               1.000000
  300127.XSHE absolute mean daily return  1.845569e-04 0.001692        15 242 0.456576               1.000000
  300037.XSHE absolute mean daily return  4.944572e-03 0.002038        15 242 0.007636               1.000000
  000736.XSHE absolute mean daily return  1.233006e-03 0.002004        15 242 0.269187               1.000000
  603337.XSHG absolute mean daily return  2.047977e-03 0.001947        15 242 0.146491               1.000000
  002013.XSHE absolute mean daily return  2.542604e-03 0.001760        15 242 0.074319               1.000000
  603681.XSHG absolute mean daily return -7.955166e-04 0.001453        15 242 0.707958               1.000000
  002446.XSHE absolute mean daily return -8.878102e-04 0.001490        15 242 0.724308               1.000000
  600136.XSHG absolute mean daily return -2.226861e-03 0.001726        15 242 0.901561               1.000000
  002781.XSHE absolute mean daily return -6.456168e-08 0.001804        15 242 0.500014               1.000000
  002853.XSHE absolute mean daily return  1.725273e-03 0.002070        15 242 0.202271               1.000000
  002379.XSHE absolute mean daily return -5.929430e-04 0.001791        15 242 0.629726               1.000000
  603256.XSHG absolute mean daily return -1.693241e-03 0.001786        15 242 0.828472               1.000000
  300248.XSHE absolute mean daily return  8.682907e-04 0.002306        15 242 0.353255               1.000000
  000862.XSHE absolute mean daily return -1.213867e-04 0.001345        15 242 0.535952               1.000000
  002288.XSHE absolute mean daily return  2.561325e-03 0.002694        15 242 0.170843               1.000000
  688025.XSHG absolute mean daily return  8.391518e-04 0.002394        15 242 0.362962               1.000000
  002982.XSHE absolute mean daily return  3.441871e-03 0.005459        12 168 0.264170               1.000000
  002515.XSHE absolute mean daily return  6.822271e-04 0.001573        15 242 0.332250               1.000000
  603896.XSHG absolute mean daily return  9.693228e-04 0.001759        15 242 0.290787               1.000000
  002940.XSHE absolute mean daily return  1.948995e-03 0.001893        15 242 0.151615               1.000000
  002319.XSHE absolute mean daily return -8.272816e-04 0.002175        15 242 0.648170               1.000000
  601827.XSHG absolute mean daily return -1.040256e-03 0.001251        11 141 0.797196               1.000000
  688321.XSHG absolute mean daily return -1.237082e-03 0.002047        15 242 0.727170               1.000000
  002916.XSHE absolute mean daily return  6.651180e-04 0.002075        15 242 0.374298               1.000000
  002159.XSHE absolute mean daily return  2.077142e-04 0.001352        15 242 0.438959               1.000000
  000042.XSHE absolute mean daily return -5.364627e-04 0.000939        15 242 0.716089               1.000000
  300487.XSHE absolute mean daily return  1.322090e-03 0.002145        15 242 0.268819               1.000000
  000755.XSHE absolute mean daily return -3.696558e-04 0.000872        15 242 0.664207               1.000000
  002865.XSHE absolute mean daily return  1.018432e-03 0.001701        15 242 0.274658               1.000000
  002463.XSHE absolute mean daily return -5.274744e-04 0.001612        15 242 0.628262               1.000000
  300210.XSHE absolute mean daily return  1.771006e-03 0.002268        15 242 0.217461               1.000000
  002380.XSHE absolute mean daily return  3.331700e-04 0.001315        15 242 0.399982               1.000000
  300428.XSHE absolute mean daily return  7.272308e-04 0.001779        15 242 0.341328               1.000000
  002459.XSHE absolute mean daily return  6.136223e-03 0.002979        15 242 0.019708               1.000000
  002724.XSHE absolute mean daily return  5.083182e-04 0.001683        15 242 0.381323               1.000000
  600768.XSHG absolute mean daily return -1.226456e-03 0.001407        15 242 0.808299               1.000000
  300295.XSHE absolute mean daily return -2.260584e-05 0.001470        15 242 0.506137               1.000000
  603901.XSHG absolute mean daily return -3.159973e-04 0.002134        15 242 0.558853               1.000000
  601168.XSHG absolute mean daily return  2.980554e-03 0.002069        15 242 0.074899               1.000000
  002287.XSHE absolute mean daily return  1.260402e-03 0.002597        15 242 0.313731               1.000000
  603289.XSHG absolute mean daily return -1.480722e-04 0.001417        15 242 0.541605               1.000000
  300291.XSHE absolute mean daily return -5.860228e-04 0.001597        15 242 0.643174               1.000000
  300140.XSHE absolute mean daily return -9.744840e-04 0.001962        15 242 0.690303               1.000000
  688180.XSHG absolute mean daily return -4.518497e-03 0.003536        10 115 0.899371               1.000000
  603053.XSHG absolute mean daily return -1.472223e-03 0.001380        15 242 0.856916               1.000000
  603888.XSHG absolute mean daily return -1.243101e-05 0.001990        15 242 0.502492               1.000000
  002519.XSHE absolute mean daily return  4.934145e-04 0.001436        15 242 0.365590               1.000000
  002119.XSHE absolute mean daily return -5.966241e-04 0.001404        15 242 0.664603               1.000000
  600215.XSHG absolute mean daily return -3.401959e-04 0.002397        15 242 0.556423               1.000000
  002900.XSHE absolute mean daily return -7.883672e-04 0.001037        15 242 0.776440               1.000000
  603363.XSHG absolute mean daily return  4.722478e-04 0.002012        15 242 0.407231               1.000000
  600435.XSHG absolute mean daily return  6.752880e-04 0.001648        15 242 0.340969               1.000000
  600823.XSHG absolute mean daily return  6.640846e-04 0.001991        15 242 0.369383               1.000000
  601139.XSHG absolute mean daily return  3.672625e-06 0.001169        15 242 0.498747               1.000000
  601919.XSHG absolute mean daily return  3.864673e-03 0.002424        15 242 0.055411               1.000000
  002093.XSHE absolute mean daily return -6.751356e-05 0.001614        15 242 0.516686               1.000000
  300241.XSHE absolute mean daily return  4.039454e-04 0.002259        15 242 0.429048               1.000000
  000016.XSHE absolute mean daily return  2.468418e-03 0.003441        15 242 0.236548               1.000000
  002140.XSHE absolute mean daily return  2.501291e-04 0.001327        15 242 0.425250               1.000000
  002069.XSHE absolute mean daily return  2.382047e-03 0.002695        15 242 0.188390               1.000000
  600080.XSHG absolute mean daily return  7.766123e-05 0.001786        15 242 0.482658               1.000000
  603189.XSHG absolute mean daily return  4.540692e-04 0.002063        15 242 0.412880               1.000000
  300502.XSHE absolute mean daily return  3.440611e-03 0.002511        15 242 0.085308               1.000000
  600926.XSHG absolute mean daily return  2.325804e-03 0.001404        15 242 0.048792               1.000000
  600128.XSHG absolute mean daily return -4.549530e-04 0.000933        15 242 0.687102               1.000000
  002522.XSHE absolute mean daily return  5.056635e-04 0.001443        15 242 0.363024               1.000000
  603823.XSHG absolute mean daily return -5.559555e-04 0.001623        15 242 0.634070               1.000000
  002503.XSHE absolute mean daily return  1.623158e-04 0.003324        15 242 0.480525               1.000000
  603328.XSHG absolute mean daily return -1.139741e-03 0.001226        15 242 0.823777               1.000000
  000636.XSHE absolute mean daily return  4.063149e-03 0.002620        15 242 0.060492               1.000000
  000048.XSHE absolute mean daily return  7.799396e-04 0.002491        15 242 0.377084               1.000000
  300300.XSHE absolute mean daily return -2.661041e-03 0.002185        15 242 0.888414               1.000000
  002240.XSHE absolute mean daily return  5.544119e-03 0.002765        15 242 0.022483               1.000000
  603421.XSHG absolute mean daily return -1.004214e-03 0.001164        15 242 0.805950               1.000000
  600693.XSHG absolute mean daily return -6.552006e-04 0.001038        15 242 0.736015               1.000000
  603713.XSHG absolute mean daily return  5.395673e-03 0.002310        15 242 0.009739               1.000000
  603633.XSHG absolute mean daily return  7.736425e-04 0.002351        15 242 0.371058               1.000000
  300641.XSHE absolute mean daily return  8.340848e-04 0.001769        15 242 0.318603               1.000000
  601186.XSHG absolute mean daily return -8.805819e-04 0.001089        15 242 0.790572               1.000000
  002636.XSHE absolute mean daily return  6.263295e-04 0.001684        15 242 0.354990               1.000000
  603505.XSHG absolute mean daily return  1.729234e-03 0.001501        15 242 0.124726               1.000000
  000411.XSHE absolute mean daily return  2.672643e-03 0.003243        15 242 0.204901               1.000000
  600137.XSHG absolute mean daily return -3.347286e-04 0.001402        15 242 0.594350               1.000000
  002612.XSHE absolute mean daily return  4.727296e-03 0.003249        15 242 0.072840               1.000000
  300008.XSHE absolute mean daily return  3.939690e-03 0.003798        15 242 0.149772               1.000000
  002278.XSHE absolute mean daily return  6.994164e-04 0.001597        15 242 0.330672               1.000000
  002157.XSHE absolute mean daily return  7.093926e-04 0.002272        15 242 0.377408               1.000000
  600551.XSHG absolute mean daily return -2.050787e-04 0.001198        15 242 0.567971               1.000000
  603429.XSHG absolute mean daily return  1.342475e-03 0.002507        15 242 0.296123               1.000000
  300070.XSHE absolute mean daily return  2.663688e-04 0.001740        15 242 0.439160               1.000000
  300802.XSHE absolute mean daily return  6.047715e-04 0.002083        15 242 0.385799               1.000000
  300669.XSHE absolute mean daily return  2.385733e-03 0.001248        15 242 0.027991               1.000000
  688363.XSHG absolute mean daily return  2.822731e-03 0.001779        15 242 0.056250               1.000000
  600119.XSHG absolute mean daily return  1.704425e-03 0.002176        15 242 0.216743               1.000000
  600490.XSHG absolute mean daily return  3.388913e-04 0.001756        15 242 0.423489               1.000000
  002112.XSHE absolute mean daily return  1.192861e-03 0.001972        15 242 0.272633               1.000000
  601456.XSHG absolute mean daily return  1.336250e-02 0.008365        10 103 0.055075               1.000000
  002199.XSHE absolute mean daily return -9.734032e-04 0.001645        15 242 0.723019               1.000000
  300218.XSHE absolute mean daily return  1.593639e-03 0.002113        15 242 0.225338               1.000000
  002259.XSHE absolute mean daily return  1.857022e-03 0.001868        15 242 0.160139               1.000000
  002602.XSHE absolute mean daily return -1.044351e-03 0.001597        15 242 0.743435               1.000000
  300144.XSHE absolute mean daily return  3.498766e-04 0.001524        15 242 0.409215               1.000000
  000612.XSHE absolute mean daily return  2.656302e-03 0.002793        15 242 0.170752               1.000000
  002315.XSHE absolute mean daily return  9.302396e-04 0.002470        15 242 0.353257               1.000000
  600376.XSHG absolute mean daily return -9.308241e-04 0.001088        15 242 0.803783               1.000000
  600779.XSHG absolute mean daily return  2.482378e-03 0.001808        15 242 0.084890               1.000000
  002631.XSHE absolute mean daily return -2.983862e-05 0.001256        15 242 0.509478               1.000000
  601019.XSHG absolute mean daily return -6.043165e-04 0.000773        15 242 0.782936               1.000000
  603506.XSHG absolute mean daily return  1.568948e-04 0.001727        15 242 0.463813               1.000000

下面对未校正与 Bonferroni 校正的结果进行可视化对比。左图展示原始p值分布,右图展示校正后的显著性阈值变化。

图 13.1 左图展示未校正的 p 值分布,右图进一步叠加 Bonferroni 校正阈值线,直观对比校正前后显著性判定标准的差异。

fig, axes = plt.subplots(1, 2, figsize=(14, 5))  # 并排比较未校正与Bonferroni规则

sort_indices = np.argsort(calculated_p_values)  # 按证据强度统一两个面板的横轴顺序
sorted_p_values = calculated_p_values[sort_indices]  # 按p值升序排列
sorted_small_p_highlight = small_p_highlight[sort_indices]  # 让颜色标记与排序后p值逐项对齐

# 左图:原始 p 值
ax1 = axes[0]  # 选取左侧子图
colors = ['red' if flag else 'gray' for flag in sorted_small_p_highlight]  # 红色仅表示原始 p<0.01
ax1.scatter(range(1, target_stock_count + 1), -np.log10(sorted_p_values),  # 在子图中绑制散点图
           c=colors, alpha=0.6, s=30)  # 绘制p值的负对数散点图
ax1.axhline(y=-np.log10(significance_level), color='blue', linestyle='--',  # 标出单项0.05拒绝边界
            linewidth=2, label=rf'$\alpha = {significance_level}$')  # 在图例中报告未校正阈值
ax1.set_xlabel('排序后的标的', fontsize=11, fontproperties='Source Han Serif SC')  # 说明横轴是p值排序而非证券真值
ax1.set_ylabel(r'$-\log_{10}(\text{p 值})$', fontsize=11)  # 用负对数尺度放大小p值差异
ax1.set_title('未校正的 p 值(筛选收益显著标的)', fontsize=13, fontproperties='Source Han Serif SC')  # 明示左图尚未控制家族错误
ax1.legend(prop={'family': 'Source Han Serif SC'})  # 标识未校正阈值线
ax1.grid(True, alpha=0.3)  # 辅助读取各点相对阈值的位置

# 右图:Bonferroni 校正
ax2 = axes[1]  # 选取右侧子图
ax2.scatter(range(1, target_stock_count + 1), -np.log10(sorted_p_values),  # 在子图中绑制散点图
           c=colors, alpha=0.6, s=30)  # 绘制同样的p值散点图
ax2.axhline(y=-np.log10(significance_level), color='blue', linestyle='--',  # 保留原始阈值作为比较基准
            linewidth=2, label=rf'$\alpha = {significance_level}$')  # 让左右面板使用同一未校正参照
ax2.axhline(y=-np.log10(bonferroni_significance_level), color='green',  # 标出控制FWER后的单项阈值
            linestyle='--', linewidth=2,  # 用虚线突出更严格的家族阈值
            label=f'Bonferroni: $\\alpha = {bonferroni_significance_level:.6f}$')  # 在图例中给出实际家族阈值
ax2.set_xlabel('排序后的标的', fontsize=11, fontproperties='Source Han Serif SC')  # 保持与左图相同的排序语义
ax2.set_ylabel(r'$-\log_{10}(\text{p 值})$', fontsize=11)  # 保持两种规则的证据尺度可比
ax2.set_title('Bonferroni 校正', fontsize=13, fontproperties='Source Han Serif SC')  # 标明右图采用FWER校正规则
ax2.legend(prop={'family': 'Source Han Serif SC'})  # 区分原始与Bonferroni阈值
ax2.grid(True, alpha=0.3)  # 辅助比较各点是否越过校正边界

plt.tight_layout()  # 自动调整子图间距
plt.show()  # 输出两种拒绝规则的同族对照
Font 'rm' does not have a glyph for '\u503c' [U+503c], substituting with a dummy symbol.
Font 'rm' does not have a glyph for '\u503c' [U+503c], substituting with a dummy symbol.
Font 'rm' does not have a glyph for '\u503c' [U+503c], substituting with a dummy symbol.
左右面板按 p 值排序显示负对数 p 值;左侧为原始阈值,右侧为更严格的 Bonferroni 阈值。
图 13.1: Bonferroni 校正效果演示:强势股筛选

真实市场样本里并不知道每只股票的零假设是否真的成立,因此不能从同一批 p 值构造“真值”,也不能据此估计实现的 FWER、FDR 或功效。下面先定义一个仅供随后“已知注入真值”的受控实验使用的度量函数。

# 计算错误度量的通用函数
def calculate_metrics(p_values_input, true_status_input, alpha_threshold):  # 汇总已知真值模拟中的错误与功效指标
    significant_flags = p_values_input < alpha_threshold  # 根据阈值判定是否显著
    actual_positives = np.sum((significant_flags == 1) & (true_status_input == 1))  # 真阳性数(TP)
    false_positives = np.sum((significant_flags == 1) & (true_status_input == 0))  # 假阳性数(FP)
    true_negatives = np.sum((significant_flags == 0) & (true_status_input == 0))  # 真阴性数(TN)
    false_negatives = np.sum((significant_flags == 0) & (true_status_input == 1))  # 假阴性数(FN)
    any_false_rejection = int(false_positives > 0)  # 单次族错误指示变量,不是FWER
    false_discovery_proportion = false_positives / max(actual_positives + false_positives, 1)  # 单次FDP,不是FDR
    true_positive_rate = actual_positives / (actual_positives + false_negatives) if (actual_positives + false_negatives) > 0 else 0  # TPR:真阳性检出率(统计功效)
    return {'TP': actual_positives, 'FP': false_positives, 'TN': true_negatives, 'FN': false_negatives,  # 汇总四格表计数
            'AnyFalseRejection': any_false_rejection, 'FDP': false_discovery_proportion, 'TPR': true_positive_rate}  # 同时返回族错误、FDP与功效

对真实样本,我们只报告两种规则的拒绝数量;“拒绝”不等于已证实的真阳性。

discovery_counts = pd.DataFrame({  # 并排组织真实样本的两种统计决策
    '规则': [f'未校正 p<{significance_level}', 'Bonferroni'],  # 区分单项阈值与家族校正
    '拒绝数量': [uncorrected_significant_flags.sum(), bonferroni_significant_flags.sum()],  # 比较规则改变后的拒绝数
    '阈值': [significance_level, bonferroni_significance_level]  # 报告每项检验实际使用的门槛
})  # 完成可审计的拒绝数汇总表
print(discovery_counts)  # 展示决策差异而不推断未知真值
           规则  拒绝数量      阈值
0  未校正 p<0.05    34  0.0500
1  Bonferroni     2  0.0001

Bonferroni 通常减少拒绝数量,但这张真实样本表本身不能告诉我们哪些拒绝是错误的。FWER 与功效的经验比较必须转到零假设状态由设计者预先知道的重复模拟中;下一节的注入实验正是这种受控演示,而不是投资绩效证据。

13.6 Benjamini-Hochberg 方法

Benjamini-Hochberg (BH) 方法是控制 FDR 的经典 step-up 方法 (Benjamini 和 Hochberg 1995年)。在独立真零 p 值下有 \(\mathrm{FDR}=q m_0/m\le q\);其保证可扩展到满足相应正依赖条件的情形,但任意依赖结构不能直接沿用这一结论。

13.6.1 算法步骤

给定 p 值\(p_1, p_2, \ldots, p_m\) 和目标FDR 水平 \(q\)

  1. 将 p 值按从小到大排序:\(p_{(1)} \leq p_{(2)} \leq \cdots \leq p_{(m)}\)
  2. 找到最大的 \(k\) 使得:\(p_{(k)} \leq \frac{k}{m} q\)
  3. 拒绝所有对应\(p_{(1)}, p_{(2)}, \ldots, p_{(k)}\) 的假设

BH 方法的直观解释

BH 方法使用了一个自适应的阈值:不是固定的 \(\alpha\),而是随着排序位置 \(k\) 线性增加。

这意味着:

  • 排名靠前的检验可以使用更宽松的阈值

在 BH 的独立或相应正依赖条件成立时,它控制 FDR;相对 Bonferroni 的功效差异取决于信号、检验数和依赖结构,须由设计与结果共同判断。

[拓展] 数学推导:独立情形下的 BH 控制

\(H_0\)\(m_0\) 个真零假设的集合,\(R\) 是 BH 的拒绝数,并约定 \(V/\max(R,1)=0\)\(R=0\)。对每个 \(i\in H_0\),令 \(I_i\) 表示该真零假设被拒绝。则 \[ \mathrm{FDR}=E\!\left[\frac{V}{\max(R,1)}\right] =\sum_{i\in H_0}\sum_{r=1}^{m}\frac1r P(I_i=1,R=r). \]\(I_i=1\)\(R=r\),BH 的自洽阈值给出 \(P_i\le rq/m\)。把 \(P_i\) 暂时置为 0,并记其余 p 值决定的拒绝数为 \(R^{(-i)}\);step-up 过程的单调性给出相应的 leave-one-out 事件 \(R^{(-i)}=r\)。在真零 p 值与其余 p 值独立且服从连续 \(U(0,1)\) 时, \[ P(P_i\le rq/m, R^{(-i)}=r) =\frac{rq}{m}P(R^{(-i)}=r). \] 因而 \[ E\!\left[\frac{I_i}{\max(R,1)}\right] =\sum_{r=1}^{m}\frac1r\frac{rq}{m}P(R^{(-i)}=r) =\frac qm. \] 对真零假设求和得到 \(\mathrm{FDR}=qm_0/m\le q\);若真零 p 值只满足 super-uniform,上式相应变为不等式。任意依赖下不能使用这一步分解。全零假设时 \(V=R\),所以 FDR=FWER;“恰等于 \(q\)”还需要独立、连续均匀等条件,不能无条件陈述。

13.6.2 中国案例:真实股票的描述性筛查与受控模拟

这里明确分开两种证据。真实 A 股样本没有“已知真 Alpha”标签:先从每日个股收益中减去当日等权市场收益,再对每只股票的平均市场调整收益做 HAC 均值检验。输出只能描述当前样本中的拒绝集合;它不能报告 TP、FP、FDP 或功效,也不能把拒绝解释成持续可交易的 Alpha。BH 的独立或正依赖保证未必适用于共同市场冲击,因此本例同时报告 BY 作为任意依赖下更保守的敏感性分析。

列表 13.1: 真实股票描述性筛查:数据与 HAC 推断设置
import os  # 读取教材统一数据根环境变量
from pathlib import Path  # 跨平台解析真实行情文件路径
import numpy as np  # 构造HAC均值回归所需数值数组
import pandas as pd  # 整理逐股票收益与校正结果
import statsmodels.api as sm  # 估计允许时间依赖的HAC标准误
from scipy.stats import norm  # 将HAC统计量转换为双侧渐近p值
from statsmodels.stats.multitest import multipletests  # 对同一股票族执行BH与BY校正
book_data_dir_text = os.environ.get('BOOK_DATA_DIR')  # 先读取可选值以提供明确的未设置诊断
if not book_data_dir_text:  # 不让环境变量缺失退化为无上下文KeyError
    raise EnvironmentError('未设置 BOOK_DATA_DIR;请将其设为包含 stock/ 子目录的教材数据根目录')  # 指明课堂运行前配置
DATA_DIR = Path(book_data_dir_text).expanduser().resolve()  # 只接受运行环境显式提供的数据根
if not DATA_DIR.is_dir():  # 读取行情前验证数据根可用
    raise FileNotFoundError(f'BOOK_DATA_DIR 不存在或不是目录: {DATA_DIR}')  # 用明确路径阻止静默回退
real_stock_price_path = DATA_DIR / 'stock' / 'stock_price_post_adjusted.h5'  # 固定真实股票筛查所需行情文件
if not real_stock_price_path.is_file():  # 在存储层查询前检查精确HDF输入
    raise FileNotFoundError(f'缺少后复权行情文件: {real_stock_price_path};请检查 BOOK_DATA_DIR')  # 报告可直接核对的缺失路径
stock_price_history = pd.read_hdf(  # 在存储层只读取 2021 年真实行情
    real_stock_price_path,  # 使用已通过存在性检查的后复权股价文件
    where="date>='2021-01-01' & date<'2022-01-01'",  # 固定市场调整筛查年份
    columns=['date', 'order_book_id', 'close'],  # 只读取 HAC 调整所需字段
).reset_index()  # 恢复日期与证券索引列
列表 13.2: 真实股票描述性筛查:市场调整收益与资格规则
stock_price_history['date'] = pd.to_datetime(stock_price_history['date'])  # 统一日期类型以核对2021年支持域
year_stock_data = stock_price_history.loc[stock_price_history['date'].dt.year.eq(2021), ['date', 'order_book_id', 'close']].copy()  # 锁定声明的年度与分析字段
year_stock_data['ret'] = year_stock_data.groupby('order_book_id')['close'].pct_change()  # 在公司内部计算日收益避免跨证券差分
market_return = year_stock_data.groupby('date')['ret'].mean().rename('market_ret')  # 形成同期A股等权市场基准
year_stock_data = year_stock_data.join(market_return, on='date')  # 按交易日对齐个股收益与市场基准
year_stock_data['market_adjusted_ret'] = year_stock_data['ret'] - year_stock_data['market_ret']  # 定义个股相对当日市场的收益差
valid_stock_distribution = year_stock_data.groupby('order_book_id')['market_adjusted_ret'].count()  # 统计每只股票可用于HAC的观测数
valid_stock_codes = valid_stock_distribution.loc[valid_stock_distribution.ge(100)].index.sort_values().tolist()  # 仅检验至少有100日支持的股票
列表 13.3: 真实股票描述性筛查:逐股票 HAC 均值检验
finance_records = []  # 保留每只合格股票的效应、标准误与p值
for stock_code in valid_stock_codes:  # 对预先确定的合格股票逐项使用同一推断规则
    stock_returns = year_stock_data.loc[year_stock_data['order_book_id'].eq(stock_code), 'market_adjusted_ret'].dropna()  # 提取该股票的完整市场调整收益序列
    hac_fit = sm.OLS(stock_returns.to_numpy(), np.ones((len(stock_returns), 1))).fit(cov_type='HAC', cov_kwds={'maxlags': 5})  # 用固定五阶HAC估计平均市场调整收益
    finance_records.append({'order_book_id': stock_code, 'n': len(stock_returns), 'mean_market_adjusted_return': hac_fit.params[0], 'hac_se': hac_fit.bse[0], 'z_statistic': hac_fit.tvalues[0], 'p_value': 2 * norm.sf(abs(hac_fit.tvalues[0]))})  # 保存单项双侧检验的完整证据
finance_results = pd.DataFrame(finance_records)  # 形成待校正的股票检验族
finance_p_values = finance_results['p_value'].to_numpy()  # 提取相同顺序的单项p值
finance_t_statistics = finance_results['z_statistic'].to_numpy()  # 保留效应方向用于描述性作图
total_test_count = len(finance_results)  # 以实际合格股票数固定校正分母
finance_results['bh_adjusted_p'] = multipletests(finance_p_values, method='fdr_bh')[1]  # 计算条件性BH对照
finance_results['by_adjusted_p'] = multipletests(finance_p_values, method='fdr_by')[1]  # 计算任意依赖下的BY敏感性结果
finance_results['bh_reject'] = finance_results['bh_adjusted_p'].le(0.05)  # 标记BH规则的描述性拒绝集合
finance_results['by_reject'] = finance_results['by_adjusted_p'].le(0.05)  # 标记BY规则的依赖稳健拒绝集合

图 13.2 展示排序 p 值与 BH 边界,并按拒绝方向标出真实样本中的描述性发现。颜色编码的是统计决定,不是真值标签。

import matplotlib.pyplot as plt  # 绘制排序边界与拒绝方向诊断
sort_indices = np.argsort(finance_p_values)  # 按BH step-up所需顺序排列p值
sorted_p_values = finance_p_values[sort_indices]  # 得到与临界线逐位对应的p值
bh_thresholds = np.arange(1, total_test_count + 1) / total_test_count * 0.05  # 计算目标FDR为0.05的BH边界
fig, axes = plt.subplots(1, 2, figsize=(15, 6))  # 并排展示排序规则与效应方向
axes[0].scatter(np.arange(1, total_test_count + 1), -np.log10(sorted_p_values), color='gray', alpha=0.5, s=10)  # 以负对数尺度呈现小p值
axes[0].plot(np.arange(1, total_test_count + 1), -np.log10(bh_thresholds), color='green', label='BH 临界线')  # 叠加逐位变化的BH拒绝边界
axes[0].set(xlabel='排序后的标的', ylabel=r'$-\log_{10}(p)$', title='排序 p 值与 BH 边界')  # 标明排序位置和证据强度
axes[0].legend(prop={'family': 'Source Han Serif SC'})  # 说明绿色曲线代表BH边界
plot_colors = np.where(finance_results['bh_reject'] & finance_results['z_statistic'].gt(0), 'red', np.where(finance_results['bh_reject'], 'blue', 'gray'))  # 仅按统计决定与方向编码颜色
axes[1].scatter(finance_t_statistics, -np.log10(finance_p_values), c=plot_colors, alpha=0.5, s=10)  # 联合展示效应方向与单项证据
axes[1].set(xlabel='HAC 统计量', ylabel=r'$-\log_{10}(p)$', title='BH 拒绝方向(非真值标签)')  # 防止把着色误读成真值
plt.tight_layout()  # 避免双面板文字重叠
plt.show()  # 输出可供课堂比较的诊断图
左图比较排序 p 值与 BH 临界线;右图按 BH 拒绝的正负方向着色,颜色不表示真实 Alpha。
图 13.2: 真实 A 股市场调整收益的描述性多重检验
表 13.2: 真实股票样本中各规则的描述性拒绝数
real_stock_rejection_summary = pd.DataFrame([  # 比较同一股票族的四种决策规则
    {'method': '未校正', 'rejections': int((finance_p_values <= 0.05).sum())},  # 统计忽略多重性的基准拒绝数
    {'method': 'Bonferroni', 'rejections': int((finance_p_values <= 0.05 / total_test_count).sum())},  # 统计任意依赖FWER拒绝数
    {'method': 'BH', 'rejections': int(finance_results['bh_reject'].sum())},  # 统计条件性FDR规则拒绝数
    {'method': 'BY', 'rejections': int(finance_results['by_reject'].sum())},  # 统计任意依赖FDR规则拒绝数
])  # 保留规则与拒绝数两列用于并排解读
print(real_stock_rejection_summary.to_string(index=False))  # 展示规则差异而不赋予真值含义
    method  rejections
       未校正         173
Bonferroni           0
        BH           0
        BY           0

表 13.2 不含 TP、FP、FDP 或功效,因为真实样本没有可观测真值。要评价这些量,必须转入下面的独立受控实验。

列表 13.4: 已知真值模拟:共同冲击、AR(1) 误差与 HAC p 值
def simulate_dependent_family(random_generator, test_count=100, time_count=160, nonnull_count=15):  # 生成真值已知且横纵向相关的检验族
    common_factor = random_generator.normal(size=time_count)  # 注入所有检验共享的同期冲击
    innovations = random_generator.normal(size=(time_count, test_count))  # 为各检验生成独立创新项
    errors = np.zeros_like(innovations)  # 为AR(1)误差递推预分配存储
    for time_index in range(1, time_count):  # 沿时间维度加入序列相关
        errors[time_index] = 0.45 * errors[time_index - 1] + innovations[time_index]  # 固定AR系数形成持续误差
    means = np.zeros(test_count)  # 默认全部检验满足零假设
    means[:nonnull_count] = 0.32  # 给预先指定的前15项注入正向效应
    observations = means + 0.35 * common_factor[:, None] + errors  # 合成含共同与序列依赖的观测
    p_values = np.empty(test_count)  # 为每项单尾HAC检验预分配p值
    for test_index in range(test_count):  # 对完整计划族使用一致的均值检验
        fitted = sm.OLS(observations[:, test_index], np.ones((time_count, 1))).fit(cov_type='HAC', cov_kwds={'maxlags': 5})  # 用HAC处理单项时间依赖
        p_values[test_index] = norm.sf(fitted.tvalues[0])  # 按预设正向备择计算单尾p值
    return p_values, np.arange(test_count) < nonnull_count  # 返回校正输入及已知非零真值掩码
表 13.3: 依赖数据下重复模拟的错误发现与功效汇总
simulation_rng = np.random.default_rng(2025)  # 固定重复模拟随机流以支持课堂复算
simulation_records = []  # 保存每次重复与每种方法的错误和功效
for repetition in range(200):  # 用200个独立检验族估计长期表现
    simulation_p, simulation_truth = simulate_dependent_family(simulation_rng)  # 取得本次真值已知的依赖数据
    for method_name, method_code in [('BH', 'fdr_bh'), ('BY', 'fdr_by')]:  # 在同一p值族上公平比较BH与BY
        simulation_reject = multipletests(simulation_p, alpha=0.05, method=method_code)[0]  # 按目标FDR 0.05形成拒绝集合
        rejection_count = int(simulation_reject.sum())  # 记录FDP分母中的总拒绝数
        false_count = int((simulation_reject & ~simulation_truth).sum())  # 利用已知真值统计误拒数
        true_count = int((simulation_reject & simulation_truth).sum())  # 利用已知真值统计正确发现数
        simulation_records.append({'repetition': repetition, 'method': method_name, 'fdp': false_count / max(rejection_count, 1), 'power': true_count / simulation_truth.sum(), 'null_rejection_rate': false_count / (~simulation_truth).sum()})  # 保存单次FDP、功效与真零拒绝率
simulation_results = pd.DataFrame(simulation_records)  # 汇总全部方法与重复的实现值
simulation_summary = simulation_results.groupby('method')[['fdp', 'power', 'null_rejection_rate']].agg(['mean', 'std'])  # 用均值和标准差描述长期分布
print(simulation_summary)  # 报告依赖机制下的经验错误与功效比较
             fdp               power           null_rejection_rate          
            mean       std      mean       std                mean       std
method                                                                      
BH      0.134089  0.131555  0.444000  0.182720            0.015647  0.018465
BY      0.046225  0.101806  0.242667  0.153724            0.003118  0.007316

表 13.3 才能用已知真值计算每次 FDP、功效和零假设拒绝率,并以 200 次重复的均值与标准差描述长期表现。模拟含共同冲击与 AR(1) 误差,单项 p 值采用 HAC;它检验本数据生成机制下的经验行为,不是对所有依赖结构的普遍证明。

13.7 其他多重检验校正方法

除了 Bonferroni 和BH 方法,还有其他重要的校正方法。

13.7.1 Holm 方法

Holm 方法(也称 Holm-Bonferroni)是逐步下降的 FWER 程序 (Holm 1979年)。只要各真零 p 值边际有效,它在任意依赖下控制 FWER,并且不弱于单步 Bonferroni。

算法

  1. 将p 值排序:\(p_{(1)} \leq p_{(2)} \leq \cdots \leq p_{(m)}\)
  2. 对于 \(k = 1, 2, \ldots, m\)
    • 如果 \(p_{(k)} > \frac{\alpha}{m - k + 1}\),停止检验
    • 否则,拒绝\(H_{(k)}\),继续到下一个

13.7.2 Storey’s q-value

q-value 是p 值在 FDR 框架下的类比。

  • p 值:\(\Pr(\text{观察到的数据或更极端 } | H_0 \text{ 为真})\)
  • q-value:包含该检验的拒绝区域可达到的最小估计 FDR

Storey q-value 把每个有序检验与包含该检验的拒绝区域联系起来,可解释为在给定模型和估计规则下所能达到的最小估计 FDR (Storey 2002年)。它衡量错误发现风险,不把任何拒绝自动认证为有效信号。 下面的极简估算器使用高 p 值区域估计真零假设比例 \(\pi_0\)。这一估计依赖真零 p 值校准、阈值选择和依赖结构;有限样本中未必比 BH 更可靠,也不能确认被拒绝标的具有可复现的超额收益。

列表 13.5: q-value 计算示例
def calculate_q_value(input_p_values):  # 按固定lambda估计真零比例并计算单调q值
    '''计算 q-value(Storey 方法),参数: input_p_values -- p值数组,返回: (q_values数组, pi0估计值)'''  # 记录函数的统计输入与输出
    test_count = len(input_p_values)  # 固定Storey估计与排序校正的家族规模

    # 估计 π0(真实零假设的比例),利用高p值区域的均匀分布特性
    lambda_threshold = 0.5  # 用预设高p值区间估计真零比例
    # 高于λ的p值个数除以理论预期值,即为π0的估计
    # 计算总和
    null_proportion_estimate = np.sum(input_p_values > lambda_threshold) / (test_count * (1 - lambda_threshold))
    null_proportion_estimate = min(null_proportion_estimate, 1.0)  # π0不超过1

    sort_indices = np.argsort(input_p_values)  # 对p值从小到大排序
    sorted_p_values = input_p_values[sort_indices]  # 排序后的p值

    calculated_q_values = np.zeros(test_count)  # 为排序后的单调q值预分配存储
    # 最大排名位置的q值 = π0 * 最大p值
    calculated_q_values[-1] = null_proportion_estimate * sorted_p_values[-1]  # 设置反向单调递推的最大排序端点

    # 从倒数第二位开始逆序计算,q(i) = min(π0*m*p(i)/(i+1), q(i+1))
    for i in range(test_count-2, -1, -1):  # 反向递推以满足q值随排序位置单调
        calculated_q_values[i] = min(  # 以反向累积最小值保证q值单调
            null_proportion_estimate * test_count * sorted_p_values[i] / (i + 1),  # 计算当前位置的估计FDR
            calculated_q_values[i + 1]  # 与下一位置比较以维持单调性
        )  # 确保q值单调递增

    original_order_q_values = np.empty(test_count)  # 为恢复输入顺序预分配q值位置
    original_order_q_values[sort_indices] = calculated_q_values  # 恢复原始排列顺序

    return original_order_q_values, null_proportion_estimate  # 返回q值和π0

使用上述 calculate_q_value 函数对全市场股票的 p 值进行 q-value 计算,展示结果。

# 对全市场股票p值计算q-value
example_q_values, example_pi0_estimate = calculate_q_value(finance_p_values)  # 调用Storey q-value函数

print('\nq-value 计算结果:')  # 标明随后输出属于Storey估计
print('='*60)  # 分隔标题与数值结果便于课堂核对
print(f'估计的 π0(真实零假设比例): {example_pi0_estimate:.2%}')  # 报告阈值规则隐含的真零比例估计

print(f'\n前10个标的的 p-value 和 q-value:')  # 限定展示行数而保留全族计算
for i in range(10):  # 对前十项并排核对原始证据与估计FDR
    print(f'标的 {i+1:3d}: p-value={finance_p_values[i]:.4f}, q-value={example_q_values[i]:.4f}')  # 展示校正如何改变单项证据尺度

q-value 计算结果:
============================================================
估计的 π0(真实零假设比例): 100.00%

前10个标的的 p-value 和 q-value:
标的   1: p-value=0.5501, q-value=0.9996
标的   2: p-value=0.2065, q-value=0.9996
标的   3: p-value=0.7594, q-value=0.9996
标的   4: p-value=0.5313, q-value=0.9996
标的   5: p-value=0.0261, q-value=0.9996
标的   6: p-value=0.9507, q-value=0.9996
标的   7: p-value=0.9695, q-value=0.9996
标的   8: p-value=0.3698, q-value=0.9996
标的   9: p-value=0.4136, q-value=0.9996
标的  10: p-value=0.7319, q-value=0.9996

上方输出给出当前样本的 \(\hat\pi_0\) 与 q-value。较大的 q-value 只表示在本检验族、基准和样本期内没有达到预设错误率阈值;它不能证明真实 Alpha 不存在,也不能单独验证有效市场假说。

13.8 实际应用案例

13.8.1 P-hacking 与量化交易中的多重检验灾难

在金融量化研究中,经常存在一种被称为 P-hacking (P值操纵)数据窥探 (Data Snooping) 的现象。当研究人员测试成百上千个交易信号(例如不同均线组合、数百个财务因子、各类技术指标),但仅报告那些在历史回测中获得显著高收益(\(p < 0.05\))的个别策略时,多重检验灾难就发生了:

如果使用了未经校正的经典假设检验:

  • 即使这1000 个策略本质上都只是随机数生成的无意义策略(零假设全部为真),按照 \(\alpha=0.05\) 的阈值,依然会有大约 50 个策略在统计上表现出“显著的高收益”。
  • 若把这些偶然拒绝未经独立验证就上线,样本外失效风险会升高;在全零模拟中其期望超额收益为零,但单次实盘损益仍受市场路径、成本和持仓规则影响,不能预先确定损益方向。

本章把 \(t>3.0\) 仅作为课堂敏感性阈值,用来观察提高单项证据门槛后结论如何变化;它不是 Bonferroni 校正,也不自动控制特定 planned family 的 FWER 或 FDR。正式研究应先预先固定检验族与依赖结构,再选用 Holm、BH、BY 等有明确错误度量的方法;若要把某一经验阈值归于特定文献,还须在参考文献表提供可核验正式来源。

13.8.2 案例 1:A股市场技术指标有效性检验

均线规则包含大量相邻且高度重叠的窗口;若只报告历史样本中最小的 p 值,就会产生数据窥探风险。下面以海康威视(002415.XSHE)为教学样本,预先预先固定 5 日到 54 日共 50 个窗口,逐项估计“次日持有或空仓”相对同期买入持有的平均收益差。这个案例只比较错误控制规则,不预设任何均线有效或无效。

表 13.4: A股技术指标(均线策略)有效性检验
import pandas as pd  # 整理均线检验族与校正结果
import numpy as np  # 构造收益数组和家族p值向量
from scipy import stats  # 将HAC统计量转换为单尾概率
import statsmodels.api as sm  # HAC协方差用于时间依赖收益
from statsmodels.stats.multitest import multipletests  # 比较依赖稳健主校正与条件性对照
import os  # 读取教材统一数据根环境变量

# 只从统一环境变量解析数据根,不回退到机器专属路径
from pathlib import Path  # 使用跨平台路径对象解析显式数据根
book_data_dir_text = os.environ.get('BOOK_DATA_DIR')  # 先识别环境变量是否提供
if not book_data_dir_text:  # 缺少配置时阻止进入数据读取
    raise EnvironmentError('未设置 BOOK_DATA_DIR;请将其设为包含 stock/ 子目录的教材数据根目录')  # 给出统一数据根设置要求
DATA_DIR = Path(book_data_dir_text).expanduser().resolve()  # 将用户提供的数据根规范化
if not DATA_DIR.is_dir():  # 在读取前验证数据根
    raise FileNotFoundError(f'BOOK_DATA_DIR 不存在或不是目录: {DATA_DIR}')  # 失败即停止,不回退固定路径
path_price = DATA_DIR / 'stock' / 'stock_price_post_adjusted.h5'  # 后复权股价路径
if not path_price.is_file():  # 在单股票HDF查询前验证目标文件
    raise FileNotFoundError(f'缺少后复权行情文件: {path_price};请检查 BOOK_DATA_DIR')  # 输出精确缺失路径供学生排障

target_stock_data = pd.read_hdf(  # 在存储层直接读取海康威视真实行情
    path_price,  # 指定后复权股价文件
    where="order_book_id='002415.XSHE'",  # 固定唯一教学标的
    columns=['date', 'order_book_id', 'close'],  # 只读取均线检验所需字段
).reset_index()  # 恢复日期与证券索引列
target_stock_data = target_stock_data.sort_values('date').set_index('date')  # 按日期排序并设为索引
stock_close_prices = target_stock_data['close']  # 提取收盘价序列
daily_returns = stock_close_prices.pct_change().shift(-1)  # 计算次日收益率(T日信号预测T+1日收益)

遍历 50 种均线周期(5 日线到 54 日线),构建“收盘价 > MA(n) 则次日持有”策略。每条规则只从均线首次可用日开始,并对相对同期买入持有的平均超额收益使用 HAC 标准误的单尾检验。

indicator_p_values = []  # 保留完整50项计划族的单项证据
indicator_names = []  # 保存窗口名称以便结果逐项追溯

# 对MA(5)至MA(54)执行完全相同的信号与推断规则
for moving_average_window in range(5, 55):  # 逐项执行预先固定的50个均线窗口
    # 计算n日移动平均线
# 以窗口内历史收盘价均值形成预设技术规则
    moving_average_series = stock_close_prices.rolling(window=moving_average_window).mean()  # 仅用当日及更早收盘价形成信号
    valid_observation = moving_average_series.notna() & daily_returns.notna()  # 同时排除均线预热期和缺失标签
    trading_signal = stock_close_prices.loc[valid_observation] > moving_average_series.loc[valid_observation]  # 定义收盘后可观测的持仓条件
    benchmark_returns = daily_returns.loc[valid_observation]  # 对齐同一日期支持域的买入持有收益
    strategy_returns = benchmark_returns.where(trading_signal, 0.0)  # 将未触发信号日期设为空仓收益
    strategy_excess_returns = strategy_returns - benchmark_returns  # 定义策略相对同股基准的收益差

    if len(strategy_excess_returns) > 10:  # 只在最低时间序列长度满足时拟合HAC均值模型
        hac_lag_count = max(1, int(np.sqrt(len(strategy_excess_returns))))  # 用统一经验规则选择HAC带宽
        hac_mean_model = sm.OLS(  # 以常数项回归估计平均策略超额收益
            strategy_excess_returns.to_numpy(), np.ones((len(strategy_excess_returns), 1))  # 将收益与截距设计矩阵逐日对齐
        ).fit(cov_type='HAC', cov_kwds={'maxlags': hac_lag_count})  # 允许收益存在异方差与序列相关
        t_statistic = float(hac_mean_model.tvalues[0])  # 提取平均超额收益的HAC统计量
        two_sided_p_value = float(hac_mean_model.pvalues[0])  # 读取模型返回的双侧p值
        p_value = two_sided_p_value / 2 if t_statistic > 0 else 1 - two_sided_p_value / 2  # 转换为预设正向单尾检验
    else:  # 样本不足时保留计划假设但禁止乐观拒绝
        p_value = 1.0  # 样本量不足则p值设为1(不显著)

    indicator_p_values.append(p_value)  # 记录p值
    indicator_names.append(f'MA({moving_average_window})')  # 记录策略名称

对 50 个高度依赖的均线策略,以任意依赖下有效的 BY 为 FDR 主分析、Bonferroni 为 FWER 稳健性分析。BH 只作为“尚未证明独立或 PRDS 条件,因此不具保证”的教学对照。

indicators_p_values_array = np.array(indicator_p_values)  # 将p值列表转为numpy数组
indicator_count = len(indicators_p_values_array)  # 以完整50项均线族固定校正分母

# Bonferroni 校正将每个 p 值乘以检验总数,并在 1 处截断
bonferroni_adjusted_p_values = np.minimum(indicators_p_values_array * indicator_count, 1)  # 在任意依赖下控制均线族FWER

by_adjusted_p_values = multipletests(indicators_p_values_array, method='fdr_by')[1]  # 任意依赖下的 FDR 主分析
bh_unverified_p_values = multipletests(indicators_p_values_array, method='fdr_bh')[1]  # 保留无条件保证的教学对照

汇总展示 50 个均线策略的原始 p 值、Bonferroni、BY 主分析和无保证的 BH 教学对照。

# 将原始证据、FWER结果和两种FDR结果按窗口对齐
indicator_test_results = pd.DataFrame({  # 汇总同一家族的原始与三类调整证据
    'Strategy': indicator_names,  # 策略名称
    'Original p': indicators_p_values_array,  # 原始p值
    'Bonferroni p': bonferroni_adjusted_p_values,  # Bonferroni校正p值
    'BY p (primary)': by_adjusted_p_values,  # 任意依赖下的主分析
    'BH p (conditions unverified)': bh_unverified_p_values  # 未证明条件的教学对照
})  # 固定四类证据列以便逐规则核对

print('均线策略有效性检验结果(前10个):')  # 标明展示的是均线家族的有限预览
print('='*60)  # 分隔逐项证据与后续拒绝数汇总
print(indicator_test_results.head(10).round(4).to_string(index=False))  # 并排核对前十项原始与调整p值

significance_level = 0.05  # 固定三种校正规则共享的拒绝水平
significant_original_count = np.sum(indicators_p_values_array < significance_level)  # 未校正显著策略数
significant_bonferroni_count = np.sum(bonferroni_adjusted_p_values < significance_level)  # Bonferroni显著数
significant_by_count = np.sum(by_adjusted_p_values < significance_level)  # BY 主分析拒绝数

print(f'\n{indicator_count} 个策略中发现显著策略数量 (α={significance_level}):')  # 标明拒绝数共享同一50项分母
print(f'未校正: {significant_original_count}')  # 报告忽略多重性的基准拒绝数
print(f'Bonferroni: {significant_bonferroni_count}')  # 报告任意依赖FWER拒绝数
print(f'BY(主分析): {significant_by_count}')  # 报告任意依赖FDR主分析拒绝数

if significant_original_count > significant_by_count:  # 比较未校正与依赖稳健主分析
    print('\n结论:未校正拒绝数更高,说明选择性报告风险需要单独控制。')  # 仅陈述现场差异
均线策略有效性检验结果(前10个):
============================================================
Strategy  Original p  Bonferroni p  BY p (primary)  BH p (conditions unverified)
   MA(5)      0.9856           1.0             1.0                        0.9963
   MA(6)      0.9861           1.0             1.0                        0.9963
   MA(7)      0.9844           1.0             1.0                        0.9963
   MA(8)      0.9864           1.0             1.0                        0.9963
   MA(9)      0.9939           1.0             1.0                        0.9963
  MA(10)      0.9950           1.0             1.0                        0.9963
  MA(11)      0.9963           1.0             1.0                        0.9963
  MA(12)      0.9923           1.0             1.0                        0.9963
  MA(13)      0.9769           1.0             1.0                        0.9963
  MA(14)      0.9852           1.0             1.0                        0.9963

在 50 个策略中发现显著策略数量 (α=0.05):
未校正: 0
Bonferroni: 0
BY(主分析): 0

本次运行会现场报告未校正、Bonferroni 和 BY 主分析的拒绝数,不预写固定结果;BH 调整值只展示条件未验证时的差异,不能承载保证。每个检验对比“收盘后形成信号、次日持有或空仓”与同期买入持有的超额收益,并用 HAC 协方差允许收益时间依赖。多重校正只是研究内部防线;交易成本、预注册和独立测试期仍不可缺少。

13.8.3 案例 2:行业平均收益的多重检验

这种对“伪阿尔法”的讨伐同样适用于更为宏观的行业配置研究。 下面以数据中最后一个交易日为锚点,取往前 3 年的可用样本,计算各行业等权日收益。这里的零基准是“平均原始收益为 0”,并非市场或因子模型调整后的 alpha;每个行业使用 HAC 标准误的均值检验。 三幅图比较同一检验族在未校正、Bonferroni 与 Benjamini–Yekutieli(BY)规则下的拒绝结果。HAC 处理每个行业序列的时间依赖,BY 则允许行业 p 值因共同市场冲击而具有任意依赖。校正后的拒绝数由现场输出决定;未拒绝不证明效应为零,拒绝也不证明可交易。

列表 13.6: 行业等权收益检验的数据加载与字段检查
# 目标:检验 30+ 个一级行业的日均收益率是否显著异于 0
# 这是一个典型的多重检验问题:检验越多,偶然出现小 p 值的机会越高

import pandas as pd  # 整理公司行情、行业映射与检验表
import numpy as np  # 构造HAC设计矩阵并验证有限数值
import matplotlib.pyplot as plt  # 比较三种校正规则的拒绝分布
from scipy import stats  # 提供行业均值检验所需统计工具
import statsmodels.api as sm  # 每个行业的 HAC 均值检验

# 1. 加载数据
import os  # 读取教材统一数据根环境变量
# 复用教材统一数据根以避免机器专属绝对路径
from pathlib import Path  # 使用跨平台路径对象解析显式数据根
book_data_dir_text = os.environ.get('BOOK_DATA_DIR')  # 将未设置状态与路径无效状态分开诊断
if not book_data_dir_text:  # 行业案例必须由运行环境显式提供数据根
    raise EnvironmentError('未设置 BOOK_DATA_DIR;请将其设为包含 stock/ 子目录的教材数据根目录')  # 说明数据根应包含的目录结构
DATA_DIR = Path(book_data_dir_text).expanduser().resolve()  # 规范化行业案例的数据根
if not DATA_DIR.is_dir():  # 在读取前验证数据根
    raise FileNotFoundError(f'BOOK_DATA_DIR 不存在或不是目录: {DATA_DIR}')  # 失败即停止,不回退固定路径
path_price = DATA_DIR / 'stock' / 'stock_price_post_adjusted.h5'  # 后复权股价数据路径
path_basic = DATA_DIR / 'stock' / 'stock_basic_data.h5'  # 上市公司基本信息路径
missing_industry_paths = [path for path in (path_price, path_basic) if not path.is_file()]  # 逐项识别行业案例缺失的精确输入
if missing_industry_paths:  # 任一输入缺失都不得启动行业检验
    raise FileNotFoundError(f'缺少行业案例文件: {[str(path) for path in missing_industry_paths]};请检查 BOOK_DATA_DIR')  # 列出实际缺失路径

下面从文件的末端索引确定真实可用日期,并只读取向前推三年的观察窗口。

stock_basic_data = pd.read_hdf(path_basic)  # 取得公司到行业的真实分类映射
with pd.HDFStore(path_price, mode='r') as industry_price_store:  # 打开只读 HDF 元数据入口
    industry_price_row_count = industry_price_store.get_storer('data').nrows  # 读取总行数而不载入全表
    industry_tail = industry_price_store.select('data', start=max(0, industry_price_row_count - 10000), columns=['close'])  # 只读末尾一万行及索引确定日期上界
available_end_date = pd.to_datetime(industry_tail.index.get_level_values('date')).max()  # 从真实尾部索引取得样本最后日期
available_start_date = available_end_date - pd.DateOffset(years=3)  # 预先固定往前三年支持域
stock_price_history = pd.read_hdf(  # 在 HDF 存储层读取三年真实行业窗口
    path_price,  # 指定后复权股价文件
    where=f"date>='{available_start_date:%Y-%m-%d}' & date<='{available_end_date:%Y-%m-%d}'",  # 下推三年日期过滤
    columns=['date', 'order_book_id', 'close'],  # 只读取行业收益所需字段
).reset_index()  # 恢复日期与证券索引列

接下来,我们将股价数据与行业分类信息合并,计算各行业的等权日均收益率,为后续的多重检验做数据准备。

列表 13.7
# 合并行业信息到股价数据
merged_stock_data = pd.merge(  # 把公司行业归属绑定到逐日行情
    stock_price_history[['order_book_id', 'date', 'close']],  # 选取股票代码、日期和收盘价
    stock_basic_data[['order_book_id', 'industry_name']],  # 选取股票代码和行业分类
    on='order_book_id', how='inner'  # 只保留能够映射行业的证券记录
)  # 形成公司—日期—行业的分析面板
merged_stock_data['date'] = pd.to_datetime(merged_stock_data['date'])  # 统一日期类型以验证窗口边界
merged_stock_data = merged_stock_data.sort_values(['order_book_id', 'date'])  # 在公司内部固定收益差分顺序
assert merged_stock_data['date'].between(available_start_date, available_end_date).all()  # 核验 HDF 日期下推没有越界
merged_stock_data['ret'] = merged_stock_data.groupby('order_book_id', sort=False)['close'].pct_change()  # 公司内按日期计算收益
industry_daily_returns = merged_stock_data.groupby(['date', 'industry_name'])['ret'].mean().unstack()  # 聚合到行业层面(等权平均)
industry_daily_returns = industry_daily_returns.dropna(axis=1, thresh=len(industry_daily_returns)*0.8).dropna()  # 去除空值过多的行业
print(f'分析 {len(industry_daily_returns.columns)} 个行业,共 {len(industry_daily_returns)} 个交易日')  # 报告现场检验族规模与时间支持
分析 82 个行业,共 725 个交易日

代码会现场报告行业数与交易日数,不预写固定的“82 个行业”或“1210 日”。行业收益共享市场冲击且在时间上相关,因此不能称为相互独立的检验;下面先对每个行业使用 HAC 标准误,再对行业检验族使用允许任意依赖的 BY 校正。

有了行业日收益率数据后,我们用 HAC 协方差检验每个行业的平均日收益是否异于零,然后分别应用 Bonferroni 和 BY 方法校正同一检验族。

列表 13.8
# 对每个行业检验 H0: 平均日收益率 = 0,用HAC允许时间相关
industry_test_results = []  # 保留完整行业族的效应量与单项证据
for industry_name in industry_daily_returns.columns:  # 对每个合格行业执行同一HAC规则
    industry_return_series = industry_daily_returns[industry_name]  # 取得该行业完整等权日收益序列
    hac_lag_count = max(1, int(np.sqrt(len(industry_return_series))))  # 对各行业采用同一经验带宽规则
    industry_mean_model = sm.OLS(  # 用截距模型估计行业日均原始收益
        industry_return_series.to_numpy(), np.ones((len(industry_return_series), 1))  # 对齐行业收益与常数设计矩阵
    ).fit(cov_type='HAC', cov_kwds={'maxlags': hac_lag_count})  # 允许行业序列存在异方差与自相关
    t_statistic = float(industry_mean_model.tvalues[0])  # 提取零均值双侧检验的HAC统计量
    p_value = float(industry_mean_model.pvalues[0])  # 保存同一校正族使用的双侧p值
    industry_test_results.append({  # 记录行业名、p值和平均收益
        # 同时保留行业名称、p值与效应方向供校正后解读
        'Industry': industry_name, 'p_value': p_value, 'Mean_Ret': industry_return_series.mean()
    })  # 保存行业、效应量与单项证据
test_results_df = pd.DataFrame(industry_test_results)  # 形成完整行业检验族
industry_p_values = test_results_df['p_value'].values  # 保持行业表顺序提取校正输入
industry_count = len(industry_p_values)  # 行业总数(即检验次数)
test_results_df['p_Bonf'] = np.minimum(industry_p_values * industry_count, 1)  # Bonferroni校正:p值乘以检验次数
列表 13.9: 共享冲击下行业检验族的 BY 调整值
def benjamini_yekutieli(p_values):  # 按调和因子实现任意依赖下的BY调整值
    '''BY 调整值:在任意检验依赖下控制 FDR。'''  # 说明本函数对应的错误率保证
    test_count = len(p_values)  # 检验总数
    assert test_count > 0 and np.isfinite(p_values).all(), '行业p值必须为非空有限数组'  # 检查校正输入
    sort_indices = np.argsort(p_values)  # 按p值升序排列的索引
    sorted_p_values = p_values[sort_indices]  # 排序后的p值
    harmonic_factor = np.sum(1 / np.arange(1, test_count + 1))  # 计算BY针对任意依赖的调和惩罚
    by_adjusted_p_values = np.ones(test_count)  # 预分配排序后的调整p值
    by_adjusted_p_values[-1] = min(sorted_p_values[-1] * harmonic_factor, 1.0)  # 设置BY反向递推的最大排序端点并截断于1
    for i in range(test_count-2, -1, -1):  # 从后向前逐步调整
        by_adjusted_p_values[i] = min(  # 取当前位置BY候选值与后继值的较小者
            sorted_p_values[i] * test_count * harmonic_factor / (i + 1),  # 按排序位置施加家族与依赖惩罚
            by_adjusted_p_values[i + 1],  # 约束调整p值随排序位置不下降
            1.0,  # 将概率尺度截断在1以内
        )  # 完成当前位置的单调BY调整
    original_order_p_values = np.empty(test_count)  # 恢复原始顺序的数组
    original_order_p_values[sort_indices] = by_adjusted_p_values  # 将校正后的p值映射回原始位置
    return original_order_p_values  # 返回校正后的p值

test_results_df['p_BY'] = benjamini_yekutieli(industry_p_values)  # 将任意依赖FDR证据写回行业表

最后,我们将原始 p 值、Bonferroni 校正 p 值和 BY 校正 p 值并排可视化,展示三种规则在当前检验族中的拒绝差异。

展开制图实现
fig, axes = plt.subplots(1, 3, figsize=(16, 5))  # 并排比较同一行业族的三种规则
sorted_test_results_df = test_results_df.sort_values('p_value')  # 按原始p值排序
industry_rank_range = range(1, industry_count + 1)  # 行业排名序列
significance_threshold = 0.05  # 显著性水平
correction_methods = [  # 三种校正方法及其对应列名
    ('Original p-value', 'p_value'),  # 展示未校正单项证据
    ('Bonferroni p-value', 'p_Bonf'),  # 展示FWER校正结果
    ('BY adjusted p-value', 'p_BY')  # 展示任意依赖下的FDR主分析
]  # 固定三个面板的比较顺序
for ax, (title, col) in zip(axes, correction_methods):  # 对每种规则使用一致的排序与尺度
    colors = ['red' if p < significance_threshold else 'gray' for p in sorted_test_results_df[col]]  # 显著为红色,不显著为灰色
    ax.scatter(industry_rank_range, -np.log10(sorted_test_results_df[col]), c=colors, s=30)  # 绘制散点图(y轴为-log10(p))
    ax.axhline(-np.log10(significance_threshold), color='blue', linestyle='--', label='alpha=0.05')  # 标出调整p值的共同拒绝门槛
    ax.set_title(title, fontsize=12, fontproperties='Source Han Serif SC')  # 标明当前面板采用的校正规则
    ax.set_xlabel('Rank', fontsize=10)  # x轴标签
    ax.set_ylabel('-log10(p)', fontsize=10)  # y轴标签
    ax.grid(True, alpha=0.3)  # 辅助读取各行业相对拒绝门槛的位置
plt.tight_layout()  # 自动调整子图间距
plt.show()  # 输出三种规则的同族对照图
significant_industries_df = test_results_df[test_results_df['p_BY'] < 0.05]  # 提取任意依赖FDR主分析的拒绝集合
print(f'\nBY 校正后拒绝零假设的行业({len(significant_industries_df)}个):')  # 报告现场拒绝数而不预设结论
if len(significant_industries_df) > 0:  # 如果存在显著行业
    print(significant_industries_df[['Industry', 'Mean_Ret', 'p_BY']].head(10))  # 展示最多十项效应方向与调整证据
else:  # 如果没有显著行业
    print('无显著行业')  # 输出提示信息
三个并列面板分别显示原始、Bonferroni 和 BY 调整 p 值的负对数排序,水平线为0.05。
图 13.3: A股行业 HAC 均值检验的未校正、Bonferroni 与 BY 对比

BY 校正后拒绝零假设的行业(1个):
   Industry  Mean_Ret      p_BY
37       未知 -0.006858  0.014844

图 13.3 展示当前有效行业数下三种规则的拒绝集合,阈值由代码按实际检验数计算。图只支持对本次检验族的描述,不能由拒绝数推出市场有效性或持续可交易收益。

13.9 实践指南与建议

13.9.1 选择合适的校正方法

多重检验会放大选择性报告与幸存者偏差,因此需要根据错误控制目标和依赖条件选择校正方法。

在决策树可视化之后,我们用一张汇总表来系统地对比 Bonferroni、Holm 和 BH 三种方法的核心差异,为实际研究中的方法选择提供简明参考。

图 13.4 把 Bonferroni、Holm、BH、BY 与 Storey q-value 放在同一选择框架中,但它是检查清单而不是自动决策器。 当至少一次误拒的代价很高时,可预先选择控制 FWER 的 Bonferroni 或 Holm;若研究目标是控制拒绝集合中的错误发现风险,可在单项 p 值有效后选择 FDR 程序。BH 需要独立或适当正依赖,任意依赖时可用 BY。检验规模本身不能决定方法,任何校正也不修复无效的单项 p 值。

下面的选择图把错误控制目标与依赖条件连在一起;图后的表再逐项列出方法条件,便于核对。

展开制图实现
# 用决策图串联错误目标、依赖条件与可用程序
fig, ax = plt.subplots(figsize=(12, 8))  # 为四层方法选择路径预留画布

# 在顶部提出需要回答的研究设计问题
ax.text(0.5, 0.95, '如何选择多重检验校正方法?',  # 放置方法选择图的入口问题
        # 用醒目字号区分入口与后续分支
        fontsize=16, fontproperties='Source Han Serif SC', ha='center', weight='bold')  # 居中强调决策起点

# 第一层:目标
ax.annotate('你的首要目标是什么?', xy=(0.5, 0.85),  # 先要求明确FWER或FDR控制目标
            xytext=(0.5, 0.85), fontsize=12, fontproperties='Source Han Serif SC',  # 把目标问题置于两条分支上方
            ha='center', va='center',  # 让问题文字在节点框中居中
            bbox=dict(boxstyle='round,pad=0.5', fc='lightblue', alpha=0.5))  # 用蓝色框标识共同决策节点

# 第二层分别给出两种不可互换的错误控制目标
ax.text(0.25, 0.70, '控制至少一次误拒的概率\n(FWER)', fontsize=11, fontproperties='Source Han Serif SC',
        ha='center', va='center',  # 将FWER定义置于左侧分支中心
        bbox=dict(boxstyle='round,pad=0.5', fc='lightyellow', alpha=0.5))  # 用黄色区分家族错误率目标

# 右侧节点强调FDR是FDP的重复抽样期望
ax.text(0.75, 0.70, '控制错误发现率\n(FDR = E[FDP])', fontsize=11, fontproperties='Source Han Serif SC',
        ha='center', va='center',  # 将FDR定义置于右侧分支中心
        bbox=dict(boxstyle='round,pad=0.5', fc='lightgreen', alpha=0.5))  # 用绿色区分错误发现率目标

# 第三层按依赖条件列出相应方法
ax.text(0.25, 0.50, '使用 FWER 控制方法:', fontsize=11, fontproperties='Source Han Serif SC',
        ha='center', weight='bold')  # 标明左侧候选都控制FWER
ax.text(0.25, 0.42, '• 任意依赖:Bonferroni / Holm\n• 独立或特定正依赖:Hochberg',  # 区分任意依赖与附条件的FWER程序
        fontsize=10, fontproperties='Source Han Serif SC', ha='center',  # 保持两行条件说明居中可读
        bbox=dict(boxstyle='round,pad=0.5', fc='white', alpha=0.8))  # 用白框承载具体方法清单

# 右侧列出FDR程序及各自依赖前提
ax.text(0.75, 0.50, '使用 FDR 控制方法:', fontsize=11, fontproperties='Source Han Serif SC',
        ha='center', weight='bold')  # 标明右侧候选都针对错误发现率
ax.text(0.75, 0.42, '• 独立/PRDS:BH\n• 任意依赖:BY\n• Storey:需额外条件',  # 区分三类FDR程序的依赖前提
        fontsize=10, fontproperties='Source Han Serif SC', ha='center',  # 对齐三类FDR方法的条件说明
        bbox=dict(boxstyle='round,pad=0.5', fc='white', alpha=0.8))  # 用白框承载依赖条件清单

# 第四层:应用场景
ax.text(0.25, 0.25, '典型应用:', fontsize=11, fontproperties='Source Han Serif SC',  # 引出误拒代价较高的应用示例
        ha='center', weight='bold')  # 标出FWER方法的高误拒代价场景
ax.text(0.25, 0.15, '• 高频交易策略验证\n• 重大投资决策\n• 高风险决策',  # 列出适合优先控制FWER的场景
        fontsize=10, fontproperties='Source Han Serif SC', ha='center',  # 让三个高风险场景保持同一视觉层级
        bbox=dict(boxstyle='round,pad=0.5', fc='white', alpha=0.8))  # 用白框限定应用示例而非普遍处方

ax.text(0.75, 0.25, '典型应用:', fontsize=11, fontproperties='Source Han Serif SC',  # 引出选择FDR程序前的核对清单
        ha='center', weight='bold')  # 标出FDR方法使用前的必要核对
ax.text(0.75, 0.15, '前提:\n• 单项 p 值有效\n• 核对依赖条件',  # 添加FDR适用前提
        fontsize=10, fontproperties='Source Han Serif SC', ha='center',  # 居中呈现两个不可省略的推断前提
        bbox=dict(boxstyle='round,pad=0.5', fc='white', alpha=0.8))  # 用白框强调前提而非结果承诺

# 用箭头把共同目标问题分流到两类错误率
ax.annotate('', xy=(0.35, 0.70), xytext=(0.45, 0.82),  # 连接入口与左侧FWER分支
            arrowprops=dict(arrowstyle='->', lw=2))  # 用实线箭头表示决策方向
ax.annotate('', xy=(0.65, 0.70), xytext=(0.55, 0.82),  # 连接入口与右侧FDR分支
            arrowprops=dict(arrowstyle='->', lw=2))  # 保持两条分支的视觉权重一致

ax.set_xlim(0, 1)  # 固定标准化横坐标以稳定节点布局
ax.set_ylim(0, 1)  # 固定标准化纵坐标以稳定层级间距
ax.axis('off')  # 隐藏坐标轴以突出方法选择路径
plt.tight_layout()  # 自动调整子图间距
plt.show()  # 输出完整方法选择路径供课堂讨论
自上而下的决策图先区分 FWER 与 FDR 目标,再连接到 Bonferroni、Holm、BH 与 q-value 及典型场景。
图 13.4: 多重检验方法选择指南

图的第一层先按错误控制目标分流:若目标是限制至少一次误拒的概率,则选择 FWER;若目标是限制 FDP 的重复抽样期望,则选择 FDR。第二层再核对单项 p 值有效性和检验间依赖结构,不能由检验数量或期望发现数直接决定方法。

表 13.5: 多重检验校正方法的控制目标与适用条件
method_comparison_table = pd.DataFrame([  # 汇总方法目标、依赖前提与相对功效
    {'方法': 'Bonferroni', '控制目标': 'FWER', '依赖条件': '任意依赖', '相对功效': '低'},  # 提供单步FWER基准
    {'方法': 'Holm', '控制目标': 'FWER', '依赖条件': '任意依赖', '相对功效': '中等'},  # 展示不弱于Bonferroni的逐步程序
    {'方法': 'BH', '控制目标': 'FDR', '依赖条件': '独立或 PRDS', '相对功效': '较高'},  # 明示BH保证所需依赖条件
    {'方法': 'BY', '控制目标': 'FDR', '依赖条件': '任意依赖', '相对功效': '较低'},  # 展示任意依赖下的FDR选择
    {'方法': 'Storey q', '控制目标': 'FDR', '依赖条件': '需验证额外条件', '相对功效': '较高'},  # 提醒自适应估计需要额外条件
])  # 将非绘图输出与图形块分离
print(method_comparison_table.to_string(index=False))  # 输出便于按目标与依赖条件逐项核对的表格
        方法 控制目标     依赖条件 相对功效
Bonferroni FWER     任意依赖    低
      Holm FWER     任意依赖   中等
        BH  FDR 独立或 PRDS   较高
        BY  FDR     任意依赖   较低
  Storey q  FDR  需验证额外条件   较高

方法选择必须同时看错误控制目标和依赖条件。FWER 方法控制的是“至少一次误拒”的概率,不保证某次研究零假阳性;检验个数少也不自动允许取消校正。Bonferroni 与 Holm 不要求独立,Hochberg、BH 与 Storey 类方法需要各自的独立或正依赖等条件;任意依赖下的 FDR 可使用 BY,但前提仍是每个输入 p 值本身有效。少簇、面板共同冲击或重叠时序应先采用相应稳健推断;无法得到有效单项 p 值时必须停止,而不是换一种多重校正补救。

13.10 本章小结

13.10.1 核心要点

  1. 多重检验问题:当同时进行大量假设检验时,假阳性会累积
  2. 两类主要错误度量
    • FWER(族错误率):至少一次假阳性的概率
    • FDP:一次实现中假阳性占所有拒绝的比例;FDR 是 FDP 的重复抽样期望
  3. 三种主要校正方法
    • Bonferroni 与 Holm:在单项 p 值有效时控制 FWER,且允许任意依赖;Holm 通常不弱于 Bonferroni
    • Benjamini–Hochberg:在独立或 PRDS 等正依赖条件下控制 FDR
    • Benjamini–Yekutieli:在任意依赖下控制 FDR,但通常损失更多功效

方法选择先由 FWER 或 FDR 的研究目标决定,再核对依赖条件;检验数量本身不能替代这两步,任何校正也不能修复无效的单项 p 值。

13.10.2 应用建议

多重检验最佳实践

  1. 预注册研究计划:在看到数据前确定主要假设
  2. 分层检验:将检验分为主要检验和探索性检验
  3. 报告完整结果:不要只报告显著结果
  4. 复制验证:使用独立数据集验证发现
  5. 效应量考虑:不要只看p 值,也要考虑实际意义

13.11 理论来源与前沿

多重检验问题的根源在于:当同时进行 \(m\) 个检验时,即使每个检验都控制在显著性水平\(\alpha\)至少一次假阳性发生的概率也可能随\(m\) 增大。Bonferroni 可由并集上界推出任意依赖下的 FWER 控制;Holm 在相同目标下通常具有更高功效。BH 则控制 FDP 的重复抽样期望,但其保证要求有效 p 值以及独立或相应正依赖条件,不能因检验规模较大就自动采用。

近年来的前沿主要集中在:

  1. 依赖性检验的校正:当检验统计量相关且无法验证 BH 所需条件时,Benjamini–Yekutieli(BY)用调和级数因子修正阈值,在任意依赖下控制 FDR (Benjamini 和 Yekutieli 2001年);代价通常是功效下降。更精细的依赖建模只有在其假设可复核时才可替代 BY。
  2. 自适应 FDR 与局部FDR:利用\(\pi_0\)(原假设比例)估计来提升功效,并用经验贝叶斯框架解释‘显著性’。
  3. 与可重复性预注册结合:把多重检验控制嵌入研究流程,强调独立样本复现与透明报告,减少p-hacking。

13.12 M13:冻结检验族与独立复现

本里程碑完全在同一个 QMD/IPYNB 中运行,并直接复用本章真实行情与行业映射。

在查看 p 值前,学生须在代码单元中固定完整检验族、正向备择、探索期、非重叠复现期、\(q=0.05\)、逐成员失败规则,以及探索期用 BY、复现期用 Holm 的校正规则。探索期的失败项以 \(p=1\) 留在完整 BY 家族;只有探索期 BY 拒绝的成员进入冻结发现集。复现期不重新选择行业、方向或窗口,失败候选仍以 \(p=1\) 留在 Holm 家族。

收益构造必须按证券和日历月排序,只在相邻月份均有有限月末价格时计算月收益;探索期与复现期都按收益标签月划分。每项检验报告 estimand、有效样本量、效应、HAC 标准误、原始 p 值、调整 p 值和失败原因。最终结论区分“不拒绝”与“证明无效”,并明确多重校正、样本内发现和一次独立复现都不能自动证明因果关系或可交易性。

统一量规(20 分):冻结检验族与时间边界 3 分;单项效应、HAC 标准误与失败日志 4 分;探索期 BY 与冻结发现 4 分;非重叠复现期 Holm 4 分;证据边界解释 5 分。

13.13 练习

13.13.1 概念题

  1. [核心|难度:1|分值:4|任务:独立] 解释 FWER 与FDR 的区别。在什么情况下应该控制 FWER 而不是FDR。

  2. [核心|难度:2|分值:4|任务:独立] Bonferroni 为什么相对 Holm 等程序可能损失功效?在什么样的检验族中这一代价尤其明显?

  3. [核心|难度:2|分值:5|任务:独立] Benjamini-Hochberg 方法的核心思想和依赖条件是什么?为什么它在某些检验族中可能比 Bonferroni 更有功效?

  4. [拓展|难度:2|分值:5|任务:独立] 解释 q-value 的含义。它和p 值有什么关系?

  5. [核心|难度:2|分值:5|任务:独立] 什么是“p-hacking”?多重检验校正如何帮助减少 p-hacking 的问题?

13.13.2 应用题

  1. [拓展|难度:3|分值:15|任务:独立] 基于本地数据的技术指标有效性检验 使用本地 stock_price_post_adjusted.h5 数据:

    • 选择一只代表性股票(如海康威视):
    • 构建 50 个不同参数的均线策略(MA5, MA6, …, MA54)。
    • 以“策略减同股买入持有”的日均超额收益为 estimand,用至少覆盖均线窗口的 HAC 标准误形成双侧原始 p 值;不可估项以 \(p=1\) 留在 50 项家族。
    • 以 BY 作为共享日期与嵌套窗口任意依赖下的 FDR 主分析,并以 Bonferroni 作为 FWER 敏感性分析;BH 只有在另交可核验设计证据证明其依赖条件后才可运行,否则标记为 not_run
    • 量规:HAC 单项推断 5 分,完整家族与失败日志 3 分,BY 主分析 3 分,Bonferroni 敏感性 2 分,BH 条件证据或 not_run 核对 2 分。
  2. [拓展|难度:3|分值:18|任务:独立] 财务指标选股的多重检验 使用 financial_statement.h5stock_price_post_adjusted.h5

    • 预注册 5 个可由本地字段稳定构造的比率:ROE、净利率、负债率、资产周转率、流动比率。
    • 采用年报期末后至少一年的保守可得期,并在公司内构造下一年度收益率。
    • 对 5 个因子的秩相关 estimand 使用公司与年度双向聚类标准误;若有效公司或年度簇不足则跳过并登记原因。
    • 因因子与收益检验共享公司—年度冲击,默认使用 BY 控制任意依赖下的 FDR;只有另行以设计证据验证适用依赖条件才可改用 BH。
    • 逐项报告 estimand、有效 \(n\)、公司数、年度数、依赖处理、标准误、原始/调整 p 值及信息时点限制,不把显著性称为“有效选股能力”。
  3. [核心|难度:3|分值:20|项目:M13] 行业动量策略的多重检验 在同一 QMD/IPYNB 中使用 stock_basic_data.h5stock_price_post_adjusted.h5:先计算行业等权月收益,再在查看 p 值前固定探索期、非重叠复现期、正向备择、\(N=1,2,\ldots,12\) 和逐成员失败规则。对完整“行业 × 窗口”探索家族使用至少覆盖回看窗口的 HAC 标准误和 BY 校正;逐字冻结 BY 拒绝项,仅在复现期检验这些成员并用 Holm 控制 FWER。代码须现场报告计划数、可估数、跳过原因、冻结发现数和复现结果,并说明证据为何不等于因果关系或可交易策略。

13.13.3 理论题

  1. [拓展|难度:3|分值:12|任务:独立] 证明 Benjamini-Hochberg 方法在独立检验的情况下控制FDR。

  2. [核心|难度:2|分值:8|任务:独立] 推导 Bonferroni 校正的 FWER 控制性质。

  3. [拓展|难度:3|分值:12|任务:独立] 比较 Holm 方法和Hochberg 方法的性质,证明它们都控制 FWER。

  4. [拓展|难度:2|分值:10|任务:独立] 研究并总结以下主题:

    • 自适应 FDR 控制方法
    • 依赖性检验的多重校正
    • 贝叶斯多重检验方法
  5. [核心|难度:2|分值:12|任务:独立] 四种校正的同族手算:给定已排序 p 值 \(p_{(1:4)}=(0.004,0.011,0.021,0.200)\),令 FWER 水平与 FDR 目标均为 \(0.05\)。分别写出 Bonferroni、Holm、BH 与 BY 的逐项阈值、最大可拒绝序号及拒绝集合;计算 \(c(4)=\sum_{j=1}^4 1/j\),并逐项说明四种方法保证所需的依赖条件。答案必须使用“拒绝/不拒绝”与证据强弱措辞,不得给任何假设命题判定真值。

13.14 练习参考解答

展开完整解答、评分点与常见失败模式

评分以题面分值为准;手算调整值允许 \(10^{-6}\) 绝对误差。应用题若缺 estimand、有效 \(n\)、依赖处理或 SE 方法,每项扣 20%;若把朴素 iid p 值直接送入校正、静默删除失败检验或把拒绝解释为可交易,推断与结论项不得分。

13.14.1 概念题解答

  1. FWER vs FDR

    • FWER:\(P(V\ge 1)\),强调‘一次假阳性都不想要’。适合监管、关键决策、confirmatory 研究(例如临床主要终点)。
    • FDR:\(E\big[\frac{V}{\max(R,1)}\big]\)。若目标是控制拒绝集合中的错误发现风险,应先核对单项 p 值有效性,再按依赖结构选择 BH、BY 等相应程序;检验规模本身不构成选择理由。
  2. Bonferroni 为什么保守

    Bonferroni 用并集上界: \[ P\Big(\cup_{j=1}^m \{p_j \le \alpha/m\}\Big) \le \sum_{j=1}^m P(p_j \le \alpha/m) = \alpha. \] 上界一般不紧,尤其当检验正相关且\(m\) 很大时,阈值\(\alpha/m\) 太小导致功效显著下降。

  3. BH 方法的核心思想

    将p 值从小到大排序\(p_{(1)}\le\dots\le p_{(m)}\),寻找最大的 \(k\) 使得 \[ p_{(k)} \le \frac{k}{m}q, \] 然后拒绝前\(k\) 个假设。阈值随排名放宽,既‘保护尾部’又‘奖励强信号’,因此通常比Bonferroni 更有功效。

  4. q-value 的含义

    q-value 可理解为:在包含某个检验的拒绝区域中,给定模型与估计规则所能达到的最小估计 FDR。它不是该假设为假的后验概率,也不等同于局部 fdr;解释依赖 p 值校准、\(\pi_0\) 估计和依赖条件。

  5. p-hacking 与校正作用

    p-hacking 指研究者在多个模型/指标/子样本上反复试验并只报告显著结果。多重检验校正把这种“搜索成本”显式计入阈值,从而降低‘偶然显著’的概率,但更根本的解决是预注册与复现。

13.14.2 应用题解答

  1. 技术指标有效性检验(完整代码)

    下例把每个参数视为预注册假设,信号在 \(t\) 日收盘后形成并对应 \(t+1\) 日收益。超额收益定义为策略相对同一股票买入持有基准的差;HAC p 值进入 BY 主分析与 Bonferroni 敏感性分析。由于未提供可核验的 BH 依赖条件证据,BH 明确记为 not_run;结果仍未计交易成本、重叠持仓或模型选择后的独立复现。

当前交付 HDF 已现场核验为 table 格式,且 order_book_iddate 均为 data_column;下面将公司过滤直接下推到 read_hdf(where=...),并只读取日期与收盘价。

列表 13.10: 习题6:环境数据根与单股票输入
import os  # 读取必需的数据根环境变量
from pathlib import Path  # 以跨平台路径对象拼接文件
import pandas as pd  # 读取并整理行情数据
import numpy as np  # 构造 HAC 回归所需数组
import statsmodels.api as sm  # 估计 HAC 均值检验
from statsmodels.stats.multitest import multipletests  # 执行家族校正
book_data_dir_text = os.environ.get('BOOK_DATA_DIR')  # 在构造路径前检查必需环境变量
if not book_data_dir_text:  # 习题答案应为未设置变量提供可操作提示
    raise EnvironmentError('未设置 BOOK_DATA_DIR;请将其设为包含 stock/ 子目录的教材数据根目录')  # 说明独立运行所需配置
book_data_dir = Path(book_data_dir_text).expanduser().resolve()  # 将显式数据根规范化为绝对路径
if not book_data_dir.is_dir():  # 在读取前验证数据根存在
    raise FileNotFoundError(f'BOOK_DATA_DIR 不存在或不是目录: {book_data_dir}')  # 失败即停止而非回退固定路径
price_path = book_data_dir / 'stock' / 'stock_price_post_adjusted.h5'  # 定位后复权行情文件
if not price_path.is_file():  # 在习题HDF查询前核验精确目标文件
    raise FileNotFoundError(f'缺少后复权行情文件: {price_path};请检查 BOOK_DATA_DIR')  # 报告缺失路径而不是底层HDF错误
one_stock = (pd.read_hdf(  # 在存储层直接读取海康威视真实行情
    price_path,  # 指定后复权股价文件
    where="order_book_id='002415.XSHE'",  # 下推唯一证券过滤
    columns=['date', 'close'],  # 只读取均线家族所需字段
).reset_index().sort_values('date')[['date', 'close']].copy())  # 恢复日期索引并固定顺序
one_stock['future_return'] = one_stock['close'].pct_change().shift(-1)  # 对齐信号形成后的下一日收益
列表 13.11: 习题6:50个均线规则的 HAC 检验与跳过日志
ma_records, ma_skip_log = [], []  # 保留成功检验和失败原因
for window in range(5, 55):  # 执行完整预注册窗口族
    moving_average = one_stock['close'].rolling(window, min_periods=window).mean()  # 排除 warm-up 期
    valid_observation = moving_average.notna() & one_stock['future_return'].notna()  # 只留完整信号与标签
    valid_count = int(valid_observation.sum())  # 记录该规则的有效样本量
    minimum_count = max(60, 4 * window)  # 使有效样本相对窗口和 HAC 带宽足够
    if valid_count < minimum_count:  # 在拟合前执行最小样本保护
        ma_skip_log.append({'window': window, 'n': valid_count, 'reason': f'需要 n>={minimum_count}'})  # 明确登记跳过原因
        continue  # 不以朴素 t 检验替代
    benchmark_return = one_stock.loc[valid_observation, 'future_return']  # 提取同日期买入持有基准
    strategy_return = benchmark_return.where(one_stock.loc[valid_observation, 'close'] > moving_average.loc[valid_observation], 0.0)  # 构造长仓或现金策略收益
    strategy_excess = strategy_return - benchmark_return  # 定义策略相对同股基准的 estimand
    if strategy_excess.nunique() < 2:  # 防止零方差导致协方差不可估
        ma_skip_log.append({'window': window, 'n': valid_count, 'reason': '超额收益为常数'})  # 登记数值失败
        continue  # 跳过不可估规则
    hac_lag_count = max(window, int(np.sqrt(valid_count)))  # HAC 同时覆盖窗口和经验带宽
    fitted_mean = sm.OLS(strategy_excess.to_numpy(), np.ones((len(strategy_excess), 1))).fit(cov_type='HAC', cov_kwds={'maxlags': hac_lag_count})  # 使用 HAC 推断均值
    estimate, standard_error = strategy_excess.mean(), fitted_mean.bse[0]  # 提取效应与HAC标准误
    test_statistic, raw_p_value = fitted_mean.tvalues[0], fitted_mean.pvalues[0]  # 提取双侧统计量和p值
    inference_valid = np.isfinite(estimate) and np.isfinite(fitted_mean.cov_params()).all() and np.isfinite(standard_error) and standard_error > 0 and np.isfinite(test_statistic) and np.isfinite(raw_p_value) and 0 <= raw_p_value <= 1  # 校正前验证全部推断量
    if not inference_valid:  # 拒绝把非有限或无效标准误送入多重校正
        ma_skip_log.append({'window': window, 'n': valid_count, 'reason': '效应/协方差/统计量/p值须有限,SE须有限且>0,p须在[0,1]'})  # 数值失败不得冒充估计成功
        continue  # 保留计划成员并在完整家族中赋p=1
    ma_records.append({'window': window, 'status': 'estimated', 'failure_reason': None, 'estimand': '日均策略减买入持有收益', 'estimate': estimate, 'n': valid_count, 'dependence': '重叠均线与日序列相关', 'se_method': f'HAC(maxlags={hac_lag_count})', 'se': standard_error, 'statistic': test_statistic, 'p_value': raw_p_value, 'start': one_stock.loc[valid_observation, 'date'].min(), 'end': one_stock.loc[valid_observation, 'date'].max()})  # 留存完整推断字段
列表 13.12: 习题6:家族校正与最小样本核对
ma_results = pd.DataFrame(ma_records, columns=['window', 'status', 'failure_reason', 'estimand', 'estimate', 'n', 'dependence', 'se_method', 'se', 'statistic', 'p_value', 'start', 'end'])  # 汇总成功估计的规则并固定空表字段要求
ma_planned_family = pd.DataFrame({'window': list(range(5, 55))})  # 在校正前保留全部50个计划假设
ma_skip_table = pd.DataFrame(ma_skip_log).rename(columns={'reason': 'failure_reason'})  # 统一失败字段
ma_family_table = ma_planned_family.merge(ma_results, on='window', how='left', validate='one_to_one')  # 以计划家族为权威左表
if not ma_skip_table.empty:  # 将跳过原因写回完整计划表
    ma_family_table = ma_family_table.merge(ma_skip_table[['window', 'failure_reason']], on='window', how='left', suffixes=('', '_skip'), validate='one_to_one')  # 一对一合并失败日志
    ma_family_table['failure_reason'] = ma_family_table['failure_reason'].fillna(ma_family_table.pop('failure_reason_skip'))  # 保留唯一失败字段
ma_family_table['status'] = ma_family_table['status'].fillna('skipped')  # 明确每个计划假设的状态
ma_family_table['p_value'] = ma_family_table['p_value'].where(ma_family_table['status'].eq('estimated'), 1.0)  # 跳过项在完整家族中显式保存p=1
ma_correction_p = ma_family_table['p_value']  # 全部50项都有合法校正输入
ma_family_table['by_adjusted_p'] = multipletests(ma_correction_p, method='fdr_by')[1]  # 任意依赖下的FDR主分析
ma_family_table['bonferroni_adjusted_p'] = multipletests(ma_correction_p, method='bonferroni')[1]  # 任意依赖下的FWER稳健性分析
ma_bh_condition_verified = False  # 嵌套窗口尚无BH正依赖或独立性设计证据
ma_bh_condition_evidence = None  # 未提交证据时不得计算BH调整值
if ma_bh_condition_verified and ma_bh_condition_evidence:  # 只有双重条件成立才运行BH
    ma_family_table['bh_adjusted_p'] = multipletests(ma_correction_p, method='fdr_bh')[1]  # 条件性BH补充分析
    ma_bh_status = 'run_with_verified_condition'  # 记录BH已经授权运行
else:  # 缺少依赖条件证据时明确跳过BH
    ma_family_table['bh_adjusted_p'] = np.nan  # 不用未验证BH数值暗示有效推断
    ma_bh_status = 'not_run'  # 明示依赖条件证据缺失
print({'planned': len(ma_family_table), 'actual': len(ma_results), 'skipped': len(ma_skip_log), 'primary': 'BY', 'sensitivity': 'Bonferroni', 'bh_status': ma_bh_status})  # 核对固定家族和方法层级
assert len(ma_family_table) == len(ma_results) + len(ma_skip_log) == 50  # planned=estimated+skipped且固定为50
print(ma_family_table.to_string(index=False))  # 显示全部计划项、状态、依赖与调整值
{'planned': 50, 'actual': 50, 'skipped': 0, 'primary': 'BY', 'sensitivity': 'Bonferroni', 'bh_status': 'not_run'}
 window    status failure_reason    estimand  estimate    n dependence       se_method       se  statistic  p_value      start        end  by_adjusted_p  bonferroni_adjusted_p  bh_adjusted_p
      5 estimated           None 日均策略减买入持有收益 -0.000500 3784 重叠均线与日序列相关 HAC(maxlags=61) 0.000229  -2.187065 0.028738 2010-06-03 2025-12-30       0.583796               1.000000            NaN
      6 estimated           None 日均策略减买入持有收益 -0.000515 3783 重叠均线与日序列相关 HAC(maxlags=61) 0.000234  -2.200103 0.027800 2010-06-04 2025-12-30       0.583796               1.000000            NaN
      7 estimated           None 日均策略减买入持有收益 -0.000499 3782 重叠均线与日序列相关 HAC(maxlags=61) 0.000232  -2.155263 0.031141 2010-06-07 2025-12-30       0.583796               1.000000            NaN
      8 estimated           None 日均策略减买入持有收益 -0.000502 3781 重叠均线与日序列相关 HAC(maxlags=61) 0.000227  -2.207743 0.027262 2010-06-08 2025-12-30       0.583796               1.000000            NaN
      9 estimated           None 日均策略减买入持有收益 -0.000560 3780 重叠均线与日序列相关 HAC(maxlags=61) 0.000224  -2.505455 0.012229 2010-06-09 2025-12-30       0.583796               0.611471            NaN
     10 estimated           None 日均策略减买入持有收益 -0.000572 3779 重叠均线与日序列相关 HAC(maxlags=61) 0.000222  -2.574323 0.010044 2010-06-10 2025-12-30       0.583796               0.502182            NaN
     11 estimated           None 日均策略减买入持有收益 -0.000621 3778 重叠均线与日序列相关 HAC(maxlags=61) 0.000232  -2.674733 0.007479 2010-06-11 2025-12-30       0.583796               0.373944            NaN
     12 estimated           None 日均策略减买入持有收益 -0.000570 3777 重叠均线与日序列相关 HAC(maxlags=61) 0.000235  -2.424173 0.015343 2010-06-17 2025-12-30       0.583796               0.767164            NaN
     13 estimated           None 日均策略减买入持有收益 -0.000467 3776 重叠均线与日序列相关 HAC(maxlags=61) 0.000234  -1.993651 0.046190 2010-06-18 2025-12-30       0.711912               1.000000            NaN
     14 estimated           None 日均策略减买入持有收益 -0.000515 3775 重叠均线与日序列相关 HAC(maxlags=61) 0.000237  -2.174691 0.029653 2010-06-21 2025-12-30       0.583796               1.000000            NaN
     15 estimated           None 日均策略减买入持有收益 -0.000407 3774 重叠均线与日序列相关 HAC(maxlags=61) 0.000230  -1.765796 0.077430 2010-06-22 2025-12-30       0.805895               1.000000            NaN
     16 estimated           None 日均策略减买入持有收益 -0.000399 3773 重叠均线与日序列相关 HAC(maxlags=61) 0.000231  -1.730323 0.083573 2010-06-23 2025-12-30       0.805895               1.000000            NaN
     17 estimated           None 日均策略减买入持有收益 -0.000359 3772 重叠均线与日序列相关 HAC(maxlags=61) 0.000228  -1.575394 0.115165 2010-06-24 2025-12-30       0.901427               1.000000            NaN
     18 estimated           None 日均策略减买入持有收益 -0.000395 3771 重叠均线与日序列相关 HAC(maxlags=61) 0.000230  -1.717010 0.085977 2010-06-25 2025-12-30       0.805895               1.000000            NaN
     19 estimated           None 日均策略减买入持有收益 -0.000394 3770 重叠均线与日序列相关 HAC(maxlags=61) 0.000224  -1.757624 0.078811 2010-06-28 2025-12-30       0.805895               1.000000            NaN
     20 estimated           None 日均策略减买入持有收益 -0.000489 3769 重叠均线与日序列相关 HAC(maxlags=61) 0.000224  -2.183268 0.029016 2010-06-29 2025-12-30       0.583796               1.000000            NaN
     21 estimated           None 日均策略减买入持有收益 -0.000519 3768 重叠均线与日序列相关 HAC(maxlags=61) 0.000226  -2.290467 0.021994 2010-06-30 2025-12-30       0.583796               1.000000            NaN
     22 estimated           None 日均策略减买入持有收益 -0.000442 3767 重叠均线与日序列相关 HAC(maxlags=61) 0.000225  -1.960388 0.049951 2010-07-01 2025-12-30       0.711912               1.000000            NaN
     23 estimated           None 日均策略减买入持有收益 -0.000363 3766 重叠均线与日序列相关 HAC(maxlags=61) 0.000225  -1.612085 0.106944 2010-07-02 2025-12-30       0.891038               1.000000            NaN
     24 estimated           None 日均策略减买入持有收益 -0.000313 3765 重叠均线与日序列相关 HAC(maxlags=61) 0.000220  -1.421240 0.155247 2010-07-05 2025-12-30       0.957727               1.000000            NaN
     25 estimated           None 日均策略减买入持有收益 -0.000288 3764 重叠均线与日序列相关 HAC(maxlags=61) 0.000217  -1.323716 0.185597 2010-07-06 2025-12-30       1.000000               1.000000            NaN
     26 estimated           None 日均策略减买入持有收益 -0.000315 3763 重叠均线与日序列相关 HAC(maxlags=61) 0.000213  -1.478490 0.139277 2010-07-07 2025-12-30       0.956411               1.000000            NaN
     27 estimated           None 日均策略减买入持有收益 -0.000251 3762 重叠均线与日序列相关 HAC(maxlags=61) 0.000211  -1.187929 0.234861 2010-07-08 2025-12-30       1.000000               1.000000            NaN
     28 estimated           None 日均策略减买入持有收益 -0.000190 3761 重叠均线与日序列相关 HAC(maxlags=61) 0.000209  -0.911470 0.362048 2010-07-09 2025-12-30       1.000000               1.000000            NaN
     29 estimated           None 日均策略减买入持有收益 -0.000261 3760 重叠均线与日序列相关 HAC(maxlags=61) 0.000202  -1.291889 0.196396 2010-07-12 2025-12-30       1.000000               1.000000            NaN
     30 estimated           None 日均策略减买入持有收益 -0.000220 3759 重叠均线与日序列相关 HAC(maxlags=61) 0.000200  -1.101420 0.270714 2010-07-13 2025-12-30       1.000000               1.000000            NaN
     31 estimated           None 日均策略减买入持有收益 -0.000263 3758 重叠均线与日序列相关 HAC(maxlags=61) 0.000194  -1.352457 0.176229 2010-07-14 2025-12-30       0.995202               1.000000            NaN
     32 estimated           None 日均策略减买入持有收益 -0.000274 3757 重叠均线与日序列相关 HAC(maxlags=61) 0.000193  -1.416858 0.156524 2010-07-15 2025-12-30       0.957727               1.000000            NaN
     33 estimated           None 日均策略减买入持有收益 -0.000345 3756 重叠均线与日序列相关 HAC(maxlags=61) 0.000198  -1.744059 0.081149 2010-07-16 2025-12-30       0.805895               1.000000            NaN
     34 estimated           None 日均策略减买入持有收益 -0.000267 3755 重叠均线与日序列相关 HAC(maxlags=61) 0.000197  -1.350187 0.176956 2010-07-19 2025-12-30       0.995202               1.000000            NaN
     35 estimated           None 日均策略减买入持有收益 -0.000299 3754 重叠均线与日序列相关 HAC(maxlags=61) 0.000194  -1.542210 0.123022 2010-07-20 2025-12-30       0.922506               1.000000            NaN
     36 estimated           None 日均策略减买入持有收益 -0.000272 3753 重叠均线与日序列相关 HAC(maxlags=61) 0.000192  -1.412089 0.157924 2010-07-21 2025-12-30       0.957727               1.000000            NaN
     37 estimated           None 日均策略减买入持有收益 -0.000254 3752 重叠均线与日序列相关 HAC(maxlags=61) 0.000192  -1.317505 0.187669 2010-07-22 2025-12-30       1.000000               1.000000            NaN
     38 estimated           None 日均策略减买入持有收益 -0.000235 3751 重叠均线与日序列相关 HAC(maxlags=61) 0.000192  -1.226480 0.220018 2010-07-23 2025-12-30       1.000000               1.000000            NaN
     39 estimated           None 日均策略减买入持有收益 -0.000289 3750 重叠均线与日序列相关 HAC(maxlags=61) 0.000196  -1.474681 0.140298 2010-07-26 2025-12-30       0.956411               1.000000            NaN
     40 estimated           None 日均策略减买入持有收益 -0.000248 3749 重叠均线与日序列相关 HAC(maxlags=61) 0.000196  -1.268641 0.204569 2010-07-27 2025-12-30       1.000000               1.000000            NaN
     41 estimated           None 日均策略减买入持有收益 -0.000297 3748 重叠均线与日序列相关 HAC(maxlags=61) 0.000197  -1.510380 0.130946 2010-07-28 2025-12-30       0.950250               1.000000            NaN
     42 estimated           None 日均策略减买入持有收益 -0.000346 3747 重叠均线与日序列相关 HAC(maxlags=61) 0.000201  -1.722035 0.085063 2010-07-29 2025-12-30       0.805895               1.000000            NaN
     43 estimated           None 日均策略减买入持有收益 -0.000361 3746 重叠均线与日序列相关 HAC(maxlags=61) 0.000200  -1.802977 0.071392 2010-07-30 2025-12-30       0.805895               1.000000            NaN
     44 estimated           None 日均策略减买入持有收益 -0.000388 3745 重叠均线与日序列相关 HAC(maxlags=61) 0.000199  -1.954570 0.050634 2010-08-02 2025-12-30       0.711912               1.000000            NaN
     45 estimated           None 日均策略减买入持有收益 -0.000431 3744 重叠均线与日序列相关 HAC(maxlags=61) 0.000198  -2.174442 0.029672 2010-08-03 2025-12-30       0.583796               1.000000            NaN
     46 estimated           None 日均策略减买入持有收益 -0.000395 3743 重叠均线与日序列相关 HAC(maxlags=61) 0.000197  -2.004734 0.044991 2010-08-04 2025-12-30       0.711912               1.000000            NaN
     47 estimated           None 日均策略减买入持有收益 -0.000360 3742 重叠均线与日序列相关 HAC(maxlags=61) 0.000200  -1.798996 0.072019 2010-08-05 2025-12-30       0.805895               1.000000            NaN
     48 estimated           None 日均策略减买入持有收益 -0.000340 3741 重叠均线与日序列相关 HAC(maxlags=61) 0.000200  -1.697411 0.089619 2010-08-06 2025-12-30       0.806429               1.000000            NaN
     49 estimated           None 日均策略减买入持有收益 -0.000327 3740 重叠均线与日序列相关 HAC(maxlags=61) 0.000199  -1.646557 0.099649 2010-08-09 2025-12-30       0.862195               1.000000            NaN
     50 estimated           None 日均策略减买入持有收益 -0.000317 3739 重叠均线与日序列相关 HAC(maxlags=61) 0.000202  -1.570906 0.116204 2010-08-10 2025-12-30       0.901427               1.000000            NaN
     51 estimated           None 日均策略减买入持有收益 -0.000289 3738 重叠均线与日序列相关 HAC(maxlags=61) 0.000204  -1.411706 0.158036 2010-08-11 2025-12-30       0.957727               1.000000            NaN
     52 estimated           None 日均策略减买入持有收益 -0.000284 3737 重叠均线与日序列相关 HAC(maxlags=61) 0.000203  -1.399117 0.161778 2010-08-12 2025-12-30       0.957727               1.000000            NaN
     53 estimated           None 日均策略减买入持有收益 -0.000256 3736 重叠均线与日序列相关 HAC(maxlags=61) 0.000203  -1.262257 0.206856 2010-08-13 2025-12-30       1.000000               1.000000            NaN
     54 estimated           None 日均策略减买入持有收益 -0.000221 3735 重叠均线与日序列相关 HAC(maxlags=61) 0.000206  -1.076033 0.281912 2010-08-16 2025-12-30       1.000000               1.000000            NaN
  1. 财务指标选股的多重检验*

本题使用真实上市公司财务数据估计多个财务因子与未来收益的秩相关,并对双向聚类 p 值实施 BY 校正。

列表 13.13: 习题7:加载财务报表与股价数据
import pandas as pd  # 整理财务版本、年度价格与因子检验族
import numpy as np  # 验证价格和推断量为有限数值
import os  # 读取教材统一数据根环境变量
import statsmodels.api as sm  # 在独立运行中提供双向聚类回归
from statsmodels.stats.multitest import multipletests  # 在独立运行中执行五项家族校正

# 从统一环境变量解析两项真实数据输入并显式核验文件
from pathlib import Path  # 使用跨平台路径对象解析显式数据根
book_data_dir_text = os.environ.get('BOOK_DATA_DIR')  # 先识别环境变量缺失以生成明确诊断
if not book_data_dir_text:  # 财务因子答案不接受隐式机器路径
    raise EnvironmentError('未设置 BOOK_DATA_DIR;请将其设为包含 stock/ 子目录的教材数据根目录')  # 指明独立运行前置条件
DATA_DIR = Path(book_data_dir_text).expanduser().resolve()  # 规范化财务因子案例的数据根
if not DATA_DIR.is_dir():  # 在读取前验证数据根
    raise FileNotFoundError(f'BOOK_DATA_DIR 不存在或不是目录: {DATA_DIR}')  # 失败即停止,不回退固定路径
m13_factor_financial_path = DATA_DIR / 'stock' / 'financial_statement.h5'  # 定位财务报表
m13_factor_price_path = DATA_DIR / 'stock' / 'stock_price_post_adjusted.h5'  # 定位后复权股价
missing_factor_paths = [path for path in (m13_factor_financial_path, m13_factor_price_path) if not path.is_file()]  # 精确识别财务因子案例缺失文件
if missing_factor_paths:  # 任一数据源缺失都阻止后续合并
    raise FileNotFoundError(f'缺少财务因子案例文件: {[str(path) for path in missing_factor_paths]};请检查 BOOK_DATA_DIR')  # 列出需要补齐的输入
financial_statement_raw = pd.read_hdf(m13_factor_financial_path).copy()  # 取得构造五项财务因子的历史报表版本
factor_required_columns = {'order_book_id', 'quarter', 'info_date', 'net_profit', 'equity_parent_company', 'operating_revenue', 'current_liabilities', 'non_current_liabilities', 'total_assets', 'current_assets'}  # 固定因子与可得时点所需字段集合
assert factor_required_columns.issubset(financial_statement_raw.columns), '财务报表缺少因子字段'  # 确保五项因子与可得时点均可构造
annual_financial_data = financial_statement_raw[financial_statement_raw['quarter'].str.endswith('q4')].copy()  # 只保留年报
annual_financial_data['report_year'] = annual_financial_data['quarter'].str[:4].astype(int)  # 提取报告年
annual_financial_data['available_year'] = annual_financial_data['report_year'] + 1  # 保守形成年
annual_financial_data['info_date'] = pd.to_datetime(annual_financial_data['info_date'], errors='coerce')  # 解析披露/修订日
assert annual_financial_data['info_date'].notna().all(), '年报info_date含缺失或无效日期'  # 检查信息可得日
annual_financial_data['formation_date'] = pd.to_datetime(annual_financial_data['available_year'].astype(str) + '-12-31')  # 年末形成日
annual_financial_data = annual_financial_data[annual_financial_data['info_date'].le(annual_financial_data['formation_date'])].copy()  # 排除未来重述
annual_financial_data = annual_financial_data.sort_values(['order_book_id', 'available_year', 'info_date']).groupby(['order_book_id', 'available_year']).tail(1)  # 逐形成日取最后可得版本

完成年报数据筛选和年份提取后,接下来基于财务报表原始字段计算五个核心财务因子(ROE、净利润率、资产负债率、总资产周转率和流动比率),作为后续相关性检验的解释变量。

列表 13.14: 习题7:计算财务因子
# 计算五个核心财务因子
# ROE = 净利润 / 归属母公司股东权益
annual_financial_data['ROE'] = annual_financial_data['net_profit'] / annual_financial_data['equity_parent_company']
# 净利润率 = 净利润 / 营业收入
annual_financial_data['Net_Margin'] = annual_financial_data['net_profit'] / annual_financial_data['operating_revenue']
# 资产负债率 = (流动负债+非流动负债) / 总资产
annual_financial_data['Debt_Ratio'] = (
    annual_financial_data['current_liabilities'].fillna(0) + annual_financial_data['non_current_liabilities'].fillna(0)  # 合并流动与非流动负债作为分子
) / annual_financial_data['total_assets']  # 填充缺失值后计算负债率
# 总资产周转率 = 营业收入 / 总资产
annual_financial_data['Asset_Turnover'] = annual_financial_data['operating_revenue'] / annual_financial_data['total_assets']
# 流动比率 = 流动资产 / 流动负债
annual_financial_data['Current_Ratio'] = annual_financial_data['current_assets'] / annual_financial_data['current_liabilities']

财务因子计算完毕后,下一步从后复权股价数据中提取每年末收盘价、计算下一年度收益率,并与财务因子表进行合并,构建因子-收益率配对数据集。

列表 13.15: 习题7:计算年度收益率并合并
# 使用后复权价格构造公司内部连续年度收益
stock_price_annual = pd.read_hdf(m13_factor_price_path)  # 取得计算跨年持有收益的价格历史
# 每个公司年度只选择真实最后交易日,避免按自然日假定交易
stock_price_annual = stock_price_annual.reset_index()  # 重置DataFrame索引
stock_price_annual['date'] = pd.to_datetime(stock_price_annual['date'], errors='coerce')  # 严格解析日期后才能定义期末
stock_price_annual['close'] = pd.to_numeric(stock_price_annual['close'], errors='coerce')  # 价格必须可验证为有限数
invalid_price_rows = stock_price_annual['date'].isna() | ~np.isfinite(stock_price_annual['close']) | stock_price_annual['close'].le(0)  # 标记无效日期或价格
assert not invalid_price_rows.any(), '股价数据含无效日期或非正有限收盘价'  # 检查日期和价格
assert not stock_price_annual.duplicated(['order_book_id', 'date']).any(), '股价数据含重复证券-日期键'  # 检查唯一键
stock_price_annual = stock_price_annual.sort_values(['order_book_id', 'date'], kind='mergesort').reset_index(drop=True)  # 固定公司内日期顺序
stock_price_annual['year'] = stock_price_annual['date'].dt.year  # 提取交易年份
year_end_dates = stock_price_annual.groupby(['order_book_id', 'year'])['date'].transform('max')  # 显式求每年最大交易日
year_end_price = stock_price_annual.loc[stock_price_annual['date'].eq(year_end_dates), ['order_book_id', 'year', 'date', 'close']].rename(columns={'date': 'year_end_date'}).copy()  # 每个公司年度只保留真实最后交易日
assert not year_end_price.duplicated(['order_book_id', 'year']).any(), '每个公司年度必须只有一个年末价格'  # 检查年度键
year_end_price = year_end_price.sort_values(['order_book_id', 'year']).reset_index(drop=True)  # 显式固定shift的公司内年序
# 严格在公司组内计算从当年末到下一年末的收益,禁止跨公司 shift
year_end_price['next_year'] = year_end_price.groupby('order_book_id')['year'].shift(-1)  # 保存下一价格观测所属年度以排除缺年跳跃
year_end_price['next_year_return'] = (year_end_price.groupby('order_book_id')['close'].shift(-1) / year_end_price['close'] - 1).where(year_end_price['next_year'].eq(year_end_price['year'] + 1))  # 缺年不得跨期冒充一年收益
year_end_price['next_year_end_date'] = year_end_price.groupby('order_book_id')['year_end_date'].shift(-1)  # 保留收益窗口的真实期末日
print({'selected_rows': len(year_end_price), 'selection_rule': '每公司每年最后交易日'})  # 报告年末选样规模与可复核规则
# 将财务因子与下一年收益率合并(基于股票代码和年份)
factor_return_merged = annual_financial_data.merge(  # 按信息可得年连接随后一年收益窗口
    year_end_price[['order_book_id', 'year', 'year_end_date', 'next_year_end_date', 'next_year_return']],  # 收益率与真实起止日一起交付
    left_on=['order_book_id', 'available_year'],  # 至少滞后到下一年后再启动收益窗
    right_on=['order_book_id', 'year'],  # 右表合并键
    how='inner'  # 内连接仅保留匹配记录
)  # 合并财务因子与未来收益率数据
{'selected_rows': 65509, 'selection_rule': '每公司每年最后交易日'}

得到因子—收益配对后,把两个变量转换为秩并标准化;标准化秩回归的斜率就是 Spearman 相关 estimand。推断使用公司与年度双向聚类协方差,以同时允许公司内持续性和同年度共同冲击。五个因子共享样本且相关,故默认用 BY,而不是把朴素 p 值送入 BH。

列表 13.16: 习题7:稳定两维簇键与数值验收
def stable_cluster_codes(frame):  # 为公司和年度两维聚类生成确定性整数键
    company_values = frame['order_book_id'].astype(str)  # 不把字符串直接交给statsmodels协方差实现
    company_levels = sorted(company_values.unique())  # 固定公司类别顺序保证复算编码一致
    year_levels = sorted(frame['year'].unique())  # 固定年度类别顺序保证第二维簇键稳定
    return np.column_stack([pd.Categorical(company_values, categories=company_levels).codes, pd.Categorical(frame['year'], categories=year_levels).codes])  # 返回与回归行逐项对齐的双维簇键
def validated_cluster_result(model):  # 提取并统一核验双向聚类推断结果
    estimate, standard_error = model.params.iloc[1], model.bse.iloc[1]  # 读取标准化因子斜率及其聚类标准误
    covariance_values = np.asarray(model.cov_params(), dtype=float)  # 检查完整协方差矩阵而非只看对角元
    statistic, raw_p_value = model.tvalues.iloc[1], model.pvalues.iloc[1]  # 提取进入家族校正的统计量和p值
    valid = np.isfinite(estimate) and np.isfinite(covariance_values).all() and np.isfinite(standard_error) and standard_error > 0 and np.isfinite(statistic) and np.isfinite(raw_p_value) and 0 <= raw_p_value <= 1  # 统一数值有效性条件
    return estimate, standard_error, statistic, raw_p_value, valid  # 同时返回推断量与可校正状态
factor_names_to_test = ['ROE', 'Net_Margin', 'Debt_Ratio', 'Asset_Turnover', 'Current_Ratio']  # 预先固定五项检验族
factor_test_results, factor_skip_log = [], []  # 分别保留有效检验与跳过原因
列表 13.17: 习题7:秩相关 estimand、公司/年度双向聚类推断与 BY 校正
for factor_name in factor_names_to_test:  # 逐项执行同一预注册程序
    required_columns = ['order_book_id', 'year', factor_name, 'next_year_return']  # 保留 estimand 与聚类键
    factor_pair = factor_return_merged[required_columns].replace([np.inf, -np.inf], np.nan).dropna().copy()  # 删除无效配对
    company_count, year_count = factor_pair['order_book_id'].nunique(), factor_pair['year'].nunique()  # 核对两维簇数
    if len(factor_pair) < 100 or company_count < 20 or year_count < 20:  # 普通渐近双聚类要求两维均有足够簇
        factor_skip_log.append({'factor': factor_name, 'n': len(factor_pair), 'companies': company_count, 'years': year_count, 'reason': '普通渐近双向聚类要求 n>=100、公司>=20、年度>=20;少簇需另行预注册 wild cluster bootstrap,本例停止'})  # 在计划家族中登记样本支持不足
        continue  # 不以朴素检验替代失败的稳健检验
    ranked_factor = factor_pair[factor_name].rank(method='average')  # 把因子转换为平均秩
    ranked_return = factor_pair['next_year_return'].rank(method='average')  # 把未来收益转换为平均秩
    if ranked_factor.nunique() < 2 or ranked_return.nunique() < 2 or not np.isfinite(ranked_factor).all() or not np.isfinite(ranked_return).all():  # 拒绝常数或非有限秩序列
        factor_skip_log.append({'factor': factor_name, 'n': len(factor_pair), 'companies': company_count, 'years': year_count, 'reason': '因子秩与收益秩必须有限且非常数'})  # 记录秩相关不可识别的原因
        continue  # 保留计划因子并进入下一项
    standardized_factor = (ranked_factor - ranked_factor.mean()) / ranked_factor.std(ddof=0)  # 标准化使斜率等于秩相关
    standardized_return = (ranked_return - ranked_return.mean()) / ranked_return.std(ddof=0)  # 标准化被解释变量
    cluster_groups = stable_cluster_codes(factor_pair)  # 稳定整数编码避免object簇键失败
    try:  # 捕获底层聚类协方差失败并转为可审计跳过项
        rank_model = sm.OLS(standardized_return, sm.add_constant(standardized_factor)).fit(cov_type='cluster', cov_kwds={'groups': cluster_groups})  # 双向聚类拟合
        estimate, standard_error, statistic, raw_p_value, inference_valid = validated_cluster_result(rank_model)  # 对拟合结果应用统一数值验收
    except Exception as error:  # 将模型失败留在输出而非静默删除因子
        factor_skip_log.append({'factor': factor_name, 'n': len(factor_pair), 'companies': company_count, 'years': year_count, 'reason': f'双向聚类拟合失败:{type(error).__name__}'})  # 保存失败类型供课堂诊断
        continue  # 不以朴素标准误替代失败的聚类推断
    if not inference_valid:  # 阻止无效统计量进入BY校正
        factor_skip_log.append({'factor': factor_name, 'n': len(factor_pair), 'companies': company_count, 'years': year_count, 'reason': '效应/协方差/统计量/p值须有限,SE须有限且>0,p须在[0,1]'})  # 登记数值验收失败
        continue  # 保留计划因子并进入下一项
    factor_test_results.append({'factor': factor_name, 'estimand': 'Spearman rho(标准化秩回归斜率)', 'estimate': estimate, 'n': len(factor_pair), 'companies': company_count, 'years': year_count, 'dependence': '公司内与年度共同冲击', 'se_method': 'company/year two-way cluster', 'se': standard_error, 'statistic': statistic, 'p_value': raw_p_value})  # 保存完整核对字段
factor_result_columns = ['factor', 'estimand', 'estimate', 'n', 'companies', 'years', 'dependence', 'se_method', 'se', 'statistic', 'p_value']  # 固定空表字段要求
factor_results_df = pd.DataFrame(factor_test_results, columns=factor_result_columns)  # 汇总所有可估检验
factor_family_df = pd.DataFrame({'factor': factor_names_to_test})  # 权威五项计划家族
factor_family_df = factor_family_df.merge(factor_results_df, on='factor', how='left', validate='one_to_one')  # 空结果也必须得到完整五行字段要求
factor_skip_df = pd.DataFrame(factor_skip_log, columns=['factor', 'n', 'companies', 'years', 'reason']).rename(columns={'reason': 'failure_reason'})  # 定型失败表
if not factor_skip_df.empty:  # 仅在存在跳过项时并入失败原因
    factor_family_df = factor_family_df.merge(factor_skip_df[['factor', 'failure_reason']], on='factor', how='left', validate='one_to_one')  # 保留唯一失败原因
else:  # 全部因子可估时仍保留统一失败原因列
    factor_family_df['failure_reason'] = None  # 用空值表示没有推断失败
factor_family_df['status'] = np.where(factor_family_df['p_value'].notna(), 'estimated', 'skipped')  # 区分有效推断与仍计入家族的失败项
factor_family_df['p_value'] = factor_family_df['p_value'].where(factor_family_df['status'].eq('estimated'), 1.0)  # 跳过项以1保留m=5
factor_correction_p = factor_family_df['p_value']  # 以完整五项计划家族作为BY输入
factor_family_df['by_adjusted_p'] = multipletests(factor_correction_p, alpha=0.05, method='fdr_by')[1]  # 固定五项BY
factor_family_df['by_reject'] = factor_family_df['status'].eq('estimated') & factor_family_df['by_adjusted_p'].le(0.05)  # 跳过不可拒绝
print({'planned': 5, 'estimated': int(factor_family_df['status'].eq('estimated').sum()), 'skipped': int(factor_family_df['status'].eq('skipped').sum())})  # 核对分母
assert len(factor_family_df) == int(factor_family_df['status'].eq('estimated').sum()) + int(factor_family_df['status'].eq('skipped').sum()) == 5  # 核验估计与跳过项恰好覆盖五项家族
print(factor_family_df.to_string(index=False))  # 展示全部五项的状态、效应与BY证据
{'planned': 5, 'estimated': 0, 'skipped': 5}
        factor estimand estimate   n companies years dependence se_method  se statistic p_value                                                       failure_reason  status by_adjusted_p  by_reject
           ROE      NaN      NaN NaN       NaN   NaN        NaN       NaN NaN       NaN     1.0 普通渐近双向聚类要求 n>=100、公司>=20、年度>=20;少簇需另行预注册 wild cluster bootstrap,本例停止 skipped             1      False
    Net_Margin      NaN      NaN NaN       NaN   NaN        NaN       NaN NaN       NaN     1.0 普通渐近双向聚类要求 n>=100、公司>=20、年度>=20;少簇需另行预注册 wild cluster bootstrap,本例停止 skipped             1      False
    Debt_Ratio      NaN      NaN NaN       NaN   NaN        NaN       NaN NaN       NaN     1.0 普通渐近双向聚类要求 n>=100、公司>=20、年度>=20;少簇需另行预注册 wild cluster bootstrap,本例停止 skipped             1      False
Asset_Turnover      NaN      NaN NaN       NaN   NaN        NaN       NaN NaN       NaN     1.0 普通渐近双向聚类要求 n>=100、公司>=20、年度>=20;少簇需另行预注册 wild cluster bootstrap,本例停止 skipped             1      False
 Current_Ratio      NaN      NaN NaN       NaN   NaN        NaN       NaN NaN       NaN     1.0 普通渐近双向聚类要求 n>=100、公司>=20、年度>=20;少簇需另行预注册 wild cluster bootstrap,本例停止 skipped             1      False

上方表逐项记录 estimand、有效 \(n\)、公司/年度簇数、依赖结构和 SE 方法。相关方向、效应量和拒绝数必须从当前输出读取;即使 BY 后拒绝零假设,也不等于具备样本外选股能力,还需独立测试期、基准收益与交易成本核对。

  1. 同一文档中的行业动量探索与复现

下面直接复用本章已经加载的 stock_price_historystock_basic_data。月收益的标签月只在相邻两个月末价格都存在时形成;样本按标签月分成互不重叠的探索期与复现期。

列表 13.18: 行业动量练习的自足月度面板与非重叠时期
m13_daily_prices = stock_price_history[['date', 'order_book_id', 'close']].copy()  # 复用本章已下推读取的真实三年行情
m13_daily_prices['date'] = pd.to_datetime(m13_daily_prices['date'])  # 统一交易日类型以构造日历月
m13_daily_prices['month'] = m13_daily_prices['date'].dt.to_period('M').dt.to_timestamp()  # 将每条行情映射到收益标签所用月份
m13_month_end = m13_daily_prices.sort_values(['order_book_id', 'month', 'date']).groupby(['order_book_id', 'month'], as_index=False).tail(1)  # 选择每只证券每月最后交易日
m13_month_end = m13_month_end.sort_values(['order_book_id', 'month'])  # 固定公司内相邻月份的差分顺序
m13_month_end['previous_month'] = m13_month_end.groupby('order_book_id')['month'].shift(1)  # 保留上一观测月以拒绝跨缺口收益
m13_month_end['monthly_return'] = m13_month_end.groupby('order_book_id')['close'].pct_change()  # 计算相邻观测月的价格收益
m13_is_adjacent = m13_month_end['previous_month'].dt.to_period('M') + 1 == m13_month_end['month'].dt.to_period('M')  # 核对两个月在日历上严格相邻
m13_month_end.loc[~m13_is_adjacent, 'monthly_return'] = np.nan  # 内部缺月时不跨月补算收益
m13_industry_map = stock_basic_data[['order_book_id', 'industry_name']].drop_duplicates('order_book_id')  # 取得每只证券唯一行业映射
m13_month_end = m13_month_end.merge(m13_industry_map, on='order_book_id', how='inner', validate='many_to_one')  # 把行业口径绑定到证券月收益
m13_industry_monthly = m13_month_end.groupby(['month', 'industry_name'])['monthly_return'].mean().unstack().sort_index()  # 构造行业等权月收益矩阵
m13_label_months = m13_industry_monthly.index  # 读取全部可评价收益标签月
m13_replication_start = m13_label_months[max(1, int(len(m13_label_months) * 0.65))]  # 在看检验结果前按时间顺序固定复现起点
m13_exploration_months = m13_label_months[m13_label_months < m13_replication_start]  # 探索期严格早于复现起点
m13_replication_months = m13_label_months[m13_label_months >= m13_replication_start]  # 复现期包含起点且与探索期不重叠
print({'exploration': [str(m13_exploration_months.min().date()), str(m13_exploration_months.max().date())], 'replication': [str(m13_replication_months.min().date()), str(m13_replication_months.max().date())]})  # 现场报告两个不重叠时期
{'exploration': ['2023-01-01', '2024-11-01'], 'replication': ['2024-12-01', '2025-12-01']}

每个成员的 estimand 是标准化过去 \(N\) 月累计收益对下月行业收益的正向回归斜率。下面的函数同时保留可估项和失败项;失败项以 \(p=1\) 进入相应校正家族。

列表 13.19: 完整行业窗口家族的 HAC 单项检验
def evaluate_m13_family(label_months, planned_members):  # 用同一规则评价探索或复现家族
    family_records = []  # 保存每个计划成员的效应、标准误、p值与失败原因
    for industry_name, lookback_months in planned_members:  # 逐一执行事前固定的行业窗口组合
        industry_returns = m13_industry_monthly[industry_name]  # 提取目标行业的完整月收益序列
        momentum_signal = industry_returns.rolling(lookback_months, min_periods=lookback_months).sum().shift(1)  # 只用标签月以前的N个月构造信号
        member_frame = pd.concat({'signal': momentum_signal, 'outcome': industry_returns}, axis=1).loc[label_months].dropna()  # 在指定时期对齐信号与下月收益
        minimum_rows = lookback_months + 4  # 预先要求窗口之外至少保留四个估计自由度
        if len(member_frame) < minimum_rows or member_frame.nunique().min() < 2:  # 样本不足或变量常数时禁止拟合
            family_records.append({'industry': industry_name, 'window': lookback_months, 'n': len(member_frame), 'effect': np.nan, 'se': np.nan, 'raw_p': 1.0, 'status': 'skipped', 'reason': 'minimum_sample_or_variation_not_met'})  # 以p=1保留失败成员
            continue  # 进入下一计划成员而不缩小家族分母
        standardized_frame = (member_frame - member_frame.mean()) / member_frame.std(ddof=0)  # 统一尺度使斜率等于Pearson相关系数
        hac_model = sm.OLS(standardized_frame['outcome'], sm.add_constant(standardized_frame['signal'])).fit(cov_type='HAC', cov_kwds={'maxlags': lookback_months})  # 用覆盖回看窗口的HAC协方差拟合
        effect_value, standard_error = float(hac_model.params['signal']), float(hac_model.bse['signal'])  # 提取标准化斜率与依赖稳健标准误
        is_valid = np.isfinite([effect_value, standard_error]).all() and standard_error > 0  # 核对效应和标准误可用于单侧推断
        raw_p_value = float(stats.norm.sf(effect_value / standard_error)) if is_valid else 1.0  # 按正向备择计算单侧p值
        family_records.append({'industry': industry_name, 'window': lookback_months, 'n': len(member_frame), 'effect': effect_value, 'se': standard_error, 'raw_p': raw_p_value, 'status': 'estimated' if is_valid else 'skipped', 'reason': None if is_valid else 'invalid_effect_or_standard_error'})  # 保存完整成员证据
    return pd.DataFrame(family_records)  # 返回不删除失败项的完整计划家族

探索期先对完整家族执行 BY,再逐项冻结拒绝成员。复现期只重新检验该冻结集合,并以 Holm 控制 FWER;若探索期没有拒绝项,空复现表本身就是可复核结果。

m13_planned_members = [(industry_name, lookback_months) for industry_name in m13_industry_monthly.columns for lookback_months in range(1, 13)]  # 在看p值前冻结完整行业窗口家族
m13_exploration_results = evaluate_m13_family(m13_exploration_months, m13_planned_members)  # 对完整探索家族执行同一HAC规则
m13_exploration_results['by_p'] = multipletests(m13_exploration_results['raw_p'], alpha=0.05, method='fdr_by')[1]  # 在任意依赖下控制探索FDR
m13_exploration_results['by_reject'] = m13_exploration_results['status'].eq('estimated') & m13_exploration_results['by_p'].le(0.05)  # 失败项不得进入发现集
m13_frozen_members = list(m13_exploration_results.loc[m13_exploration_results['by_reject'], ['industry', 'window']].itertuples(index=False, name=None))  # 在进入复现期前冻结发现集合
m13_replication_results = evaluate_m13_family(m13_replication_months, m13_frozen_members) if m13_frozen_members else pd.DataFrame(columns=['industry', 'window', 'n', 'effect', 'se', 'raw_p', 'status', 'reason'])  # 只检验冻结成员
m13_replication_results['holm_p'] = multipletests(m13_replication_results['raw_p'], alpha=0.05, method='holm')[1] if len(m13_replication_results) else pd.Series(dtype=float)  # 对非空冻结集控制复现FWER
m13_replication_results['holm_reject'] = m13_replication_results['status'].eq('estimated') & m13_replication_results['holm_p'].le(0.05) if len(m13_replication_results) else pd.Series(dtype=bool)  # 失败项不能形成复现拒绝
print({'planned': len(m13_planned_members), 'estimated': int(m13_exploration_results['status'].eq('estimated').sum()), 'skipped': int(m13_exploration_results['status'].eq('skipped').sum()), 'frozen_discoveries': len(m13_frozen_members), 'replication_members': len(m13_replication_results)})  # 现场核对完整家族与冻结规模
print(m13_exploration_results.sort_values('by_p').head(12).to_string(index=False))  # 展示探索期最强证据但保留完整表对象
m13_replication_results  # 显示全部冻结成员的独立复现与Holm结果
{'planned': 996, 'estimated': 738, 'skipped': 258, 'frozen_discoveries': 0, 'replication_members': 0}
    industry  window  n    effect       se    raw_p    status reason  by_p  by_reject
    黑色金属矿采选业       1 21 -0.204646 0.113277 0.964588 estimated   None   1.0      False
    黑色金属矿采选业       2 20 -0.112202 0.140869 0.787129 estimated   None   1.0      False
    黑色金属矿采选业       3 19 -0.375454 0.173914 0.984569 estimated   None   1.0      False
    黑色金属矿采选业       4 18 -0.604233 0.187447 0.999367 estimated   None   1.0      False
    黑色金属矿采选业       5 17 -0.435764 0.226633 0.972746 estimated   None   1.0      False
    黑色金属矿采选业       6 16 -0.399048 0.150210 0.996053 estimated   None   1.0      False
    黑色金属矿采选业       7 15 -0.227991 0.140659 0.947478 estimated   None   1.0      False
    黑色金属矿采选业       8 14 -0.452732 0.119566 0.999924 estimated   None   1.0      False
黑色金属冶炼和压延加工业       5 17 -0.399114 0.168565 0.991051 estimated   None   1.0      False
黑色金属冶炼和压延加工业       6 16 -0.449948 0.118803 0.999924 estimated   None   1.0      False
黑色金属冶炼和压延加工业       7 15 -0.189998 0.075660 0.993984 estimated   None   1.0      False
黑色金属冶炼和压延加工业       8 14 -0.274598 0.092833 0.998452 estimated   None   1.0      False
表 13.6: 行业动量探索期 BY 与非重叠复现期 Holm 结果
industry window n effect se raw_p status reason holm_p holm_reject

结论必须从本次输出读取。探索期 BY 拒绝只表示在预设错误率规则下形成候选;复现期 Holm 不拒绝不等于证明效应为零,拒绝也不自动说明策略可交易。基准收益、交易成本、执行约束和新的样本外时期仍需另行检验。

13.14.3 理论题解答

  1. BH 在独立检验下控制 FDR

    \(I_i\) 表示真零假设 \(i\) 被拒绝,\(R\) 为总拒绝数。按 \(R=1,\ldots,m\) 分解 \[ E\!\left[\frac{I_i}{\max(R,1)}\right] =\sum_{r=1}^m\frac1rP\!\left(P_i\le \frac{rq}{m},R^{(-i)}=r\right), \] 其中 \(R^{(-i)}\) 是把 \(P_i\) 置零后的 step-up 拒绝数。独立且连续均匀的真零 p 值给出 \[ \sum_{r=1}^m\frac1r\frac{rq}{m}P(R^{(-i)}=r)=\frac qm. \]\(m_0\) 个真零假设求和,\(\mathrm{FDR}=qm_0/m\le q\)。若只满足 super-uniform,则等号改为上界;任意依赖下该分解不成立。

  2. Bonferroni 控制 FWER(完整推导)

    \(A_j=\{p_j\le\alpha/m\}\),则 \[ \mathrm{FWER} = P\Big(\cup_{j\in H_0} A_j\Big) \le \sum_{j\in H_0} P(A_j) \le \sum_{j\in H_0} \alpha/m \le \alpha. \] 不需要独立性。

  3. Holm 与 Hochberg 的 FWER 控制

    Holm 中若发生至少一个真零误拒绝,设排序中第一个真零位于 \(k\),则在到达它时至少还有 \(m_0\) 个真零未处理,必有 \(P_{(k)}\le\alpha/(m-k+1)\le\alpha/m_0\)。对真零 p 值用并集上界,概率至多 \(m_0(\alpha/m_0)=\alpha\),无需独立性。

    Hochberg 是 step-up 过程。发生真零误拒绝时,某个真零有序 p 值必须满足相应 Simes 边界;在独立或满足所需正依赖条件时,Simes 不等式把该事件概率界在 \(\alpha\)。因此 Hochberg 控制 FWER,但不能像 Holm 一样在任意依赖下无条件保证;任意依赖时应改用 Holm 等方法。

  4. 自适应、依赖与贝叶斯多重检验的比较

    自适应 FDR 方法先估计真零假设比例 \(\pi_0\),再据此放宽固定 BH 阈值;若 \(\pi_0\) 估计偏低,FDR 可能失控,因此应报告估计方法、调参和敏感性。BY 把 BH 阈值除以 \(c(m)=\sum_{j=1}^m1/j\),在任意依赖下仍控制 FDR,但功效通常更低;如果要使用较不保守的依赖校正,必须说明并验证相关结构假设。

    贝叶斯多重检验从效应与零假设的先验混合出发,计算后验零概率或局部 FDR;它能把效应大小和先验信息纳入决策,但结论依赖先验、混合模型和估计稳定性。完整答案应对三类方法分别说明目标错误率、主要假设、优势、失效情形和一个金融应用,并强调任何方法都不能把统计拒绝自动转换成可交易策略。

  5. 四种校正的同族手算

    \(H_{(i)}\) 对应 \(p_{(i)}\)。Bonferroni 的共同阈值为 \(0.05/4=0.0125\),故拒绝 \(\{H_{(1)},H_{(2)}\}\)。Holm 阈值依次为 \((0.0125,0.0167,0.0250,0.0500)\);前三步通过、第四步停止,拒绝 \(\{H_{(1)},H_{(2)},H_{(3)}\}\)

    BH 阈值为 \((0.0125,0.0250,0.0375,0.0500)\),最大满足序号为 \(k=3\),拒绝前三项。BY 有 \(c(4)=1+1/2+1/3+1/4=25/12\),阈值为 \((0.0060,0.0120,0.0180,0.0240)\),最大满足序号为 \(k=2\),拒绝前两项。

    Bonferroni 与 Holm 在边际 p 值有效时允许任意依赖并控制 FWER;BH 控制 FDR 需要独立或适当正依赖(PRDS);BY 在边际 p 值有效时允许任意依赖并控制 FDR。其余假设均记为“不拒绝”;所有结论只描述当前设计下反对零假设的证据强弱,不给任何假设命题判定真值。

13.15 章末回顾

学习自检:定义计划检验族与错误度量;为每个假设选择依赖稳健的单项标准误;说明 Holm、BH 与 BY 的适用条件;在查看 p 值前确定跳过规则和复现期;把探索发现集带到不重叠时期复现。完整答案应保留全部计划假设,区分单项时间依赖与家族依赖控制,并保证探索与复现时期不重叠。

禁用情境:单项 p 值无效、planned family 事后缩小或探索/复现时期重叠时,不应报告“经过校正的发现”;没有成本、基准和执行假设时,不应把拒绝零假设称为可交易收益。常见误区是把未拒绝解释为无效应,或把 FDR 控制误解为每个已拒绝假设为真的概率保证。

无提示检索:1)任意依赖下为何通常选择 BY 而不是直接套用 BH?2)跳过的预注册假设为何仍须留在 planned family 核对中?3)多重校正与独立时期复现分别解决什么问题?

展开检索反馈与全书出口决策

1)经典 BH 保证依赖独立或适当正依赖条件;BY 通过调和级数修正提供任意依赖下的控制。2)事后删除失败项会改变家族并带来选择性报告;应保留状态和原因,按事前规则处理分母。3)校正控制同一家族内的错误累积,独立复现检查探索选择在新时期能否重现,两者不能互相替代。需要复习时,可分别改用相关 p 值矩阵、包含不可估项目的新家族和改变边界月的新时间轴重做。

全书综合出口:提交各里程碑的数据与时点说明、可运行代码、基线、模型或推断比较、误差分析和业务解释;再由同伴在干净环境抽取一个里程碑复算,并在答辩中解释哪些结论只适用于当前数据、损失与验证设计。无法复算的环节进入补修清单,不得以展示性图表替代证据。

Benjamini, Yoav, 和 Yosef Hochberg. 1995年. 《Controlling the False Discovery Rate: A Practical and Powerful Approach to Multiple Testing》. Journal of the Royal Statistical Society: Series B (Methodological) 57 (1): 289~300. https://doi.org/10.1111/j.2517-6161.1995.tb02031.x.
Benjamini, Yoav, 和 Daniel Yekutieli. 2001年. 《The Control of the False Discovery Rate in Multiple Testing under Dependency》. The Annals of Statistics 29 (4): 1165~88. https://doi.org/10.1214/aos/1013699998.
Holm, Sture. 1979年. 《A Simple Sequentially Rejective Multiple Test Procedure》. Scandinavian Journal of Statistics 6 (2): 65~70. https://www.jstor.org/stable/4615733.
Storey, John D. 2002年. 《A Direct Approach to False Discovery Rates》. Journal of the Royal Statistical Society: Series B (Statistical Methodology) 64 (3): 479~98. https://doi.org/10.1111/1467-9868.00346.