halo 的技术博客

返回

你刚跑完一个「聪明」的策略:用过去 5 天的收益预测今天。回测夏普 1.2,你有点心动。

但理性先问一句——这 1.2 里有多少是真预测力,有多少只是收益序列天然的自相关被你「无意中」捕捉到了?

再比如,你发现 A 股和美股「日收益相关 0.35」。你激动地要拿去做配置。但等等:两根独立随机游走,光是有限样本就能给你撞出 ±0.2 的相关。0.35 到底是结构,还是噪声?

这两类问题,本质都是同一件事:我看到的序列依赖,到底能不能被「纯随机(白噪声)」解释?

把这个问题交给 Ljung-Box 白噪声检验,它会给你一个 p 值,干脆地回答:能(通过,视作白噪声)/ 不能(拒绝,序列里有真结构)。本文从零复现它,并用三类合成序列把「怎么用、怎么误用」跑出数字。

一、什么是「白噪声」#

对一个零均值平稳序列 {xt}\{x_t\},如果:

  • E[xt]=0E[x_t]=0
  • Var(xt)=σ2\text{Var}(x_t)=\sigma^2(常数)
  • 任意两个不同时刻不相关E[xtxtk]=0,  k0E[x_t x_{t-k}]=0,\;\forall k\neq 0

就称它为白噪声。换句话说,知道了过去,对预测未来毫无帮助——过去的信息含量为零。

所以「白噪声检验」等价于问:这个序列里,还有没有没被利用的可预测结构? 这就是为什么它是每一个时间序列模型(ARIMA、GARCH、因子残差诊断)出厂必做的「健康证」。

二、ACF 的缺陷:单点看不全#

最直观的工具是自相关函数(ACF)

ρ^k=t=k+1n(xtxˉ)(xtkxˉ)t=1n(xtxˉ)2\hat\rho_k = \frac{\sum_{t=k+1}^{n}(x_t-\bar x)(x_{t-k}-\bar x)}{\sum_{t=1}^{n}(x_t-\bar x)^2}

然后画「懒汉置信带」±1.96/n\pm 1.96/\sqrt{n}:单个 ρ^k\hat\rho_k 落进带里就算「不显著」。

但问题来了:

  1. 你只看了一个滞后阶,万一结构藏在 k=7k=7 呢?
  2. 多个滞后同时看,单个不显著但合起来显著,你根本看不见;
  3. 置信带本身粗糙,它假设「每个 ρ^k\hat\rho_k 独立近似正态」,实际上相邻 ACF 是相关的。

所以单点 ACF 只能「看个大概」。要做一个对全部滞后统一判决的检验,就得把前 hh 个自相关打包——这正是 Ljung-Box 做的事。

三、Ljung-Box Q 统计量#

把前 hh 个自相关 ρ^1,,ρ^h\hat\rho_1,\dots,\hat\rho_h 拼成一个数:

Q(h)=n(n+2)k=1hρ^k2nkQ(h) = n(n+2)\sum_{k=1}^{h}\frac{\hat\rho_k^2}{n-k}

在「序列是白噪声」的原假设下:

Q(h)  d  χh2Q(h) \;\xrightarrow{d}\; \chi^2_h

于是:

  • Q(h)Q(h),查卡方分布得 p 值 p=1Fχh2(Q)p = 1 - F_{\chi^2_h}(Q)
  • p 很小(如 < 0.05)→ 拒绝白噪声 → 序列里有依赖结构;
  • p 很大 → 不能拒绝 → 视作白噪声,过去对预测未来无益。

相比 Box-Pierce 原始 QBP=nρk2Q_{BP}=n\sum\rho_k^2,Ljung-Box 在分母里加了 n+2nk\frac{n+2}{n-k} 的小样本修正,有限样本下更稳——所以今天默认都用 Ljung-Box。

下面用纯 numpy 自己实现一遍(不依赖 statsmodels,看清每一步):

四、三类序列:白噪声一眼现形#

造三组长度 500 的序列:

  • 白噪声noise = rng.normal(0, 1, T)(纯独立);
  • AR(1)x_t = 0.6·x_{t-1} + ε_t(强序列依赖);
  • AR(1) 残差:先把 AR(1) 用 OLS 拟合成 x_t = a + b·x_{t-1} + u_t,取残差 u_t。如果模型抓对了结构,残差应该变回白噪声。

先看它们的 ACF:

T = 500
rng = np.random.default_rng(20260719)
noise = rng.normal(0, 1, T)
phi = 0.6
ar = np.zeros(T); ar[0] = rng.normal(0, 1)
for t in range(1, T):
    ar[t] = phi * ar[t-1] + rng.normal(0, 1)
ar = ar / np.std(ar)
# 拟合 AR(1) 取残差
X = np.column_stack([np.ones(T-1), ar[:-1]])
beta, *_ = np.linalg.lstsq(X, ar[1:], rcond=None)
resid = ar[1:] - X @ beta
python

白噪声 vs AR(1) 的 ACF:前者全在带内,后者指数衰减出带

图 1:左=纯白噪声 ACF,所有柱都落在 ±1.96/√T 虚线内;右=AR(1) φ=0.6 的 ACF,呈典型指数衰减、好几根戳出带外。但光看单点 ACF,你能确定「右图整体显著」吗?得靠下面打包的 Q 检验。

五、Ljung-Box:把 ACF 打包成 p 值#

对三组序列分别算 h=1..20 的 p 值:

Hs = np.arange(1, 21)
p_noise, p_arraw, p_arres = [], [], []
for h in Hs:
    p_noise.append(ljung_box(noise, h)[1])
    p_arraw.append(ljung_box(ar, h)[1])
    p_arres.append(ljung_box(resid, h)[1])
python

Ljung-Box p 值随滞后阶:白噪声≈1、AR 原始→0、AR 残差回到高位

图 2:蓝线(白噪声)p 值始终 ≈1,稳稳通过;橙线(AR(1) 原始序列)p 值一路砸到 0,明确拒绝白噪声;绿线(AR(1) 拟合后的残差)p 值回到 0.4 以上——说明模型把结构抓走了,残差变干净了。红色虚线是 α=0.05 显著性线。

实跑单点数字(取 h=10):

序列LB(h=10) p 值结论
白噪声0.617通过(视作白噪声)
AR(1) 原始0.000拒绝(有强结构)
AR(1) 残差0.406通过(模型抓走了结构)

这串数字把白噪声检验的用法讲透了:

  • 它对真白噪声大方放行(p=0.617);
  • 藏了依赖的序列铁面拒绝(p≈0);
  • 更妙的是第三行——它其实是模型诊断工具:当你说「我的 AR(1) 拟合完了」,残差必须过白噪声检验,否则说明你还漏了结构没抓。这是 Box-Jenkins 建模流程的「出厂质检」。

六、蒙特卡洛:检验靠谱吗?#

一个检验好不好,看两件事:原假设下不乱杀(一类错误率≈α)备择假设下能抓到(功效→1)。跑 400 次:

  • H0 组:每次生成纯白噪声(T=300),记 p<0.05 的比例;
  • H1 组:每次生成 AR(1) φ=0.35,记 p<0.05 的比例。
M, Tmc = 400, 300
p_h0, p_h1 = [], []
for i in range(M):
    z = rng.normal(0, 1, Tmc)
    p_h0.append(ljung_box(z, 10)[1])
    a = np.zeros(Tmc); a[0] = rng.normal(0, 1)
    for t in range(1, Tmc):
        a[t] = 0.35 * a[t-1] + rng.normal(0, 1)
    p_h1.append(ljung_box(a, 10)[1])
rate_h0 = np.mean(np.array(p_h0) < 0.05)   # 应≈0.05
rate_h1 = np.mean(np.array(p_h1) < 0.05)   # 应→1
python

p 值分布:H0 下≈均匀,H1 下堆向 0

图 3:左=H0 下 p 值近似均匀(红线是均匀基准),说明「没结构时不冤枉好人」;右=H1 下 p 值严重堆向 0,说明「有结构时抓得住」。

实测:

  • H0 下 p<0.05 比例 = 0.070(理论 0.05,略高但属 400 次抽样的随机波动,±2 SE 内);
  • H1 下 p<0.05 比例 = 0.988(检验功效接近满分,几乎不漏真信号)。

结论:这个检验在「该放时放、该抓时抓」上是站得住的。

七、四类真实陷阱#

  1. 滞后阶 h 怎么选:这是 Ljung-Box 最容易踩的坑。h 太小(如 h=5)可能漏掉长周期依赖;h 太大(如 h=50 而 T=100),分母 n-k 变小、卡方自由度虚高,检验会「过度敏感」乱拒绝。经验法则 h ≈ min(10, T/5),或按你关心的周期(如交易策略看 20 日周期就取 h≈20)。不同 h 给出不同结论,必须在文中固定并披露
  2. 短期记忆被误判为「无结构」:很多金融序列依赖只在前几阶显著、之后迅速衰减(如日收益弱 AR(1) φ≈0.05)。此时 h 取太大,把大量「不显著」的滞后一起打包,Q 统计量被稀释,可能漏报弱结构。对策:先看 ACF 图定位显著滞后的范围,再针对性取 h。
  3. 小样本下卡方近似失真:Q 的 χh2\chi^2_h 极限分布要求 n 足够大(经验 n>50,最好 n>100)。n=30 的短线,p 值偏差明显,可能把噪声当结构(一类错误飙升)。小样本改用 Box-Pierce 修正版或 bootstrap 重抽样得 p 值更稳。
  4. 多序列「家族错误率」:你同时对 50 个因子残差做白噪声检验,每个 α=0.05,期望就有 2.5 个被误判「有结构」。这和 FDR 问题同源——要用 BH 程序(见本专栏 FDR 一文)控制发现集的假阳性比例,别挨个看 p<0.05。

八、结论#

Ljung-Box 是把「序列里还有没有没榨干的结构」变成一句话判决的工具:

  • ACF 看单点,可能漏掉藏在深处的依赖;
  • Ljung-Box Q 把前 h 个自相关打包成卡方统计量,给一个干净的 p 值;
  • 本文实跑:白噪声 p=0.617 通过、AR(1) 原始 p≈0 拒绝、AR(1) 残差 p=0.406 回到白噪声——它既是「结构探测器」,也是「模型出厂质检」;
  • 蒙特卡洛证明它在 H0 下不乱杀、H1 下功效 0.988;
  • 但 h 的选择、弱结构漏报、小样本失真、多序列家族错误,四个坑任一踩中都会让你把噪声当信号、或把信号当噪声。

记住:白噪声检验不证明「模型对」,它只证明「残差里暂时找不到还能利用的依赖」。找不到了 ≠ 真的不存在,只是你当前的工具没抓到。


本文数据均为自洽合成(白噪声、AR(1) 及其 OLS 残差),仅用于演示方法。真实落地时:按周期固定并披露滞后阶 h、先画 ACF 定位显著范围再选 h、小样本改用 bootstrap p 值、多序列用 BH 控制家族错误率。代码已全部跑通,ACF/Q/p 值即输出。

白噪声检验 Ljung-Box:用自相关把「真信号」和「伪相关」分开
https://blog.halo26812.eu.org/blog/white-noise-ljung-box
Author halo
Published at 2026年7月19日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨