如何正确判断天文计数数据拟合的好坏——修正 Z 检验的原理与实现
How to properly assess goodness-of-fit for astronomical count data — the Corrected Z-test
想象一下:你用一台望远镜观察一颗遥远的恒星,每隔一秒记录一下光子打到探测器上的次数。得到的数据可能是这样的:
Imagine this: you point a telescope at a distant star and record how many photons hit the detector each second. Your data might look like:
时间(s): 1 2 3 4 5 6 7 8 9 10 计数: 3 2 4 1 5 2 3 4 1 2
现在你想知道:这些数据是不是符合某个理论模型(比如"平均每秒 3 个光子")?这就是拟合优度检验(goodness-of-fit test)要做的事。
Now you ask: do these data match a theoretical model (e.g. "an average of 3 photons per second")? That's exactly what a goodness-of-fit test does.
在天文学(特别是 X 射线天文、γ 射线天文)里,我们几乎总是用Cash C 统计量来衡量拟合的好坏。这个统计量在 XSPEC、SPEX、Sherpa 这些专业软件里都是默认选项。
In astronomy (especially X-ray and γ-ray astronomy), we almost always use the Cash C statistic to measure goodness-of-fit. It's the default in XSPEC, SPEX, and Sherpa.
2025 年 10 月,Li、Chen、Meng、van Dyk、Bonamente 和 Kashyap 在 arXiv:2510.03466 上发表了一篇论文(Making high-order asymptotics practical: correcting goodness-of-fit test for astronomical count data),系统地解决了这个问题。
In October 2025, Li, Chen, Meng, van Dyk, Bonamente, and Kashyap published a paper (arXiv:2510.03466, Making high-order asymptotics practical: correcting goodness-of-fit test for astronomical count data) that systematically solves this problem.
这篇教程的目标,是让你(大一物理本科生)不需要先学完概率论与数理统计,也能跟着代码把论文的核心算法跑起来,并理解为什么它工作。
The goal of this tutorial is to let you (a first-year physics undergraduate) run the paper's core algorithm by following the code, and understand why it works — without first having to finish a full probability course.
如果你在一段时间内数光子,每次数到的数量是随机的。这种"在固定时间间隔内随机事件发生的次数"服从泊松分布:
If you count photons over a fixed time interval, the number you get is random. The number of random events in a fixed interval follows a Poisson distribution:
$$P(N_i = k) = \frac{s_i^k e^{-s_i}}{k!}, \quad k = 0, 1, 2, \ldots$$
这里 \(s_i\) 是第 \(i\) 个 bin 的期望计数——也就是"理论模型说这个 bin 平均应该有多少个光子"。\(N_i\) 是你实际数到的。
Here \(s_i\) is the expected count for bin \(i\) — "how many photons the theory says should be there on average." \(N_i\) is what you actually observed.
在普通物理实验里,我们常用卡方检验来判断数据是否拟合模型:
In ordinary physics labs, we often use the chi-squared test to judge whether data fit a model:
$$\chi^2 = \sum_{i=1}^{n} \frac{(N_i - s_i)^2}{s_i}$$
如果模型正确,这个统计量应该服从自由度为 \(n-d\) 的卡方分布(\(n\) = bin 数,\(d\) = 拟合参数个数)。p 值 = 卡方分布尾部概率。
If the model is correct, this statistic should follow a chi-squared distribution with \(n-d\) degrees of freedom (\(n\) = number of bins, \(d\) = number of fitted parameters). p-value = chi-squared tail probability.
卡方检验有个隐藏的前提:每个 bin 的计数要"足够多"。统计学家通常要求每个 bin 的期望计数 ≥ 5。但在 X 射线天文里,很多 bin 的 sᵢ 远小于 1——因为高能光子本来就很少。
Chi-squared has a hidden requirement: each bin must have "enough" counts. Statisticians usually require expected count ≥ 5 per bin. But in X-ray astronomy, many bins have sᵢ far below 1 — because high-energy photons are naturally scarce.
1979 年,天体物理学家 W. Cash 提出了一个专门为泊松数据设计的统计量。它直接来自于最大似然的思想:对每个 bin,泊松分布的似然函数是:
In 1979, astrophysicist W. Cash proposed a statistic designed specifically for Poisson data. It comes directly from maximum likelihood: for each bin, the Poisson likelihood is:
$$L(\theta) = \prod_{i=1}^{n} \frac{s_i(\theta)^{N_i} e^{-s_i(\theta)}}{N_i!}$$
取负对数似然,乘以 2(让它和卡方"长得像"),再减掉一个不依赖参数的常数,就得到 C 统计量:
Take the negative log-likelihood, multiply by 2 (to make it "look like" chi-squared), and subtract a constant that doesn't depend on parameters. You get the C statistic:
$$C_n(\theta) = 2 \sum_{i=1}^{n} \left[ s_i(\theta) - N_i \log s_i(\theta) - N_i + N_i \log N_i \right]$$
这个公式看起来复杂,但理解起来很简单:对每个 bin 计算 \(s_i - N_i \log s_i - N_i + N_i \log N_i\),乘以 2,加起来。当模型完美时(\(N_i = s_i\)),每一项都是 0,所以 C = 0。模型越差,C 越大。
The formula looks complex, but the idea is simple: for each bin compute \(s_i - N_i \log s_i - N_i + N_i \log N_i\), multiply by 2, and sum. When the model is perfect (\(N_i = s_i\)), each term is 0, so C = 0. The worse the model, the larger C.
天文学家通常这样做:拟合模型得到参数估计 \(\hat{\theta}\),然后计算 \(C_n(\hat{\theta})\),把这个值当成一个 χ² 分布的随机变量,查表得到 p 值。自由度 = \(n - d\)(bin 数减参数个数)。
Astronomers usually do this: fit the model to get parameter estimate \(\hat{\theta}\), then compute \(C_n(\hat{\theta})\), treat it as a chi-squared random variable, and look up the p-value. Degrees of freedom = \(n - d\) (bins minus parameters).
这个做法的理论基础是Wilks 定理(1938):当样本量很大时,似然比统计量渐近服从卡方分布。但 Wilks 定理有前提条件(正则条件),在低计数场景下不满足。
The theoretical basis is Wilks' theorem (1938): for large samples, the likelihood ratio statistic asymptotically follows a chi-squared distribution. But Wilks' theorem has regularity conditions that are violated in the low-count regime.
一个自然的想法:既然真实参数 \(\theta^*\) 未知,那就用估计值 \(\hat{\theta}\) 代替,直接计算 C 统计量的均值和方差。这叫朴素代入法(naïve plug-in)。
A natural idea: since the true parameter \(\theta^*\) is unknown, just use the estimate \(\hat{\theta}\) instead, and directly compute the mean and variance of the C statistic. This is the naïve plug-in approach.
论文证明(Proposition 4):这样做会引入 O(1) 量级的偏差——也就是说偏差不会随样本量增大而消失。更糟的是,连 bootstrap 也受影响,因为 bootstrap 本质上也是一种"代入"。
The paper proves (Proposition 4): this introduces a bias of order O(1) — meaning the bias doesn't vanish as the sample size grows. Worse, even bootstrap is affected, because bootstrap is essentially a form of "plug-in."
论文的关键灵感来自统计学家 McCullagh(1986)的一个洞察:当你已经拟合了模型、得到了 \(\hat{\theta}\) 之后,你关心的分布不是"在所有可能的 \(\hat{\theta}\) 下 C 的分布",而是"在这个特定的 \(\hat{\theta}\) 下 C 的分布"。
The key inspiration comes from statistician McCullagh (1986): once you've fitted the model and obtained \(\hat{\theta}\), the distribution you care about is not "the distribution of C over all possible \(\hat{\theta}\)" but "the distribution of C given this particular \(\hat{\theta}\)".
论文的定理 5 和定理 6 分别给出了无条件矩和条件矩的渐近公式:
The paper's Theorem 5 and Theorem 6 give asymptotic formulas for unconditional moments and conditional moments respectively:
无条件矩(Theorem 5, Eq. 21):
Unconditional moments (Theorem 5, Eq. 21):
$$E[C_n(\hat{\theta})] = \kappa_1 - d + O(n^{-1})$$
$$\text{Var}[C_n(\hat{\theta})] = \kappa_2 + O(1)$$
条件矩(Theorem 6, Eqs. 22–23):
Conditional moments (Theorem 6, Eqs. 22–23):
$$E[C_n(\hat{\theta}) \mid \hat{\theta}] = \hat{\kappa}_1 - \frac{1}{2} \mathbf{1}^\top \hat{X}^\top \hat{\Sigma} \hat{X} (\hat{X}^\top \hat{V}^{-1} \hat{X})^{-1} \mathbf{1} + O(n^{-1/2})$$
$$\text{Var}[C_n(\hat{\theta}) \mid \hat{\theta}] = \hat{\kappa}_2 - \hat{\kappa}_{11}^\top \hat{X} (\hat{X}^\top \hat{V}^{-1} \hat{X})^{-1} \hat{X}^\top \hat{\kappa}_{11} + O(1)$$
这里 \(\kappa_1, \kappa_2\) 是 C 函数的各阶累积量(cumulants),\(X\) 是设计矩阵(模型对参数的梯度),\(V = \text{diag}(s_i)\),\(\kappa_{11}\) 是 C 函数和计数之间的协方差。看起来复杂,但核心思想很简洁:
Here \(\kappa_1, \kappa_2\) are cumulants of the C function, \(X\) is the design matrix (gradient of the model wrt parameters), \(V = \text{diag}(s_i)\), and \(\kappa_{11}\) is the covariance between C and the counts. It looks complex, but the core idea is clean:
论文的定理 2 给出了一个可计算的检验统计量:
The paper's Theorem 2 gives a computable test statistic:
$$T = \frac{C_n(\hat{\theta}) - E[C_n(\hat{\theta}) \mid \hat{\theta}]}{\sqrt{\text{Var}(C_n(\hat{\theta}) \mid \hat{\theta})}}$$
在零假设(模型正确)下,T 渐近服从标准正态分布 \(N(0,1)\)。所以 p 值 = 标准正态的尾部概率。
Under the null hypothesis (model is correct), T asymptotically follows the standard normal \(N(0,1)\). So p-value = standard normal tail probability.
论文比较了 4 种算法,按精确度从低到高排列:
The paper compares 4 algorithms, ordered by accuracy:
| # | 名称 | Name | 适用条件 | When valid | 计算量 | Cost |
|---|---|---|---|---|---|---|
| 1 | LR-χ² | LR-χ² | sᵢ > 1 均匀 | sᵢ > 1 uniformly | O(n) | O(n) |
| 2c | 朴素 Z | Naïve Z | 大计数 | Large counts | O(n) | O(n) |
| 3b | 修正 Z | Corrected Z | 所有计数(推荐) | All counts (recommended) | O(n·d²) | O(n·d²) |
| 4 | 参数化自助 | Parametric Bootstrap | 所有计数 | All counts | O(B·n·fit) | O(B·n·fit) |
最传统的方法:直接把 C 统计量当 χ² 分布处理。p 值通过卡方分布的累积分布函数计算:
The most traditional method: treat the C statistic as chi-squared. p-value via the chi-squared CDF:
$$p = 1 - F_{\chi^2_{n-d}}(C_n(\hat{\theta}))$$
优点:快,一行代码。缺点:低计数时 p 值系统性偏低(过度拒绝)。
Pros: fast, one line of code. Cons: systematically underestimates p-values at low counts (over-rejection).
用无条件矩公式(Theorem 5)计算均值和方差,然后做 Z 检验。比 χ² 好一点,但仍然有偏差:
Use unconditional moments (Theorem 5) to compute mean and variance, then do a Z-test. Better than χ², but still biased:
$$Z = \frac{C_n(\hat{\theta}) - (\kappa_1 - d)}{\sqrt{\kappa_2}}, \quad p = 1 - \Phi(Z)$$
偏差来源:用 \(\hat{\theta}\) 代替 \(\theta^*\) 时的 O(1) 偏差(Proposition 4)。
Bias source: O(1) bias from replacing \(\theta^*\) with \(\hat{\theta}\) (Proposition 4).
论文的核心贡献。使用条件矩(Theorem 6)而非无条件矩,从而修正了偏差:
The paper's core contribution. Uses conditional moments (Theorem 6) instead of unconditional moments, thereby correcting the bias:
$$Z = \frac{C_n(\hat{\theta}) - E[C_n(\hat{\theta}) \mid \hat{\theta}]}{\sqrt{\text{Var}(C_n(\hat{\theta}) \mid \hat{\theta})}}, \quad p = 1 - \Phi(Z)$$
条件矩需要计算一个修正项 \(Q(\hat{\theta})\),它涉及设计矩阵 \(X\) 和累积量 \(\kappa_{11}\)。计算量 \(O(n \cdot d^2)\)(\(d\) 是参数个数),通常 \(d \leq 5\),所以几乎和算法 1 一样快。
The conditional moments require computing a correction term \(Q(\hat{\theta})\) involving the design matrix \(X\) and cumulant \(\kappa_{11}\). Computational cost is \(O(n \cdot d^2)\) (where \(d\) is the number of parameters, typically ≤ 5), so it's nearly as fast as Algorithm 1.
"黄金标准":用 \(\hat{\theta}\) 生成 B 组模拟数据,每组重新拟合,计算 C 统计量,看真实 C 值在模拟分布中的分位数:
The "gold standard": use \(\hat{\theta}\) to generate B simulated datasets, refit each, compute C statistics, and see where the observed C falls in this simulated distribution:
$$p = \frac{1}{B} \sum_{m=1}^{B} \mathbf{1}\{C_n(\hat{\theta}^{[m]}) \geq C_n(\hat{\theta})\}$$
优点:最精确。缺点:慢(需要 B 次拟合,通常 B=1000–10000)。
Pros: most accurate. Cons: slow (needs B refits, typically B=1000–10000).
下面是论文 4 个算法的完整 Python 实现。你只需要 numpy 和 scipy(两个最常用的科学计算包)。点击代码块可以复制。
Below is the complete Python implementation of the paper's four algorithms. You only need numpy and scipy (the two most common scientific computing packages). Click any code block to copy.
import numpy as np
from scipy.stats import chi2, norm, poisson
def c_stat(n_obs, s_model):
"""Cash C statistic: C_n(θ) = 2 Σ [sᵢ − Nᵢ log sᵢ − Nᵢ + Nᵢ log Nᵢ]"""
n_obs = np.asarray(n_obs, dtype=float)
s_model = np.asarray(s_model, dtype=float)
with np.errstate(divide='ignore', invalid='ignore'):
ratio = np.where(n_obs > 0, n_obs / s_model, 1.0)
term = np.where(n_obs > 0, n_obs * np.log(ratio), 0.0)
return 2.0 * float(np.sum(s_model - n_obs + term))
累积量是描述随机变量分布的数学量。对泊松分布,我们可以通过数值求和精确计算每个 bin 的 C 函数的各阶累积量:
Cumulants are mathematical quantities describing a random variable's distribution. For Poisson data, we can compute each bin's C-function cumulants exactly via numerical summation:
def cumulants_poisson(s):
"""Compute per-bin cumulants of the C function for Poisson(s)."""
k1 = np.zeros_like(s)
k2 = np.zeros_like(s)
k11 = np.zeros_like(s)
for i, si in enumerate(s):
max_n = max(int(si + 10*np.sqrt(si) + 50), 100)
n_vals = np.arange(max_n + 1)
pmf = poisson.pmf(n_vals, si)
log_n = np.where(n_vals > 0, np.log(n_vals), 0.0)
c_i = 2.0 * (si - n_vals*np.log(si) - n_vals + n_vals*log_n)
k1[i] = np.sum(c_i * pmf)
k2[i] = np.sum((c_i - k1[i])**2 * pmf)
k11[i] = np.sum((c_i - k1[i]) * (n_vals - si) * pmf)
return k1, k2, k11
# --- Algorithm 1: LR-χ² test ---
def lr_chi2_pvalue(c_val, n_bins, n_params):
"""p = P(χ²_{n-d} ≥ C)"""
dof = n_bins - n_params
return 1.0 - chi2.cdf(c_val, df=dof)
# --- Algorithm 2c: Naïve Z-test ---
def naive_z_pvalue(c_val, s_hat, n_params):
"""Plug-in unconditional moments (Theorem 5)"""
k1, k2, _ = cumulants_poisson(s_hat)
mu = np.sum(k1) - n_params # E[C] = κ₁ - d
sigma2 = np.sum(k2) # Var[C] = κ₂
z = (c_val - mu) / np.sqrt(sigma2)
return 1.0 - norm.cdf(z)
# --- Algorithm 3b: Corrected Z-test (recommended) ---
def corrected_z_pvalue(c_val, s_hat, theta_hat, s_func, n_params):
"""Conditional moments (Theorem 6) with bias correction"""
k1, k2, k11 = cumulants_poisson(s_hat)
# Unconditional moments
mu = np.sum(k1) - n_params
sigma2 = np.sum(k2)
# Design matrix X via finite differences
eps = 1e-6
d = len(theta_hat)
n_bins = len(s_hat)
X = np.zeros((n_bins, d))
for j in range(d):
tp = theta_hat.copy(); tp[j] += eps
tm = theta_hat.copy(); tm[j] -= eps
X[:, j] = (s_func(tp) - s_func(tm)) / (2*eps)
Vinv = np.diag(1.0 / np.maximum(s_hat, 1e-12))
XtVinvX = X.T @ Vinv @ X
I_inv = np.linalg.inv(XtVinvX)
# Correction term Q(θ̂) = κ₁₁ᵀ X I⁻¹ Xᵀ κ₁₁
Q = k11.T @ X @ I_inv @ X.T @ k11
# Conditional moments
cond_mean = mu
cond_var = sigma2 - Q
z = (c_val - cond_mean) / np.sqrt(cond_var)
return 1.0 - norm.cdf(z)
# --- Algorithm 4: Parametric Bootstrap ---
def bootstrap_pvalue(c_val, s_hat, s_func, fit_func, B=1000):
"""Gold standard: simulate B datasets"""
rng = np.random.default_rng(42)
boot_c = np.zeros(B)
for m in range(B):
n_boot = rng.poisson(s_hat)
theta_boot = fit_func(n_boot)
s_boot = s_func(theta_boot)
term = np.where(n_boot > 0, n_boot*np.log(n_boot/s_boot), 0.0)
boot_c[m] = 2.0 * np.sum(s_boot - n_boot + term)
return np.mean(boot_c >= c_val)
# Simulate: power-law spectrum s(E) = K * E^(-Gamma)
# 幂律谱模型: s(E) = K * E^(-Γ)
energies = np.linspace(1.0, 10.0, 50)
theta_true = np.array([5.0, 2.0]) # K=5, Γ=2
def s_func(theta):
K, Gamma = theta
return K * energies**(-Gamma)
# Generate observed counts / 生成观测计数
rng = np.random.default_rng(42)
s_true = s_func(theta_true)
n_obs = rng.poisson(s_true)
print(f"Mean counts per bin: {s_true.mean():.2f}")
print(f"Min counts per bin: {s_true.min():.2f}")
# Compute C statistic / 计算 C 统计量
c_val = c_stat(n_obs, s_true)
print(f"C statistic: {c_val:.3f}")
# Compare all 4 algorithms / 比较 4 个算法
p1 = lr_chi2_pvalue(c_val, 50, 2)
p2 = naive_z_pvalue(c_val, s_true, 2)
p3 = corrected_z_pvalue(c_val, s_true, theta_true, s_func, 2)
# p4 = bootstrap_pvalue(c_val, s_true, s_func, fit_func, B=1000)
print(f"Algorithm 1 (LR-χ²): p = {p1:.4f}")
print(f"Algorithm 2c (Naïve Z): p = {p2:.4f}")
print(f"Algorithm 3b (Corrected Z): p = {p3:.4f}")
论文用 Chandra HETG/LETG 观测的类星体 PG 1116+215 的 X 射线光谱来验证算法。这个数据有 159 个能量 bin,典型 bin 的期望计数在 0.1–2 之间——正好是低计数区间。
The paper validates the algorithms using the Chandra HETG/LETG X-ray spectrum of quasar PG 1116+215. This dataset has 159 energy bins, with typical expected counts per bin in the 0.1–2 range — squarely in the low-count regime.
| 数据集 | Dataset | C_n(θ̂) | LR-χ² p | LR-χ² p | 修正 Z p | Corrected Z p | Bootstrap p | Bootstrap p |
|---|---|---|---|---|---|---|---|---|
| 源(2002) | Source (2002) | 190.72 | 0.039 | 0.039 | 0.053 | 0.053 | 0.057 | 0.057 |
| 源(2018) | Source (2018) | 167.67 | 0.28 | 0.28 | 0.31 | 0.31 | 0.33 | 0.33 |
| 背景(2002) | Background (2002) | — | 0.22 | 0.22 | 0.24 | 0.24 | 0.26 | 0.26 |
| 背景(2018) | Background (2018) | — | 0.59 | 0.59 | 0.61 | 0.61 | 0.62 | 0.62 |
这正是低计数场景下 χ² 近似的典型问题:它系统性低估 p 值,导致天文学家可能"发现"实际上并不存在的模型偏差。修正 Z 检验通过条件矩修正,避免了这个问题。
This is the classic problem of chi-squared in low-count settings: it systematically underestimates p-values, leading astronomers to potentially "discover" model deviations that don't actually exist. The Corrected Z-test, through conditional moment correction, avoids this problem.
X 射线天文、γ 射线天文、中微子天文——这些领域的数据本质上都是低计数泊松数据。天文学家每天都在用 C 统计量做拟合优度检验,但很多人不知道在低计数时 χ² 近似会失效。这篇论文给出了一个实际可用的修正方案。
X-ray astronomy, γ-ray astronomy, neutrino astronomy — these fields all deal with low-count Poisson data by nature. Astronomers use the C statistic for goodness-of-fit every day, but many don't know that the χ² approximation fails at low counts. This paper provides a practical correction.