遵守顺序与窗口的漏斗及渠道归因
目标
根据“Moanoteu”的记录,构建遵守顺序和 14 天窗口的按人数统计的漏斗,测量常见错误会把数字虚增多少,然后用首次触点、末次触点归因计算并比较各渠道的 CAC。
为什么重要
漏斗决定人在哪里流失,归因与 CAC 则决定把钱花在哪里。如果统计事件数,或者无视顺序和窗口,格子就会虚高;如果悄悄更换归因模型,同一个渠道的 CAC 就会不同。要把定义固定在代码里,并能重新计算,团队才会用同样的数字交谈。
材料与定义
/opt/fixtures/founder/product/users.csv、events.csv(与指标模块相同)、/opt/fixtures/founder/product/touches.csv(user_id,ts,channel,注册前的触点)、/opt/fixtures/founder/product/spend.csv(month,channel,spend_krw,2026-03 至 05)。- 排除内部账户(is_internal=1)。时间可以直接按 UTC 比较(窗口和顺序是时间之差,与时区无关)。
- 漏斗:注册 → create_doc → share_doc → invite_sent → upgrade。每一步,以在到达上一步的时刻之后(含相同时刻)的第一次发生来到达,所有步骤都在从注册时刻起 14×24 小时之内(含边界)。按人数统计。结果是
[가입, create_doc, share_doc, invite_sent, upgrade]五个整数。 - 付费转化用户:观测期间内有 upgrade 的外部用户。首次触点 = 在 touches.csv 中该用户 ts 最早的那一行,末次触点 = 最晚的那一行。
- CAC(韩元)= 该渠道 spend_krw 三个月的合计 ÷ 归因到该渠道的付费转化用户数,向下取整为整数(
//)。归因为 0 人时为null。 - 五个渠道:organic_search、paid_search、social_ads、referral、content。
步骤
- 在
/root/founder/funnel/counts.json中,以外部用户为准、整个观测期,写入漏斗四种事件(create_doc、share_doc、invite_sent、upgrade)各自的{"events": 줄 수, "users": 사람 수}。 - 在
/root/founder/funnel/funnel.py中创建funnel(fix)——按定义的五个整数列表。 - 在
/root/founder/funnel/order.json中,写入无视顺序时(14 天窗口内,只要前面各步骤的事件不论顺序都全部存在就算到达)的五个格子unordered,以及按定义的ordered。 - 在
/root/founder/funnel/window.json中,写入取消窗口时(直到观测结束,遵守顺序)的五个格子no_window,以及window_14d。 - 在
/root/founder/funnel/conversion.json中写入step_rates(格子 i ÷ 格子 i−1,四个,小数点后第四位)、overall(upgrade ÷ 注册)、biggest_drop(转化率最低的那一步的到达步骤的名称,例如 "share_doc")。 - 当
funnel.py的funnel(fix, channel=None)接收到 channel 时,只用末次触点是该渠道的人来给出漏斗。在/root/founder/funnel/channel.json中写入五个渠道的漏斗。 - 在
/root/founder/funnel/attribution.json中,分别写入first_touch、last_touch的各渠道付费转化用户数。 - 在
/root/founder/funnel/cac.json中,分别写入first_touch、last_touch的各渠道 CAC,以及cheapest_paid_last(费用大于 0 的渠道中,末次触点 CAC 最低的渠道)、rank_changed(有费用的渠道的 CAC 排名在两个模型中不同则为 true)。
参考
- 先把每个人的(时刻,事件)列表排序,再把步骤指针一格一格地往前移,就成了有顺序的漏斗。
- 首次、末次触点,用字符串时间比较(
"2026-03-01T…Z")顺序也是对的——因为格式是固定长度。 - 常见错误:把事件行数写成人数,无视顺序,没有窗口,把首次触点和末次触点混在一起,把 CAC 四舍五入后写入。
事件数与人数不同
在 /root/founder/funnel/counts.json 中,以外部用户为准,写入 create_doc、share_doc、invite_sent、upgrade 的 events(行数)、users(人数)。
在同一个事件列表中,并排求出 len(列表) 和 len(set(用户)) 即可。内部账户通过 users.csv 过滤。
遵守顺序和 14 天窗口的按人数统计的漏斗
在 /root/founder/funnel/funnel.py 中创建 funnel(fix)。[注册, create_doc, share_doc, invite_sent, upgrade] 五个整数。
为每个人把(时刻,事件)排序,如果与接下来等待的步骤相同的事件,出现在到达上一步的时刻之后,就前进一格。注册后超过 14×24 小时的事件要丢弃。
无视顺序,后面的格子就会虚高
在 /root/founder/funnel/order.json 中,写入 ordered(按定义)和 unordered(14 天窗口内,前面各步骤的事件不论顺序都全部存在就算到达)两种漏斗。
窗口保持不变,只改变到达的判定。unordered 只看窗口内的事件种类集合中是否包含前面所有的步骤。
取消窗口,越老的用户转化就越显得高
在 /root/founder/funnel/window.json 中,写入 window_14d(按定义)和 no_window(直到观测结束,遵守顺序)两种漏斗。
顺序判定保持不变,只取消 14 天窗口。
各步骤的转化率和漏掉最多的格子
在 /root/founder/funnel/conversion.json 中写入 step_rates(四个)、overall、biggest_drop(转化率最低的那一步的到达步骤的名称)。
step_rates[i] 是漏斗格子 i+1 ÷ 格子 i。biggest_drop 是最低的比率所“到达”的步骤的名称(create_doc 到 upgrade 中的一个)。
按注册渠道的漏斗
当 funnel(fix, channel=None) 接收到 channel 时,只用末次触点是该渠道的人来给出漏斗。在 /root/founder/funnel/channel.json 中写入五个渠道的漏斗。
在 touches.csv 中,用每个人 ts 最晚的触点的 channel,把注册用户名单缩小,其余计算保持不变。
用首次触点和末次触点来归因转化
在 /root/founder/funnel/attribution.json 中,分别写入 first_touch、last_touch 的各渠道付费转化用户数。
付费转化用户,是观测期间内有 upgrade 的外部用户(不是 14 天窗口)。首次触点是 ts 最早的那个的 channel,末次触点是 ts 最晚的那个的 channel。
归因模型会改变渠道 CAC
在 /root/founder/funnel/cac.json 中,写入 first_touch、last_touch 的各渠道 CAC(向下取整为整数,归因为 0 则为 null),以及 cheapest_paid_last、rank_changed。
CAC = 三个月费用合计 // 归因转化用户数。费用为 0 的渠道不参与排名比较。rank_changed 是把有费用的渠道按 CAC 升序排列的顺序,在两个模型中是否不同。