幸存者偏差如何扭曲留存指标?滚动留存与队列留存详解
幸存者偏差survivorship bias出现在留存指标里的时候通常不会报错也不会让数据看板变得难看——相反它往往会让数字看起来相当健康。最近在 Hacker News 上看到一个标题非常直白的小项目Survivorship bias in retention metrics, reproducible in 12 seconds。作者把一个统计陷阱塞进了一个只需要 12 秒就能跑完的复现实验里你每天在看板上读到的留存率很可能只是“活到今天的人”的留存率而不是所有用户的留存率。我先讲一个我实际见过的场景。1. 那场让我意识到“分母有问题”的数据评审会1.1 一个 90% 的滚动留存率为什么让在场的人沉默有次讨论产品粘性增长同学打开后台指着一个数字说我们昨天活跃的用户里今天 90% 还在活跃说明产品粘性很好。我问这个数字的分母是什么他说昨天活跃用户。我又问那如果我们这个月新增了一万个用户一个月后还剩多少他查了一下沉默了一会儿。一个月后这批用户里还在活跃的不到 5%。神奇的事情发生了同一个产品两个指标一个显示 90%一个显示 5%。而且两个数字都没有造假计算也没有算错。问题出在“90%”这个指标只回答了一个很窄的问题——“昨天还活着的人今天还活着吗”而它被拿出来的时候听上去像是在回答另一个问题——“新用户有多大可能留下来”真正和留存相关的问题通常是后者。于是那个 90%就成了一个典型的数据误导。1.2 从“数据很漂亮”到“数据不能信”只有一层窗户纸这个 90% 之所以具有误导性不是因为计算过程错了而是因为它把分析对象限定在了一个已经被筛选过的群体里。分母是“昨天还活跃”的用户也就是已经通过了至少一轮淘汰的幸存者。那些昨天之前就已经流失的用户从一开始就不在视野里。这层窗户纸一旦捅破问题就非常明显。但难就难在绝大多数看板默认展示的就是这样的指标而且它每天都显示得很高看起来理所当然没人会主动去质疑一个好看的数字。等到某天团队真的追问“分母是谁”“被排除的人怎么了”的时候往往已经过去了几个月产品决策早就被这些被幸存者污染的数字推着走了好几轮。2. 幸存者偏差是怎么混进留存分母的2.1 偏差的本质用幸存者推算全体幸存者偏差有一个流传很广的跨领域例子二战时工程师统计返航轰炸机的弹孔分布发现机翼中弹最多于是准备加厚机翼。后来有人提醒被击落的飞机没有返航它们的弹孔没有被统计到。机翼中弹多恰恰说明机翼并不致命真正致命的位置反而是那些在返航飞机上看不到弹孔的地方。留存指标里的逻辑完全一样。一个用户之所以还在“昨日活跃用户”这个集合里本身就意味着他扛过了之前的流失筛选。用这拨人作为全体用户的代表天然会高估产品对普通用户的吸引力。这不是数学错误而是样本选择错误——你用来推算整体的样本本身就已经被结果筛选过了。2.2 三个最常见的错误口径在实际业务里幸存者偏差通常以三种形式混进留存指标滚动留存分子是“今天活跃的用户”分母是“昨天活跃的用户”。这个指标衡量的是一个条件概率但它经常被当作留存率来汇报。行为筛选后的留存只统计“完成过新手流程”“有过 3 次以上会话”或“产生过付费行为”的用户再在这个子集里算留存。这个子集本身就是幸存者集合。用存量活跃用户算留存把“本月活跃用户里有多少上月也活跃”叫做月留存。分母是存量活跃池而不是当月新增用户。这三种口径的共同点是都绕开了“最开始进来那批人”。而留存这个指标的价值恰恰在于回答最开始进来那批人到底留下了多少。一旦分母不是那批人数字再高也无法回答这个问题。2.3 一个数字例子同一份数据两种结论假设你模拟 1000 个新用户每天有 80% 的概率继续活跃。到第 1 天约剩 800 人第 2 天约 640 人第 3 天约 512 人第 7 天约 210 人第 30 天约 12 人。用正确的队列口径留存曲线是一条持续下降的曲线第 30 天只剩 1.2%。用滚动留存口径数字会一直稳定在 80% 附近。天数活跃人数滚动留存队列留存第 0 天1000-100%第 1 天约 800约 80%约 80%第 2 天约 640约 80%约 64%第 3 天约 512约 80%约 51%第 7 天约 210约 80%约 21%第 30 天约 12约 80%约 1.2%这两列数据来自同一份模拟结果但带给决策者的信号截然相反。滚动留存会让人以为产品粘性非常稳定队列留存则会清楚地告诉你绝大多数新用户在一个月内就流失了。如果你负责一个新产品你真正该盯的几乎一定是右边那一列。3. 为什么真实业务里这个问题很难被看见3.1 看板默认展示的是“现有用户”不是“新用户”很多团队的第一屏数据是 DAU、WAU、MAU以及由此衍生的“活跃用户的下一次活跃率”。这类指标天然属于幸存者视角——月活的定义本身就是“这个月还活着的人”。你很难从一个只统计存量的看板里看出新用户是怎么一层层流失的。更隐蔽的是这类看板往往已经和团队的目标绑定。当所有人的眼睛都盯着 DAU 和“活跃用户回头率”的时候一个“新增 10000 人30 天后只剩 5%”的事实反而被淹没在漂亮的存量曲线里。看板没有撒谎但它只展示了幸存者的世界。3.2 平均值会把层叠流失抹平当不同批次的新用户表现差异很大时一个总平均值会把“早期用户留存很好”和“近期用户留存崩了”这两件事混在一起。你看到一个中等的留存数字既不会警觉也不会兴奋。但如果按注册批次拆开看很可能发现新鲜血液的留存正在持续走低。幸存者偏差让指标“看起来稳定”平均值又把稳定背后的结构变化抹平。两者叠加问题更难被发现。一个 45% 的总留存可能是所有批次都稳定在 45%也可能是老批次 80%、新批次 10% 的平均结果。这两个世界需要的产品决策完全相反。3.3 人很容易把“指标在变好”当成“产品在变好”这一点和统计关系不大和心理有关。幸存者偏差的可怕之处在于它不会让人看到坏消息它只会让人看到高数字。团队为了一个好看的指标做出各种决策直到某一天真正追问“分母是谁”的时候才发现过去几个月的增长分析建立在一块浮冰上。更现实的情况是当“提升留存”成为目标时最快的手段往往不是改进产品而是调整口径。把分母从全部注册用户收紧到“完成过新手引导的用户”留存率立刻上升。指标变得好看了产品没有任何变化。这不是造假但比造假更难察觉。4. 12 秒复现实验到底在演示什么4.1 实验结构人为设定一个已知流失率那个 Hacker News 项目的价值不在算法有多复杂而在于把“幸存者偏差导致指标失真”这件事做成了可复现的实验。你不必相信作者的结论自己跑一遍就能看到结果。这种实验的通用结构是生成一批模拟用户给每个人设定一个已知的每日留存概率然后分别用滚动留存和队列留存去统计同一批数据。因为底层流失率是已知的所以哪个口径更接近真相一目了然。这就是“可复现”的意义——它把抽象的统计概念变成了你能亲手触碰的输出。4.2 示例代码两种口径一次算清下面是一个示例结构用来展示口径差异。它不是线上生产代码只是一个方便理解的最小演示# 示例结构模拟 1000 人队列每人每天以 0.8 概率保持活跃 import numpy as np import pandas as pd rng np.random.default_rng(42) n_users 1000 n_days 14 p_survive 0.8 rows [] for uid in range(n_users): active True for day in range(n_days): rows.append((uid, day, int(active))) if active and rng.random() p_survive: active False df pd.DataFrame(rows, columns[user_id, day, is_active]) active_by_day df.groupby(day)[is_active].sum() # 口径一滚动留存 当天活跃数 / 前一天活跃数 rolling_retention (active_by_day / active_by_day.shift(1)).dropna() # 口径二队列留存 当天活跃数 / 第 0 天队列人数 cohort_size active_by_day.iloc[0] cohort_retention active_by_day / cohort_size print(滚动留存近似恒定) print(rolling_retention.round(3).head()) print(\n队列留存逐日下降) print(cohort_retention.round(3).head())如果你手头有一张真实的“用户-天”事件表把这段代码里的模拟数据替换成真实数据就能立刻验证自己的产品是不是也有同样的问题。12 秒的意义不在于快而在于把一个抽象的统计概念变成了可以亲手验证的对象。4.3 你在输出里会看到什么运行之后你大概率会看到两件事滚动留存稳定在 0.8 附近波动很小队列留存在第 1 天约 0.8第 2 天约 0.64之后一路下降。这正是幸存者偏差的可视化底层流失概率完全相同只是换了一个分母就得到了两张完全不同的“真相”。第一次跑通这个实验的人通常会觉得自己之前的留存看板白看了。判断一个留存指标有没有幸存者偏差最直接的办法不是看数字而是看分母分母里包含的是“所有进入流程的人”还是“已经表现出某些行为的人”5. 正确口径队列留存的关键设计5.1 分母从“进入那一刻”开始算修复问题不复杂核心原则只有一条分母必须是“同一批进入产品的用户”而不是“当前还活跃的用户”。以新用户留存为例分母应当是某一天或某一周的所有新增用户分子是这些人中间在第 N 天仍然活跃的人数。这就是队列留存cohort retention的基本形态。只要分母对计算本身几乎不会出错。一个留存指标如果没有明确的“进入时刻”和“观察窗口”就不是一个可复现的指标而是一个可操纵的故事。5.2 先定义“活跃”再定义“留存”活跃的定义看似简单实际决定了整个指标的性质。常见选择包括第 N 天当天有至少一次会话第 N 天前后各 1 天内有会话第 N-2 天到第 N 天之间有会话。窗口越宽留存越高窗口越窄留存越接近真实的产品使用频率。关键不是选一个“好看”的口径而是先想清楚你所理解的留存是指用户必须第 30 天当天回来还是他在这附近三天内回来过一次就算留存这两种定义在低频应用和高频应用之间差别巨大。如果是工具类产品一周打开一次很正常D30 当天会话的留存会严重低估真实粘性如果是社交或内容产品窗口太宽又会掩盖“用户其实已经不来了”的真相。窗口选择必须回到产品使用周期而不是数字观感。5.3 分组对比比单一指标更重要一个队列留存数字说明不了全部问题。更有效的做法是把它拆开按获
