🎓 レベル:基礎 | 重要度:A(必須)
📎 関連:マーケティングデータとKPIの体系 | 効果検証:A/Bテスト(第7章予定)
要点(BLUF)
- ファネルは訪問から購入までの段階の連なり。各段階の通過率が段階CVR、1 から段階CVR を引いた値が離脱率です。
- 全体CVR = 段階CVRの積()。掛け算なので、ある1段階の小さな改善も全体に乗って効きます。
- ボトルネックは段階CVRが最も低い段階。同じ「+5ポイント」改善でも、最終CV の増分は「最終CV × (改善ポイント ÷ 段階CVR)」となり、段階CVRが小さい段階ほど大きくなります。
1. ファネルと段階CVR・離脱率
EC の購買は、訪問してから完了するまでにいくつもの段階を通ります。先に進むほど人数が減るので、漏斗(ファネル)の形になります。
- 段階CVR:その段階を通過して次へ進んだ割合。(次段階の到達人数 ÷ 当該段階の到達人数)。
- 離脱率:その段階で進まず離れた割合。。
flowchart TD S0["訪問 100,000人"] -->|"CVR 0.60"| S1["商品閲覧 60,000人"] S1 -->|"CVR 0.30"| S2["カート投入 18,000人"] S2 -->|"CVR 0.70"| S3["購入手続き 12,600人"] S3 -->|"CVR 0.80"| S4["購入完了 10,080人"]
段階CVRは「率」、離脱人数は「絶対数」です。後段ほど母数(到達人数)が小さくなるため、離脱率が高い段階と、離脱人数が多い段階は一致しないことがあります。両方を見るのが基本です。
2. 全体CVR は段階CVRの積
入口(訪問)から出口(購入完了)まで通り抜ける割合が全体CVRです。各段階を独立に通過していくので、全体CVR は段階CVRの積になります。
最終的な購入人数は、入口人数に全段階の通過率を掛けたものです。
積であることが重要です。1段階が なら、他がどれだけ高くても全体は 倍より小さくなります。逆に言えば、弱い段階を底上げすると掛け算で全体に効く。これがファネル改善の基本発想です。
3. ボトルネックの特定と感度(数式)
どの段階を直すべきか。2つの見方があります。
(a) 率(弾力性)で見る と、全段階が等価になります。全体CVR の片対数を で微分すると、
つまり「ある段階のCVR を 1% 上げると全体CVR も 1% 上がる」——率では優劣がつきません。
(b) 同じ絶対ポイント改善で見る と、差が出ます。段階 のCVR を (例:+5ポイント)に上げたときの最終CV 増分は、
増分は に反比例します。だから段階CVRが最も低い段階を直すと、最終CV が最も増える。これが「ボトルネック=最低段階を最優先で改善」という定石の数理的な裏付けです。実務では、これに離脱人数の絶対値(その段階で何人失っているか)を併せて、優先順位を決めます。
4. ファネルを計算する(コード)
合成ファネルから段階CVR・離脱率・全体CVR を求め、「同じ +5ポイント改善で最終購入が何人増えるか」でボトルネックを特定します。
import numpy as np
import pandas as pd
# 合成ファネル:各段階の到達人数(上から下へ減っていく)
stages = ["訪問", "商品閲覧", "カート投入", "購入手続き", "購入完了"]
users = np.array([100_000, 60_000, 18_000, 12_600, 10_080])
# 段階CVR = 次段階人数 / 当該段階人数、離脱率 = 1 - 段階CVR、離脱人数 = 失った絶対人数
step_cvr = users[1:] / users[:-1]
drop_rate = 1 - step_cvr
dropped = users[:-1] - users[1:]
funnel = pd.DataFrame({
"段階遷移": [f"{a}→{b}" for a, b in zip(stages[:-1], stages[1:])],
"到達人数": users[1:],
"段階CVR": step_cvr,
"離脱率": drop_rate,
"離脱人数": dropped,
})
overall_cvr = users[-1] / users[0] # 全体CVR = 購入完了 / 訪問
overall_cvr_prod = np.prod(step_cvr) # = 段階CVRの積(一致するはず)
print(funnel.to_string(index=False, formatters={
"到達人数": "{:,.0f}".format, "段階CVR": "{:.3f}".format,
"離脱率": "{:.3f}".format, "離脱人数": "{:,.0f}".format}))
print(f"\n全体CVR = 購入完了/訪問 = {overall_cvr:.4f}")
print(f"段階CVRの積 = {overall_cvr_prod:.4f} (全体CVRと一致)")
# 感度分析:各段階の段階CVRを「+5ポイント」改善したら最終CVは何人増えるか
final = users[-1]
delta = 0.05
gain = final * delta / step_cvr # 増分 = 最終CV × Δp / p_i
sens = pd.DataFrame({
"段階遷移": funnel["段階遷移"],
"段階CVR": step_cvr,
"+5ptで増える購入数": gain,
}).sort_values("+5ptで増える購入数", ascending=False)
print("\n=== 感度ランキング(同じ+5ポイント改善で増える最終購入数)===")
print(sens.to_string(index=False, formatters={
"段階CVR": "{:.3f}".format, "+5ptで増える購入数": "{:,.0f}".format}))
worst = funnel.loc[funnel["段階CVR"].idxmin()]
print(f"\nボトルネック(段階CVR最小):{worst['段階遷移']} "
f"(段階CVR {worst['段階CVR']:.3f}、離脱 {worst['離脱人数']:,.0f}人)")
出力:
段階遷移 到達人数 段階CVR 離脱率 離脱人数
訪問→商品閲覧 60,000 0.600 0.400 40,000
商品閲覧→カート投入 18,000 0.300 0.700 42,000
カート投入→購入手続き 12,600 0.700 0.300 5,400
購入手続き→購入完了 10,080 0.800 0.200 2,520
全体CVR = 購入完了/訪問 = 0.1008
段階CVRの積 = 0.1008 (全体CVRと一致)
=== 感度ランキング(同じ+5ポイント改善で増える最終購入数)===
段階遷移 段階CVR +5ptで増える購入数
商品閲覧→カート投入 0.300 1,680
訪問→商品閲覧 0.600 840
カート投入→購入手続き 0.700 720
購入手続き→購入完了 0.800 630
ボトルネック(段階CVR最小):商品閲覧→カート投入 (段階CVR 0.300、離脱 42,000人)
出力の意味:段階CVR は [0.60, 0.30, 0.70, 0.80] で、その積が全体CVR 0.1008(=「全体CVR は段階CVRの積」が数値でも一致)。感度ランキングでは、商品閲覧→カート投入(段階CVR 0.300)を +5ポイント上げると最終購入が +1,680人で最大。これは段階CVRが最も低く、離脱人数も最大(42,000人)の段階です。後段(購入手続き 0.70・購入完了 0.80)を同じだけ改善しても増分は小さい。最も弱い段階を直すのが最優先、と数値で確認できました。
5. ファネル図で見る(コード)
同じデータを japanize_matplotlib で漏斗の形に描きます。中央そろえのバーにすると、どこで急に細くなる(離脱が大きい)かが一目でわかります。
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib # 日本語ラベル対応
stages = ["訪問", "商品閲覧", "カート投入", "購入手続き", "購入完了"]
users = np.array([100_000, 60_000, 18_000, 12_600, 10_080])
y = np.arange(len(stages))[::-1] # 上から 訪問 → … → 購入完了
left = (users[0] - users) / 2 # 中央そろえ → 漏斗の形にする
fig, ax = plt.subplots(figsize=(8, 4.2))
ax.barh(y, users, left=left, color="#4C72B0", edgecolor="white")
label_x = users[0] * 1.03 # ラベルは右側に揃えて表示
for yi, u in zip(y, users):
ax.text(label_x, yi, f"{u:,}人 (全体CVR {u / users[0]:.1%})", va="center")
ax.set_yticks(y, stages)
ax.set_xticks([])
ax.set_xlim(0, users[0] * 1.55)
ax.set_title("購入ファネル(上から下へ人数が減る)")
plt.tight_layout()
plt.show()
出力(図):中央そろえの漏斗が描かれ、各段階の到達人数と「全体CVR(入口比)」が右に並びます。商品閲覧(60.0%)からカート投入(18.0%)でバーが急に細くなるのが見て取れ、ここが最大の離脱=ボトルネックだと視覚的に確認できます。前節の感度分析の結論と一致します。
⚠️ よくある誤解
- 「全体CVR は段階CVRの平均」ではない:正しくは積です。平均で考えると全体CVR を大きく過大評価します。
- 「離脱率が高い段階=離脱人数が最多」ではない:後段は母数が小さいので、率が高くても失う人数は少ないことがあります。率と絶対数の両方を見ます。
- 「段階CVR が低い段階は必ず最優先」ではない:同じポイント改善なら最低段階が最も効くのは事実ですが、実現可能な改善幅は段階で違います。伸びしろとセットで判断します。
- ファネルは単純化したモデル:実際は段階を行き来したり、離脱後に再訪したりします。観測された段階CVR は相関であって因果効果ではないので、改善施策の効果は第7章の A/Bテストで検証します。
関連ノート
- マーケティングデータとKPIの体系(前のトピック・KGI の乗法分解)
- 第1章 マーケティングサイエンスの枠組み 目次
- 第7章 実験と因果推論:段階CVR 改善施策の効果は A/Bテストで検証/第2章 顧客価値の測定でリテンション(継続)を扱う予定
- マーケティング・サイエンス 全体目次