1. 三个公式到底能干什么:先把它们扔进真实问题里
我做过很多次内部分享,讲联合概率、条件概率和贝叶斯法则,台下点头的人不少,散会之后真能用起来的没几个。原因不复杂:教科书把它们放在同一章里,配同一类掷骰子、摸球的例题,导致很多人的记忆是"三个长得挺像的公式",而不是"三件解决不同问题的工具"。这篇我就按自己的使用习惯,把这三个公式拆开,讲清楚它们各自在什么场景下出场、出场时手里拿着什么数据、算完之后那个数字应该怎么读。如果你正在做数据分析、策略评估、风控打分、搜索排序,或者只是想让自己的判断别被"99%准确率"这种说法骗到,下面的内容基本都是可以直接搬去用的。
先说结论性的定位,后面再逐条展开。联合概率回答的是"两件事一起发生有多常见",它是特征工程、共现统计、关联规则的地基;条件概率回答的是"已知一件事发生了,另一件事的概率怎么变",它是漏斗分析、分层评估、全概率拆解的核心,也是把样本空间"砍掉一块"之后重新归一化的动作;贝叶斯法则回答的是"结果看到了,原因是什么",它把因果方向倒过来算,是所有"从证据反推原因"的场景唯一正确的框架,包括垃圾内容识别、故障根因定位、检测类问题的解读。三者不是并列关系,而是层层递进:联合概率是原始素材,条件概率是切片方式,贝叶斯法则是把切片结果反着用。
我见过最常见的误用,是把条件概率的方向搞反。P(阳性|有病) 和 P(有病|阳性) 在两套完全不同的样本空间里,前者由设备和试剂决定,通常能到 95% 以上;后者由人群患病率决定,可能只有个位数。这两个数在业务报表上经常被混着引用,导致的决策偏差不是小数点级别,而是量级级别。所以这篇的重点不是推公式,而是讲清楚每个公式的输入从哪里来、输出该怎么读、以及在什么情况下它会骗你。
1.1 一张用户行为表,就能把三个公式串起来
我习惯用一个具体的表来开场。假设手里有一份用户行为日志,字段是「是否点击推荐位」和「是否完成下单」,一千条记录。联合概率就是直接数格子:点击且下单的有 80 条,那 P(点击, 下单) = 0.08。条件概率是限定一行再看另一列:在点击的 200 条里,有 80 条下单,P(下单|点击) = 0.4。贝叶斯法则是反过来问:在下单的 120 条里,有多少是点过推荐位的?P(点击|下单) = 80/120 ≈ 0.667。
这三个数字来自同一张表,含义完全不同。0.08 是"整体上看这件事有多稀有",做资源预估时用它;0.4 是"推荐位这个动作带来的转化强度",做漏斗优化时用它;0.667 是"成交订单里推荐位的贡献占比",做归因和预算分配时用它。同一批数据,三个问题,三个答案,谁也不能替代谁。我在实际项目里见过用 0.08 去论证"推荐位没用"的,也见过用 0.667 去论证"推荐位是主要来源"的,两边都没错,但都在拿错误的数字回答错误的问题。
注意:拿到任何一组概率数字,第一件事是确认它的分母是什么。分母是全体样本、是某个条件下的子集、还是某个结果的反向子集,决定了这个数字能回答哪一类问题。
1.2 为什么很多人学完公式仍然不会用
我的观察是,卡点通常不在公式本身,而在"先验从哪来""独立性成不成立""分母该用哪个"这三个工程问题上。公式是干净的,数据是脏的。比如你想算 P(故障|某指标飙升),公式写出来就一行,但 P(某指标飙升|故障) 需要历史故障样本,P(某指标飙升) 需要全量监控数据,P(故障) 需要故障率统计。这三样东西分散在三个系统里,采样口径还不一致,这才是真正的工作量。
所以后面每一节,我都会把"公式长什么样"和"数据怎么凑齐"放在一起讲。纯讲公式的部分我会压缩到最短,因为那些内容任何一本教材都有,而口径对齐、平滑处理、先验选择这些,才是踩过坑之后才会写进笔记的东西。
2. 联合概率:把"同时发生"变成可计算的共现结构
2.1 联合分布表的本质是一次全量分组统计
联合概率 P(A, B) 指的是 A 和 B 同时成立的那部分样本占总体的比例。它最朴素的实现就是一次分组计数:按 A 的取值分行、按 B 的取值分列,每个格子里的计数除以总数,得到的就是联合分布。这件事听起来没有技术含量,但它是后面所有分析的底座,一旦这张表算错,条件概率和贝叶斯结果全都会跟着错。
我用一段代码把这件事说透。假设有一份行为日志,两个字段分别是渠道来源和是否转化:
import pandas as pd df = pd.DataFrame({ "channel": ["search"] * 600 + ["feed"] * 400, "converted": [1] * 120 + [0] * 480 + [1] * 40 + [0] * 360, }) joint = pd.crosstab(df["channel"], df["converted"], normalize=True) print(joint) # converted 0 1 # channel # feed 0.36 0.04 # search 0.48 0.12这张表里,0.12 就是 P(channel=search, converted=1)。四个格子加起来等于 1,这是联合分布的基本约束。把行或列加起来,得到的就是边缘概率:P(converted=1) = 0.04 + 0.12 = 0.16,P(channel=search) = 0.48 + 0.12 = 0.60。
边缘概率这个概念特别值得强调,因为它是条件概率的分母。很多人算条件概率时习惯性拿总样本数当分母,那算出来的其实是联合概率,不是条件概率。P(converted=1 | channel=search) 的分母是 0.60,不是 1。这个错误在 Excel 里尤其容易犯,因为透视表的百分比选项有好几种,选错了表面上数字还挺合理。
2.2 独立性和条件独立:一个被用烂但经常不成立的假设
两个事件独立,等价于 P(A, B) = P(A) · P(B)。在上面的例子里,P(search, 转化) = 0.12,而 P(search) · P(转化) = 0.60 × 0.16 = 0.096。0.12 明显大于 0.096,说明渠道和转化不独立,搜索渠道的转化倾向更强。这个比较看起来很朴素,但它就是特征筛选里卡方检验、互信息这类方法的直观来源:两个变量如果接近独立,放在一起建模的增量价值就有限。
真正容易出事的是条件独立。朴素贝叶斯假设"给定类别之后,各个特征之间相互独立",这个假设在文本场景里几乎从来不成立,因为词与词之间存在强关联,"机"和"器"经常一起出现。但这个假设即使被违反,分类效果往往还能接受,原因在于决策只需要后验概率的相对大小排序正确,不需要绝对值准确。这个性质在实际工程里非常有用,它意味着你不必为了追求假设成立而把特征做得很干净,但你必须知道什么时候它会失效——当特征之间高度冗余且方向一致时,错误会被放大。
提示:条件独立假设不成立,影响的是后验概率的绝对值,通常不影响排序。如果你只用它做"选哪个",一般问题不大;如果你要用它做"概率是多少"对外输出,就必须校准。
2.3 实操:共现矩阵与平滑处理
联合概率在工程里最常见的落地形态是共现矩阵,比如商品组合、标签共现、搜索词与点击页面的对应关系。原始计数直接用会有个问题:长尾项计数很小,概率估计抖动剧烈,一个只出现过一次的组合会被估成 1/N,噪声极大。这时候需要平滑。
def joint_with_smoothing(counts, total, alpha=1.0, vocab_size=None): """加 alpha 平滑的联合概率估计""" vocab_size = vocab_size or len(counts) denom = total + alpha * vocab_size return {k: (v + alpha) / denom for k, v in counts.items()}拉普拉斯平滑(alpha=1)的直觉是:假装每个组合在动手统计之前就已经被看到过一次。这个"假装"的强度由 alpha 控制,样本量越小,alpha 的影响越明显。我一般在小样本场景用 alpha=1,样本量上万之后 alpha 取 0.1 甚至更小,避免平滑把真实信号抹平。
这里有个经验:平滑的强度应该和你的置信需求匹配。做召回候选时,我倾向于把 alpha 调大一点,宁可把长尾项的概率估高,也别让冷门组合直接归零;做精确打分时,alpha 要小,否则头部项的概率会被系统性压低。同一个数据集,两种用途,两套参数,这很正常。
3. 条件概率:真正让概率变得可用的那一刀
3.1 样本空间收缩:条件概率最直观的理解方式
条件概率的定义是 P(A|B) = P(A, B) / P(B),前提是 P(B) > 0。公式本身没什么好讲的,我更喜欢用"样本空间收缩"来解释:原来你面对的是全体样本,现在已知 B 发生了,那么所有 B 不成立的样本都被排除,剩下的样本构成一个新的、更小的空间,然后在这个新空间里重新算 A 的占比。
举个例子,一千个用户里有两百个点了推荐位,八十个既点又下单。问"点了之后下单的概率",你要做的不是拿 80 除以 1000,而是先在心里把样本空间从一千收缩到两百,然后 80/200 = 0.4。这个动作在 SQL 里就是一次 WHERE 过滤,在 pandas 里就是一次布尔索引。所以条件概率在工程上是极度自然的,它对应的是"分组统计"这个最基础的操作。
理解了这一点,很多事情就顺了。为什么分层分析比整体分析更可信?因为分层之后每一层的样本空间更纯净,混杂因素被排除在空间之外。为什么小样本的分层结果不可靠?因为空间收缩之后剩下的样本太少,估计方差被放大了。这两件事是同一个机制的两面。
3.2 全概率公式:把一团乱麻拆成互斥的几条路
全概率公式是条件概率最实用的推论:如果 B1 到 Bn 是一组互斥且完备的事件,那么 P(A) = Σ P(A|Bi) · P(Bi)。翻译成人话就是,你要算某件事的总概率,先把它按几条互不重叠的路径拆开,每条路径单独算,再按路径的占比加权求和。
这个公式在业务分析里的价值极高,因为它强制你做一件事:把"总盘子"和"结构"分开看。举个我自己项目里的例子,整体转化率下降,直接看总数只能得到一个"降了"的结论,用全概率拆开之后,你立刻能看到是每条的转化率降了(能力问题),还是用户结构变了(流量问题)。这两种情况的处理方式完全不同,前者要改产品,后者要改投放。
# 整体转化率 = Σ 各渠道转化率 × 各渠道占比 channels = { "search": {"share": 0.6, "cvr": 0.20}, "feed": {"share": 0.3, "cvr": 0.10}, "direct": {"share": 0.1, "cvr": 0.35}, } overall = sum(v["share"] * v["cvr"] for v in channels.values()) print(round(overall, 4)) # 0.185这个 0.185 就是全概率公式的产物。它的好处是,任何一个渠道的 cvr 或者 share 变化,你都能立刻定位到整体数字的变化来源。我在做周报的时候经常用这个结构,比单纯报一个总数有用得多。
3.3 分层与辛普森悖论:条件概率最容易被忽略的陷阱
条件概率用得越多,越容易撞上辛普森悖论:分层看每一层都是 A 更好,合起来看却是 B 更好。这不是数学错误,而是权重结构不同导致的加权平均反转。我自己就踩过一次,两个版本的推荐策略,分人群看新版全面占优,整体指标却是负的,排查了两天才反应过来是流量结构变了。
用具体数字说清楚。某次实验里,A 版本和 B 版本各覆盖一千人,按用户活跃度分两层:
| 版本 | 高活跃人数 | 高活跃转化率 | 低活跃人数 | 低活跃转化率 | 整体转化率 |
|---|---|---|---|---|---|
| A 版本 | 300 | 40% | 700 | 12% | 20.4% |
| B 版本 | 700 | 35% | 300 | 10% | 27.5% |
每一层看,A 都赢了(40% > 35%,12% > 10%),但整体 B 赢了(27.5% > 20.4%)。原因很直接:B 版本覆盖的人群里高活跃用户占了七成,而高活跃用户的转化率天然更高,把整体数字拉上去了。这种情况下,正确的结论是"在相同人群结构下 A 更优",而不是"B 更好"。
注意:看到"整体提升但分层下降"或者反过来,第一反应应该是检查人群结构有没有变化。实验分流如果没做分层随机,这种反转几乎必然出现。
3.4 条件概率的三个典型误用
第一个是把方向搞反。前面提过 P(阳性|有病) 和 P(有病|阳性) 的区别,在业务上对应的就是 P(点击|曝光) 和 P(曝光|点击)。后者在逻辑上恒等于 1(点了必然曝光过),拿它做指标毫无意义。
第二个是条件选得太多。层层过滤之后样本量急剧缩小,剩下的几十条记录算出来的比例没有任何统计意义。我见过按"渠道 × 城市等级 × 设备型号 × 时段"四层交叉之后得出"某类用户转化率 100%"的结论,实际上那组只有三个人。经验做法是每一层的样本量至少要有几百,低于这个量级就往上合并维度。
第三个是忽略了条件本身是可被影响的。P(下单|领券) 高,不代表发券就能带来下单,因为领券这个行为本身筛出了高意向用户。这是选择偏差的经典形态,用条件概率解释因果时,必须问一句"这个条件是不是被干预出来的"。
4. 贝叶斯法则:把因果倒过来算的唯一正确姿势
4.1 公式拆解:先验、似然、后验、证据
贝叶斯法则写作 P(A|B) = P(B|A) · P(A) / P(B)。四个部分各有名字,也各有工程含义:
- P(A)是先验,是你看到证据之前对 A 的信念。在业务里它通常是基础发生率,比如整体故障率、整体作弊率。
- P(B|A)是似然,是 A 成立时观察到证据 B 的概率。它由你的检测手段决定,比如监控规则的召回率。
- P(B)是证据,是观察到 B 的总概率,用全概率公式算:P(B|A)P(A) + P(B|¬A)P(¬A)。
- P(A|B)是后验,是你要的答案。
这四个部分里,最容易出错的是先验。很多人下意识把先验当成 0.5,觉得"我不知道就五五开",这叫均匀先验,在没有任何信息时勉强能用,但在有历史数据的情况下就是浪费信息。更常见的问题是先验取了全量平均值,而实际场景的先验分布差异极大,比如不同品类、不同地区的作弊率可能差一个数量级,用统一的先验会把后验全部带偏。
4.2 一个检测案例:99% 准确率为什么仍然可能不靠谱
这是我最常拿来举例的场景,因为它能一次性讲清楚基率谬误。假设某类问题的发生率是 0.1%,检测手段的灵敏度是 99%(真有问题时能测出来),特异度也是 99%(没问题时不会误报)。现在某人被测出阳性,他真正有问题的概率是多少?
很多人会脱口而出 99%,实际算一下:
prior = 0.001 # 先验:发生率 sens = 0.99 # 灵敏度 P(阳性|有问题) fpr = 0.01 # 假阳性率 P(阳性|没问题) p_pos = sens * prior + fpr * (1 - prior) posterior = sens * prior / p_pos print(round(p_pos, 5), round(posterior, 4)) # 0.01098 0.0902结果是 9% 左右,而不是 99%。原因在于分母:一万人里只有 10 个真有问题,但另外 9990 个没问题的人里会有约 100 个被误报。真阳性 10 个,假阳性 100 个,阳性结果里绝大多数是误报。这就是基率谬误,也是所有"高准确率检测"在低发生率场景下的共同困境。
有意思的是,如果同一个人再测一次,结果还是阳性,情况会完全反转:
prior2 = posterior # 用第一次的后验当前验 p_pos2 = sens * prior2 + fpr * (1 - prior2) posterior2 = sens * prior2 / p_pos2 print(round(posterior2, 4)) # 0.9075第二次之后,概率升到 90% 以上。这就是贝叶斯更新的威力:证据会累加,每一次观测都在修正信念。这个性质在工程上极其有用,它意味着你可以用多次弱信号叠加出强判断,而不需要一个完美的单点检测。
提示:任何"准确率 99%"的宣传,都要追问一句"在什么基率下"。同一个模型,在发生率 30% 的场景里表现很好,搬到发生率 0.1% 的场景可能完全不可用。
4.3 手写一个朴素贝叶斯分类器
光看公式容易飘,我习惯让学生手写一遍。下面这个实现刻意写得笨拙,把所有中间量都打印出来,方便对照公式:
import math from collections import defaultdict def train(docs, labels): prior = defaultdict(int) lik = defaultdict(lambda: defaultdict(int)) vocab = set() for doc, y in zip(docs, labels): prior[y] += 1 for w in doc: lik[y][w] += 1 vocab.add(w) total = len(docs) for y in prior: prior[y] /= total return prior, lik, vocab def predict(doc, prior, lik, vocab, alpha=1.0): scores = {} for y in prior: score = math.log(prior[y]) denom = sum(lik[y].values()) + alpha * len(vocab) for w in doc: if w in vocab: score += math.log((lik[y][w] + alpha) / denom) scores[y] = score best = max(scores, key=scores.get) return best, scores有几个细节值得说。第一是取对数,概率连乘在几百个特征之后必然下溢成 0,取对数之后变成加法,数值稳定,而且不改变排序。第二是拉普拉斯平滑,分母加 alpha × 词表大小,分子加 alpha,保证没见过的词不会被算成 0 概率——一旦出现 0,整个连乘就归零了。第三是分母的计算方式,这里用的是该类别下所有词频之和,等价于该类别的总词数。
跑一个小例子验证:
docs = [ ["免费", "领取", "点击", "链接"], ["免费", "中奖", "速速", "点击"], ["项目", "进度", "同步", "会议"], ["会议", "纪要", "项目", "排期"], ] labels = ["spam", "spam", "normal", "normal"] prior, lik, vocab = train(docs, labels) print(predict(["免费", "项目", "链接"], prior, lik, vocab))你会看到两类得分接近,这正是朴素贝叶斯的典型表现:短文本、词汇重叠时区分度有限。实际使用中要靠更长的文档、更干净的词表、以及合理的先验来拉开差距。
4.4 先验怎么定:三种做法和各自的坑
第一种是用历史统计。这是最常用的做法,比如取过去 30 天的作弊率作为先验。坑在于历史统计的口径必须和当前口径一致,账号体系改过、埋点改过、统计窗口改过,先验都会失真。我一般会保留一个分母校验环节,先验对应的分子分母能不能对得上当前的数据源。
第二种是用同类场景迁移。新业务没有历史数据时,可以借用相似业务的先验。坑在于相似性很难定义,两个看起来很像的业务,实际发生率可能差一个数量级。做法上我倾向于取一个保守的先验,也就是往中间靠,然后用较强的平滑让数据快速覆盖先验的影响。
第三种是用专家判断给一个区间,再转成分布。这种做法在样本极少时反而更稳,因为人为设定的先验至少量级是对的。坑在于主观性太强,不同人给的先验可能差很远,所以一定要写下来、留档,后续用数据回测先验的合理性。
注意:先验不是一次设定就不动的。业务会漂移,先验也要定期重估,尤其是在异常检测这类对基率敏感的任务上。
5. 工程落地:这些公式在系统里长什么样
5.1 评分卡与异常检测里的贝叶斯叠加
实际的异常检测很少只用一个信号,通常是十几个弱规则各自给出一个判断,最后合成一个分数。这个合成过程如果用贝叶斯框架来做,逻辑非常顺:每条规则对应一个似然比,多个规则在条件独立的假设下连乘,得到后验几率。
具体做法是把规则输出转成对数几率增量。设某条规则在有异常时的触发率是 p1,在正常时是 p0,那么这条规则提供的证据强度就是 log(p1/p0)。多条规则的增量相加,再加到基础对数几率上,就得到最终评分。这个做法的好处是可解释:每条规则贡献了多少分,一目了然,排查的时候能直接定位是哪条规则把分数顶上去了。
坑在于规则之间高度相关。同一类异常往往会同时触发好几条规则,如果按独立处理,证据会被重复计算,分数虚高。常见的缓解方式是规则分组,组内取最大值,组间才相加。这个改动看起来土,但实测下来能显著降低误报。
5.2 点击率预估里的条件概率与平滑
点击率本质就是一个条件概率:P(点击|曝光)。但这个数字在工程上非常难估,因为长尾物品的曝光量极小,原始比值要么是 0 要么是 1,毫无参考价值。行业里通用的做法是贝叶斯平滑,把先验(全局点击率)和观测(该物品的点击数)按样本量加权:
平滑后 CTR = (clicks + alpha * prior) / (impressions + alpha)alpha 的物理含义是"先验相当于多少次虚拟曝光"。alpha 越大,先验的影响越强,新物品的估计越靠近全局均值;alpha 越小,越信任观测数据,样本少时抖动大。我一般把 alpha 设在几十的量级,具体取值靠离线回测。
这个公式还有一个变体,按曝光量分段取不同的 alpha,低曝光用大 alpha,高曝光用小 alpha。这个做法在冷启动场景里效果明显,因为不同阶段的偏差结构不一样。
5.3 根因定位:按后验概率给可疑项排序
线上出问题的时候,面对几十个指标同时异常,人工排查很慢。这时候可以把每个指标看作一个证据,计算 P(某个根因 | 观测到的指标组合),然后按后验降序排。这个流程听起来复杂,实际落地可以简化成打分:每个"指标异常"对每个"根因"维护一个条件概率表,观测到的异常项按表查分、相乘、归一化,得到一个排序。
维护这张条件概率表是主要工作量,通常靠历史故障复盘积累。前期数据少的时候可以先用规则,等积累了几十次故障之后,统计出来的表比拍脑袋准得多。我在一个项目里做过这件事,最初靠人工规则,误报率高得没法用;积累半年之后改成统计表,Top3 命中率能到七成以上。
5.4 算不出来的时候:采样兜底
不是所有后验都有解析解。当模型变复杂,比如有隐变量、有层级结构,分母那个积分根本算不出来。这时候的主流做法是采样:从后验分布里抽一批样本,用样本的统计量近似后验。最朴素的是拒绝采样,好一点的是重要性采样,更复杂的有各种马尔可夫链方法。
工程上的判断标准很简单:如果解析解算得动,就别上采样,因为采样引入的方差和收敛判断会让调试变得很难。只有在维度高、分布复杂、解析解确实不存在时才用。另外采样结果的稳定性要单独验证,同一份数据跑两次结果差很多,说明样本量不够或者链没收敛,这时候的结果不能直接拿去决策。
6. 常见问题与排查实录
6.1 概率连乘下溢怎么办
这是手写贝叶斯时第一个撞上的问题。几百个特征连乘,结果很快就变成 0,所有类别得分都是 0,没法比较。解决办法是全程在对数空间里算,乘法变加法,最后比较对数得分。如果要输出真实概率,再用 log-sum-exp 技巧归一化:
def softmax_from_log(log_scores): m = max(log_scores.values()) exps = {k: math.exp(v - m) for k, v in log_scores.items()} total = sum(exps.values()) return {k: v / total for k, v in exps.items()}先减去最大值再取指数,避免 exp 溢出。这个技巧看着小,但没有它,线上服务在长文本上必然出问题。
6.2 后验概率和实际发生率对不上
模型输出的概率如果直接对外使用,必须做校准。常见现象是模型说有 80% 把握,实际发生率只有 50%,这叫过度自信。检查方法是把预测概率分桶,看每桶的实际正例率,画一条校准曲线。如果曲线偏离对角线,说明需要校准,常用的方法有保序回归和温度缩放。
我在项目里遇到过更隐蔽的情况:离线校准做得很好,上线之后偏了。原因是线上的特征分布和离线不一致,尤其是那些依赖实时聚合的特征。这类问题的排查思路是先加一层"特征分布监控",确认输入没漂,再怀疑模型。
6.3 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 所有类别得分相同 | 特征全未命中词表 | 检查分词与词表覆盖 |
| 得分全为 0 或 inf | 概率连乘下溢或除零 | 改对数空间,加平滑 |
| 后验概率异常高 | 先验取得过高或证据重复计算 | 校准先验,做规则去相关 |
| 离线好线上差 | 特征分布漂移 | 对比线上线下特征分布 |
| 分层与整体结论相反 | 样本结构变化 | 检查分流随机性与权重 |
| 小样本分层结果极端 | 样本量不足 | 合并维度或加平滑 |
6.4 几条踩过坑之后才记住的经验
第一,任何概率结论都要带上样本量。单独说"转化率 30%",不如说"300 次曝光里 90 次转化,转化率 30%"。前者无法判断可信度,后者一眼能看出置信区间大概多宽。
第二,条件概率的分母要写进报表。我见过太多报表只报了比例不报分母,导致看的人误判。加一列分母,成本极低,收益很大。
第三,贝叶斯更新的顺序不影响最终结果,但影响中间过程的可用性。如果你需要在每次观测后就给出判断,那就得保证每一步的后验都经过校准,否则误差会累积。
第四,别迷信复杂模型。在样本量有限的情况下,一个参数估计得准的简单贝叶斯,往往打得过参数一大堆的复杂模型。我做过几次对比,中小规模数据上朴素贝叶斯的表现经常超出预期,尤其是特征维度高、样本相对少的场景。
第五,把公式写进文档。不是写给同事看,是写给三个月后的自己看。概率相关的逻辑一旦隔了时间,很容易忘记当初为什么选这个先验、为什么这么平滑。我现在每个涉及概率计算的模块都会留一段注释,写清楚公式来源、参数含义、数据口径,回头看的时候省了大量时间。
最后分享一个我自己的习惯:每接触一个新的概率结论,先问三个问题——分母是什么、条件是自然的还是被干预出来的、先验用的是哪份数据。这三个问题问下来,大部分看起来很唬人的数字都会露出原形,剩下的那些才是真正值得信的。