🎓 レベル:標準 | 重要度:A(必須)
📎 関連:第6章 セグメンテーション 目次 | 前提:マーケティングデータとKPIの体系
要点(BLUF)
- RFM分析は、顧客を R(Recency=最終購入からの経過日数。小さいほど良い)・F(Frequency=購入回数)・M(Monetary=累計購入額) の3軸で測り、各軸を**5分位スコア(1〜5)**に直して組み合わせ、解釈しやすいセグメント(優良客・常連・離反予兆・新規・休眠…)に分ける、取引ログだけで動く単純で実務的な手法です。モデルも仮定もほぼ要らず、「誰に何をするか」がすぐ見えるのが強みです。
- スコア化は各指標を分位点で5等分するだけ。ただし R は「小さいほど良い」ので逆順にし、最も最近買った20%に 5、最も昔の20%に 1 を与えます。3軸のスコアを並べた3桁の RFM スコア(例 =最優良、=最低)と、スコアの組み合わせルールでセグメントを命名します。
- 合成した顧客1000人の取引ログ(行)で、優良客 167人(全体の 16.7%)が売上の 27.9% を占める集中が見え、さらに離反予兆 110人は F・M が優良客並みに高い(F ・M 円)のに平均 R が 日と最も悪い——つまり「かつて優良だったが離れつつある、最優先で取り戻すべき層」が浮かび上がります。ただし RFM は過去行動の記述であって将来予測でも因果でもありません。将来の購買確率や 顧客生涯価値(LTV) を予測したいなら確率モデル(第9章)、施策の効果を測るなら実験(第7章)が別途要ります。
1. RFM分析とは:3つの問いで顧客を測る
RFM は、もともとカタログ通販・ダイレクトマーケティングで使われてきた古典的な顧客評価法です。発想はとても素朴で、1人の顧客について次の3つの問いを立てます。
- R(Recency):最後に買ったのはいつか?(最終購入からの経過日数。小さい=最近ほど良い)
- F(Frequency):これまで何回買ったか?(購入回数。多いほど良い)
- M(Monetary):これまでいくら使ったか?(累計購入額。多いほど良い)
なぜこの3軸かというと、経験的にこの3つが「次も買ってくれるか・どれだけ価値があるか」をよく説明するからです。直近に買った人ほど次も買いやすく(R)、何度も買う人ほどロイヤルで(F)、たくさん使う人ほど現に価値が高い(M)。とくに R は予測力が高いと言われます——半年前に1回きりの顧客より、先週も買った顧客のほうが、明らかに「生きている」のです。
RFM の魅力は、その単純さにあります。必要なのは「誰が・いつ・いくら買ったか」の取引ログだけ。回帰もベイズも要らず、出てきたセグメントは名前を付ければそのまま現場で使えます。「離反予兆の人にクーポンを送る」「新規を育成する」「優良客を特別扱いする」——施策に直結する解釈可能性が、この古い手法がいまも生き残っている理由です。
flowchart LR LOG["取引ログ(誰が・いつ・いくら)"] --> R["R:最終購入からの経過日数"] LOG --> F["F:購入回数"] LOG --> M["M:累計購入額"] R --> S["各軸を5分位スコア 1〜5(R は逆順)"] F --> S M --> S S --> SEG["3桁RFMスコア → ルールでセグメント命名"]
2. 5分位スコアとセグメント定義(数式)
各指標を分位点(quantile)で5等分し、〜 のスコアを割り当てます。F・M は大きいほど良いので、顧客全体を値の小さい順に並べて5等分し、下位20%群に 、上位20%群に を与えます。
R だけは扱いが逆です。R は「経過日数」なので小さいほど良い——最近買った人を高く評価したい。そこで R は昇順5分位スコア を反転させます。
こうすると、最も最近(R 最小)の20%に 、最も昔(R 最大)の20%に が付きます。3軸のスコアを並べると3桁の RFM スコアになります。
は「最近・高頻度・高額」のすべてが揃った理想客、 は「ずっと来ず・ほとんど買わず・少額」の最低評価です。 通りのスコアが出ますが、実務ではこれを意味のあるセグメントにまとめます。本ノートで使うルール(上から順に判定し、最初に当てはまったものを採用)はこうです。
- 優良客:(最近・高頻度・高額の三拍子)
- 離反予兆:(F・M は高いがしばらく来ていない)
- 常連:(よく買うが R は中位)
- 新規:(最近来たが購入回数はまだ少ない)
- 休眠:(長く来ず購入も少ない)
- 一般:上のどれにも当てはまらない中間層
ここで肝心なのは離反予兆の定義です。F・M(過去の買いっぷり)は優良客と同じくらい高いのに、R(最近の活動)だけが落ちている——これは「優良客だった人が離れかけている」危険信号で、RFM が3軸を分けて持つからこそ捉えられます。M だけ・F だけでは「優良客」と「離反予兆」は区別できません。
3. 取引ログからRFMセグメントを作る(コード)
顧客1000人の取引ログを合成し、 を集計 → pandas の qcut で5分位スコア(R は逆順)→ ルールでセグメント命名 → セグメント別の人数・平均 R/F/M・売上シェアを出します。数式との対応: が R_score, F_score, M_score、3桁スコアが RFM、§2のルールが segment_of 関数です。
import numpy as np
import pandas as pd
# === 顧客1000人の取引ログを合成 ===
# 顧客ごとに購入回数 n_tx、各取引の日付(基準日からの経過日数)と金額を乱数生成。
rng = np.random.default_rng(0)
n_customers = 1000
ref_date = pd.Timestamp("2026-07-01") # 集計の基準日
rows = []
for cid in range(n_customers):
n_tx = int(rng.integers(1, 26)) # この顧客の購入回数(1〜25回)
recent = int(rng.integers(1, 365)) # 最終購入が基準日から何日前か(1〜364日)
for _ in range(n_tx):
days_ago = recent + int(rng.integers(0, 400)) # 各取引は最終購入と同時かそれ以前
date = ref_date - pd.Timedelta(days=days_ago)
amount = float(rng.lognormal(mean=8.5, sigma=0.6)) # 1取引の金額(円、中央値≒4915)
rows.append((cid, date, amount))
log = pd.DataFrame(rows, columns=["customer_id", "date", "amount"])
print(f"取引ログ:{len(log):,} 行 / 顧客 {log['customer_id'].nunique()} 人")
# === 顧客別に R / F / M を集計 ===
# R=最終購入からの経過日数(小さいほど良い), F=購入回数, M=累計購入額
rfm = log.groupby("customer_id").agg(
R=("date", lambda s: (ref_date - s.max()).days),
F=("date", "size"),
M=("amount", "sum"),
)
# === 各指標を5分位スコア(1〜5)に。R は小さいほど高スコア=逆順 ===
# 離散値の同点で分位境界が重ならないよう rank で順位化してから qcut
rfm["R_score"] = pd.qcut(rfm["R"].rank(method="first"), 5, labels=[5, 4, 3, 2, 1]).astype(int)
rfm["F_score"] = pd.qcut(rfm["F"].rank(method="first"), 5, labels=[1, 2, 3, 4, 5]).astype(int)
rfm["M_score"] = pd.qcut(rfm["M"].rank(method="first"), 5, labels=[1, 2, 3, 4, 5]).astype(int)
rfm["RFM"] = (rfm["R_score"].astype(str) + rfm["F_score"].astype(str) + rfm["M_score"].astype(str))
# === スコアの組み合わせルールでセグメント命名 ===
def segment_of(r, f, m):
if r >= 4 and f >= 4 and m >= 4: # 最近・高頻度・高額
return "優良客"
if r <= 2 and f >= 4 and m >= 4: # FMは高いがしばらく来ていない
return "離反予兆"
if f >= 4 and m >= 4: # よく買う(R は中位)
return "常連"
if r >= 4 and f <= 2: # 最近来たが購入回数は少ない
return "新規"
if r <= 2 and f <= 2: # 長く来ず購入も少ない
return "休眠"
return "一般"
rfm["segment"] = [segment_of(r, f, m) for r, f, m in
zip(rfm["R_score"], rfm["F_score"], rfm["M_score"])]
# === セグメント別の人数・平均R/F/M・売上シェア ===
seg_order = ["優良客", "常連", "離反予兆", "新規", "休眠", "一般"]
summary = rfm.groupby("segment").agg(
人数=("R", "size"),
平均R=("R", "mean"),
平均F=("F", "mean"),
平均M=("M", "mean"),
売上=("M", "sum"),
)
summary["売上シェア"] = summary["売上"] / summary["売上"].sum()
summary = summary.reindex([s for s in seg_order if s in summary.index])
print("\n=== RFMスコア上位の例(RFM=555 が最優良)===")
print(rfm.sort_values(["R_score", "F_score", "M_score"], ascending=False)
.head(5)[["R", "F", "M", "R_score", "F_score", "M_score", "RFM", "segment"]]
.to_string(formatters={"M": "{:,.0f}".format}))
print("\n=== セグメント別サマリー(人数・平均R/F/M・売上シェア)===")
print(summary.to_string(formatters={
"人数": "{:.0f}".format, "平均R": "{:.0f}".format, "平均F": "{:.1f}".format,
"平均M": "{:,.0f}".format, "売上": "{:,.0f}".format, "売上シェア": "{:.1%}".format}))
# 全顧客の何割が上位2割(優良客)に売上を依存しているかの一言
top = summary.loc["優良客"]
print(f"\n優良客は {top['人数']:.0f} 人(全体の {top['人数']/len(rfm):.1%})で売上シェア {top['売上シェア']:.1%}")
出力:
取引ログ:12,691 行 / 顧客 1000 人
=== RFMスコア上位の例(RFM=555 が最優良)===
R F M R_score F_score M_score RFM segment
customer_id
6 40 24 126,255 5 5 5 555 優良客
11 40 23 127,120 5 5 5 555 優良客
55 95 22 175,186 5 5 5 555 優良客
58 76 24 152,585 5 5 5 555 優良客
64 34 24 145,454 5 5 5 555 優良客
=== セグメント別サマリー(人数・平均R/F/M・売上シェア)===
人数 平均R 平均F 平均M 売上 売上シェア
segment
優良客 167 107 21.0 124,680 20,821,608 27.9%
常連 88 229 20.6 122,695 10,797,182 14.4%
離反予兆 110 329 20.8 124,124 13,653,638 18.3%
新規 126 125 5.9 35,714 4,499,967 6.0%
休眠 203 390 4.2 24,982 5,071,436 6.8%
一般 306 218 11.4 65,030 19,899,081 26.6%
優良客は 167 人(全体の 16.7%)で売上シェア 27.9%
出力の意味:
RFM=555 の正体。スコア上位5人はいずれも =優良客で、R が 〜 日と最近、F が 〜 回と高頻度、M が 万〜万円と高額——「最近・よく・たくさん」の三拍子です。 という3桁がそのまま顧客像を語るのが RFM の良さで、上司に「 の人を優遇します」と言えば話が通じます。
売上の集中。セグメント別サマリーが本題です。優良客は 167人(全体の 16.7%)にすぎないのに、売上の 27.9% を生んでいます。「上位2割弱が売上の3割弱」という、いわゆるパレート的な偏りです。さらに常連(14.4%)を足せば、人で売上の4割超。少数の顧客が売上を支える構造が一目で分かり、「全員に同じ施策」がいかに非効率かが見えます。
離反予兆という発見。表で最も注目すべきは離反予兆 110人です。F が ・M が 円と、優良客(F ・M 円)とほとんど同じ——過去の買いっぷりは優良客と区別がつきません。違いは R だけ:平均 R が** 日と全セグメント中で最悪です。つまり「かつて優良客だったのに、ここ1年近く来ていない」層で、彼らだけで売上シェアの 18.3% を握っています。これは最優先で取り戻すべき流出予備軍**です。M や F だけ眺めていたら「優良客」に紛れて見逃しますが、R を分けて持つ RFM はこれを名指しできます——RFM が古典なのに廃れない核心が、この1セグメントに凝縮しています。
残りの層と打ち手。新規 126人(R 良いが F ・M 小)は育成対象、休眠 203人(R 日・F ・M 小)は深追いしないか低コストで再活性化、一般 306人は中間です。RFM の出口は常にこの**「誰に何をするか」**——離反予兆にはカムバック施策、新規にはオンボーディング、優良客にはロイヤルティ特典、休眠には控えめに、と打ち手がセグメントごとに自然に決まります。
この構造を1枚にまとめます(このブロックはデータ生成からセグメント命名までを内部で再実行し、上の表と同じ値を描きます)。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
# データ生成 → R/F/M 集計 → スコア化 → セグメント命名 をこのブロック内で再実行し、
# (左) R-F 平面でのセグメント分離(点の大きさ=M)、(右) セグメント別の売上シェアを描く。
rng = np.random.default_rng(0)
n_customers = 1000
ref_date = pd.Timestamp("2026-07-01")
rows = []
for cid in range(n_customers):
n_tx = int(rng.integers(1, 26))
recent = int(rng.integers(1, 365))
for _ in range(n_tx):
days_ago = recent + int(rng.integers(0, 400))
rows.append((cid, ref_date - pd.Timedelta(days=days_ago),
float(rng.lognormal(8.5, 0.6))))
log = pd.DataFrame(rows, columns=["customer_id", "date", "amount"])
rfm = log.groupby("customer_id").agg(
R=("date", lambda s: (ref_date - s.max()).days),
F=("date", "size"), M=("amount", "sum"))
rfm["R_score"] = pd.qcut(rfm["R"].rank(method="first"), 5, labels=[5, 4, 3, 2, 1]).astype(int)
rfm["F_score"] = pd.qcut(rfm["F"].rank(method="first"), 5, labels=[1, 2, 3, 4, 5]).astype(int)
rfm["M_score"] = pd.qcut(rfm["M"].rank(method="first"), 5, labels=[1, 2, 3, 4, 5]).astype(int)
def segment_of(r, f, m):
if r >= 4 and f >= 4 and m >= 4: return "優良客"
if r <= 2 and f >= 4 and m >= 4: return "離反予兆"
if f >= 4 and m >= 4: return "常連"
if r >= 4 and f <= 2: return "新規"
if r <= 2 and f <= 2: return "休眠"
return "一般"
rfm["segment"] = [segment_of(r, f, m) for r, f, m in
zip(rfm["R_score"], rfm["F_score"], rfm["M_score"])]
seg_order = ["優良客", "常連", "離反予兆", "新規", "休眠", "一般"]
colors = {"優良客": "C2", "常連": "C0", "離反予兆": "C3",
"新規": "C1", "休眠": "C7", "一般": "C8"}
fig, ax = plt.subplots(1, 2, figsize=(11.5, 4.6))
# 左:R-F 平面でセグメントが象限に分かれる(点の大きさは M)
for s in seg_order:
d = rfm[rfm["segment"] == s]
ax[0].scatter(d["R"], d["F"], s=d["M"] / 4000, c=colors[s],
alpha=0.6, edgecolor="none", label=s)
ax[0].set_xlabel("R:最終購入からの経過日数(小さいほど良い)")
ax[0].set_ylabel("F:購入回数")
ax[0].set_title("RFMセグメントは R-F 平面で象限に分かれる(点の大きさ=M)")
ax[0].legend(loc="upper right", fontsize=8.5, ncol=2)
ax[0].grid(alpha=0.3)
# 右:セグメント別の売上シェア
share = (rfm.groupby("segment")["M"].sum() / rfm["M"].sum()).reindex(seg_order)
bars = ax[1].bar(range(len(seg_order)), share.values * 100,
color=[colors[s] for s in seg_order])
for b, v in zip(bars, share.values * 100):
ax[1].text(b.get_x() + b.get_width() / 2, v + 0.4, f"{v:.1f}", ha="center", fontsize=9)
ax[1].set_xticks(range(len(seg_order)))
ax[1].set_xticklabels(seg_order, fontsize=9)
ax[1].set_ylabel("売上シェア(%)")
ax[1].set_title("セグメント別の売上シェア:少数の優良客に売上が集中")
ax[1].grid(alpha=0.3, axis="y")
fig.tight_layout()
plt.show()
左パネルは R-F 平面です。横軸 R(左ほど最近)、縦軸 F(上ほど高頻度)、点の大きさが M。色分けされたセグメントが象限ごとに分かれているのが分かります——左上に優良客(R 小・F 大)、右上に離反予兆(R 大なのに F 大)、右下に休眠(R 大・F 小)、左下に新規(R 小・F 小)。とくに右上の離反予兆は、F が高い(=過去は優良)のに右側(=最近来ていない)に居る点群で、まさに「取り戻すべき層」が視覚的に浮きます。右パネルは売上シェアの棒で、優良客と一般の2本が突出し、少数の優良客への集中と、人数は多いが薄く広がる一般層の対比が見えます。
⚠️ よくある誤解
- RFM は過去行動の「記述」であって、予測でも因果でもない:RFM は「これまでどう買ったか」を要約しているだけです。「優良客()だから今後も買う」という予測や、「優遇したから優良客になった」という因果を、RFM 自体は何も保証しません。将来の購買確率や 顧客生涯価値(LTV) を見積もりたいなら、継続率を確率でモデル化する手法や BG/NBD などの**確率的購買モデル(第9章)が要ります。施策の効果(クーポンで本当に離反が減ったか)を知りたいなら実験・因果推論(第7章)**です。RFM は「現状把握と振り分け」のための道具と割り切りましょう。
- 分位スコアは絶対評価ではなく相対評価:5分位は「自社顧客の中での順位」です。 は「自社で上位」を意味するだけで、他社や別期間と比べられる絶対値ではありません。母集団が変われば基準も動きます——大型キャンペーンで新規が大量流入すれば、同じ買い方の顧客でもスコアが下がり得ます。時系列で の人数を比較するときはとくに注意(母集団が動いていないか確認)。
- 3軸固定なので行動の多様性は捉えきれない:RFM が見るのは「いつ・何回・いくら」だけ。何を買ったか(カテゴリ嗜好)・どのチャネルか・割引にどれだけ反応するかといった軸は入りません。同じ でも、高級品を定価で買う人と、セールだけ大量買いする人は別物ですが RFM では同じです。より多くの変数で柔軟に分けたいなら、ルールを捨てて似た顧客を自動グループ化する クラスタリングによるセグメンテーション(k-means) に進みます。
- Monetary は「累計」か「平均」かで意味が変わる:本ノートの M は累計購入額(期間内の総支出)です。累計は F と強く相関し、「たくさん買った人=高 M」になりがちで、F とほぼ同じ情報を二重に数える面があります。「1回あたりどれだけ気前よく使うか」を見たいなら**平均単価(M÷F)**を使うべきで、目的に応じて定義を選ぶ必要があります。どちらが正解ということはなく、何を測りたいかで決めます。
- 離散値の F は
qcutがそのままでは失敗しがち:F のように同じ値(同点)が多い指標をpd.qcutでいきなり5分位すると、「ビン境界が一意でない」というエラーになりがちです。本コードのようにrank(method="first")で順位に直してから5分位すると、同点を機械的にばらして安定します。スコアが厳密に各20%ずつにならない端数は出ますが、実務上は問題ありません。
関連ノート
- クラスタリングによるセグメンテーション(k-means)(RFM の固定ルールを離れ、多変量の特徴量から似た顧客を自動でグループ化する一般化。本ノートとの対比)
- 第6章 セグメンテーション 目次
- 顧客生涯価値(LTV)(セグメントの価値づけ。RFM で分けた層ごとに LTV を見れば、どの層にいくらかけてよいかが分かる)
- 将来の購買回数や生存を確率モデルで予測する BG/NBD などは第9章、RFM やクラスタリングの結果を実際の施策ターゲットへ落とすターゲティング(ペルソナ設計)は 06-03(予定)で扱います
- マーケティング・サイエンス 全体目次