1. 概率论到底在算什么:把"可能"翻译成可计算的数
接口错误率 0.3%,日均调用 200 万次,明天会不会恰好撞上一次失败?64 位哈希表里存了 3 亿条记录,碰撞的概率到底是万分之一还是百分之一?A/B 实验跑了三天,转化率涨了 1.2%,这个数字是真信号还是噪声?这几个问题横跨后端、存储、数据运营,看起来八竿子打不着,但拆到底层是同一件事:概率论基础知识。我做过不少数据分析和线上容量评估的活,最深的体会是——大部分人不是不会算,而是根本没意识到眼前这个问题是个概率问题,于是凭直觉拍了个数,然后在某个深夜被现实教育。
这篇文章不走教材路线。我会把样本空间、条件概率、随机变量、极限定理这些概念,全部放到工程和业务的真实场景里讲一遍,同时把那些"直觉一定会骗你"的地方单独拎出来说清楚。适合谁看?写过代码但概率早还给老师的人、做数据分析和实验评估的人、以及所有需要在信息不全的情况下做决策的人。零基础也能读,因为每一个抽象概念我都会先给生活类比,再给可跑的代码。
1.1 为什么工程问题到最后都会变成概率问题
确定性思维有个隐含假设:只要我的逻辑没写错、参数没填错,结果就应该是可预测的。这个假设在单机、小数据量、低并发的时候勉强成立。一旦系统规模上去,变量数量爆炸,你就再也没法穷举所有情况了。这时候唯一能做的,是描述"各类结果出现的可能性有多大"。
举几个我实际遇到过的例子。评估一套推荐系统上线后的效果,你不可能让同一个用户在完全相同的时刻既看旧版本又看新版本,所以只能比较两个用户群体的平均指标,而群体之间的差异里天然混着随机波动。做容量规划时,单次请求的耗时不是固定值,而是一条分布曲线,你要是按平均值去配置机器,那必然有一半的请求超时。做风控时,一条规则命中了,也不能直接判定为欺诈,因为你还要考虑这个行为在正常用户里的基础发生率。
这三个场景的共同点在于:我们能观测到的只是随机变量的一次抽样,而决策需要基于背后的分布。概率论提供的正是从"样本"倒推"分布"、从"分布"推算"风险"的整套工具。理解了这一层,你就不会再把概率当成一门需要背公式的课,而是当成一套日常决策的语言。
1.2 样本空间与事件:把模糊描述翻译成集合
几乎所有概率计算的第一个错误,都发生在建模阶段,而不是计算阶段。而建模的核心动作就一件事:把自然语言的描述,翻译成明确的集合。
三个基本概念:
- 样本空间 Ω:一次试验所有可能结果的集合。掷一次骰子,Ω = {1,2,3,4,5,6};一次接口调用,Ω = {成功, 超时, 业务错误, 服务端异常},注意这里必须把失败拆开,否则后面算不出"超时占比"。
- 事件 A:样本空间的一个子集,也就是我们关心的那些结果的集合。比如"点数大于 4"对应 {5,6}。
- 概率 P:一个从事件到 [0,1] 实数的映射。
这里最容易踩的坑是样本空间划分不完整或者重叠。比如你要评估"用户是否会下单",如果样本空间只写了 {下单, 不下单},后面的分析就做不下去了,因为你没法区分"浏览了但没买"和"压根没打开 App"。我一般建议在建模阶段就把样本空间拆到互斥且穷尽(业界叫 MECE),宁可多几个状态,也别留模糊地带。多状态带来的计算量增加是线性的,而模糊地带带来的返工是指数级的。
1.3 三条公理和它们的直接推论
柯尔莫哥洛夫给出的三条公理,是整个概率论的地基,值得原样抄一遍:
- 非负性:对任意事件 A,P(A) ≥ 0。
- 规范性:P(Ω) = 1。
- 可列可加性:若 A₁, A₂, … 两两互斥,则 P(∪Aᵢ) = ΣP(Aᵢ)。
看着像废话,但由它们能推出所有你在用的公式。比如两个事件 A、B(不一定互斥),有:
P(A ∪ B) = P(A) + P(B) − P(A ∩ B)
很多人会顺口说成 P(A) + P(B),这在 A、B 互斥的时候才成立。而现实中 A、B 往往不互斥。举个例子:某天"支付服务告警"的概率是 0.05,"订单量下跌"的概率是 0.08,两者同时发生的概率是 0.03。那么"至少出现一个异常"的概率是 0.05 + 0.08 − 0.03 = 0.10,而不是 0.13。这个减法看着微不足道,但在做告警风暴归因的时候,如果你不去重,会系统性地高估故障范围。
还有一个推论我用得很多,叫容斥与补集:P(A) = 1 − P(Ā)。当你算"至少出现一次"很难、但算"一次都不出现"很容易时,补集是救命工具。后面讲哈希碰撞、生日悖论,用的都是这一招。
2. 条件概率:线上问题几乎都藏在这个"前提"里
如果说有一块内容被误用得最狠,那一定是条件概率和贝叶斯公式。我在评审数据报告时见过太多次这样的表述:""告警命中说明故障概率很高,准确率 99%"",但你追问一句"故障本身的发生率是多少",对方就答不上来了。而这个被忽略的数字,恰恰决定了结论对不对。
2.1 条件概率的定义与乘法公式的真实含义
条件概率的定义很简洁:
P(A|B) = P(A ∩ B) / P(B),要求 P(B) > 0
它回答的是"在已知 B 发生的前提下,A 发生的可能性"。这里的关键在于:条件概率不是把 A 的概率改一改,而是把样本空间从 Ω 缩小到了 B。分母 P(B) 就是缩小的动作,分子 P(A ∩ B) 就是在新的样本空间里 A 占的部分。
把定义式两边乘以 P(B),就得到乘法公式:
P(A ∩ B) = P(A|B) · P(B) = P(B|A) · P(A)
这条式子在实际排查里非常有用。比如你观测到"缓存命中率突然下降 20%",你想知道是不是某个上游业务变更导致的。已知:上游变更发生的概率是 0.2;变更发生后命中率下降的概率是 0.6;没变更的情况下命中率下降的概率是 0.05。那么"命中率下降且发生了上游变更"的概率是 0.6 × 0.2 = 0.12,而"下降但没变更"的概率是 0.05 × 0.8 = 0.04。所以在这次下降事件里,有 12/(12+4) = 75% 的可能性是变更引起的。这个 75% 就是后面要讲的贝叶斯结论。
要注意一个高频误区:P(A|B) 和 P(B|A) 完全不是一回事。前者是"变更导致下降",后者是"下降意味着变更"。混淆这两个方向的概率,是误判的主要来源之一。
2.2 全概率公式:把复杂场景切成互斥分支
现实问题往往不能直接算出目标事件的概率,但可以把它按某个维度切开。全概率公式干的就是这件事:
若 B₁, B₂, …, Bₙ 两两互斥且并集为 Ω,则 P(A) = Σ P(A|Bᵢ) · P(Bᵢ)
用人话说:把整体拆成几类互不重叠的情况,分别算每种情况下目标发生的概率,再按各类的占比加权求和。
举个容量评估的例子。一个服务的请求来自三个渠道:App(占 60%,平均耗时 120ms)、小程序(占 25%,平均耗时 200ms)、开放 API(占 15%,平均耗时 400ms)。那么整体平均耗时就是:
| 渠道 | 流量占比 P(Bᵢ) | 平均耗时 P(A|Bᵢ) | 加权贡献 |
|---|---|---|---|
| App | 0.60 | 120 ms | 72 ms |
| 小程序 | 0.25 | 200 ms | 50 ms |
| 开放 API | 0.15 | 400 ms | 60 ms |
| 合计 | 1.00 | — | 182 ms |
看到没,开放 API 只占 15% 的流量,却贡献了 60ms,接近总量的三分之一。如果你只盯着 App 的性能优化,收益天花板是很低的。全概率公式的价值就在这儿:它把"整体平均"这个模糊指标,拆成了可归因、可排序的分项贡献。
使用时唯一的要求是分组必须互斥且穷尽。如果允许一个请求同时属于两个渠道,公式就不成立了。实操中我建议分组维度一次只用一个,不要交叉,否则很容易出现既漏又重的局面。
2.3 贝叶斯公式与基率谬误:一次告警误报率的完整推演
把全概率公式代进条件概率的对称式里,就得到了贝叶斯公式:
P(Bᵢ|A) = P(A|Bᵢ) · P(Bᵢ) / Σ P(A|Bⱼ) · P(Bⱼ)
它做的事是把先验知识和新证据结合起来,更新我们的判断。P(Bᵢ) 是看到证据之前的判断(先验),P(Bᵢ|A) 是看到证据之后的判断(后验)。
现在用一个真实的告警场景算一遍。假设:
- 线上真实故障的发生率 P(故障) = 0.1%(也就是一天里大约千分之一的时段处于故障状态)
- 告警规则对真实故障的召回率 P(告警|故障) = 99%
- 告警规则在正常状态下误报的概率 P(告警|正常) = 5%
一条单看指标很漂亮的规则:99% 的召回、95% 的准确率(这里说的是特异度)。但真实命中率是多少?
| 项 | 计算 | 结果 |
|---|---|---|
| 故障且告警 | 0.99 × 0.001 | 0.00099 |
| 正常且告警 | 0.05 × 0.999 | 0.04995 |
| 总告警概率 | 0.00099 + 0.04995 | 0.05094 |
| 告警中真故障的比例 | 0.00099 / 0.05094 | ≈ 1.94% |
也就是说,每 100 条告警里,只有不到 2 条是真故障,剩下 98 条都是误报。这就是"告警疲劳"的数学根源。不是规则写得差,而是故障的基率太低,任何非零的误报率都会被低基率放大。
我见过很多团队处理这个问题的方向是错的——他们拼命优化召回率,把 99% 提到 99.9%,但对这个比例的影响微乎其微。真正有效的是两条路:降低误报率,或者提高基率(比如把多条弱信号聚合成一条复合告警,或者缩短观测窗口让故障在窗口内的占比上升)。前者是分母变小,后者是分子变大,效果都比压召回率来得直接。这个结论如果不靠贝叶斯推一遍,光凭直觉是得出来的。
3. 随机变量与分布选型:九成的建模错误出在这一步
算条件概率之前得先知道"概率是多少"。到了这一步,我们会把试验结果映射成数字,也就是随机变量。这一步看起来只是个形式化动作,但分布选型的正确与否,决定了后面所有计算的成败。而且这类错误往往不会立刻暴露,等到数据量大了才突然离谱。
3.1 离散与连续:先分清你的数据属于哪一类
随机变量分两类:
- 离散型:取值可数。请求次数、失败条数、用户点击数、一段时间内到达的请求数。这类用一个"概率质量函数"(PMF)描述每个取值的概率。
- 连续型:取值在某个区间内连续。响应耗时、金额、温度、误差。这类用"概率密度函数"(PDF)描述,注意单点概率为 0,只有区间才有概率。
为什么要强调这个区别?因为很多人会拿离散的套路去套连续数据。最典型的错误是"响应耗时等于 200ms 的概率是多少"——这个问题在连续分布下答案就是 0。应该问的是"响应耗时落在 180ms 到 220ms 之间的概率是多少"。
另外,连续分布里有一个操作叫"离散化",就是把耗时切成若干个桶。这个操作本身没问题,但桶的边界会影响结论。我在做耗时分析时踩过一次坑:按 100ms 切桶时看不到任何异常,改成按对数刻度(10ms、30ms、100ms、300ms、1000ms)切之后,异常立刻显现出来了。原因很简单,耗时本身近似对数正态分布,线性切桶会把长尾挤在最后一个桶里,信息全丢了。
3.2 常见分布速查与适用场景对照
下面这张表是我自己整理的工作用速查表,列的都是在工程和数据场景里真正会碰到的分布:
| 分布 | 参数 | 均值 | 方差 | 典型适用场景 |
|---|---|---|---|---|
| 伯努利 | p | p | p(1−p) | 单次点击、单次成功/失败 |
| 二项 | n, p | np | np(1−p) | n 次独立试验的成功次数、转化用户数 |
| 泊松 | λ | λ | λ | 单位时间到达的请求数、故障次数 |
| 几何 | p | 1/p | (1−p)/p² | 首次成功所需的试验次数、重试次数 |
| 超几何 | N, K, n | nK/N | — | 不放回抽样、缓存池命中 |
| 均匀 | a, b | (a+b)/2 | (b−a)²/12 | 哈希取模、随机分桶 |
| 指数 | λ | 1/λ | 1/λ² | 事件间隔时间、连接空闲时长 |
| 正态 | μ, σ² | μ | σ² | 测量误差、聚合后的指标 |
| 对数正态 | μ, σ² | — | — | 响应耗时、文件大小、收入分布 |
| 伽马 | α, β | αβ | αβ² | 多个指数事件的总耗时 |
| 贝塔 | α, β | — | — | 转化率本身的不确定性建模 |
选分布的时候,我会问自己三个问题:取值是次数还是时间?事件之间是否独立?有没有明显的长尾?
次数类问题,独立的话优先看二项或泊松。泊松有个非常好用的性质:期望等于方差。所以你可以拿样本均值和样本方差比一下,如果方差远大于均值(业内叫过离散),说明事件之间有聚集,泊松不合适,得换成负二项或者混合模型。这个检查我基本每次做到达率分析都会跑一遍,成本几行代码,能挡掉不少低级错误。
时间间隔类问题,如果事件独立且发生率恒定,间隔服从指数分布。指数分布有个著名的无记忆性:已经等了 10 分钟,接下来还需要等多久的分布,和一开始就是一样的。这个性质在排队论里是关键假设,但现实中"用户行为"往往不满足——用户等久了就会关掉页面,这时候得换成带有"耐心上限"的分布。
耗时类问题,我个人的默认起点是对数正态。它的形状是正偏的,右尾长,正好符合"大部分请求很快,少量请求极慢"的现实。只有在明确知道尾部的成因(比如某个慢查询触发了超时重试)时,才会考虑用帕累托之类的重尾分布。
3.3 期望与方差的工程含义
期望 E[X] 和方差 Var(X) 这两个数字,在工程语境里有非常具体的对应:
- 期望 = 长期的、平均意义上的成本。它是可加的,而且不要求独立性:E[X + Y] = E[X] + E[Y],无论 X、Y 是否相关。这条"期望线性性"是被严重低估的工具,很多看起来很吓人的问题,用它可以一行解出来。比如 n 个用户各自独立地以概率 p 下单,总下单数的期望就是 np,不需要任何复杂的推导。
- 方差 = 波动 / 不确定性 / 风险。它衡量的是结果偏离期望的幅度。方差不是线性可加的:Var(X + Y) = Var(X) + Var(Y) + 2Cov(X, Y)。只有 X、Y 相互独立(或至少不相关)时,Cov 项才为 0。
这个区别在实际中很重要。举例:一个服务的平均耗时是 100ms,方差很小,那你按 120ms 配超时就够用。但如果耗时均值同样是 100ms,方差极大,即使均值达标,超时率依然可能很高。只看均值做容量规划,必然会漏掉长尾引发的超时。我一般会同时看 P50、P90、P99 三个分位数,因为分位数比方差更直观地告诉你会影响多少比例的用户。
还有一个工具是协方差与相关系数:
Corr(X, Y) = Cov(X, Y) / (σ_X · σ_Y)
相关系数的取值范围是 [−1, 1],无量纲,便于比较不同量纲的变量。但必须强调一句:相关不等于因果。我在做归因分析时反复提醒自己这一点,两个指标同涨同跌,可能只是都被第三个因素驱动。要确定因果,得靠实验或者结构化的假设检验,光看相关系数是不够的。
4. 大数定律与中心极限定理:样本量到底要多少才够
这一节解决的问题是:我跑了一周的实验,看到的差异能不能信?这是数据分析和 A/B 实验里最核心的问题,而答案完全建立在两个极限定理上。
4.1 大数定律在说什么,以及它不说什么
大数定律说:当样本量 n 趋于无穷时,样本均值 X̄ 依概率收敛到总体均值 μ。
通俗版:样本足够大,平均值就会接近真实值。这解释了为什么实验要跑够量。但它的边界比想象中窄得多,有三个必须记住的点:
- 它不保证收敛速度。n 要多大才"足够",完全取决于方差。方差大,收敛就慢。抛硬币收敛很快,因为单次抛掷的方差只有 0.25;而用户消费金额方差极大,可能需要几十万样本才能稳定。
- 它对单个样本无约束。大数定律说的是序列的均值,不是说"抛了十次正面之后该出反面了"。这就是赌徒谬误的来源,后面会专门说。
- 它要求同分布。如果样本来自不同的分布(比如把工作日和周末的流量混在一起算),大数定律的前提就被破坏了,算出来的均值没有明确含义。这就是为什么做指标对比时要先做时间维度上的分层。
4.2 中心极限定理与置信区间的关系
如果说大数定律告诉你"均值会收敛",中心极限定理则告诉你"围绕均值有多大的波动,以及这个波动长什么样"。
对独立同分布、方差有限的随机变量,当 n 足够大时, (X̄ − μ) / (σ / √n) 近似服从标准正态分布 N(0, 1)
这条定理有两个反直觉的地方,我认为是它最有价值的点:
第一,它不要求原始数据服从正态分布。原始数据可以是抛硬币、可以是极偏的长尾耗时,只要 n 够大(实践中 30 就已经不错),样本均值就会呈现钟形分布。这就是为什么"均值"这个统计量如此通用,它天然自带正态性。
第二,波动是随 √n 衰减的。要把误差减半,样本量得翻两番。这个 1/√n 的规律解释了为什么实验后期提升样本量变得极其昂贵。从 1000 到 10000,样本翻了 10 倍,精度只提升约 3.16 倍。所以做实验设计时,与其一味延长实验时间,不如先从降低指标的方差入手,比如用协变量调整、用 CUPED 之类的方差缩减技术,效果通常是数倍的。
有了 CLT,置信区间就顺理成章了。当总体方差未知时,用样本标准差 s 代替:
X̄ ± z · s / √n
其中 95% 置信度对应 z = 1.96,99% 对应 2.58。
4.3 用切比雪夫不等式估样本量
CLT 好用,但它是个近似,而且在方差未知、样本量还不够大的时候不太可靠。这时候可以退一步用切比雪夫不等式,它不需要任何分布假设:
P(|X − μ| ≥ kσ) ≤ 1 / k²
它只要求方差存在。代价是非常保守。取 k = 2,它告诉你偏离超过 2 个标准差的概率不超过 25%;而正态分布给出的实际值是约 4.6%。差了五倍多。所以切比雪夫适合"兜底",不适合精算。
但我还是经常用它,因为它在做粗略样本量估算时非常快。把不等式套到样本均值上,可以得到:
P(|X̄ − μ| ≥ ε) ≤ σ² / (n · ε²)
如果要让误差超过 ε 的概率小于 α,只需要 n ≥ σ² / (α · ε²)。这个式子甚至不需要分布假设,能给你一个数量级上的直觉:方差翻倍,样本量翻倍;精度提高一倍,样本量变成四倍。
对于比例型指标(比如转化率),平方根公式更常用:
n ≈ z² · p(1 − p) / E²
其中 E 是你能接受的误差幅度。取 p = 0.5(最保守),95% 置信度,不同误差下的样本量:
| 可接受误差 E | 所需样本量 n |
|---|---|
| ±5% | 385 |
| ±3% | 1068 |
| ±1% | 9604 |
这张表值得记住。很多人做小流量实验,只有几百个样本,却想去判断 1% 的差异,这在数学上就是不可能的——误差幅度比信号还大。我见过太多这样的实验结论被推翻,根因都是这个。
5. 概率陷阱清单:直觉最容易骗人的几个地方
概率论有个特殊性:它不仅反直觉,而且你越相信直觉,错得越离谱。这一节我把几个最经典的陷阱和它们背后的数学原因放在一起,每一个都能对应到实际工作中。
5.1 生日悖论与哈希碰撞:为什么 23 个人就够
问题是这样的:一个房间里有多少人,才能让"至少有两人生日相同"的概率超过 50%?
直觉答案通常是 183(365 的一半)。正确答案是23。
算一遍。用补集:先算"所有人生日都不同"的概率。第一个人随便哪天,第二个人要和他不同,概率 364/365;第三个人要跟前两个都不同,概率 363/365……所以:
P(全不同) = (365/365) × (364/365) × … × ((365−n+1)/365)
当 n = 23 时,这个乘积约等于 0.493,所以至少两人生日相同的概率是 1 − 0.493 ≈ 0.507。当 n = 70 时,这个概率已经超过 99.9%。
直觉错在哪?因为我们习惯性地把自己代入,想的是"有人和我同生日",那是 n/365 的量级;但题目问的是"任意两个人之间",配对数按 n(n−1)/2 增长,23 个人就有 253 对。数量的增长方式是平方级的,不是线性的。
这个结论直接对应哈希碰撞。对 64 位哈希空间,用近似式 P ≈ 1 − exp(−n²/(2·2⁶⁴)),要让碰撞概率达到 50%,解出来 n ≈ 1.177 × 2³² ≈ 51 亿。想让碰撞概率低于百万分之一,n 大约 2³² × √(2×10⁻⁶) ≈ 610 万条。这个数量级非常关键:64 位哈希在几百万量级上基本安全,在几十亿量级上就会开始出问题。做分布式 ID、去重、内容指纹的时候,这个数字要心里有数。
5.2 蒙提霍尔问题:为什么换门更优
三门问题:三扇门,一扇后面有车,你选了一扇。主持人知道车在哪,然后打开另一扇后面是羊的门,问你换不换。
正确答案是换,胜率 2/3,不换只有 1/3。
解释这件事最清楚的方式是重新理解信息结构。你第一次选的时候,选中的概率是 1/3,选错的概率是 2/3。关键在于:主持人打开门这个动作不是随机的,他在利用自己的知识避开有车的门。所以如果你一开始选错了(概率 2/3),那么剩下的那扇没开的门必然有车,换门就赢;如果你一开始选对了(概率 1/3),换门就输。所以换门胜率就是 2/3。
我一开始也想不明白,后来把它推广到 100 扇门就想通了:你选一扇,主持人打开 98 扇羊门,只剩两扇。这时候你还觉得是 50/50 吗?显然不是,因为主持人几乎是在替你排除错误选项。
这个案例的工程意义在于:当信息提供方拥有额外知识时,他的行为本身就是信号。做推荐、做搜索排序、看监控告警,都要考虑这一点。用户点击某一项,可能是内容真的相关,也可能只是因为排在前面;系统给出某个结果,可能是模型真的判断相关,也可能是被某个先验偏置推上去的。忽略"信息提供方的知识",就会把选择偏差当成真实偏好。
5.3 赌徒谬误与热手效应:两头的坑
赌徒谬误:连出 5 次正面,就觉得下一次该出反面了。这是错的——如果硬币是公平的,每次抛掷相互独立,第 6 次的正面概率还是 1/2。前面 5 次的结果不会"欠"你一次反面。
热手效应的另一面是,看到某个东西连续出现,就觉得它"正在状态",会继续出现。这在独立事件上同样是错的。
两个方向都错,根源是同一个:混淆了"独立事件"和"有记忆的过程"。判断的关键是问:这个过程有没有反馈回路?抛硬币没有,所以独立。而篮球手感的争议在于,投篮命中会影响信心,信心会改变下次命中率,所以可能真的存在弱相关——但这需要统计检验来确认,不能凭感觉。
在工作里,这个陷阱的常见形式是:看到一个接口昨天失败率突然上升,就判断"它进入了一个不稳定期"。如果失败是独立随机的,那昨天的上升不改变今天的分布。真正该做的是看失败是否有聚集性——用前面提到的方差/均值比检验,或者做游程检验。有聚集才说明存在正反馈(比如雪崩、重试风暴),那才是真正需要干预的。
5.4 辛普森悖论:分组数据和整体数据打架
这是数据分析里最阴险的一个坑,因为它不需要你算错,每一步计算都是对的,结论却是反的。
假设有两种方案 A 和 B,按用户类型分成两组,看转化率:
| 分组 | 方案 A | 方案 B |
|---|---|---|
| 新用户 | 93/100 = 93% | 87/100 = 87% |
| 老用户 | 40/100 = 40% | 3/10 = 30% |
| 合计 | 133/200 = 66.5% | 90/110 = 81.8% |
在每一个分组里,A 都优于 B。但合并起来,B 反而更好。
原因在于两组样本量的分布不同。A 方案的样本中老用户占比 50%,B 方案中老用户只占 9%。而老用户的整体转化率天然低得多。所以 B 的"好成绩"其实是靠把样本集中在高转化的新用户群体上堆出来的,是一种混杂变量造成的假象。
处理方式只有一条:要么在分层内比较,要么对权重做标准化。绝对不要把不同构成的群体直接合在一起比平均值。我在做渠道效果对比、版本效果评估时,第一件事就是检查各组的用户构成是否一致。如果不一致,先做分层,或者用倾向得分加权、双重稳健估计这类方法做校正,然后再比。
6. 把公式落到代码里:模拟验证与实验落地
前面讲了不少公式和结论,但坦白讲,光看推导是记不住的,而且很容易在某一步悄悄理解错。我自己的做法是:每学一个反直觉的结论,就写几行代码模拟一遍,看数字能不能对上。对上了,说明真懂了;对不上,说明某个前提理解有偏差。这个方法帮我纠过好几次概念错误。
6.1 为什么要用模拟验证公式
蒙特卡洛模拟有三个好处:
第一,它不需要你会解析求解。有些问题解析解很难推,但模拟起来只有几行。比如"三个服务串联,每个成功率 0.95,整体成功率是多少"——这个可以直接乘,但如果每个服务有重试、有超时上限,解析就很麻烦,模拟反而简单。
第二,它能暴露你的前提假设错误。如果你模拟出来的数字和公式算出来的对不上,百分之百是某个前提被你想当然了。这种反馈是最有效的学习信号。
第三,它能给出置信区间。模拟跑 N 次,得到 N 个结果,你不仅知道期望,还知道波动范围。做容量规划时,我关心的从来不是"平均需要多少台机器",而是"95% 的情况下需要多少台",这只有模拟才能直接给出来。
6.2 四个可复现的验证脚本
脚本一:验证贝叶斯告警误报率。用 100 万次模拟,看看告警中真故障的比例是不是接近 1.94%。
import numpy as np rng = np.random.default_rng(42) N = 1_000_000 prevalence = 0.001 # 真实故障基率 sensitivity = 0.99 # 故障时告警的概率 false_alarm = 0.05 # 正常时误报的概率 is_fault = rng.random(N) < prevalence alarm = np.where( is_fault, rng.random(N) < sensitivity, rng.random(N) < false_alarm, ) print("告警总数:", alarm.sum()) print("告警中真故障比例:", is_fault[alarm].mean()) # 期望约 0.0194跑出来大约 0.0194,和手算完全一致。这时候你对"基率决定命中率"这件事的理解就有实感了。
脚本二:验证蒙提霍尔。用最简洁的等价描述:换门赢,当且仅当第一次选错了。
import numpy as np rng = np.random.default_rng(0) N = 200_000 car = rng.integers(0, 3, N) # 车在哪扇门后 choice = rng.integers(0, 3, N) # 初始选择 win_stay = (choice == car).mean() # 不换:初始就选中 win_switch = (choice != car).mean() # 换:初始没选中就一定赢 print(f"不换胜率: {win_stay:.4f}") # ≈ 0.3333 print(f"换门胜率: {win_switch:.4f}") # ≈ 0.6667这个等价关系是理解三门问题最省脑力的方式。你不需要模拟主持人的行为,因为主持人"永远打开羊门"这个约束,已经把换门和"初始是否选错"一一对应起来了。
脚本三:验证生日碰撞。向量化写法,比逐个模拟快很多。
import numpy as np rng = np.random.default_rng(7) def collide_prob(n, days=365, trials=5000): # 生成 trials 组,每组 n 个生日,排序后看有没有相邻相等 b = rng.integers(0, days, size=(trials, n)) b.sort(axis=1) return (np.diff(b, axis=1) == 0).any(axis=1).mean() for n in (10, 23, 40, 70): print(n, round(collide_prob(n), 4))输出大约是 0.117、0.507、0.891、0.9992。23 对应 0.507,这就是那个反直觉结论的来源。
脚本四:用 CLT 验证样本量的必要性。掷骰子是最经典的例子,单次结果的分布是均匀的,完全不正态。
import numpy as np rng = np.random.default_rng(1) n, trials = 30, 20000 means = rng.integers(1, 7, size=(trials, n)).mean(axis=1) print("均值:", means.mean()) # 理论 3.5 print("标准差:", means.std(ddof=1)) # 理论 sqrt((35/12)/30) ≈ 0.3118跑出来的标准差约 0.31,和理论值吻合。如果你把 n 改成 5,会发现标准差的实测值偏离理论值更远,因为 n 太小时 CLT 的近似还不够好。这个实验能让你直观感受到"n ≥ 30"这个经验值是怎么来的。
6.3 A/B 实验里的概率思维落地
最后说说这套知识在实验评估里的实际用法。我一般按这个顺序走:
第一步,明确指标和它的方差。转化率类指标用 p(1−p) 估方差,均值类指标用历史数据估样本标准差。这一步决定了后面所有计算。
第二步,算最小样本量。用 n ≈ 2(z_{α/2} + z_β)² · σ² / Δ²,其中 Δ 是你要检测的最小效应量。或者更简单,直接用前面的比例公式表感受一下量级。
第三步,检查各组构成是否一致。这就是辛普森悖论的防线。用户类型、渠道、地域、新老客占比,都得先对齐。不一致就分层或者加权。
第四步,看置信区间,而不是只看点估计。"提升了 1.2%"这个说法信息量很低。真正该说出来的是"提升 1.2%,95% 置信区间 [−0.3%, 2.7%]",这告诉你结论的方向都还没确定。
第五步,警惕多重比较。如果你同时看了 20 个指标,那么至少有一个指标出现 p < 0.05 的概率是 1 − 0.95²⁰ ≈ 64%。这不是发现,这是概率的必然结果。要么事先指定主指标,要么做 Bonferroni 之类的校正。
我自己踩过的最大的一个坑,是在早期做实验时只看点估计,看到方向对了就上线。后来复盘发现,那个"提升"完全在噪声范围内。从那时候起,我给自己定了一条规矩:任何结论必须带上区间,没有区间的结论不出报告。这条规矩后来省下的返工时间,远超当初多花的那几分钟计算。
再补一个我个人很受用的小习惯:遇到任何"感觉不太对"的数字,先别急着解释,先写十行代码模拟一下最朴素的模型,看看模拟结果和实际观测的差距有多大。如果差距在正常波动范围内,那就不需要解释,也不值得追查。这个习惯能帮你从大量的假信号里脱身出来,把精力留给真正的异常。