RFM 分析|顧客価値を 3 指標で直感的にセグメント(実務テンプレ付き)
RFM は「最近性(R)」「頻度(F)」「金額(M)」の 3 指標で顧客価値を直感的に可視化し、施策の優先順位を付けるための定番手法です。
TL;DR
- 目的: R(最近性)× F(頻度)× M(金額)で顧客をスコア化し、施策対象を即決
- 手順: 期間定義 → 分位点スコア(R は逆符号)→ 合成スコア → セグメント施策
- 注意: 業種の購買周期・キャンペーン影響・外れ値に敏感(期間と分位点を調整)
まずはここだけ(やさしい導入)
- 何をする?: 「最近買ったか」「よく買うか」「いくら買うか」で顧客をざっくり分ける
- いつ使う?: 休眠復帰、ロイヤル育成、クーポン配布、CRM 優先度付け
- どう読む?: R は小さいほど良、F/M は大きいほど良。合成スコアで上位から順に見る
用語ミニ辞典(1 行で)
- 最近性(R): 最終購買からの経過日数(小さい=最近買った)
- 頻度(F): 一定期間の購買回数(多い=よく買う)
- 金額(M): 一定期間の購買金額(大きい=高単価)
- 分位点スコア: データを等しい人数のグループに分け、順位を 1–5 などで表す方法
- 合成スコア: R/F/M を 3 桁で並べたり加重合算した総合指標
指標の定義(テンプレ)
- R: 最終購買日からの経過日数(小さいほど良い)
- F: 一定期間の購入回数(多いほど良い)
- M: 一定期間の購買金額(多いほど良い)
スコアリング設計
- 分位点(例: 五分位)で 1–5 を割当(R は逆符号に注意)
- 合成スコア = 100×R + 10×F + M(表現はプロジェクトで統一)
最小コード(ダミーデータ)
import pandas as pd
import numpy as np
df = pd.DataFrame({
'customer':['A','B','C','D','E','F','G','H'],
'recency':[10,45,5,120,60,15,200,8], # 日
'frequency':[6,3,10,1,2,5,1,8],
'monetary':[52000,18000,98000,3000,8000,42000,1500,76000]
})
def qcut_score(x, q=5, reverse=False):
s = pd.qcut(x.rank(method='first'), q, labels=False) + 1
return (q - s + 1) if reverse else s
df['R_score'] = qcut_score(df['recency'], reverse=True)
df['F_score'] = qcut_score(df['frequency'])
df['M_score'] = qcut_score(df['monetary'])
df['RFM'] = 100df['R_score'] + 10df['F_score'] + df['M_score']
print(df.sort_values('RFM', ascending=False)[['customer','R_score','F_score','M_score','RFM']])
読み方の例:
- RFM 高: ロイヤル顧客。LTV 拡大型の施策
- R 高 F/M 低: 直近来訪だが軽量。育成施策/クロスセル
- R 低 F/M 高: 休眠危険の大口。復帰クーポン/リマーケ
可視化(表・ヒートマップ)
import matplotlib.pyplot as plt
import pandas as pd
# R×F の件数ヒートマップ
rf_table = pd.crosstab(df['R_score'], df['F_score']).sort_index().sort_index(axis=1)
plt.imshow(rf_table.values, cmap='Blues', origin='lower')
plt.xticks(range(len(rf_table.columns)), rf_table.columns)
plt.yticks(range(len(rf_table.index)), rf_table.index)
plt.xlabel('F_score'); plt.ylabel('R_score'); plt.title('R×F count heatmap')
plt.colorbar(); plt.tight_layout(); plt.show()
# RFM セグメント件数の棒グラフ
df['segment'] = df['R_score'].astype(str)+df['F_score'].astype(str)+df['M_score'].astype(str)
ax = df['segment'].value_counts().sort_index().plot(kind='bar', figsize=(6,3), title='RFM segment counts')
ax.set_xlabel('RFM'); ax.set_ylabel('count')
plt.tight_layout(); plt.show()
実務ケーススタディ(休眠危険を“先回り”で復帰)
目的: 直近の大口顧客(F/M 高)だが最近性が悪化した層に、期限付き復帰オファーを配布し LTV を守る。
- 期間とルール
- 直近 6 か月を評価期間。キャンペーン期間は除外 or 別期間として評価
- 外れ値(超大口)には Winsorize などで上限クリップ
- スコアと選定
- 分位点で R/F/M のスコアを作成(R は逆符号)
- ターゲット:
R_score ≤ 2かつF_score ≥ 4またはM_score ≥ 4
- オファー設計
- 14 日期限の復帰クーポン + パーソナライズ(過去カテゴリ)
- テスト: 送信有無の A/B(回帰 to mean を避けるため無作為抽出)
- 評価指標
- 復帰率、復帰後 60 日の累積粗利、オファーコスト差引の増分
target = df[(df['R_score'] <= 2) & ((df['F_score'] >= 4) | (df['M_score'] >= 4))]
print(len(target), '件を対象に復帰施策を検討')
落とし穴と対策
- F/M の偏り(ヘビーユーザー依存)→ ロバスト指標や Winsorize
- 業種差(購買周期)→ セグメント別に分位点を分ける
- 一時的キャンペーン効果 → 期間設計を分けて比較
練習問題(理解を定着)
- R は日数なので「小さいほど良い」。分位点スコアをどう割り当てる?
- ヒント: R は逆符号(最近買った人に高スコア)
- セール月を含むと F/M が急増した。何を調整する?
- ヒント: 期間を分けて評価 or キャンペーンフラグで分位点を別計算
- 高単価だが頻度が低い VIP と、低単価だが頻度が高い常連。施策は同じで良い?
- ヒント: 目的別(LTV 拡大 vs 滞在時間/来店頻度)で分ける
- R は小さいほど良なので、分位点 1 が最低、5 が最高になるように逆割当。
- セール影響を切り出し、平常期と別計算。フラグで混ぜない。
- 異なる打ち手。VIP にはアップセル/会員特典、常連には来店頻度向上策やサブスク訴求。
よくある質問(FAQ)
- Q: 分位点の段階数は固定?
- A: データ量と施策解像度に応じて 3–7 段階で調整。比較可能性を優先
- Q: 外れ値はどう扱う?
- A: Winsorize などロバスト化や上限クリップで安定化。業務観点で上限設定
- Q: R/F/M の重みは?
- A: まず等重み。目的に応じて A/B テストや回帰で最適化
RFM分析についてのご相談はこちら