Mímisbrunnr知恵の泉

← マーケティングサイエンス 一覧

🎓 レベル:基礎 | 重要度:A(必須)

📎 関連:マーケティングデータとKPIの体系 | 効果検証:A/Bテスト(第7章予定)

要点(BLUF)

1. ファネルと段階CVR・離脱率

EC の購買は、訪問してから完了するまでにいくつもの段階を通ります。先に進むほど人数が減るので、漏斗(ファネル)の形になります。

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のになります。

全体CVR=N完了N訪問=i=1kpi\text{全体CVR} = \frac{N_{\text{完了}}}{N_{\text{訪問}}} = \prod_{i=1}^{k} p_i

最終的な購入人数は、入口人数に全段階の通過率を掛けたものです。

N完了=N訪問×i=1kpiN_{\text{完了}} = N_{\text{訪問}} \times \prod_{i=1}^{k} p_i

積であることが重要です。1段階が 0.300.30 なら、他がどれだけ高くても全体は 0.300.30 倍より小さくなります。逆に言えば、弱い段階を底上げすると掛け算で全体に効く。これがファネル改善の基本発想です。

3. ボトルネックの特定と感度(数式)

どの段階を直すべきか。2つの見方があります。

(a) 率(弾力性)で見る と、全段階が等価になります。全体CVR の片対数を lnpi\ln p_i で微分すると、

ln(全体CVR)lnpi=1(すべての i)\frac{\partial \ln(\text{全体CVR})}{\partial \ln p_i} = 1 \qquad (\text{すべての } i)

つまり「ある段階のCVR を 1% 上げると全体CVR も 1% 上がる」——率では優劣がつきません。

(b) 同じ絶対ポイント改善で見る と、差が出ます。段階 ii のCVR を pipi+Δpp_i \to p_i + \Delta p(例:+5ポイント)に上げたときの最終CV 増分は、

ΔN完了=N訪問(jipj)Δp=N完了Δppi\Delta N_{\text{完了}} = N_{\text{訪問}}\Big(\prod_{j \ne i} p_j\Big)\,\Delta p = N_{\text{完了}} \cdot \frac{\Delta p}{p_i}

増分は pip_i反比例します。だから段階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%)でバーが急に細くなるのが見て取れ、ここが最大の離脱=ボトルネックだと視覚的に確認できます。前節の感度分析の結論と一致します。

⚠️ よくある誤解

関連ノート