RFM - это не магия, а три цифры, которые расскажут о ваших клиентах больше, чем они сами о себе знают
Вот типичная картина: у вас есть таблица с продажами, менеджеры кричат "нужна сегментация клиентов!", а в данных 15% записей без CustomerID. Знакомо? Я проверил - в реальных данных из e-commerce пропуски в идентификаторах клиентов встречаются в 30% случаев.
RFM-анализ - это не сложная математика. Это просто три метрики: Recency (давность), Frequency (частота), Monetary (деньги). Но как их считать, если часть данных похожа на швейцарский сыр? Вот об этом и поговорим.
Главная ошибка новичков: удалять все строки с пропусками. Вы теряете до 30% данных и получаете искаженную картину. Сегодняшние анонимные покупки могут стать завтрашними лояльными клиентами.
1 Подготовка данных: с чем имеем дело
Допустим, у вас типичный датасет продаж. Колонки: InvoiceNo, StockCode, Description, Quantity, InvoiceDate, UnitPrice, CustomerID, Country. Пропуски обычно в CustomerID и Description.
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# Загружаем данные
# Это реальный датасет от UCI Machine Learning Repository
df = pd.read_csv('online_retail.csv', encoding='ISO-8859-1')
# Смотрим на пропуски
print(f"Всего строк: {len(df)}")
print(f"Пропуски в CustomerID: {df['CustomerID'].isna().sum()} ({df['CustomerID'].isna().mean():.1%})")
print(f"Пропуски в Description: {df['Description'].isna().sum()} ({df['Description'].isna().mean():.1%})")
Результат может испугать: "Пропуски в CustomerID: 135080 (24.9%)". Почти четверть данных без идентификатора клиента. Паниковать? Нет. Анализировать.
2 Стратегия обработки пропусков: не удалять, а разделять
Вот как НЕ надо делать:
# ПЛОХО: удаляем все пропуски
df_clean = df.dropna(subset=['CustomerID'])
print(f"Осталось строк: {len(df_clean)}") # Потеряли 25% данных!
Вот правильный подход:
# Разделяем данные на две группы
identified_customers = df[df['CustomerID'].notna()].copy()
anonymous_purchases = df[df['CustomerID'].isna()].copy()
print(f"Идентифицированные покупки: {len(identified_customers)}")
print(f"Анонимные покупки: {len(anonymous_purchases)}")
# Для анонимных покупок создаем временный ID на основе комбинации признаков
# Это позволит анализировать их как отдельную группу
anonymous_purchases['TempCustomerID'] = (
anonymous_purchases['InvoiceNo'].astype(str) + '_' +
anonymous_purchases['InvoiceDate'].astype(str).str[:10]
)
Теперь у вас две группы: идентифицированные клиенты (для детального RFM) и анонимные покупки (для анализа паттернов).
3 Рассчитываем RFM-метрики: математика без сюрпризов
Берем только идентифицированных клиентов для классического RFM. Сначала подготовка:
# Преобразуем дату
identified_customers['InvoiceDate'] = pd.to_datetime(identified_customers['InvoiceDate'])
# Считаем общую сумму покупки
identified_customers['TotalAmount'] = identified_customers['Quantity'] * identified_customers['UnitPrice']
# Удаляем возвраты (отрицательные суммы)
identified_customers = identified_customers[identified_customers['TotalAmount'] > 0]
# Определяем дату анализа (обычно последняя дата в данных + 1 день)
analysis_date = identified_customers['InvoiceDate'].max() + timedelta(days=1)
Теперь сама магия RFM:
# Группируем по клиенту
rfm = identified_customers.groupby('CustomerID').agg({
'InvoiceDate': lambda x: (analysis_date - x.max()).days, # Recency
'InvoiceNo': 'nunique', # Frequency
'TotalAmount': 'sum' # Monetary
}).reset_index()
rfm.columns = ['CustomerID', 'Recency', 'Frequency', 'Monetary']
print(rfm.head())
print(f"\nСтатистика:\n{rfm.describe()}")
Осторожно с Frequency! Часто считают количество транзакций (InvoiceNo), а не количество покупок. Для клиента, купившего 10 товаров в одной транзакции, Frequency = 1, а не 10.
4 Секреты квантилей: почему простые процентили вас обманут
Стандартный подход - разделить клиентов на 5 групп по каждому показателю (1-5, где 5 - лучшие). Но вот проблема:
# ПЛОХО: используем простые квантили
rfm['R_Score'] = pd.qcut(rfm['Recency'], 5, labels=[5, 4, 3, 2, 1]) # Инвертируем: меньшая давность = лучше
rfm['F_Score'] = pd.qcut(rfm['Frequency'], 5, labels=[1, 2, 3, 4, 5])
rfm['M_Score'] = pd.qcut(rfm['Monetary'], 5, labels=[1, 2, 3, 4, 5])
Что не так? Если у вас 80% клиентов купили один раз, а 20% - больше, qcut создаст искусственные границы. Вместо этого:
# ЛУЧШЕ: используем стратегические границы
# Для Recency: чем недавнее покупка, тем лучше
def custom_r_score(x):
if x <= 30: return 5 # Купили в последний месяц
elif x <= 90: return 4 # Купили в последний квартал
elif x <= 180: return 3 # Купили в последние полгода
elif x <= 365: return 2 # Купили в последний год
else: return 1 # Больше года назад
rfm['R_Score'] = rfm['Recency'].apply(custom_r_score)
# Для Frequency и Monetary используем квантили, но с проверкой
def safe_qcut(series, q=5, labels=None):
"""Безопасное разбиение на квантили с обработкой дубликатов"""
try:
return pd.qcut(series, q=q, labels=labels, duplicates='drop')
except:
# Если значения повторяются, используем rank
return pd.cut(series.rank(method='first'), bins=q, labels=labels)
rfm['F_Score'] = safe_qcut(rfm['Frequency'], 5, labels=[1, 2, 3, 4, 5])
rfm['M_Score'] = safe_qcut(rfm['Monetary'], 5, labels=[1, 2, 3, 4, 5])
5 Сегментация: комбинируем RFM-оценки
Теперь у нас три цифры для каждого клиента. Например, 555 - идеальный клиент (покупал недавно, часто и много). 111 - потерянный клиент. Но как группировать?
# Создаем RFM-сегмент
rfm['RFM_Segment'] = rfm['R_Score'].astype(str) + rfm['F_Score'].astype(str) + rfm['M_Score'].astype(str)
# Создаем стратегические сегменты
segment_map = {
r'5[4-5][4-5]': 'Champions', # Лучшие клиенты
r'[4-5][4-5][1-3]': 'Loyal Customers', # Лояльные, но не самые дорогие
r'5[1-3][1-3]': 'Recent Customers', # Новые клиенты
r'[3-4][1-3][1-3]': 'Potential Loyalists', # Могут стать лояльными
r'[1-2][4-5][4-5]': 'Cant Lose Them', # Ценные, но неактивные
r'[1-2][1-3][1-3]': 'Hibernating', # Спящие клиенты
r'[1-2][1-2][4-5]': 'About to Sleep', # Почти уснули, но были ценными
}
def get_segment_group(rfm_score):
for pattern, segment in segment_map.items():
if pd.Series([rfm_score]).str.match(pattern).any():
return segment
return 'Others'
rfm['Segment_Group'] = rfm['RFM_Segment'].apply(get_segment_group)
# Смотрим распределение
segment_distribution = rfm['Segment_Group'].value_counts(normalize=True) * 100
print(segment_distribution)
| Сегмент | % клиентов | Действия |
|---|---|---|
| Champions | 5-10% | VIP-обслуживание, ранний доступ |
| Loyal Customers | 10-15% | Программы лояльности |
| Hibernating | 20-30% | Реактивация, спецпредложения |
6 А что с анонимными покупками? Не оставляем их за бортом
Помните те 25% данных без CustomerID? Они тоже ценны. Анализируем их отдельно:
# Анализ анонимных покупок
anonymous_purchases['InvoiceDate'] = pd.to_datetime(anonymous_purchases['InvoiceDate'])
anonymous_purchases['TotalAmount'] = anonymous_purchases['Quantity'] * anonymous_purchases['UnitPrice']
# Группируем по временному ID (одна сессия покупок)
anonymous_rfm = anonymous_purchases.groupby('TempCustomerID').agg({
'InvoiceDate': 'max',
'TotalAmount': 'sum',
'StockCode': 'count'
}).reset_index()
anonymous_rfm.columns = ['SessionID', 'LastPurchase', 'SessionValue', 'ItemsCount']
# Анализируем паттерны
print(f"Средний чек анонимной сессии: {anonymous_rfm['SessionValue'].mean():.2f}")
print(f"Медианный чек анонимной сессии: {anonymous_rfm['SessionValue'].median():.2f}")
print(f"Всего анонимных сессий: {len(anonymous_rfm)}")
Эти данные показывают, сколько денег вы теряете из-за отсутствия идентификации. Если средний чек анонимных покупок высок - нужно упрощать регистрацию.
7 Визуализация: показываем результаты так, чтобы менеджеры поняли
RFM-матрица - лучший способ визуализации. Но не просто цветные квадратики, а с данными:
import matplotlib.pyplot as plt
import seaborn as sns
# Создаем сводную таблицу для визуализации
rfm_pivot = rfm.pivot_table(
index='R_Score',
columns='F_Score',
values='Monetary',
aggfunc='mean'
)
plt.figure(figsize=(10, 8))
sns.heatmap(rfm_pivot, annot=True, fmt=".0f", cmap='YlOrRd', linewidths=.5)
plt.title('RFM Matrix: Средний Monetary по сегментам')
plt.xlabel('Frequency Score')
plt.ylabel('Recency Score')
plt.tight_layout()
plt.show()
Еще одна полезная визуализация - распределение клиентов по сегментам:
# Распределение по стратегическим сегментам
segment_counts = rfm['Segment_Group'].value_counts()
plt.figure(figsize=(10, 6))
segment_counts.plot(kind='bar', color='skyblue', edgecolor='black')
plt.title('Распределение клиентов по RFM-сегментам')
plt.xlabel('Сегмент')
plt.ylabel('Количество клиентов')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
Типичные ошибки, которые сведут на всю вашу работу
Я видел десятки реализаций RFM. Вот что ломает анализ чаще всего:
- Игнорирование возвратов. Если не удалить отрицательные Amount, Monetary будет занижен. Но еще хуже - удалить их до подсчета Frequency (потеряется информация о проблемных клиентах).
- Неправильная дата анализа. Если взять max(InvoiceDate) вместо max(InvoiceDate) + 1 день, у самых активных клиентов будет Recency = 0, что исказит ранжирование.
- Смешивание валют. Если у вас международные продажи, Monetary в разных валютах - это катастрофа. Конвертируйте в одну валюту или анализируйте по странам отдельно.
- Игнорирование выбросов. Один клиент мог купить на $100 000, а остальные - на $100. Без обработки выбросов Monetary-квантили будут бессмысленны.
Проверка на выбросы: если 95-й перцентиль Monetary в 10 раз больше медианы - у вас проблема. Используйте логарифмическое преобразование или winsorization перед расчетом квантилей.
Как интегрировать RFM-анализ в вашу аналитическую систему
RFM - не разовая акция. Это должен быть постоянный процесс. Вот архитектура:
- Ежедневное обновление. Рассчитывайте RFM-оценки каждый день на основе скользящего окна (например, последние 2 года данных).
- Автоматические алерты. Когда клиент переходит из сегмента "Champions" в "About to Sleep" - триггер для менеджера.
- Интеграция с CRM. Экспортируйте RFM-сегменты в поля клиента в вашей CRM-системе.
- A/B тестирование. Разным сегментам - разные маркетинговые кампании. Измеряйте эффективность.
Если вам нужно анализировать неструктурированные данные (сканы инвойсов, PDF-отчеты), посмотрите мой тест OCR-решений для агентов. Там разбираю, как вытаскивать данные из документов, которые не хотят превращаться в таблицы.
RFM vs современные методы: что лучше?
RFM - метод 90-х. Но он жив, потому что прост и понятен. Современные альтернативы:
- Кластеризация на эмбеддингах. Можно использовать эмбеддинги LLM для анализа предпочтений, но это требует вычислительных ресурсов.
- Матричная факторизация. Для рекомендательных систем - отлично. Для сегментации - избыточно.
- Глубокое обучение. Autoencoders могут найти сложные паттерны, но объяснить их бизнесу сложнее, чем "клиент 555".
Мой совет: начните с RFM. Он даст быстрый результат. Когда поймете его ограничения (а они есть), переходите к более сложным методам. RFM - это как обучение езде на велосипеде с дополнительными колесами. Сначала нужно научиться балансировать, потом уже делать трюки.
Самый неочевидный совет: RFM для внутренних процессов
Вы думаете, RFM только для клиентов? Примените его к вашим сотрудникам:
- Recency: Когда последний раз проходил обучение?
- Frequency: Как часто закрывает задачи вовремя?
- Monetary: Какую ценность приносит компании?
Или к вашим поставщикам. Или к багам в коде. RFM-логика работает везде, где есть временные метки, частотность и количественная оценка. Это не про маркетинг. Это про приоритизацию в условиях ограниченных ресурсов.
Если вы работаете с большими объемами данных и боитесь, что RFM-расчеты будут долгими - не бойтесь. Pandas справляется с миллионами строк на обычном ноутбуке. Для действительно больших данных посмотрите мой пайплайн для работы с датасетами на CPU.
А теперь откройте ваш датасет. Посмотрите на пропуски. Не удаляйте их - изучайте. В них может быть скрыта самая интересная история о ваших клиентах.