RFM-анализ в Pandas: сегментация клиентов с обработкой пропусков | AiManual
AiManual Logo Ai / Manual.
06 Янв 2026 Гайд

RFM-анализ в Pandas: как разобрать клиентов по полочкам, даже если половина данных пропала

Пошаговое руководство по RFM-анализу в Python с Pandas. Учимся сегментировать клиентов и правильно обрабатывать пропуски в данных. Практический код для аналитик

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%)". Почти четверть данных без идентификатора клиента. Паниковать? Нет. Анализировать.

💡
Пропуски в CustomerID часто означают гостевые покупки, покупки без регистрации или технические ошибки. Это не "мусор", а отдельный сегмент клиентов, который может быть очень ценным.

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 - не разовая акция. Это должен быть постоянный процесс. Вот архитектура:

  1. Ежедневное обновление. Рассчитывайте RFM-оценки каждый день на основе скользящего окна (например, последние 2 года данных).
  2. Автоматические алерты. Когда клиент переходит из сегмента "Champions" в "About to Sleep" - триггер для менеджера.
  3. Интеграция с CRM. Экспортируйте RFM-сегменты в поля клиента в вашей CRM-системе.
  4. A/B тестирование. Разным сегментам - разные маркетинговые кампании. Измеряйте эффективность.

Если вам нужно анализировать неструктурированные данные (сканы инвойсов, PDF-отчеты), посмотрите мой тест OCR-решений для агентов. Там разбираю, как вытаскивать данные из документов, которые не хотят превращаться в таблицы.

RFM vs современные методы: что лучше?

RFM - метод 90-х. Но он жив, потому что прост и понятен. Современные альтернативы:

  • Кластеризация на эмбеддингах. Можно использовать эмбеддинги LLM для анализа предпочтений, но это требует вычислительных ресурсов.
  • Матричная факторизация. Для рекомендательных систем - отлично. Для сегментации - избыточно.
  • Глубокое обучение. Autoencoders могут найти сложные паттерны, но объяснить их бизнесу сложнее, чем "клиент 555".

Мой совет: начните с RFM. Он даст быстрый результат. Когда поймете его ограничения (а они есть), переходите к более сложным методам. RFM - это как обучение езде на велосипеде с дополнительными колесами. Сначала нужно научиться балансировать, потом уже делать трюки.

Самый неочевидный совет: RFM для внутренних процессов

Вы думаете, RFM только для клиентов? Примените его к вашим сотрудникам:

  • Recency: Когда последний раз проходил обучение?
  • Frequency: Как часто закрывает задачи вовремя?
  • Monetary: Какую ценность приносит компании?

Или к вашим поставщикам. Или к багам в коде. RFM-логика работает везде, где есть временные метки, частотность и количественная оценка. Это не про маркетинг. Это про приоритизацию в условиях ограниченных ресурсов.

Если вы работаете с большими объемами данных и боитесь, что RFM-расчеты будут долгими - не бойтесь. Pandas справляется с миллионами строк на обычном ноутбуке. Для действительно больших данных посмотрите мой пайплайн для работы с датасетами на CPU.

А теперь откройте ваш датасет. Посмотрите на пропуски. Не удаляйте их - изучайте. В них может быть скрыта самая интересная история о ваших клиентах.