为什么讲这个方法:pr11/pr12 已经把"怎么测"跑通了,但如果止步于"看到 p<0.05 就宣布赢家",等于把实证做成了抽签。要训练的是把结论建立在证据强度上——先写下你要检验什么,再看数据说话,最后报告"显著吗 / 大吗 / 可复现吗"三问。
阅读路径:§1 预注册(抗事后偏差),§2 因子设计与模型,§3 选检验,§4 效应量,§5 完整分析脚本(可直接跑),§6 报告表模板,§7 效度威胁,§8 可复现,§9 反面清单,§10 与前后案例的接口。
诚实标注:下文示例数字均为演示值,非实测结论;统计方法以标准教材与 scipy / statsmodels 官方文档为准。
1. 为什么要预注册
实证的可信度不取决于你算了多少种检验,而取决于分析计划在数据到手之前是否已经锁死。先看三种最常见的自欺:
- p-hacking(p 值操纵):反复换指标、换分组、加删协变量,直到某个
p<0.05出现为止。做的检验越多,纯靠运气撞显著的概率越高。 - HARKing(已知结果后编假设)(Hypotheses After Results Known):先看到"8-bit 更快",再倒推一条"我早就猜到位宽越低越快"的假设,把探索性发现伪装成验证性结论。
- 事后挑显著:跑了 TTFT、吞吐、显存、错误率十项,只把显著的那一项写进报告——分母被藏起来了。
1.1 预注册在抗什么
预注册不是形式主义,它把"我验证了一个假设"和"我观察到一件有意思的事"区分开。写死四要素后,任何偏离都必须在报告里显式声明为探索性分析,读者才知道证据的分量。
1.2 预注册模板(先写死再开跑)
把下面这份 YAML 随 traces.csv 一起提交进仓库,时间戳由 CI 记录(对接 §8 与 pr15)。四件套:假设 / 指标 / 样本量 / 分析计划。
# 预注册:在采集任何数据之前锁定,提交即哈希留痕
hypotheses:
- id: H1
text: "量化位宽 quant_bits 降低会缩短 TTFT(主效应)"
direction: one_sided # 事先声明方向,避免事后挑符号
- id: H2
text: "投机解码 spec_decode=on 提升吞吐(主效应)"
- id: H3
text: "quant_bits 与 spec_decode 对 TTFT 存在交互(收益不叠加)"
primary_metric: ttft_ms # 唯一主指标,其余列为次级
secondary_metrics: [throughput_tps, vram_peak_mb]
design: 2x2_full_factorial # 见 §2
sample_size:
n_per_cell: 30 # 事先做功效分析算出来的,不是"跑跑看"
repeats_seed: [20260901, 20260902, 20260903]
analysis_plan:
test: welch_t_and_ols_interaction # 见 §3 / §5,锁死不临场换
effect_size: cohens_d_partial_eta # 见 §4,主报效应量而非只报 p
alpha: 0.05
multiplicity: holm # 多个假设的校正方法也事先定
deviations_must_be_flagged: true # 任何偏离都标 exploratory
实操要点:让读者先照模板写死,再打开数据。若结果与预注册不符,正确做法是如实报告负面/零结果(§8),而不是回头改假设——改了就回到 HARKing。
1.3 事前功效分析:n_per_cell 从哪来
模板里的 n_per_cell: 30 不能拍脑袋填。样本量决定统计功效(真有效应时能检出的概率,通常目标 0.8)。功效不足正是"结论效度"的头号威胁(§7)——要么漏检真效应,要么效应估计极不稳定。用 TTestIndPower 反解所需 n:
from statsmodels.stats.power import TTestIndPower
analysis = TTestIndPower
# 想稳定检出的最小工程效应(而非"任意非零"):这里以 d=0.5 为例
n = analysis.solve_power(effect_size=0.5, alpha=0.05, power=0.8,
ratio=1.0, alternative="two-sided")
print("每组至少需 n =", int(n) + 1) # 结果写进 prereg.yaml 的 n_per_cell
# 反向:若每组只有 30,最小可检出效应(mde)是多少?检不出更小说明功效不足
mde = analysis.solve_power(nobs1=30, alpha=0.05, power=0.8, ratio=1.0)
print("每组30时可检出的最小 d ≈", round(mde, 2))
关键区分:预注册的
effect_size应填工程上关心的最小效应(比如"至少省 3 ms 才值得上线"),而不是"任意不为零"。否则你会为了检出一个毫无意义的 d=0.05 而盲目堆样本——又回到 §4 的教训。
2. 现成的 2×2 因子设计
pr11 扫了"量化位宽",pr12 扫了"投机解码开关",两批实验的因子恰好正交,天然拼成一个 2×2 全因子设计:两个主效应 + 一个交互项。这比"一次只改一个变量"省一半运行量,还能捕捉交互(一个因子的效应取决于另一个因子的水平)。
2.1 因子与水平
| 因子 | 记号 / 水平 | 来源 | 预期方向 |
|---|---|---|---|
| 量化位宽 quant_bits | x₁:4-bit / 8-bit | pr11 | 位宽↓ → TTFT↓、精度风险↑ |
| 投机解码 spec_decode | x₂:off / on | pr12 | on → 吞吐↑ |
| 因变量(主) | ttft_ms(首 token 延迟) | pr08 口径 | 越低越好 |
| 因变量(次) | throughput_tps(token/s) | pr08 口径 | 越高越好 |
2.2 两因素线性模型
把两个因子编码成 x₁, x₂ ∈ {0,1},带交互项的模型写成:
其中 β_A、β_B 是两个主效应,β_AB 是交互效应——只有 β_AB 显著,才说明"位宽带来的收益会随投机解码开/关而改变",两个因子不能各算各的。这正好把 pr11、pr12 的独立结论升级为"是否耦合"的判断。
2.3 2×2 方格图(平均指标示意)
3. 选对检验
没有"万能检验",只有"匹配数据形态的检验"。选择依据三问:分布?配对否?几组?
3.1 先验假设:正态 + 方差齐性
t 检验要求残差近似正态(Shapiro-Wilk 粗检),经典 Student t 还要求两组方差齐(Levene)。pr11/pr12 里不同配置耗时波动常不一致,因此默认走 Welch t——它不假设等方差,几乎总是更安全的选择。
3.2 决策表
| 数据形态 | 推荐检验 | 理由 |
|---|---|---|
| 两组独立、近正态 | Welch t(equal_var=False) | 不假设等方差,稳健 |
| 配对(同种子/同任务前后) | 配对 t / Wilcoxon 符号秩 | 消除个体差异,功效更高 |
| 小样本 / 明显非正态 | 置换检验 / bootstrap | 不依赖分布假设,直接对观测重采样 |
| ≥3 组或带交互(本文 2×2) | 方差分析 / OLS 回归 | 一次拟合主效应 + 交互项,比多次 t 更抗多重比较 |
为什么 2×2 优先用回归而非四场 t 检验:把
quant_bits、spec_decode、交互项一起塞进OLS,系数即主/交互效应,一次给出,天然避免"跑很多次 t 抬高假阳性"。置换检验则在样本量小、正态存疑时兜底。
3.3 假设检查代码(先跑这段再选检验)
选检验不是凭感觉,先跑一遍正态 / 方差齐性检查,再照 §3.2 决策表落地。下面这段是"检验路由器"的雏形:
from scipy import stats
def check_assumptions(a, b, alpha=0.05):
a, b = np.asarray(a, float), np.asarray(b, float)
# 1) 正态性:对两组分别做 Shapiro-Wilk(样本小功效也低,仅作参考)
pA = stats.shapiro(a).pvalue
pB = stats.shapiro(b).pvalue
normal = (pA > alpha) and (pB > alpha)
# 2) 方差齐性:Levene;不齐则坚决用 Welch 而非 Student
lev_p = stats.levene(a, b).pvalue
equal_var = lev_p > alpha
# 3) 路由结论
if normal:
route = "Welch t (equal_var=False)" if not equal_var else "Student t"
else:
route = "Mann-Whitney U + bootstrap CI"
return dict(shapiro=(round(pA, 3), round(pB, 3)), levene=round(lev_p, 3),
normal=normal, route=route)
print(check_assumptions(df[df.qbits == 4].ttft_ms, df[df.qbits == 8].ttft_ms))
# 输出 route 即应写回 prereg.yaml 的 analysis_plan.test,禁止跑完再改
注意:Shapiro-Wilk 在小样本上功效低,"不拒绝正态"不等于"一定正态"。pr11/pr12 每格重复次数不多时,稳妥做法是同时报 Welch t 与非参数旁证(§5.2/§5.3),两者结论一致才敢下判断。
4. 效应量才是重点
大样本下再小的差异也能压出 p<0.001;反过来 6σ 的显著性,若实际只快 0.3 ms,工程上毫无价值。报告的主角是效应量(差多大)与置信区间(范围多宽),不是 p(是否非零)。
4.1 Cohen's d
约定俗成:|d|≈0.2 小、0.5 中、0.8 大。判读永远是语境相关的——对 TTFT 这种关键指标,d=0.3 若对应稳定省 5 ms 也可能值得上线;反之 d=0.05 再显著也先别看。
4.2 置信区间
95% CI 回答"效应真实大小可能落在哪一段"。若区间跨过 0 → 与"无效应"相容;若区间整体很窄且贴 0 → 说明"确实没什么差别",这比一句"不显著"信息量大得多。
核心一句话:6σ 显著但 d=0.05 → 统计显著 ≠ 工程有意义。永远把 d 与 CI 放在 p 前面讲。
5. 完整分析脚本(可直接跑)
以下代码把 traces.csv(pr11 量化扫描 + pr12 投机扫描合并,含 quant_bits, spec_decode, ttft_ms, throughput_tps, seed 列)从读取一路做到出图,覆盖 §3 检验、§4 效应量、§2 因子回归。骨架可直接跑,函数签名与 §1 预注册的分析计划一致。
5.1 读数据 + 组均值
import pandas as pd, numpy as np
from scipy import stats
df = pd.read_csv("traces.csv") # pr11 + pr12 合并后的实验轨迹
df["spec"] = (df.spec_decode == "on").astype(int) # 0/1 编码
df["qbits"] = df.quant_bits # 4 / 8
# 每格均值(对应图 2-1 的 2x2)
cell = df.groupby(["quant_bits", "spec_decode"]).ttft_ms.agg(["mean", "std", "count"])
print(cell.round(2))
# 主效应方向粗看:位宽、投机各自的边际均值
print("4-bit mean TTFT :", df[df.qbits == 4].ttft_ms.mean)
print("8-bit mean TTFT :", df[df.qbits == 8].ttft_ms.mean)
5.2 Welch t + bootstrap CI + Cohen's d
def welch_compare(a, b):
a, b = np.asarray(a, float), np.asarray(b, float)
t, p = stats.ttest_ind(a, b, equal_var=False) # 不假设等方差
n1, n2 = len(a), len(b)
sp = np.sqrt(((n1 - 1) * a.var(ddof=1) + (n2 - 1) * b.var(ddof=1))
/ (n1 + n2 - 2))
d = (a.mean - b.mean) / sp # Cohen's d
diff = a.mean - b.mean
return dict(t=t, p=p, diff=diff, d=d, n1=n1, n2=n2)
def bootstrap_ci(a, b, stat=np.mean, n=5000, seed=42):
"""对差值的 95% 置信区间做 percentile bootstrap,分布无关。"""
rng = np.random.default_rng(seed)
a, b = np.asarray(a, float), np.asarray(b, float)
boots = [rng.choice(a, len(a)) .mean - rng.choice(b, len(b)).mean
for _ in range(n)]
lo, hi = np.percentile(boots, [2.5, 97.5])
return lo, hi
# 例:位宽主效应 4-bit vs 8-bit 的 TTFT
res = welch_compare(df[df.qbits == 4].ttft_ms, df[df.qbits == 8].ttft_ms)
lo, hi = bootstrap_ci(df[df.qbits == 4].ttft_ms, df[df.qbits == 8].ttft_ms)
print(f"Welch t={res['t']:.2f} p={res['p']:.4f} diff={res['diff']:.2f} ms "
f"Cohen's d={res['d']:.2f} 95%CI=[{lo:.2f},{hi:.2f}] n={res['n1']}+{res['n2']}")
print("→ 先读 d 和 CI,再看 p;d 很小则工程上未必值得(§4)")
5.3 因子分析 + 交互项检验(OLS)
import statsmodels.formula.api as smf
# y ~ A * B 展开即 A + B + A:B,一次拟合主效应 + 交互(对应 §2.2 模型)
model = smf.ols("ttft_ms ~ C(quant_bits) + spec", data=df).fit
model_int = smf.ols("ttft_ms ~ C(quant_bits) * spec", data=df).fit
print(model.summary.tables[1]) # 主效应系数表
print("\n交互项检验(比较是否加入 A:B 提升拟合):")
print("无交互 R^2 =", round(model.rsquared, 4),
" 有交互 R^2 =", round(model_int.rsquared, 4))
print(model_int.compare_f_test(model)) # (F, p, df) —— p 小说明确有交互
# 小样本 / 非正态兜底:对交互项做置换检验
perm = stats.mannwhitneyu(df[df.qbits == 4].ttft_ms,
df[df.qbits == 8].ttft_ms, alternative="two-sided")
print("非参数旁证 Mann-Whitney U p =", round(perm.pvalue, 4))
5.4 出图:带误差棒的分组柱状 + 因子交互图
import matplotlib.pyplot as plt
g = df.groupby(["quant_bits", "spec_decode"]).ttft_ms.agg(["mean", "sem"]).reset_index
# (1) 分组柱状 + 95% 误差棒(sem * t 临界值近似)
fig, ax = plt.subplots(figsize=(6, 4))
labels = [f"{r.quant_bits}b/{r.spec_decode}" for r in g.itertuples]
ax.bar(labels, g["mean"], yerr=1.96 * g["sem"], capsize=5, color="#2563eb")
ax.set_ylabel("平均 TTFT (ms) 越低越好")
ax.set_title("2x2 因子:均值 ± 95% CI")
# (2) 交互图:两条线不平行 = 存在交互
fig2, ax2 = plt.subplots(figsize=(6, 4))
for spec in ["off", "on"]:
sub = g[g.spec_decode == spec].sort_values("quant_bits")
ax2.plot(sub.quant_bits, sub["mean"], marker="o", label=f"spec={spec}")
ax2.set_xlabel("quant_bits"); ax2.set_ylabel("mean TTFT (ms)")
ax2.set_title("因子交互图(线不平行 → β_AB≠0)"); ax2.legend; ax2.grid(alpha=.3)
fig.tight_layout; fig.savefig("cells_bar.pdf"); fig2.savefig("interaction.pdf")
诚实提示:脚本里的
spec、quant_bits、列名以你实际合并的traces.csv为准;statsmodels的公式字符串与分类编码C需按数据微调。换数据集只改列名不改分析逻辑——这正是把 pr11/pr12 复用为 pr14 输入的接口收益。
6. 报告段模板
结论必须同时回答三问:显著吗 / 大吗 / 可复现吗。把每个效应压进一张 workshop-paper 风格的结果表:
| 效应 | 估计 (Δ) | 95% CI | p | Cohen's d | n | 结论三问 |
|---|---|---|---|---|---|---|
| quant_bits 4→8(TTFT 主效应 A) | +8.0 ms | [6.1, 9.9] | <0.001 | 0.92 | 30+30 | 显著 / 大 / 可复现 |
| spec on(TTFT 主效应 B) | −5.5 ms | [−7.2, −3.8] | <0.001 | 0.61 | 30+30 | 显著 / 中 / 可复现 |
| A×B 交互(TTFT) | −1.3 ms | [−3.0, 0.4] | 0.13 | 0.11 | 60 | 不显著 / 极小 / 无交互证据 |
| spec on(吞吐副效应) | +2.4 tps | [2.2, 2.6] | <0.001 | 1.05 | 30+30 | 显著 / 大 / 复现待 pr15 |
写法要点:先给 Δ 与 CI("多大、范围多宽"),再给 d("值不值"),p 放最后作为"是否非零"的附注。
不显著 + CI 窄贴 0要如实写成"没有证据表明有交互",而非含糊略过。
6.1 结论段范例(三问一次答清)
把表格翻译成一段可读结论,示范正确语序(先效应、后 p、再复现声明):
"在本机 模型-任务 组合下,4-bit 相比 8-bit 将 TTFT 平均降低 8.0 ms(95% CI [6.1, 9.9],Cohen's d=0.92,Welch p<0.001)——效应大且显著;投机解码开启再降 5.5 ms(d=0.61)。但两者的交互项不显著(Δ=−1.3 ms,CI [−3.0, 0.4] 跨过 0,d=0.11),因此可近似独立叠加地报告。结论基于固定 seed 的 pr11/pr12 轨迹,将在 pr15 CI 中自动复算。效度威胁:仅覆盖单一模型、单一提示分布,外推需谨慎(§7)。"
反面示范(禁用):"p<0.001,实验成功,4-bit 更好。"——只报 p、不报效应大小与区间、不报交互、不报复现与效度,等于没做实证。
7. 效度威胁清单
实证报告里必须有一节诚实列出"我的结论可能被什么推翻"。四类效度各给典型威胁与缓解:
| 效度类型 | 威胁 | 缓解 |
|---|---|---|
| 内部效度 | 混淆变量(GPU 温度 / 后台进程);多次比较抬高假阳性 | 随机化运行顺序、预热固定;§1 预注册 + Holm 校正 |
| 外部效度 | 模型 / 任务代表性不足,结论难外推 | 声明边界(本模型本任务);多任务重复 |
| 构念效度 | 指标是否真的衡量目标(TTFT ≠ 用户体感全部) | 多指标交叉(TTFT + 吞吐 + 尾延迟);pr08 口径统一 |
| 结论效度 | 功效不足(样本太小 → 漏检真效应) | 事前功效分析定 n(§1);报 CI 而非只报 p |
动手练习:让读者为本文的 2×2 结论各写一条最可能致命的效度威胁,并给出"若补做哪个实验能排除它"。效度威胁写得越诚实,报告越可信。
8. 诚实与可复现
- ☑ 报告负面 / 零结果:交互项不显著也要写进表 §6,不能只留赢家——这是抗发表偏差的基本功。
- ☑ 种子与数据固定:
traces.csv每条含seed(pr11/pr12 已写入),bootstrap用default_rng(seed)保证重采样可复算。 - ☑ 预注册随数据入库:
prereg.yaml与分析脚本同仓,时间戳由版本历史背书。 - ☑ 对接 pr15 的 CI 跑分析:把 §5 脚本作为 CI 步骤,每次
traces.csv变更自动重算 d、CI、交互检验,保证"结论可复算、不漂移"。 - ☑ 统计方法可溯源:检验/效应量口径以 scipy、statsmodels 官方文档为准,不自定义算法。
9. 常见误用反面清单
| 反面做法 | 为什么错 | 正确姿势 |
|---|---|---|
| 只看 p 就下结论 | 大样本什么都能显著,忽略效应大小 | 先 d 与 CI,再 p(§4) |
| 把相关当因果 | 观测数据混杂变量在背后作祟 | 用 §2 受控因子设计,随机化 |
| 样本不足硬下结论 | 功效低,假阴性/不稳效应并存 | 事前功效分析定 n(§1),报 CI |
| 事后换指标 / 换分组 | 典型 p-hacking,分母被藏 | 主指标写死,偏离标 exploratory |
| 跑很多次 t 挑显著 | 多重比较抬高假阳性 | 一次 OLS 拟合 + 预注册校正 |
一句话:反面清单的每一条,都在 §1 预注册和 §4 效应量里被正面堵住——预注册防事后偏差,效应量防"唯 p 论"。
10. 接口:与前后案例的呼应
量化扫描 + 投机扫描的 traces.csv,天然拼成 §2 的 2×2 因子输入。
指标口径与因变量定义来自 pr08,§4 的效应量判读靠它。
CI 里自动跑 §5 脚本,保证结论可复算不漂移(§8)。
pr02 的实验设计与 §2/§3 同构;pr17 把本流水线用于真实缺陷复盘。
本文是"测量(pr11/pr12) → 结论(pr14) → 复算(pr15)"链条的中段:pr11/pr12 负责把数据采对,pr08 负责把口径统对,pr09 负责把因变量选对,pr14 负责把结论判对,pr15 负责让它可复算。
一页纸总结:预注册锁死四要素(假设/指标/样本量/分析计划)→ 复用 pr11/pr12 拼 2×2 因子(主效应 + 交互)→ 按数据形态选检验(Welch / 置换 / OLS)→ 主报 Cohen's d 与 95% CI → 三问式报告(显著吗/大吗/可复现吗)→ 效度威胁清单 → 负面结果如实报 + 种子固定 + 接 pr15 CI 复算。贯穿论点:统计显著 ≠ 工程有意义,p 只是其中一环。