0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RFM分析で顧客セグメンテーション

Posted at

はじめに

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: 1RFMスコア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分析で得られること

  1. 顧客を客観的に評価できる

    • 感覚ではなく、データで判断
  2. セグメント別の施策が明確になる

    • 全員に同じメールを送る時代は終わり
  3. リソースを効率的に配分できる

    • At RiskやCan't Lose Themに集中投資
  4. 実装が簡単

    • SQL一本、Python数十行で完成
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?