Представьте: вы тратите бюджет на email-рассылки, но CTR не превышает 20%. Причина — ручная сегментация по 3-5 группам, которые не отражают реального поведения клиентов. ML-подход автоматически выявляет до 20 статистически значимых кластеров на основе RFM-признаков, временных паттернов и демографии. Например, для интернет-магазина бытовой техники мы выявили 12 сегментов вместо 4 ручных, что повысило CTR email-кампаний с 18% до 55%. В результате CTR маркетинговых кампаний растёт с 15-25% до 40-60%. Наш стек: Python, PyTorch, KMeans, DBSCAN, PCA, UMAP, LLM (Claude, GPT). Опыт — 30+ проектов по ML-сегментации для e-commerce и fintech.
Проблемы, которые решаем
Неоптимальное число сегментов. Ручной выбор 3-5 групп игнорирует естественную структуру данных. Мы используем elbow-метод и силуэтный коэффициент для автоматического определения числа кластеров (обычно 6-15). Метод [Silhouette] (https://en.wikipedia.org/wiki/Silhouette_(clustering)) позволяет оценить качество кластеризации.
Интерпретируемость чёрного ящика. Кластеры KMeans без описания бесполезны для маркетинга. Поэтому мы добавили LLM-генерацию названий и портретов сегментов — модель получает центроиды и статистику, возвращает готовый текст на естественном языке.
Масштабирование на миллионы клиентов. Кластеризация 1M записей занимает ~25 минут, real-time назначение сегмента — 5-10 мс. Мы применяем PCA для ускорения и инкрементальный пересчёт при добавлении 10%+ новых пользователей.
Как мы это делаем: pipeline
Feature engineering — ключевой этап. Мы строим 25+ признаков: RFM, среднеквадратичное отклонение суммы заказа, доля покупок в выходные, ночные покупки, средний межзаказный интервал и его регулярность. Полный пайплайн включает 6 шагов:
- Сбор и предобработка данных (очистка, объединение таблиц).
- Feature engineering: генерация 25+ признаков (RFM, временные, поведенческие).
- Выбор алгоритма кластеризации: KMeans для выпуклых форм, DBSCAN для невыпуклых.
- Обучение модели с автоопределением числа кластеров (elbow + silhouette).
- LLM-генерация описаний сегментов (Claude, GPT).
- Развёртывание REST API для real-time назначения.
Код класса CustomerSegmentation демонстрирует полный пайплайн.
import pandas as pd import numpy as np from anthropic import Anthropic from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans, DBSCAN from sklearn.decomposition import PCA import umap class CustomerSegmentation: def __init__(self, customers_df: pd.DataFrame, orders_df: pd.DataFrame): self.customers = customers_df self.orders = orders_df self.llm = Anthropic() self.scaler = StandardScaler() self.segments = None def build_rfm_features(self) -> pd.DataFrame: """RFM + поведенческие признаки""" now = pd.Timestamp.now() rfm = self.orders.groupby('customer_id').agg( recency_days=('order_date', lambda x: (now - x.max()).days), frequency=('order_id', 'nunique'), monetary=('amount', 'sum'), avg_order_value=('amount', 'mean'), first_order_days_ago=('order_date', lambda x: (now - x.min()).days), order_std=('amount', 'std'), max_order=('amount', 'max'), category_diversity=('category', 'nunique'), ).reset_index() # Заполнение NaN для одиночных заказов rfm['order_std'] = rfm['order_std'].fillna(0) # Временные паттерны self.orders['order_hour'] = pd.to_datetime(self.orders['order_date']).dt.hour self.orders['order_dow'] = pd.to_datetime(self.orders['order_date']).dt.dayofweek time_features = self.orders.groupby('customer_id').agg( preferred_hour=('order_hour', lambda x: x.mode()[0]), weekend_ratio=('order_dow', lambda x: (x >= 5).mean()), night_ratio=('order_hour', lambda x: ((x >= 22) | (x < 6)).mean()), ).reset_index() # Межзаказный интервал self.orders_sorted = self.orders.sort_values(['customer_id', 'order_date']) self.orders_sorted['prev_order'] = self.orders_sorted.groupby('customer_id')['order_date'].shift(1) self.orders_sorted['days_between'] = ( pd.to_datetime(self.orders_sorted['order_date']) - pd.to_datetime(self.orders_sorted['prev_order']) ).dt.days interval_features = self.orders_sorted.groupby('customer_id').agg( avg_days_between=('days_between', 'mean'), purchase_regularity=('days_between', lambda x: 1 / (x.std() + 1)) ).reset_index() # Объединение всех признаков features = rfm.merge(time_features, on='customer_id', how='left') features = features.merge(interval_features, on='customer_id', how='left') features = features.merge(self.customers[['customer_id', 'city', 'age', 'gender']], on='customer_id', how='left') return features Кластеризация с оптимальным числом сегментов
def find_optimal_segments(self, features_df: pd.DataFrame, max_k: int = 20) -> int: """Метод elbow + silhouette для выбора числа кластеров""" from sklearn.metrics import silhouette_score X = features_df.select_dtypes(include='number').fillna(0) X_scaled = self.scaler.fit_transform(X) # Снижение размерности для ускорения pca = PCA(n_components=min(20, X_scaled.shape[1])) X_pca = pca.fit_transform(X_scaled) inertias = [] silhouettes = [] for k in range(2, min(max_k + 1, len(X_pca))): km = KMeans(n_clusters=k, random_state=42, n_init=10) labels = km.fit_predict(X_pca) inertias.append(km.inertia_) if k <= 15: # Silhouette дорогой для больших k silhouettes.append(silhouette_score(X_pca, labels, sample_size=2000)) # Elbow method diffs = np.diff(inertias) diff2 = np.diff(diffs) elbow_k = np.argmax(diff2) + 3 # +3 из-за двойного diff и смещения # Проверяем, что silhouette подтверждает sil_optimal = np.argmax(silhouettes) + 2 # Компромисс optimal_k = round((elbow_k + sil_optimal) / 2) return max(4, min(optimal_k, max_k)) def cluster_customers(self, features_df: pd.DataFrame, n_clusters: int = None) -> pd.DataFrame: """Кластеризация и описание сегментов""" numeric_features = features_df.select_dtypes(include='number').fillna(0) X_scaled = self.scaler.fit_transform(numeric_features) if n_clusters is None: n_clusters = self.find_optimal_segments(features_df) # K-Means как основной алгоритм km = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) features_df['cluster'] = km.fit_predict(X_scaled) # UMAP для визуализации (2D) reducer = umap.UMAP(n_components=2, random_state=42) X_2d = reducer.fit_transform(X_scaled) features_df['umap_x'] = X_2d[:, 0] features_df['umap_y'] = X_2d[:, 1] self.segments = features_df self.cluster_centers = pd.DataFrame( self.scaler.inverse_transform(km.cluster_centers_), columns=numeric_features.columns ) return features_df LLM-описание сегментов
def describe_segments(self) -> dict[int, dict]: """Автоматическое описание каждого кластера через LLM""" if self.segments is None: raise ValueError("Run cluster_customers first") segment_descriptions = {} for cluster_id in self.segments['cluster'].unique(): cluster_data = self.segments[self.segments['cluster'] == cluster_id] center = self.cluster_centers.iloc[cluster_id] # Статистика по кластеру stats = { 'size': len(cluster_data), 'pct_of_total': len(cluster_data) / len(self.segments) * 100, 'avg_recency_days': cluster_data['recency_days'].mean(), 'avg_frequency': cluster_data['frequency'].mean(), 'avg_monetary': cluster_data['monetary'].mean(), 'avg_order_value': cluster_data['avg_order_value'].mean(), 'weekend_ratio': cluster_data.get('weekend_ratio', pd.Series([0])).mean(), } response = self.llm.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=400, messages=[{ "role": "user", "content": f"""Ты маркетинговый аналитик. Опиши сегмент клиентов по данным. Статистика сегмента: - Размер: {stats['size']:,} клиентов ({stats['pct_of_total']:.1f}% от базы) - Средняя давность покупки: {stats['avg_recency_days']:.0f} дней назад - Средняя частота: {stats['avg_frequency']:.1f} заказов - Средняя выручка: {stats['avg_monetary']:,.0f} руб. - Средний чек: {stats['avg_order_value']:,.0f} руб. - Доля покупок в выходные: {stats['weekend_ratio']:.1%} Дай: 1. Название сегмента (2-4 слова, например "Лояльные чемпионы" или "Группа риска") 2. Описание в 2-3 предложениях — кто эти люди, их паттерн поведения 3. Рекомендуемую маркетинговую стратегию (1-2 конкретных действия)""" }] ) text = response.content[0].text lines = text.strip().split('\n') segment_descriptions[cluster_id] = { 'stats': stats, 'name': lines[0].replace('1. ', '').strip() if lines else f"Segment {cluster_id}", 'description': text, 'cluster_id': cluster_id } return segment_descriptions Автоматическое назначение сегментов новым клиентам
def assign_new_customer(self, customer_features: dict) -> dict: """Real-time сегментация нового клиента""" feature_vector = pd.DataFrame([customer_features]) numeric_cols = self.cluster_centers.columns.tolist() for col in numeric_cols: if col not in feature_vector.columns: feature_vector[col] = 0 feature_vector_scaled = self.scaler.transform(feature_vector[numeric_cols]) # Расстояния до центров кластеров from sklearn.metrics import pairwise_distances centers_scaled = self.scaler.transform(self.cluster_centers) distances = pairwise_distances(feature_vector_scaled, centers_scaled)[0] cluster_id = distances.argmin() confidence = 1 - distances[cluster_id] / distances.sum() return { 'cluster_id': int(cluster_id), 'confidence': float(confidence), 'distance': float(distances[cluster_id]) } Как выбрать оптимальное число сегментов?
Мы не гадаем на кофейной гуще. Используем комбинацию elbow-метода (поиск точки перегиба) и силуэтного коэффициента. На практике оптимальное число — от 6 до 15. Для баз свыше 100K записей применяем PCA с 20 компонентами — это ускоряет расчёты в 3 раза без потери качества.
Почему LLM-описание эффективнее ручного анализа?
Маркетолог тратит 2-3 дня на анализ каждого кластера и часто ошибается в интерпретации. LLM (мы используем Claude, GPT) получает числовые центроиды и за 10 секунд генерирует название, описание и стратегию. Например, сегмент с высокой частотой и низкой давностью получает имя "Лояльные чемпионы" и рекомендацию по программам лояльности. Это снижает время на сегментацию для маркетинга на 90% и повышает точность таргетированного маркетинга.
Производительность на реальных данных
| Размер базы | Число признаков | Время кластеризации | Оптимальных кластеров |
|---|---|---|---|
| 10K клиентов | 25 | ~30 сек | 6-8 |
| 100K клиентов | 25 | ~3 мин | 10-15 |
| 1M клиентов | 25 | ~25 мин | 15-25 |
| 1M клиентов | 25 + PCA(20) | ~8 мин | 15-25 |
Для real-time назначения сегментов предобученная модель скорит нового клиента за 5-10 мс. Пересчёт сегментации полной базы — раз в неделю или при накоплении 10%+ новых клиентов.
Сравнение алгоритмов кластеризации
| Критерий | K-Means | DBSCAN |
|---|---|---|
| Форма кластеров | Выпуклая | Любая |
| Чувствительность к шуму | Высокая | Низкая |
| Необходимость числа кластеров | Да | Нет |
| Скорость | Высокая | Средняя |
| Масштабируемость | Отличная | Хорошая |
Выбор алгоритма зависит от структуры данных: для чётких сферических групп — K-Means, для сложных форм с выбросами — DBSCAN.
Типичные ошибки при сегментации
- Использование только RFM-признаков без поведенческих (время покупок, регулярность). - Выбор числа кластеров на глаз без объективных метрик. - Игнорирование сезонности и выбросов. - Отсутствие интерпретации сегментов — кластеры остаются «чёрным ящиком». - Нерегулярное обновление модели при изменении клиентской базы.Что входит в работу
- Feature engineering под вашу структуру данных (до 30 признаков).
- Обучение моделей KMeans/DBSCAN с автовыбором числа кластеров.
- LLM-описание каждого сегмента на русском языке.
- REST API для real-time назначения сегментов (latency p99 < 20 мс).
- Документация пайплайна и кода.
- Интеграция с вашей CRM или email-платформой.
- Обучение команды работе с сегментацией.
Сроки: от 2 до 8 недель в зависимости от объёма данных и сложности интеграции. Стоимость проекта — от 2000 до 8000 у.е. в зависимости от объёма данных.
Получите консультацию: наши инженеры с 5+ лет опыта в ML анализируют вашу базу, подбирают оптимальный алгоритм. Гарантируем повышение CTR и экономию маркетингового бюджета до 20-30%. Свяжитесь с нами для демонстрации пайплайна на ваших данных.







