3 параметра клиента:
Каждому клиенту присваиваем скор от 1 до 5 по каждому параметру через NTILE или процентили:
WITH customer_metrics AS (
SELECT
customer_id,
CURRENT_DATE - MAX(order_date) AS days_since_last_order,
COUNT(*) AS total_orders,
SUM(order_amount) AS total_revenue
FROM orders
GROUP BY customer_id
),
rfm_scores AS (
SELECT
customer_id,
-- Reverse для Recency (меньше дней = выше скор)
6 - NTILE(5) OVER (ORDER BY days_since_last_order) AS R,
NTILE(5) OVER (ORDER BY total_orders) AS F,
NTILE(5) OVER (ORDER BY total_revenue) AS M
FROM customer_metrics
)
SELECT
customer_id, R, F, M,
CONCAT(R, F, M) AS rfm_code,
CASE
WHEN R >= 4 AND F >= 4 AND M >= 4 THEN 'Champions'
WHEN R >= 3 AND F >= 3 AND M >= 3 THEN 'Loyal'
WHEN R >= 4 AND F < 3 THEN 'New Customers'
WHEN R < 2 AND F >= 3 THEN 'At Risk'
WHEN R < 2 AND F < 3 THEN 'Lost'
ELSE 'Average'
END AS segment
FROM rfm_scores;
Топ-10 сегментов по RFM-коду:
| Сегмент | RFM-код | Действие |
|---|---|---|
| Champions | 555, 554 | VIP-программа, эксклюзивы |
| Loyal Customers | 454, 553 | Реферальная программа |
| Potential Loyalists | 543, 542 | Cross-sell, upsell |
| New Customers | 511, 512 | Welcome series, onboarding |
| Promising | 512, 521 | Бренд-awareness email |
| At Risk | 244, 144 | Reactivation кампания + бонус |
| Cannot Lose Them | 155, 254 | Personal call, премиум-сервис |
| Lost | 111, 112 | Reactivation скидка 50%+ |
Тот же расчёт на pandas:
import pandas as pd
def rfm_analysis(orders: pd.DataFrame, today=pd.Timestamp.now()) -> pd.DataFrame:
# Метрики на клиента
metrics = orders.groupby('customer_id').agg(
recency_days=('order_date', lambda x: (today - x.max()).days),
frequency=('order_id', 'count'),
monetary=('order_amount', 'sum'),
).reset_index()
# RFM скоры (1-5)
metrics['R'] = pd.qcut(metrics['recency_days'], 5, labels=[5,4,3,2,1])
metrics['F'] = pd.qcut(metrics['frequency'].rank(method='first'), 5, labels=[1,2,3,4,5])
metrics['M'] = pd.qcut(metrics['monetary'].rank(method='first'), 5, labels=[1,2,3,4,5])
# RFM код
metrics['RFM'] = metrics['R'].astype(str) + metrics['F'].astype(str) + metrics['M'].astype(str)
# Сегмент
metrics['Segment'] = 'Average'
metrics.loc[(metrics['R']>=4) & (metrics['F']>=4) & (metrics['M']>=4), 'Segment'] = 'Champions'
metrics.loc[(metrics['R']<=2) & (metrics['F']<=2), 'Segment'] = 'Lost'
# ... etc
return metrics
Реальные кейсы из РФ-компаний:
Классически — 5×5×5 = 125 возможных кодов, группируются в 8-12 actionable сегментов. Меньше — теряется детализация, больше — нечего делать с микро-группами.
NTILE(5) даёт равные группы по 20%. Custom thresholds (например, R=5 if days<7) — более интерпретируемые. На production используют второе.
Меньше дней с последней покупки = более активен = должен иметь высокий R-скор. NTILE(5) ORDER BY days даёт ASC (1 = меньше дней), поэтому делаем 6-NTILE = 5 для свежих покупок.
Раз в месяц для большинства бизнесов. Для fast-moving (food delivery) — раз в неделю. Для luxury — раз в квартал.
RFM сегментирует клиентов в моменте (snapshot). Когорты отслеживают изменения во времени. RFM используется для action (на кого таргетить), когорты — для measurement (что меняется).