順序と窓を守ったファネルとチャネル帰属
目標
「モアノート」の記録で、順序と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]の5つの整数です(韓国語で「登録」を意味する語です)。 - 有料転換者: 観測期間内にupgradeがある外部ユーザー。ファーストタッチ = touches.csvで、その人のtsが最も早い行、ラストタッチ = 最も遅い行。
- CAC(ウォン) = そのチャネルのspend_krwの3か月の合計 ÷ そのチャネルに帰属した有料転換者数、整数に切り捨て(
//)。帰属が0人ならnull。 - チャネル5つ: organic_search・paid_search・social_ads・referral・content。
ステップ
/root/founder/funnel/counts.jsonに、ファネルのイベント4種類(create_doc・share_doc・invite_sent・upgrade)それぞれの{"events": 줄 수, "users": 사람 수}(プレースホルダーは行数と人数です)を、外部ユーザー基準、観測全体で書く。/root/founder/funnel/funnel.pyに、funnel(fix)を作る: 定義どおりの、5つの整数のリスト。/root/founder/funnel/order.jsonに、順序を無視したとき(14日の窓の中に、前の段階のイベントが、順序と無関係にすべてあれば到達)の5つの欄unorderedと、定義どおりのorderedを書く。/root/founder/funnel/window.jsonに、窓をなくしたとき(観測の最後まで、順序は守る)の5つの欄no_windowと、window_14dを書く。/root/founder/funnel/conversion.jsonに、step_rates(欄i ÷ 欄i−1、4個、小数第4位)、overall(upgrade ÷ 登録)、biggest_drop(コンバージョン率が最も低い段階の到着段階の名前、例: "share_doc")を書く。funnel.pyのfunnel(fix, channel=None)は、channelを受け取ったら、ラストタッチがそのチャネルの人だけでファネルを出す。/root/founder/funnel/channel.jsonに、5つのチャネルのファネルを書く。/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の順位が、2つのモデルで違えばtrue)を書く。
参考
- 人ごとに(時刻, イベント)のリストを並べ替えておき、段階のポインターを1つずつ進めれば、順序ファネルになります。
- ファースト/ラストタッチは、文字列の時刻の比較(
"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]の5つの整数。
人ごとに(時刻, イベント)を並べ替え、次に待っている段階と同じイベントが、前の段階の到達時刻以降に出てきたら、1欄前進します。登録後14×24時間を過ぎたイベントは捨てます。
順序を無視すると、後ろの欄が膨らむ
/root/founder/funnel/order.jsonに、ordered(定義どおり)とunordered(14日の窓の中に、前の段階のイベントが順序と無関係にすべてあれば到達)の、2つのファネルを書く。
窓はそのままにして、到達の判定だけを変えます。unorderedは、窓の中のイベント種類の集合に、前の段階がすべてあるかどうかだけを見ます。
窓をなくすと、古い人ほど転換が高く見える
/root/founder/funnel/window.jsonに、window_14d(定義どおり)とno_window(観測の最後まで、順序は守る)の、2つのファネルを書く。
順序の判定はそのままにして、14日の窓だけをなくします。
段階別のコンバージョン率と、最も大きく漏れる欄
/root/founder/funnel/conversion.jsonに、step_rates(4個)・overall・biggest_drop(コンバージョン率が最も低い段階の、到着段階の名前)を書く。
step_rates[i]は、ファネルの欄i+1 ÷ 欄iです。biggest_dropは、最も低い比率が「到着する」段階の名前(create_doc–upgradeのうちの1つ)です。
登録経路別のファネル
funnel(fix, channel=None)は、channelを受け取ったら、ラストタッチがそのチャネルの人だけでファネルを出す。/root/founder/funnel/channel.jsonに、5つのチャネルのファネルを書く。
touches.csvで、人ごとにtsが最も遅い接点のchannelで、登録者の名簿だけを絞り、残りの計算はそのままです。
ファーストタッチとラストタッチで、転換を帰属する
/root/founder/funnel/attribution.jsonに、first_touch・last_touchそれぞれの、チャネル別の有料転換者数を書く。
有料転換者は、観測期間内にupgradeがある外部ユーザーです(14日の窓ではありません)。ファーストタッチは最も早いts、ラストタッチは最も遅いtsのchannelです。
アトリビューションモデルが、チャネルCACを変える
/root/founder/funnel/cac.jsonに、first_touch・last_touchのチャネル別のCAC(整数に切り捨て、帰属が0ならnull)と、cheapest_paid_last・rank_changedを書く。
CAC = 3か月の費用の合計 // 帰属した転換者数。費用が0のチャネルは、順位の比較から除きます。rank_changedは、費用があるチャネルをCACの昇順に並べた順序が、2つのモデルで違うかどうかです。