RFMの限界と確率論的顧客分析
RFM分析は強力だが、根本的な限界がある。
企業によって定義は様々あるが「最終購買から180日経った顧客」を「休眠顧客」と呼ぶ。しかしこの顧客は本当に「まだいつか戻ってくる可能性がある顧客」なのか、それとも「もう二度と買わない顧客(チャーン済み)」なのか、RFMだけでは判断できない。
その問いに確率論的な答えを与えるのが BG/NBD モデル(Beta-Geometric / Negative Binomial Distribution model)だ。
2004年にFader・Hardie・Leeが提唱した手法で、非契約型ビジネス(= 退会手続きなしに黙って去るECや通販)における顧客の「生死」と「購買頻度」を同時にモデル化する。
このモデルが与えてくれる答えは2つだ。
- この顧客がまだアクティブである確率(P(alive))
- 次のN期間でこの顧客が購買する期待回数(期待購買数)
Python の lifetimes ライブラリを使えば自動計算できるが、今回はその推定ロジックをSQLで近似実装する。なぜか。データが Treasure Data にある場合、わざわざ Python にエクスポートしなくてもSQLだけで P(alive) を計算し、そのままセグメント配信に使えるからだ。
BG/NBDモデルの直感的な理解
数式の前にモデルの考え方を整理する。
BG/NBD は2つのプロセスを仮定する。
プロセス1:購買のプロセス(Negative Binomial Distribution)
アクティブな顧客は「ランダムなタイミング」で購買する。顧客ごとに購買頻度(λ)が異なり、その分布がガンマ分布に従う、と仮定する。
プロセス2:離脱のプロセス(Beta-Geometric)
顧客は「購買のたびに一定確率 p で離脱する」と仮定する。
この離脱確率 p も顧客ごとに異なり、ベータ分布に従う、と仮定する。
つまり「たくさん買う顧客」と「あまり買わない顧客」が混在し、「すぐ離脱する顧客」と「長く続く顧客」が混在する現実を、2つの分布の組み合わせで表現する。
観察データから何が推定できるか
各顧客について観察できるのは次の3つだ。
x:観察期間中の購買回数t_x:最終購買日から観察開始日までの経過時間(観察期間内での最終購買タイミング)T:観察期間の長さ(最初の購買日から観察終了日まで)
この3つだけから、「この顧客がまだアクティブである確率」を推定できる。
使用するテーブル
-- orders テーブル
-- order_id : 注文ID
-- customer_id : 顧客ID
-- order_date : 注文日(DATE型)
-- total_amount : 注文金額
-- status : 'completed' / 'cancelled'STEP 1 ― BG/NBD の入力パラメータを顧客ごとに計算する
まず各顧客の x(購買回数)、t_x(最終購買タイミング)、T(観察期間)を計算する。
観察期間は「初回購買日から基準日まで」とする。
-- STEP1: BG/NBD 入力パラメータの計算
WITH customer_rfm AS (
SELECT
customer_id,
MIN(order_date) AS first_order_date, -- 初回購買日
MAX(order_date) AS last_order_date, -- 最終購買日
COUNT(order_id) AS x, -- 総購買回数
-- T: 観察期間(初回購買日から基準日まで、週単位)
DATE_DIFF('day', MIN(order_date), DATE '2024-12-31') / 7.0 AS T,
-- t_x: 最終購買タイミング(初回購買日から最終購買日まで、週単位)
DATE_DIFF('day', MIN(order_date), MAX(order_date)) / 7.0 AS t_x
FROM orders
WHERE
status = 'completed'
AND order_date >= DATE '2022-01-01' -- 観察開始日
AND order_date <= DATE '2024-12-31' -- 観察終了日(基準日)
GROUP BY customer_id
HAVING COUNT(order_id) >= 1 -- 少なくとも1回購買
)
SELECT
customer_id,
first_order_date,
last_order_date,
x,
ROUND(T, 2) AS T_weeks,
ROUND(t_x, 2) AS t_x_weeks,
-- 初回購買が最終購買の場合、t_x = 0(1回のみ購買)
CASE WHEN x = 1 THEN 0 ELSE ROUND(t_x, 2) END AS t_x_safe
FROM customer_rfm
ORDER BY x DESC, T DESC;出力イメージ
| customer_id | first_order_date | last_order_date | x | T_weeks | t_x_weeks |
|---|---|---|---|---|---|
| C004 | 2022-03-12 | 2024-11-15 | 18 | 143.4 | 136.1 |
| C001 | 2022-08-05 | 2024-10-20 | 8 | 125.3 | 116.0 |
| C023 | 2023-01-20 | 2024-09-08 | 4 | 97.7 | 81.3 |
| C089 | 2024-10-01 | 2024-10-01 | 1 | 13.0 | 0.0 |
STEP 2 ― モデルパラメータを決定する
BG/NBD モデルには4つのパラメータがある。
α, β : 購買頻度の分布(ガンマ分布)のパラメータ
r, s : 離脱確率の分布(ベータ分布)のパラメータ本来はこれらを最尤推定で求める(Python の lifetimes ライブラリが自動計算する)。SQLでの近似では、事前に Python で推定したパラメータ値を定数として使うことが現実的だ。
典型的な通販データでの参考値(実際のデータで必ずキャリブレーションする)
-- モデルパラメータ(事前にPythonで推定した値を定数として定義)
-- 実データで推定した値に必ず置き換えること
WITH model_params AS (
SELECT
0.243 AS r, -- 購買頻度分布の形状パラメータ
4.414 AS alpha, -- 購買頻度分布のスケールパラメータ
0.793 AS a, -- 離脱確率分布のパラメータ
2.425 AS b -- 離脱確率分布のパラメータ
)Pythonでのパラメータ推定(SQLの外で実行する)
from lifetimes import BetaGeoFitter bgf = BetaGeoFitter() bgf.fit(df['frequency'], df['recency'], df['T']) print(bgf.params_) # {'r': 0.243, 'alpha': 4.414, 'a': 0.793, 'b': 2.425}このパラメータを上記CTEに貼り付けてSQLで使う。
STEP 3 ― P(alive) を計算する(BG/NBDの核心)
BG/NBD モデルで「顧客がまだアクティブである確率」は次の式で求まる。
P(alive | x, t_x, T) = 1 / (1 + (a / (b + x - 1)) × A)
ここで:
A = (α + T)^(r + x) / (α + t_x)^(r + x)
× Σ_{j=0}^{x-1} [ C(x-1, j) × (-1)^j × (b + x - 1) / (b + x - 1 - j) ]
× [(α + T)/(α + t_x)]^(-j) (超幾何関数の近似)これを厳密に SQL で実装するのは複雑すぎるため、実務でよく使われる簡略近似式を使う。
顧客がアクティブな確率の直感的な近似として、最も単純かつ実用的なのは「最終購買日から現在までの沈黙期間が、その顧客の通常の購買間隔と比べてどれだけ異常か」に基づくアプローチだ。
-- STEP3: P(alive) の実用的近似計算
WITH model_params AS (
SELECT 0.243 AS r, 4.414 AS alpha, 0.793 AS a, 2.425 AS b
),
customer_rfm AS (
SELECT
customer_id,
COUNT(order_id) AS x,
DATE_DIFF('day', MIN(order_date), MAX(order_date)) / 7.0 AS t_x,
DATE_DIFF('day', MIN(order_date), DATE '2024-12-31') / 7.0 AS T,
DATE_DIFF('day', MAX(order_date), DATE '2024-12-31') / 7.0 AS recency_weeks -- 最終購買からの経過週数
FROM orders
WHERE status = 'completed'
AND order_date BETWEEN DATE '2022-01-01' AND DATE '2024-12-31'
GROUP BY customer_id
),
palive_calc AS (
SELECT
c.customer_id,
c.x,
ROUND(c.t_x, 2) AS t_x,
ROUND(c.T, 2) AS T,
ROUND(c.recency_weeks, 2) AS recency_weeks,
-- BG/NBD P(alive) 近似式
-- 参考:Fader et al. 2005 の式(7)を数値近似
-- δ = a / (b + x - 1) ← 離脱確率のオッズ
-- λ = (r + x) / (α + T) ← 期待購買率
ROUND(
1.0 / (
1.0 + (
(p.a / NULLIF(p.b + c.x - 1, 0))
* POWER((p.alpha + c.T) / NULLIF(p.alpha + c.t_x, 0.001), p.r + c.x)
)
)
, 4) AS p_alive,
-- 期待購買頻度(週あたり)
ROUND((p.r + c.x) / (p.alpha + c.T), 4) AS lambda_hat
FROM customer_rfm c
CROSS JOIN model_params p
WHERE c.x >= 1 -- 1回以上購買
)
SELECT
customer_id,
x AS purchase_count,
t_x AS recency_at_last_weeks,
T AS observation_weeks,
recency_weeks AS weeks_since_last_order,
p_alive,
lambda_hat AS weekly_purchase_rate,
-- P(alive) のセグメント分類
CASE
WHEN p_alive >= 0.80 THEN 'アクティブ(確実)'
WHEN p_alive >= 0.60 THEN 'アクティブ(高確率)'
WHEN p_alive >= 0.40 THEN 'グレーゾーン'
WHEN p_alive >= 0.20 THEN '離脱リスク高'
ELSE 'チャーン済みの可能性大'
END AS alive_segment,
-- 次の12週間での期待購買数
ROUND(p_alive * lambda_hat * 12, 2) AS expected_orders_next_12w
FROM palive_calc
ORDER BY p_alive DESC, lambda_hat DESC;出力イメージ
| customer_id | purchase_count | weeks_since_last | p_alive | alive_segment | expected_orders_next_12w |
|---|---|---|---|---|---|
| C004 | 18 | 6.7 | 0.9821 | アクティブ(確実) | 1.82 |
| C001 | 8 | 10.4 | 0.9134 | アクティブ(確実) | 0.87 |
| C023 | 4 | 24.1 | 0.6412 | アクティブ(高確率) | 0.31 |
| C089 | 1 | 13.0 | 0.4821 | グレーゾーン | 0.12 |
| C112 | 3 | 78.2 | 0.0841 | チャーン済みの可能性大 | 0.01 |
C004 は18回購買・最終購買から6.7週間と直近で、P(alive)=0.98。ほぼ確実にまだアクティブだ。C112 は3回購買したが78週間(約1.5年)沈黙しており、P(alive)=0.08。ほぼチャーンと見てよい。
STEP 4 ― RFM×P(alive) でセグメントを精緻化する(完成版)
RFMスコアと P(alive) を組み合わせることで、従来のRFMだけでは区別できなかった顧客を分離できる。
-- STEP4: RFM + P(alive) による精緻なセグメント分類
WITH model_params AS (
SELECT 0.243 AS r, 4.414 AS alpha, 0.793 AS a, 2.425 AS b
),
customer_rfm AS (
SELECT
customer_id,
COUNT(order_id) AS x,
DATE_DIFF('day', MIN(order_date), MAX(order_date)) / 7.0 AS t_x,
DATE_DIFF('day', MIN(order_date), DATE '2024-12-31') / 7.0 AS T,
DATE_DIFF('day', MAX(order_date), DATE '2024-12-31') / 7.0 AS recency_weeks,
SUM(total_amount) AS monetary
FROM orders
WHERE status = 'completed'
AND order_date BETWEEN DATE '2022-01-01' AND DATE '2024-12-31'
GROUP BY customer_id
),
with_palive AS (
SELECT
c.*,
ROUND(
1.0 / (
1.0 + (
(p.a / NULLIF(p.b + c.x - 1, 0))
* POWER((p.alpha + c.T) / NULLIF(p.alpha + c.t_x, 0.001), p.r + c.x)
)
)
, 4) AS p_alive,
ROUND((p.r + c.x) / (p.alpha + c.T), 4) AS lambda_hat
FROM customer_rfm c
CROSS JOIN model_params p
)
SELECT
customer_id,
x,
ROUND(recency_weeks, 1) AS weeks_since_last,
monetary,
ROUND(p_alive, 3) AS p_alive,
ROUND(p_alive * lambda_hat * 52, 1) AS predicted_orders_next_year,
ROUND(p_alive * lambda_hat * 52 * (monetary / NULLIF(x, 0)), 0)
AS predicted_revenue_next_year,
-- 4象限マトリクス(P(alive) × 購買金額)
CASE
WHEN p_alive >= 0.7 AND monetary >= 50000 THEN 'VIP顧客(高価値・高生存)'
WHEN p_alive >= 0.7 AND monetary < 50000 THEN '育成顧客(低価値・高生存)'
WHEN p_alive < 0.7 AND monetary >= 50000 THEN '緊急ウィンバック対象(高価値・低生存)'
ELSE '自然消滅待ち(低価値・低生存)'
END AS strategic_segment
FROM with_palive
ORDER BY predicted_revenue_next_year DESC NULLS LAST;出力イメージ
| customer_id | x | weeks_since_last | monetary | p_alive | predicted_revenue_next_year | strategic_segment |
|---|---|---|---|---|---|---|
| C004 | 18 | 6.7 | 284,600 | 0.982 | 308,000 | VIP顧客(高価値・高生存) |
| C001 | 8 | 10.4 | 98,400 | 0.913 | 112,000 | VIP顧客(高価値・高生存) |
| C088 | 5 | 42.1 | 182,000 | 0.412 | 75,000 | 緊急ウィンバック対象(高価値・低生存) |
| C019 | 12 | 8.2 | 28,400 | 0.951 | 32,000 | 育成顧客(低価値・高生存) |
| C112 | 3 | 78.2 | 98,000 | 0.084 | 8,000 | 自然消滅待ち(低価値・低生存) |
「緊急ウィンバック対象」の C088 は累計18万円購買しているが、P(alive)=0.41 と離脱リスクが高い。RFMだけ見ると「高M・低R」で通常のウィンバック対象だが、P(alive) まで計算することで「今すぐ手を打たないとほぼ戻らない」という判断が数値に基づいてできる。
STEP 5 ― P(alive) を使ったコホート別の「生存率推移」
全顧客の P(alive) を月次バッチで計算し、コホート別の平均 P(alive) の推移を追うと、「どのコホートが速く死んでいくか」が定量化できる。
-- STEP5: 初回購買月(コホート)別の平均P(alive)推移
WITH model_params AS (SELECT 0.243 AS r, 4.414 AS alpha, 0.793 AS a, 2.425 AS b),
customer_rfm AS (
SELECT
customer_id,
DATE_TRUNC('month', MIN(order_date)) AS cohort_month,
COUNT(order_id) AS x,
DATE_DIFF('day', MIN(order_date), MAX(order_date)) / 7.0 AS t_x,
DATE_DIFF('day', MIN(order_date), DATE '2024-12-31') / 7.0 AS T
FROM orders
WHERE status = 'completed'
AND order_date BETWEEN DATE '2022-01-01' AND DATE '2024-12-31'
GROUP BY customer_id
),
with_palive AS (
SELECT
c.cohort_month,
c.customer_id,
ROUND(
1.0 / (
1.0 + (
(p.a / NULLIF(p.b + c.x - 1, 0))
* POWER((p.alpha + c.T) / NULLIF(p.alpha + c.t_x, 0.001), p.r + c.x)
)
)
, 4) AS p_alive
FROM customer_rfm c CROSS JOIN model_params p
)
SELECT
cohort_month,
COUNT(customer_id) AS cohort_size,
ROUND(AVG(p_alive), 3) AS avg_p_alive,
ROUND(APPROX_PERCENTILE(p_alive, 0.5), 3) AS median_p_alive,
SUM(CASE WHEN p_alive >= 0.7 THEN 1 ELSE 0 END) AS high_survival_count,
ROUND(
SUM(CASE WHEN p_alive >= 0.7 THEN 1 ELSE 0 END) * 100.0
/ NULLIF(COUNT(customer_id), 0)
, 1) AS high_survival_pct
FROM with_palive
GROUP BY cohort_month
ORDER BY cohort_month;出力イメージ
| cohort_month | cohort_size | avg_p_alive | high_survival_pct |
|---|---|---|---|
| 2022-01-01 | 842 | 0.512 | 41.2% |
| 2022-06-01 | 918 | 0.498 | 39.8% |
| 2023-01-01 | 1,241 | 0.621 | 52.4% |
| 2023-06-01 | 1,184 | 0.638 | 54.1% |
| 2024-01-01 | 1,892 | 0.784 | 68.3% |
2022年初頭のコホートは avg P(alive) が 0.51 と低下しており、2年以上経過して離脱が進んでいる。2023年以降のコホートは P(alive) が高く、近年獲得した顧客の生存率が改善していることが分かる。
実務での運用ヒント
① パラメータは必ず自社データで推定する
参考値として示したパラメータ(r=0.243 など)は Fader らの論文でのサンプル値だ。業種・商材・購買サイクルによって大きく変わる。Python の lifetimes ライブラリを使い、自社の過去データ(最低でも12〜24ヶ月)でパラメータを推定してから SQL に組み込む。
# Pythonでのパラメータ推定
from lifetimes import BetaGeoFitter
bgf = BetaGeoFitter(penalizer_coef=0.0)
bgf.fit(rfm_df['frequency'], rfm_df['recency_weeks'], rfm_df['T_weeks'])
print(bgf.params_)② 購買1回のみの顧客の扱い
x = 1(1回購買のみ)の場合、t_x = 0 となり分母にゼロが生じるリスクがある。NULLIF(p.alpha + c.t_x, 0.001) で小さな値でのゼロ除算を防ぐか、初回のみ購買者は別途「新規顧客リスト」として扱う設計が安全だ。
③ 毎月の再計算サイクルを組む
P(alive) は新しい購買があるたびに更新される。少なくとも月次バッチで再計算し、Treasure Data の monthly_palive テーブルとして保存する。このテーブルをセグメント配信の基礎データとして使うことで、常に最新の「生存確率ベースのリスト」が維持できる。
④ 厳密な実装ではなく「BG/NBDの精神」を使う
学術的に厳密な BG/NBD の P(alive) 計算式は超幾何関数を含む複雑なものだ。今回の実装は近似だが、「沈黙期間が長いほど P(alive) が下がる」という本質的な動作は正確に再現している。精度を求めるなら Python の lifetimes ライブラリで計算し、結果をTDにロードする方法が最も正確だ。
まとめ
BG/NBD モデルの SQL 近似実装をまとめる。
- 顧客ごとに
x(購買回数)・t_x(最終購買タイミング)・T(観察期間)を週単位で計算する - Python で推定した4パラメータ(r, α, a, b)を定数として CTE に定義する
- P(alive) の近似式を
POWERとNULLIFで実装する - RFM の M(購買金額)と P(alive) を組み合わせて「高価値・高生存」「高価値・低生存」の4象限に分類する
- 「次の1年の期待購買数」=
P(alive) × λ̂ × 52で将来価値を推計する
RFM が「現在の状態のスナップショット」なら、BG/NBD は「将来の行動の確率的予測」だ。この2つを組み合わせることで、「今すぐ手を打つべき高価値・低生存顧客」を精確に特定し、施策のROIを最大化できる。
契約内容によるが、treasure dataでもdigdagでpythonを起動させることもできるのでまずはSQLが面倒くさいならばPythonでやるのもいいかもしれない。