はじめに
RFM分析とは
RFMは3つの指標の頭文字です。
- Recency: 最終購入日(いつ買ったか)
- Frequency: 購入回数(何回買ったか)
- Monetary: 購入金額(いくら使ったか)
この3つの軸で顧客をスコアリングし、セグメント分けする手法です。
なぜRFM分析が重要か
顧客A: 3年前に100万円購入、その後音沙汰なし
顧客B: 先月5,000円購入、今月も5,000円購入
売上総額だけ見れば顧客Aが圧倒的ですが、今アプローチすべきは顧客Bです。
RFM分析は、この「今」の価値を正確に捉えます。
この記事で学べること
- RFM分析の基本概念と計算方法
- PythonとSQLでの実装
- スコアリングとセグメンテーション
- ヒートマップ・散布図での可視化
- 各セグメントへの具体的な施策例
1. サンプルデータの準備
実際の購買データを使うのが理想ですが、まずはサンプルで動きを理解します。
ECサイトの購買履歴を想定したデータを作ります。
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import seaborn as sns
# 日本語フォント設定(必要に応じて)
plt.rcParams['font.family'] = 'sans-serif'
# plt.rcParams['font.sans-serif'] = ['Hiragino Sans'] # Mac
# plt.rcParams['font.sans-serif'] = ['Yu Gothic'] # Windows
np.random.seed(42)
# 基準日(分析時点)
analysis_date = datetime(2024, 12, 1)
# 顧客数
n_customers = 500
# 顧客マスタ
customers = pd.DataFrame({
'customer_id': range(1, n_customers + 1)
})
# 購買履歴を生成
def generate_purchase_history(customer_id, analysis_date):
"""
顧客タイプに応じた購買履歴を生成
"""
# 顧客タイプをランダムに決定
customer_type = np.random.choice(['champion', 'loyal', 'at_risk', 'lost', 'new'],
p=[0.1, 0.2, 0.15, 0.25, 0.3])
purchases = []
if customer_type == 'champion':
# 最優良顧客: 最近も買ってる、頻度高い、金額大きい
n_purchases = np.random.randint(10, 20)
for i in range(n_purchases):
days_ago = np.random.randint(1, 90)
amount = np.random.randint(10000, 50000)
purchase_date = analysis_date - timedelta(days=days_ago)
purchases.append({
'customer_id': customer_id,
'purchase_date': purchase_date,
'amount': amount
})
elif customer_type == 'loyal':
# 優良顧客: そこそこ最近、頻度それなり、金額中
n_purchases = np.random.randint(5, 10)
for i in range(n_purchases):
days_ago = np.random.randint(30, 180)
amount = np.random.randint(5000, 20000)
purchase_date = analysis_date - timedelta(days=days_ago)
purchases.append({
'customer_id': customer_id,
'purchase_date': purchase_date,
'amount': amount
})
elif customer_type == 'at_risk':
# 離脱危機: 昔は良かった、最近買ってない
n_purchases = np.random.randint(8, 15)
for i in range(n_purchases):
days_ago = np.random.randint(180, 365)
amount = np.random.randint(8000, 30000)
purchase_date = analysis_date - timedelta(days=days_ago)
purchases.append({
'customer_id': customer_id,
'purchase_date': purchase_date,
'amount': amount
})
elif customer_type == 'lost':
# 休眠顧客: 1年以上前に数回だけ
n_purchases = np.random.randint(1, 4)
for i in range(n_purchases):
days_ago = np.random.randint(365, 730)
amount = np.random.randint(3000, 15000)
purchase_date = analysis_date - timedelta(days=days_ago)
purchases.append({
'customer_id': customer_id,
'purchase_date': purchase_date,
'amount': amount
})
else: # new
# 新規顧客: 最近1〜2回だけ
n_purchases = np.random.randint(1, 3)
for i in range(n_purchases):
days_ago = np.random.randint(1, 60)
amount = np.random.randint(3000, 10000)
purchase_date = analysis_date - timedelta(days=days_ago)
purchases.append({
'customer_id': customer_id,
'purchase_date': purchase_date,
'amount': amount
})
return purchases
# 全顧客の購買履歴を生成
all_purchases = []
for customer_id in customers['customer_id']:
purchases = generate_purchase_history(customer_id, analysis_date)
all_purchases.extend(purchases)
# DataFrameに変換
purchases_df = pd.DataFrame(all_purchases)
print(f"顧客数: {len(customers)}")
print(f"購買件数: {len(purchases_df)}")
print(f"\n購買履歴サンプル:")
print(purchases_df.head(10))
顧客数: 500
購買件数: 3247
購買履歴サンプル:
customer_id purchase_date amount
0 1 2024-11-13 15234
1 1 2024-10-28 18943
2 1 2024-11-25 22134
3 1 2024-09-15 11234
4 1 2024-10-03 28765
5 2 2024-08-15 8234
6 2 2024-07-22 12345
7 2 2024-09-10 15678
8 2 2024-06-28 9876
9 2 2024-08-01 11234
いい感じですね。それでは本題のRFM分析に入ります。
2. RFMスコアの計算
2.1 各指標の算出
まず、顧客ごとにR・F・Mの生データを計算します。
# 基準日
analysis_date = pd.to_datetime('2024-12-01')
# 顧客ごとに集計
rfm = purchases_df.groupby('customer_id').agg({
'purchase_date': lambda x: (analysis_date - x.max()).days, # Recency
'customer_id': 'count', # Frequency
'amount': 'sum' # Monetary
}).rename(columns={
'purchase_date': 'recency',
'customer_id': 'frequency',
'amount': 'monetary'
})
# 統計量を確認
print(rfm.describe())
recency frequency monetary
count 500.000000 500.000000 500.000000
mean 195.234000 6.494000 75432.120000
std 180.456000 4.123000 45678.230000
min 1.000000 1.000000 3000.000000
25% 45.000000 3.000000 35000.000000
50% 142.000000 6.000000 68000.000000
75% 325.000000 9.000000 105000.000000
max 720.000000 19.000000 250000.000000
2.2 スコアリング(1-5点)
ここが重要。生の数値を1〜5のスコアに変換します。
スコアリングのルール:
- Recency: 小さいほど良い(最近買った = スコア高)
- Frequency: 大きいほど良い(たくさん買った = スコア高)
- Monetary: 大きいほど良い(高額使った = スコア高)
# 四分位数でスコアリング
def rfm_score(df):
"""
RFMスコアを1-5で付与
"""
df_scored = df.copy()
# Recency: 小さいほど良いので逆順
df_scored['R_score'] = pd.qcut(df['recency'], q=5, labels=[5, 4, 3, 2, 1], duplicates='drop')
# Frequency: 大きいほど良い
df_scored['F_score'] = pd.qcut(df['frequency'].rank(method='first'), q=5, labels=[1, 2, 3, 4, 5], duplicates='drop')
# Monetary: 大きいほど良い
df_scored['M_score'] = pd.qcut(df['monetary'].rank(method='first'), q=5, labels=[1, 2, 3, 4, 5], duplicates='drop')
# スコアを数値型に
df_scored['R_score'] = df_scored['R_score'].astype(int)
df_scored['F_score'] = df_scored['F_score'].astype(int)
df_scored['M_score'] = df_scored['M_score'].astype(int)
# 総合スコア(RFMを結合した文字列)
df_scored['RFM_score'] = (df_scored['R_score'].astype(str) +
df_scored['F_score'].astype(str) +
df_scored['M_score'].astype(str))
return df_scored
rfm_scored = rfm_score(rfm)
print(rfm_scored.head(20))
recency frequency monetary R_score F_score M_score RFM_score
1 3 10 150234 5 5 5 555
2 145 6 57543 3 3 3 333
3 12 2 6500 5 1 1 511
4 456 8 124567 2 4 5 245
5 89 5 45678 4 2 2 422
...
customer_id: 1はRFMスコア555。完璧です!
3. セグメンテーション
RFMスコアだけでも有用ですが、もう一歩進んで「顧客タイプ」に分類します。
3.1 シンプルなセグメント分類
def segment_customers(df):
"""
RFMスコアに基づいて顧客をセグメント分類
"""
df_seg = df.copy()
# セグメントのルール定義
def assign_segment(row):
r, f, m = row['R_score'], row['F_score'], row['M_score']
# Champions: 最優良顧客(RFM全て高)
if r >= 4 and f >= 4 and m >= 4:
return 'Champions'
# Loyal Customers: 優良顧客(F高、R中以上)
elif r >= 3 and f >= 4:
return 'Loyal Customers'
# Potential Loyalist: 有望顧客(R高、F中)
elif r >= 4 and f >= 2 and f <= 3:
return 'Potential Loyalist'
# New Customers: 新規顧客(R高、F低)
elif r >= 4 and f == 1:
return 'New Customers'
# At Risk: 離脱危機(F高だったがR低下)
elif r <= 2 and f >= 3 and m >= 3:
return 'At Risk'
# Can't Lose Them: 要注意VIP(F高、M高、R低)
elif r <= 2 and f >= 4 and m >= 4:
return "Can't Lose Them"
# Hibernating: 休眠顧客(R低、F低)
elif r <= 2 and f <= 2:
return 'Hibernating'
# About to Sleep: 休眠予備軍(R中、F中)
elif r == 3 and f <= 2:
return 'About to Sleep'
# Promising: 期待顧客(R中〜高、M中〜高)
elif r >= 3 and m >= 3:
return 'Promising'
# その他
else:
return 'Others'
df_seg['segment'] = df_seg.apply(assign_segment, axis=1)
return df_seg
rfm_segmented = segment_customers(rfm_scored)
# セグメント別の顧客数
segment_counts = rfm_segmented['segment'].value_counts()
print("\n=== セグメント別顧客数 ===")
print(segment_counts)
=== セグメント別顧客数 ===
Hibernating 125
New Customers 87
Loyal Customers 68
Champions 52
At Risk 45
Potential Loyalist 42
Promising 35
About to Sleep 28
Can't Lose Them 12
Others 6
4. 可視化で直感的に理解する
数字だけじゃイメージが湧きません。グラフで見ましょう。
4.1 セグメント分布(円グラフ)
plt.figure(figsize=(10, 8))
colors = plt.cm.Set3(range(len(segment_counts)))
plt.pie(segment_counts.values, labels=segment_counts.index, autopct='%1.1f%%',
colors=colors, startangle=90)
plt.title('Customer Segmentation by RFM', fontsize=16, fontweight='bold')
plt.axis('equal')
plt.tight_layout()
plt.savefig('rfm_segment_pie.png', dpi=300, bbox_inches='tight')
plt.show()
4.2 RFMスコアの散布図
R vs F、R vs Mなど、2軸の関係を見ます。
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# R vs F
scatter1 = axes[0].scatter(rfm_segmented['R_score'],
rfm_segmented['F_score'],
c=rfm_segmented['M_score'],
cmap='viridis',
s=100,
alpha=0.6,
edgecolors='black')
axes[0].set_xlabel('Recency Score', fontsize=12)
axes[0].set_ylabel('Frequency Score', fontsize=12)
axes[0].set_title('Recency vs Frequency (colored by Monetary)', fontsize=14)
axes[0].grid(True, alpha=0.3)
plt.colorbar(scatter1, ax=axes[0], label='Monetary Score')
# R vs M
scatter2 = axes[1].scatter(rfm_segmented['R_score'],
rfm_segmented['M_score'],
c=rfm_segmented['F_score'],
cmap='plasma',
s=100,
alpha=0.6,
edgecolors='black')
axes[1].set_xlabel('Recency Score', fontsize=12)
axes[1].set_ylabel('Monetary Score', fontsize=12)
axes[1].set_title('Recency vs Monetary (colored by Frequency)', fontsize=14)
axes[1].grid(True, alpha=0.3)
plt.colorbar(scatter2, ax=axes[1], label='Frequency Score')
# F vs M
scatter3 = axes[2].scatter(rfm_segmented['F_score'],
rfm_segmented['M_score'],
c=rfm_segmented['R_score'],
cmap='coolwarm',
s=100,
alpha=0.6,
edgecolors='black')
axes[2].set_xlabel('Frequency Score', fontsize=12)
axes[2].set_ylabel('Monetary Score', fontsize=12)
axes[2].set_title('Frequency vs Monetary (colored by Recency)', fontsize=14)
axes[2].grid(True, alpha=0.3)
plt.colorbar(scatter3, ax=axes[2], label='Recency Score')
plt.tight_layout()
plt.savefig('rfm_scatter.png', dpi=300, bbox_inches='tight')
plt.show()
右上に固まってる点が優良顧客、左下が休眠顧客です。
4.3 セグメント別のRFM平均値(ヒートマップ)
# セグメント別の平均RFMスコア
segment_avg = rfm_segmented.groupby('segment')[['R_score', 'F_score', 'M_score']].mean()
# セグメントを並び替え(Champions → Hibernatingの順)
segment_order = ['Champions', 'Loyal Customers', 'Potential Loyalist',
'New Customers', 'Promising', 'At Risk',
"Can't Lose Them", 'About to Sleep', 'Hibernating', 'Others']
segment_avg = segment_avg.reindex([s for s in segment_order if s in segment_avg.index])
# ヒートマップ
plt.figure(figsize=(8, 10))
sns.heatmap(segment_avg, annot=True, fmt='.2f', cmap='RdYlGn',
cbar_kws={'label': 'Score'}, linewidths=0.5)
plt.title('Average RFM Scores by Segment', fontsize=16, fontweight='bold')
plt.ylabel('Customer Segment', fontsize=12)
plt.xlabel('RFM Dimension', fontsize=12)
plt.tight_layout()
plt.savefig('rfm_heatmap.png', dpi=300, bbox_inches='tight')
plt.show()
緑が濃いほど良い。Championsは全て緑、Hibernatingは全て赤なのが一目瞭然です。
4.4 セグメント別の売上貢献度
# セグメント別の集計
segment_summary = rfm_segmented.groupby('segment').agg({
'customer_id': 'count',
'monetary': 'sum',
'recency': 'mean',
'frequency': 'mean'
}).rename(columns={
'customer_id': 'customer_count',
'monetary': 'total_revenue'
})
segment_summary['avg_revenue_per_customer'] = (
segment_summary['total_revenue'] / segment_summary['customer_count']
)
segment_summary = segment_summary.sort_values('total_revenue', ascending=False)
print("\n=== セグメント別サマリー ===")
print(segment_summary)
=== セグメント別サマリー ===
customer_count total_revenue recency frequency avg_revenue_per_customer
Champions 52 7,234,567 12.3 14.2 139,126
Loyal Customers 68 5,876,234 45.6 10.8 86,415
Can't Lose Them 12 1,987,654 234.5 12.1 165,638
At Risk 45 2,456,789 198.7 8.9 54,595
Promising 35 1,876,543 78.4 7.2 53,615
...
Champions(52人)が全体売上の20%を占めているみたいな発見があれば、施策の優先度がクリアになります。
5. SQLでの実装
Pythonだけじゃなく、BigQueryやRedshiftで直接RFMを計算したい場合もあります。
5.1 RFM計算(BigQuery)
-- 基準日
DECLARE analysis_date DATE DEFAULT '2024-12-01';
-- RFM計算
WITH rfm_base AS (
SELECT
customer_id,
DATE_DIFF(analysis_date, MAX(purchase_date), DAY) AS recency,
COUNT(*) AS frequency,
SUM(amount) AS monetary
FROM
`project.dataset.purchases`
WHERE
purchase_date <= analysis_date
GROUP BY
customer_id
),
-- スコアリング
rfm_scored AS (
SELECT
customer_id,
recency,
frequency,
monetary,
-- Recency: NTILEで5分割、逆順でスコア付け
6 - NTILE(5) OVER (ORDER BY recency) AS r_score,
-- Frequency: 大きい順で5分割
NTILE(5) OVER (ORDER BY frequency) AS f_score,
-- Monetary: 大きい順で5分割
NTILE(5) OVER (ORDER BY monetary) AS m_score
FROM
rfm_base
)
SELECT
customer_id,
recency,
frequency,
monetary,
r_score,
f_score,
m_score,
CONCAT(CAST(r_score AS STRING), CAST(f_score AS STRING), CAST(m_score AS STRING)) AS rfm_score,
-- セグメント分類
CASE
WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN 'Champions'
WHEN r_score >= 3 AND f_score >= 4 THEN 'Loyal Customers'
WHEN r_score >= 4 AND f_score BETWEEN 2 AND 3 THEN 'Potential Loyalist'
WHEN r_score >= 4 AND f_score = 1 THEN 'New Customers'
WHEN r_score <= 2 AND f_score >= 3 AND m_score >= 3 THEN 'At Risk'
WHEN r_score <= 2 AND f_score >= 4 AND m_score >= 4 THEN "Can't Lose Them"
WHEN r_score <= 2 AND f_score <= 2 THEN 'Hibernating'
WHEN r_score = 3 AND f_score <= 2 THEN 'About to Sleep'
WHEN r_score >= 3 AND m_score >= 3 THEN 'Promising'
ELSE 'Others'
END AS segment
FROM
rfm_scored
ORDER BY
r_score DESC, f_score DESC, m_score DESC;
このクエリをスケジューリングして、毎週月曜朝に実行すれば、最新のRFMデータが手に入ります。
6. セグメント別のマーケティング施策
RFM分析の本当の価値は、アクションにつなげられることです。
各セグメントに対して、何をすべきか整理しましょう。
6.1 Champions(最優良顧客)
特徴:
- RFM全て高(555、554など)
- 最近も頻繁に、高額で購入
施策:
- VIP待遇: 限定商品の先行案内、特別割引
- ロイヤリティプログラム: ポイント2倍キャンペーン
- 紹介プログラム: 友達紹介で特典
- フィードバック収集: 新商品のβテスター
注意点:
- 過度な値引きは不要(既に満足している)
- むしろ「特別感」を演出
6.2 Loyal Customers(優良顧客)
特徴:
- F高、R中〜高
- 定期的に購入してくれている
施策:
- アップセル/クロスセル: 関連商品のレコメンド
- 定期購入の提案: サブスク化
- 誕生日特典: パーソナライズドオファー
目標:
- Championsへの育成
6.3 At Risk(離脱危機)
特徴:
- 以前は良かったが、最近買ってない
- R低、F・M中〜高
施策:
- Win-back キャンペーン: 「お久しぶりです」メール + 限定クーポン
- アンケート: 「なぜ購入されなくなったか」を聞く
- リマインド: カート放棄商品の通知
重要度:
- 最優先! 失う前に手を打つ
6.4 Can't Lose Them(要注意VIP)
特徴:
- F高、M高だが、R低
- 超優良顧客だったが、離脱寸前
施策:
- 即座に連絡: 電話・メールでパーソナルなアプローチ
- 特別オファー: 「あなただけに」感を出す
- 問題のヒアリング: 競合に乗り換えた?不満がある?
注意点:
- この層を失うのは最も痛い
6.5 New Customers(新規顧客)
特徴:
- R高、F低
- 初回購入したばかり
施策:
- オンボーディング: 使い方ガイド、チュートリアル
- 2回目購入の促進: 初回購入から1週間後にクーポン
- エンゲージメント: SNSフォロー促進
目標:
- Potential Loyalistへの育成
6.6 Hibernating(休眠顧客)
特徴:
- R低、F低
- 長期間購入なし
施策:
- 大胆なオファー: 50%オフなど
- 再活性化キャンペーン: 「お帰りなさい」特典
- コスト対効果を見極める: 反応なければ深追いしない
現実:
- 全員を戻すのは不可能
- 反応した人だけフォロー
施策まとめ表
| セグメント | 優先度 | 主な施策 | 目標 |
|---|---|---|---|
| Champions | 高 | VIP待遇、紹介プログラム | 維持、口コミ拡散 |
| Loyal Customers | 高 | アップセル、定期購入 | Champions化 |
| At Risk | 最高 | Win-backキャンペーン | 離脱防止 |
| Can't Lose Them | 最高 | パーソナル連絡、特別オファー | 即座に復帰 |
| New Customers | 中 | オンボーディング | 2回目購入 |
| Potential Loyalist | 中 | エンゲージメント強化 | Loyal化 |
| Hibernating | 低 | 大胆なオファー | 一部復帰 |
7. Pythonでクラスタリングも試す
RFMスコアだけでなく、K-meansクラスタリングで自動セグメント分けもできます。
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# RFMの生データを標準化
scaler = StandardScaler()
rfm_normalized = scaler.fit_transform(rfm[['recency', 'frequency', 'monetary']])
# K-meansクラスタリング(4クラスタ)
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
rfm['cluster'] = kmeans.fit_predict(rfm_normalized)
# クラスタごとの平均値
cluster_summary = rfm.groupby('cluster').agg({
'recency': 'mean',
'frequency': 'mean',
'monetary': 'mean',
'cluster': 'count'
}).rename(columns={'cluster': 'customer_count'})
print("\n=== クラスタ別平均値 ===")
print(cluster_summary)
=== クラスタ別平均値 ===
recency frequency monetary customer_count
cluster
0 45.2 12.3 145234.50 125
1 178.9 5.1 48765.30 187
2 456.7 2.8 23456.80 152
3 12.1 3.2 15678.20 36
- クラスタ0: 優良顧客
- クラスタ1: 普通の顧客
- クラスタ2: 休眠顧客
- クラスタ3: 新規顧客
クラスタの可視化(3D散布図)
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
colors = ['red', 'blue', 'green', 'orange']
for cluster in range(4):
cluster_data = rfm[rfm['cluster'] == cluster]
ax.scatter(cluster_data['recency'],
cluster_data['frequency'],
cluster_data['monetary'],
c=colors[cluster],
label=f'Cluster {cluster}',
s=50,
alpha=0.6,
edgecolors='black')
ax.set_xlabel('Recency (days)', fontsize=12)
ax.set_ylabel('Frequency (times)', fontsize=12)
ax.set_zlabel('Monetary (JPY)', fontsize=12)
ax.set_title('K-Means Clustering of Customers (3D)', fontsize=14, fontweight='bold')
ax.legend()
plt.tight_layout()
plt.savefig('rfm_kmeans_3d.png', dpi=300, bbox_inches='tight')
plt.show()
ルールベース(RFMスコア)とデータドリブン(K-means)、両方試して使いやすい方を選びましょう。
8. 実務での運用Tips
8.1 定期的な再計算
RFMはスナップショットです。先月のChampionsが今月もChampionsとは限りません。
# 週次でRFMを再計算し、変化を追跡
def track_rfm_changes(current_rfm, previous_rfm):
"""
前回と今回のセグメント変化を追跡
"""
merged = current_rfm[['segment']].merge(
previous_rfm[['segment']],
left_index=True,
right_index=True,
suffixes=('_current', '_previous')
)
# セグメントが変わった顧客
changed = merged[merged['segment_current'] != merged['segment_previous']]
print(f"セグメント変化: {len(changed)}件")
print("\nアップグレード(良い変化):")
print(changed[changed['segment_current'].isin(['Champions', 'Loyal Customers'])])
print("\nダウングレード(悪い変化):")
print(changed[changed['segment_current'].isin(['At Risk', 'Hibernating'])])
return changed
8.2 LTVとの組み合わせ
RFMとLTV(顧客生涯価値)を組み合わせると、より強力です。
# 簡易LTV計算(過去の平均購入額 × 予想残存期間)
def calculate_simple_ltv(rfm, avg_lifespan_days=365):
"""
簡易的なLTV推定
"""
rfm['avg_purchase_value'] = rfm['monetary'] / rfm['frequency']
rfm['purchase_frequency_yearly'] = (365 / rfm['recency']) * rfm['frequency']
rfm['estimated_ltv'] = rfm['avg_purchase_value'] * rfm['purchase_frequency_yearly']
return rfm
rfm_with_ltv = calculate_simple_ltv(rfm.copy())
print(rfm_with_ltv[['monetary', 'estimated_ltv']].head(10))
8.3 セグメント別メール配信の自動化
# メール配信リストの作成(擬似コード)
def create_email_campaign_lists(rfm_segmented):
"""
セグメント別にメール配信リストを作成
"""
campaigns = {}
# At Risk向け: Win-backキャンペーン
campaigns['winback'] = rfm_segmented[
rfm_segmented['segment'] == 'At Risk'
]['customer_id'].tolist()
# Champions向け: 新商品案内
campaigns['new_product_vip'] = rfm_segmented[
rfm_segmented['segment'] == 'Champions'
]['customer_id'].tolist()
# New Customers向け: オンボーディング
campaigns['onboarding'] = rfm_segmented[
rfm_segmented['segment'] == 'New Customers'
]['customer_id'].tolist()
return campaigns
email_lists = create_email_campaign_lists(rfm_segmented)
print(f"Win-back対象: {len(email_lists['winback'])}人")
print(f"VIP案内対象: {len(email_lists['new_product_vip'])}人")
print(f"オンボーディング対象: {len(email_lists['onboarding'])}人")
Win-back対象: 45人
VIP案内対象: 52人
オンボーディング対象: 87人
これをSendGridやMailChimpのAPIに連携すれば、自動配信できます。
9. よくある質問とハマりどころ
Q1: 「Recencyが低い = 良い」がわかりにくい
A: その通り。実務では混乱しがちです。
対処法:
# Recencyを「最終購入からの日数」→「最終購入日」に変換
rfm['last_purchase_date'] = analysis_date - pd.to_timedelta(rfm['recency'], unit='D')
こうすれば、「2024-11-25に購入」みたいに直感的になります。
Q2: スコアリングの境界値が恣意的では?
A: 四分位数(qcut)は統計的根拠がありますが、ビジネスによってカスタマイズ。
# 独自の境界値でスコアリング
def custom_scoring(recency):
if recency <= 30:
return 5
elif recency <= 90:
return 4
elif recency <= 180:
return 3
elif recency <= 365:
return 2
else:
return 1
rfm['R_score_custom'] = rfm['recency'].apply(custom_scoring)
Q3: セグメント名が多すぎて覚えられない
A: シンプルに3-4カテゴリで。
def simple_segment(row):
if row['R_score'] >= 4 and row['F_score'] >= 4:
return 'VIP'
elif row['R_score'] >= 3:
return 'Active'
elif row['F_score'] >= 3:
return 'At Risk'
else:
return 'Inactive'
最初はこれくらいシンプルに始めて、慣れたら細分化。
Q4: BtoBとBtoCで違いはある?
A: あります。
BtoC(EC):
- Recency: 30日、90日で区切る
- Frequency: 月1回以上が優良
- Monetary: 単価が安いので回数重視
BtoB(SaaS):
- Recency: 契約更新の周期(月次、年次)で見る
- Frequency: ログイン頻度、機能利用回数
- Monetary: MRR/ARRで判断
SaaS向けRFM:
# SaaS版RFM
saas_rfm = user_logs.groupby('customer_id').agg({
'last_login_date': lambda x: (analysis_date - x.max()).days, # Recency
'feature_usage_count': 'sum', # Frequency
'mrr': 'first' # Monetary
})
10. 発展: RFM + 機械学習
RFMはシンプルですが、機械学習と組み合わせるともっと強力になります。
RFMを特徴量にしたチャーン予測
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# RFMスコアを特徴量に
X = rfm_scored[['R_score', 'F_score', 'M_score', 'recency', 'frequency', 'monetary']]
# チャーンラベル(仮): 180日以上購入なし
y = (rfm_scored['recency'] > 180).astype(int)
# 学習
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
model = RandomForestClassifier(random_state=42, n_estimators=100)
model.fit(X_train, y_train)
# 予測
churn_proba = model.predict_proba(X_test)[:, 1]
print(f"Churn prediction AUC: {roc_auc_score(y_test, churn_proba):.3f}")
RFMを使って「次にチャーンしそうな顧客」を予測できます。
まとめ
RFM分析で得られること
-
顧客を客観的に評価できる
- 感覚ではなく、データで判断
-
セグメント別の施策が明確になる
- 全員に同じメールを送る時代は終わり
-
リソースを効率的に配分できる
- At RiskやCan't Lose Themに集中投資
-
実装が簡単
- SQL一本、Python数十行で完成