RFMを「なんとなく知っている」から「自分で計算できる」へ
RFM分析という言葉は、マーケティングに携わる人なら一度は耳にしたことがあるはず。
でも「RFMって何ですか?」と聞かれたとき、こう答えられる人がどれだけいるだろう。
「Recency(最終購買日)、Frequency(購買頻度)、Monetary(購買金額)の3軸で顧客をスコアリングし、ランク別に分けてアプローチを変える手法です。SQLのウィンドウ関数と NTILE を使えば、外部ツールなしで計算できます」
この記事を最後まで読めば、上の一文を自分の言葉で説明できるようになる。
そして何より、自分のデータベースに対してそのまま実行できるクエリを手に入れられる。
RFMとは何か ― 3つの軸の意味をきちんと理解する
RFMは3つの英単語の頭文字。
| 指標 | 英語 | 意味 | 高いほど |
|---|---|---|---|
| R | Recency | 最終購買からの経過日数 | 最近買っている(≒ Rの値は小さいほど良い) |
| F | Frequency | 一定期間内の購買回数 | よく買っている |
| M | Monetary | 一定期間内の購買総額 | たくさん使っている |
「最近(R)、何度も(F)、たくさん(M)買っている顧客が優良顧客」というのが基本的な考え方だ。
なぜRFMが重要なのか?
全顧客に同じメルマガを送る時代は終わった。
コスト効率とエンゲージメント率を両立するには、顧客を正しくセグメントして適切なメッセージを届ける必要がある。
RFMを使うと、たとえばこんな分類ができる。
- R高・F高・M高 → ロイヤル顧客。VIP特典・先行案内を届ける
- R低・F高・M高 → 離脱危機の優良顧客。ウィンバック施策を最優先で打つ
- R高・F低・M低 → 新規顧客。2回目購買を促すステップメールを送る
- R低・F低・M低 → 休眠顧客。大規模割引か、コスト考慮してリストから外す
このセグメントを毎月自動で更新できるのが、SQLによるRFM計算の最大の価値だ。
使用するテーブル
今回は2つのテーブルを使う。
-- 注文テーブル(orders)
-- order_id : 注文ID(主キー)
-- customer_id : 顧客ID
-- order_date : 注文日(DATE型)
-- total_amount : 注文金額(税抜)
-- status : 注文ステータス('completed' / 'cancelled' など)| order_id | customer_id | order_date | total_amount | status |
|---|---|---|---|---|
| 1001 | C001 | 2024-09-10 | 5800 | completed |
| 1002 | C002 | 2024-10-15 | 12400 | completed |
| 1003 | C001 | 2024-11-02 | 3200 | completed |
| 1004 | C003 | 2023-12-25 | 8800 | completed |
| 1005 | C002 | 2024-11-20 | 4600 | completed |
| 1006 | C001 | 2024-12-01 | 9200 | completed |
| … | … | … | … | … |
実務では status = 'completed' の行だけを対象にする。キャンセル注文を含めて集計すると、F・Mの数値が実態より高くなってしまうためだ。これは見落としがちなポイント。
STEP 1 ― 顧客ごとにR・F・Mの生の値を計算する
まずは「スコアリング」の前に、各顧客のR・F・Mを実数で出す。
-- STEP1: 顧客別R・F・M実値の計算
-- 分析基準日:2024-12-31 とする(実務では CURRENT_DATE に置き換える)
WITH rfm_raw AS (
SELECT
customer_id,
-- R: 最終購買日から基準日までの経過日数(小さいほど最近)
DATE_DIFF('day',
MAX(order_date),
DATE '2024-12-31'
) AS recency_days,
-- F: 購買回数
COUNT(order_id) AS frequency,
-- M: 購買総額
SUM(total_amount) AS monetary
FROM orders
WHERE
status = 'completed'
AND order_date >= DATE '2024-01-01' -- 集計対象期間(直近1年)
AND order_date <= DATE '2024-12-31'
GROUP BY customer_id
)
SELECT *
FROM rfm_raw
ORDER BY monetary DESC;
DATE_DIFFについて Treasure Data(Presto)ではdate_diff('day', start, end)の形式を使う。MySQLならDATEDIFF(end, start)、BigQueryならDATE_DIFF(end, start, DAY)と書き方が変わる。
出力イメージ
| customer_id | recency_days | frequency | monetary |
|---|---|---|---|
| C001 | 30 | 3 | 18,200 |
| C002 | 41 | 2 | 17,000 |
| C003 | 371 | 1 | 8,800 |
| C004 | 15 | 5 | 38,400 |
| … | … | … | … |
C003は371日前にしか買っていない(Rが悪い)、C004は5回買っている(Fが良い)というのが読み取れる。
STEP 2 ― NTILEでスコアリングする
生の数値をそのまま使っても良いが、RFM分析では一般的に1〜5のスコアに変換してから使う。
スコア変換に使うのが NTILE() 関数だ。
NTILE(5) OVER (ORDER BY ...) は「全顧客を5等分して1〜5の番号を振る」という意味だ。
注意点が1つある。Rだけは「小さい値=良い」なので、ORDER BY recency_days DESC(降順)にしてスコアを振る必要がある。F・Mは大きい値が良いので昇順でよい。
-- STEP2: R・F・MスコアをNTILEで付与
WITH rfm_raw AS (
SELECT
customer_id,
DATE_DIFF('day', MAX(order_date), DATE '2024-12-31') AS recency_days,
COUNT(order_id) AS frequency,
SUM(total_amount) AS monetary
FROM orders
WHERE
status = 'completed'
AND order_date BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
GROUP BY customer_id
),
rfm_scored AS (
SELECT
customer_id,
recency_days,
frequency,
monetary,
-- Rスコア:経過日数が少ない(最近買った)ほど高スコア
NTILE(5) OVER (ORDER BY recency_days DESC) AS r_score,
-- Fスコア:購買回数が多いほど高スコア
NTILE(5) OVER (ORDER BY frequency ASC) AS f_score,
-- Mスコア:購買金額が高いほど高スコア
NTILE(5) OVER (ORDER BY monetary ASC) AS m_score
FROM rfm_raw
)
SELECT *
FROM rfm_scored
ORDER BY r_score DESC, f_score DESC, m_score DESC;出力イメージ
| customer_id | recency_days | frequency | monetary | r_score | f_score | m_score |
|---|---|---|---|---|---|---|
| C004 | 15 | 5 | 38,400 | 5 | 5 | 5 |
| C001 | 30 | 3 | 18,200 | 4 | 4 | 4 |
| C002 | 41 | 2 | 17,000 | 4 | 3 | 3 |
| C003 | 371 | 1 | 8,800 | 1 | 1 | 2 |
| … | … | … | … | … | … | … |
C004はすべて5点満点のロイヤル顧客だ。C003はRもFも最低スコアで、明らかに休眠状態にある。
STEP 3 ― スコアからセグメント名をつける
スコアの組み合わせに意味のある名前をつける。ここは自社のビジネス定義に合わせてカスタマイズしてほしい。今回は代表的な6セグメントを定義する。
-- STEP3: セグメント名の付与(完成版)
WITH rfm_raw AS (
SELECT
customer_id,
DATE_DIFF('day', MAX(order_date), DATE '2024-12-31') AS recency_days,
COUNT(order_id) AS frequency,
SUM(total_amount) AS monetary
FROM orders
WHERE
status = 'completed'
AND order_date BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
GROUP BY customer_id
),
rfm_scored AS (
SELECT
customer_id,
recency_days,
frequency,
monetary,
NTILE(5) OVER (ORDER BY recency_days DESC) AS r_score,
NTILE(5) OVER (ORDER BY frequency ASC) AS f_score,
NTILE(5) OVER (ORDER BY monetary ASC) AS m_score
FROM rfm_raw
),
rfm_segmented AS (
SELECT
customer_id,
recency_days,
frequency,
monetary,
r_score,
f_score,
m_score,
CASE
WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4
THEN 'ロイヤル顧客'
WHEN r_score >= 4 AND f_score >= 3
THEN '優良顧客'
WHEN r_score >= 3 AND f_score <= 2
THEN '新規・見込み顧客'
WHEN r_score <= 2 AND f_score >= 4
THEN '離脱危機優良顧客'
WHEN r_score <= 2 AND f_score >= 2
THEN '休眠顧客'
ELSE
'その他'
END AS segment
FROM rfm_scored
)
SELECT *
FROM rfm_segmented
ORDER BY r_score DESC, f_score DESC, m_score DESC;STEP 4 ― セグメント別のサマリを出す
最後に、経営会議やレポートで使えるセグメント別集計も付け加えよう。
-- STEP4: セグメント別サマリ
WITH rfm_raw AS (
SELECT
customer_id,
DATE_DIFF('day', MAX(order_date), DATE '2024-12-31') AS recency_days,
COUNT(order_id) AS frequency,
SUM(total_amount) AS monetary
FROM orders
WHERE
status = 'completed'
AND order_date BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
GROUP BY customer_id
),
rfm_scored AS (
SELECT
customer_id,
recency_days,
frequency,
monetary,
NTILE(5) OVER (ORDER BY recency_days DESC) AS r_score,
NTILE(5) OVER (ORDER BY frequency ASC) AS f_score,
NTILE(5) OVER (ORDER BY monetary ASC) AS m_score
FROM rfm_raw
),
rfm_segmented AS (
SELECT
customer_id,
monetary,
CASE
WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN 'ロイヤル顧客'
WHEN r_score >= 4 AND f_score >= 3 THEN '優良顧客'
WHEN r_score >= 3 AND f_score <= 2 THEN '新規・見込み顧客'
WHEN r_score <= 2 AND f_score >= 4 THEN '離脱危機優良顧客'
WHEN r_score <= 2 AND f_score >= 2 THEN '休眠顧客'
ELSE 'その他'
END AS segment
FROM rfm_scored
)
SELECT
segment,
COUNT(customer_id) AS customer_count,
ROUND(
COUNT(customer_id) * 100.0 / SUM(COUNT(customer_id)) OVER ()
, 1) AS customer_ratio,
SUM(monetary) AS total_monetary,
ROUND(AVG(monetary), 0) AS avg_monetary
FROM rfm_segmented
GROUP BY segment
ORDER BY total_monetary DESC;出力イメージ
| segment | customer_count | customer_ratio | total_monetary | avg_monetary |
|---|---|---|---|---|
| ロイヤル顧客 | 312 | 8.2% | 48,230,000 | 154,583 |
| 優良顧客 | 891 | 23.4% | 29,180,000 | 32,749 |
| 新規・見込み顧客 | 743 | 19.5% | 8,920,000 | 12,008 |
| 離脱危機優良顧客 | 224 | 5.9% | 19,640,000 | 87,678 |
| 休眠顧客 | 1,284 | 33.7% | 9,870,000 | 7,688 |
| その他 | 354 | 9.3% | 3,210,000 | 9,068 |
このサマリから、たとえばこんな洞察が生まれる。
- 「離脱危機優良顧客」は全体の5.9%しかいないのに、1人あたりの平均購買額が87,678円と突出して高い。ここへのウィンバック投資対効果は大きい
- 「休眠顧客」が33.7%と最多。全員にアプローチするよりも、M値が高い休眠顧客に絞って掘り起こすのが現実的だ
実務での運用ヒント
① 基準日は CURRENT_DATE にする
本番運用では DATE '2024-12-31' を CURRENT_DATE(または TD_SCHEDULED_TIME() など環境固有の関数)に置き換えることで、毎月実行しても自動的に最新の日付を基準にできる。
② 集計期間の定義を統一する
「直近1年」「直近2年」など、チーム内でどの期間を使うかを先に決めておかないと、人によって出す数字がズレる。クエリのコメントに明記しておくのが鉄則だ。
③ Treasure DataでNTILEを使う際の注意
Treasure DataのPrestoエンジンでは NTILE() はそのまま動く。ただし顧客数が少ない場合、NTILEで5等分すると1スコアあたりの人数が偏る。顧客数が500人以下の場合は3分割(NTILE(3))を検討するとよい。
④ セグメント定義は聖域ではない
今回の CASE WHEN のセグメント定義はあくまで一例だ。自社のビジネスモデルや商材特性によって「Rが2週間以内なら高スコア」「年4回以上購買がF高」など、閾値をチューニングすることが重要だ。まずこのクエリで出してみて、実際の顧客リストを見ながら定義を調整していくのが現場の流儀だ。