搭建留存三角表,用数字看清三个陷阱
目标
用“Moanoteu”的每周注册同期群构建留存三角形,用数字确认分母、空格、幸存者这三个陷阱,然后依据正确的加权留存率和按注册渠道的留存率来作判断。
为什么重要
总的活跃数把新进来的人和离开的人混在同一个数字里。要把同一周注册的人分开追踪,才能看出产品是否留住了人。而且这个计算,只要改变分母,或者把空格填成 0,就很容易变好或变差——它牵系着广告费和路线图,所以必须按定义重新计算。
材料与定义
/opt/fixtures/founder/product/users.csv、events.csv(与指标模块相同的材料),注册渠道是/opt/fixtures/founder/product/touches.csv中注册前最后一个接触点(user_id,ts,channel中 ts 最晚的那一行)的 channel。- 以首尔时间为准的周(星期一 00:00 开始),第 0 周 = 2026-03-02,观测到第 15 周为止。排除内部账户(is_internal=1)。
- 同期群 c = 注册时刻所在的周次(0–11)。第 N 周留存 = 在周次 c+N 有 session_start 至少 1 次。分母是同期群的注册人数。c+N > 15 的格子尚未观测,所以是
null。 - 所有函数的第一个参数是材料目录。比率在小数点后第四位四舍五入。
步骤
- 在
/root/founder/cohort/sizes.json中,以{"0": n, ..., "11": n}的形式写入各同期群的注册人数。 - 在
/root/founder/cohort/cohort.py中创建retention(fix, max_n)——{"0": [r0, r1, ..., r_max_n], ...},尚未观测的格子为None。 - 在
/root/founder/cohort/denominator.json中,以两种分母写入同期群 4 的第 4 周留存率:rate_cohort_size(正确的分母)、rate_week0_active(以第 0 周活动者为分母——故意犯的错误)。 - 在
/root/founder/cohort/censor.json中,以两种方法写入第 6 周留存率:pooled_rate(只含观测完毕的同期群,按人数加权)、naive_rate_with_zeros(所有同期群,尚未观测的格子按 0 处理),以及eligible_cohorts(观测完毕的同期群数)。 - 在
/root/founder/cohort/survivor.json中,以两份名单写入第 4 周留存率:all_users_rate(同期群 0–11 的全体,只含观测完毕的同期群)、survivors_only_rate(只挑出在第 15 周有活动的人,做同样的计算)、survivors(这些人的人数)。 - 在
cohort.py中加入pooled(fix, n, unbounded=False)——只收集观测完毕的同期群、按人数加权的留存率{"cohorts": k, "users": u, "retained": r, "rate": 0.0000}。unbounded 为真时,按“在 c+N 周之后的任何一周(含 c+N)有活动”来统计。 - 在
/root/founder/cohort/channel.json中,以{"organic_search": r, ...}的形式,写入按注册渠道(最后接触点)的第 4 周加权留存率,共五个渠道。 - 在
/root/founder/cohort/report.json中写入curve(n=0..8 的 bounded 加权留存率 9 个)、week4、week4_unbounded、flatten_week(与上一周相比下降小于 0.02 的第一个 n,1 以上)、best_channel、worst_channel。
参考
- 周次:
(서울 시각 - datetime(2026, 3, 2, tzinfo=서울)).days // 7 - 先建立每个人“有活动的周次集合”,那么所有步骤都只是一次集合检查。
- 常见错误:把分母定为第 0 周活动者,把尚未观测的格子填成 0 再取平均,对同期群比率取简单平均,只挑现在还留下的人来看过去。
各同期群的注册人数
在 /root/founder/cohort/sizes.json 中,以字符串键写入按注册周次(0–11)统计的外部注册用户数。
把 signup_at 转换为首尔时间,计算距 2026-03-02 有多少天,再除以 7 所得的商就是同期群。排除内部账户。
留存三角形——尚未观测的格子为 None
在 /root/founder/cohort/cohort.py 中创建 retention(fix, max_n)。同期群字符串键 → n=0..max_n 的留存率列表,c+n>15 的格子为 None。
先为每个人建立有 session_start 的周次集合。分母是同期群的注册人数,尚未观测完毕的格子不是 0,而是 None。
改变分母,留存率就显得更好
在 /root/founder/cohort/denominator.json 中,以 rate_cohort_size 和 rate_week0_active 两种分母,写入同期群 4 的第 4 周留存率。
分子相同(周次 8 有活动的同期群 4 的人)。只把分母换成注册人数和第 0 周(周次 4)的活动者数,试一试。
把尚未观测的格子填成 0,曲线就会虚假地折下去
在 /root/founder/cohort/censor.json 中写入第 6 周留存率的 pooled_rate、naive_rate_with_zeros、eligible_cohorts。
正确的一边,只收集 c+6 ≤ 15 的同期群,用全部留存人数 ÷ 全部注册人数。错误的一边,把所有同期群的注册人数都放进分母(尚未观测的人被算作留存 0)。
只看现在剩下的人,过去就显得更好
在 /root/founder/cohort/survivor.json 中,以 all_users_rate(全体)和 survivors_only_rate(只含第 15 周有活动的人)写入第 4 周加权留存率,并写入 survivors(这些人的人数)。
两种计算的同期群和周次规则相同,只是人的名单不同。名单原则上要在注册时点就固定,所以第二个数字是故意犯的错误。
加权留存率函数——bounded 与 unbounded
在 cohort.py 中加入 pooled(fix, n, unbounded=False)。只含观测完毕的同期群,按人数加权,返回 {cohorts, users, retained, rate}。
不是同期群比率的简单平均,而是留存人数之和 ÷ 注册人数之和。unbounded 看的是在 c+n 周及其之后的任何一周是否有过活动。
按注册渠道的第 4 周留存率
在 /root/founder/cohort/channel.json 中,写入最后接触点的五个渠道各自的第 4 周加权留存率。
在 touches.csv 中,每个人 ts 最晚的那一行的 channel 就是注册渠道。只把名单缩小到该渠道的人,做与 pooled 相同的计算。
把曲线和判断放在一页上
在 /root/founder/cohort/report.json 中写入 curve(n=0..8)、week4、week4_unbounded、flatten_week、best_channel、worst_channel。
flatten_week 是从 n=1 开始看,curve[n-1] − curve[n] < 0.02 的第一个 n。渠道取第 7 步结果中最高的和最低的。