前阵子被一个“留存率突然下滑”的诡异需求折腾到凌晨,连茶叶都泡得没味道了。业务线老板发来截图,说数据后台显示次日留存从42%掉到35%,是不是昨天产品上线了什么鬼东西?我看了一眼代码提交记录,最近一次发布只是改了Logo颜色和文案,正常来说用户不可能一夜之间就集体流失,这数据一定有猫腻。
为了搞清楚这数字怎么来的,我把相关口径拉出来逐一比对,发现三个团队在用三种完全不同的语法描述“留存”。数据组同事的口径是“设备ID次日仍活跃”——只要一个手机明天再开机,就算留存;产品经理用的是“账号次日活跃”——同一个人用两台手机访问会被算成两个留存;运营那边更直接,把最近买量拉进来的那批“新用户”全兜在分母里,但很多新用户其实是连验证码都没收到就被标记成注册的测试机。我把三天的数据拉在一起算了一下,结果你看下表:
| 计算口径 | 分母定义 | 次日留存率 |
|---|---|---|
| 设备口径 | 当日活跃设备数 | 42.3% |
| 账号口径 | 当日活跃账号数 | 38.9% |
| 买量运营口径 | 当日新增用户(含外部买量) | 35.1% |
| 修正后清洗数据 | 当日有效真实账号数 | 38.7% |
同一份数据,设备口径次日留存是42.3%,账号口径是38.9%,运营口径因为掺杂了一批无效买量直接掉到35.1%。如果只看第一列,你会觉得产品严重衰退;换成运营口径,又觉得是投放渠道在骗钱;其实产品什么都没做,只是计算逻辑各说各话而已。
好奇心上来后,我下载了某天的全量事件日志,压缩包1.2GB,解压后大约199.8万行。我一开始想用Excel开一下,结果等了三四分钟还是“正在加载”,风扇像直升机一样起飞;又试了之前的BI刷新逻辑,Tableau Prep每次连接同一个数据源全量计算要跑近3小时,中间还经常因为超时中断。最后没办法,还是回到了Python + pandas 2.1.4,用分块读入的方式处理。清洗步骤分四步:先按照user_id中含test、qa、demo等关键词把内部测试数据过滤掉,过滤了大概11.7万行;接着用user_agent识别包含bot、spider、crawler的机器请求,又去掉8.2万行;然后做重复判断,把同一user_id在同一秒访问同一页面的重复点击去掉,主要是埋点重复调用;最后删除那些只有启动事件但没有任何有效交互的账号,避免“打开就闪退”的池子混进来。
简单调试后,逻辑跑通总算松了口气。整个过程跑了4分钟左右,这比BI工具动不动卡半小时的体验好太多了。关键脚本长下面这样,有兴趣可以自己跑一下:
import pandas as pd
chunks = []
for chunk in pd.read_csv('events.csv', chunksize=50000,
dtype={'user_id': 'str', 'device_id': 'str'}):
tmp = chunk[~chunk['user_id'].str.contains('test|qa|demo', regex=True, na=False)]
tmp = tmp[~tmp['user_agent'].str.contains('bot|spider|crawler', regex=True, case=False, na=False)]
tmp = tmp[~tmp.duplicated(['user_id', 'event_time', 'page_url'], keep=False)]
chunks.append(tmp)

df = pd.concat(chunks)
df = df[df['has_valid_screen'] | df['has_click'] | df['has_input']]
清洗完随手重新计算账号口径的次日留存,结果在38.7%左右。和数据组原本的38.9%差得不多,但和老板看到的那个“42%降到35%”完全就不是一回事。问题根源不是产品改坏了,也不是算法有bug,而是那一套自动更新dashboard的数据源,一直拿设备口径去跑,而业务同事却拿它和包含买量分母的日报做对比,越对比越慌。
这件事让我产生了一个有点“政治不正确”的想法:我们真不一定需要那么多实时大屏和自动化监控,大部分情况下,先拉几天下来的原始样本,自己亲手清洗一下数据,在旁边写清楚每个指标的准确定义,比堆砌一堆漂亮的可视化有用得多。如果再有人跑过来告诉你“留存率跌了”或者“转化率出了问题”,别急着分析原因,先翻开那个指标底层的SQL,看它的分母里藏没藏着某个市场总监带来的一批“假用户”。你会有意外发现,而且这种发现往往不值钱,却能让一个团队少折腾两周。