Вы знаете это неприятное чувство, когда осознаёте, что ваш любимый клиент только что ушёл? Оказывается, есть способ увидеть, что они уходят, прежде чем они соберутся с вещами. Добро пожаловать в чудесный мир прогнозирования оттока клиентов, где дата-сайенс встречается с инстинктом выживания бизнеса.

В этом подробном обзоре я покажу вам, как построить надёжную систему прогнозирования оттока клиентов с использованием XGBoost. Мы говорим не просто о теории — это практические вещи, которые действительно работают в продакшене. К концу вы получите модель, которая сможет идентифицировать клиентов, рискующих уйти, давая вашему бизнесу шанс их сохранить.

Почему прогнозирование оттока важнее, чем вы думаете

Будем честными: привлечение новых клиентов дорого. Очень дорого. Привлечение нового клиента обходится примерно в 5–25 раз дороже, чем удержание существующего. Когда клиенты начинают уходить, дело не только в потере дохода — вы теряете нарастающую ценность. Клиент, который заплатил бы ещё в течение 5 лет? Ушёл. Рекомендации, которые он мог бы дать? Не случилось.

Прогнозирование оттока меняет эту ситуацию. Выявляя клиентов с риском ухода на ранней стадии, вы можете применить целевые стратегии удержания. Может быть, это скидка, персонализированная поддержка или функция, о которой они не знали. Суть в том, что у вас появляется шанс.

Понимание ситуации

Прежде чем мы углубимся в код, давайте разберёмся, с чем мы работаем. Прогнозирование оттока клиентов — это задача бинарной классификации: клиенты либо уходят, либо нет. Сложность заключается в том, что реальные данные об оттоке часто несбалансированы: 80–90% клиентов не уходят, а только 10–20% уходят. Этот дисбаланс может ввести в заблуждение наивные модели, заставляя их предсказывать «отсутствие оттока» для всех и выглядеть довольно хорошо по точности.

Именно здесь XGBoost проявляет себя. Это алгоритм градиентного бустинга, известный своей способностью работать с сложными наборами данных. В отличие от более простых моделей, XGBoost последовательно строит ансамбль решающих деревьев, где каждое дерево исправляет ошибки предыдущего. Результат? Превосходная производительность на сложных, реальных наборах данных.

Архитектура системы

graph LR A[Необработанные данные о клиентах] --> B[Очистка данных и EDA] B --> C[Разработка признаков] C --> D[Кодирование и масштабирование] D --> E[Разделение на обучающую и тестовую выборки] E --> F[Обработка дисбаланса классов] F --> G[Обучение модели XGBoost] G --> H[Настройка гиперпараметров] H --> I[Оценка модели] I --> J[Интерпретируемость SHAP] J --> K[Развёртывание в продакшене]

Приступаем к работе: реализация

Шаг 1: Настройка среды

Сначала давайте подготовим нужные инструменты:

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from xgboost import XGBClassifier
from sklearn.metrics import (
    accuracy_score, 
    precision_score, 
    recall_score, 
    f1_score,
    roc_auc_score,
    confusion_matrix,
    classification_report
)
from imblearn.over_sampling import SMOTE
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# Установить случайное начальное число для воспроизводимости
np.random.seed(42)

Шаг 2: Загрузка и исследование данных

Мы будем использовать набор данных Telco Customer Churn, классический в ML сообществе:

# Загрузка данных
df = pd.read_csv('telco_customer_churn.csv')
# Первый взгляд
print(f"Форма набора данных: {df.shape}")
print(f"\nПервые несколько строк:")
print(df.head())
# Проверка на наличие пропущенных значений
print(f"\nПропущенные значения:\n{df.isnull().sum()}")
# Понимание целевой переменной
print(f"\nРаспределение оттока:")
print(df['Churn'].value_counts())
print(f"\nПроцент оттока:")
print(df['Churn'].value_counts(normalize=True) * 100)

Набор данных содержит информацию о клиентах, включая демографические данные (возраст, пол), особенности услуг (интернет-сервис, онлайн-безопасность) и шаблоны использования. Наша целевая переменная — Churn — указывает, ушёл ли клиент из компании.

Шаг 3: Очистка данных и разработка признаков

Здесь начинается интересное. Необработанные данные беспорядочны, и их очистка — половина дела:

# Обработка столбца TotalCharges (в нём есть нечисловые записи)
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
# Заполнение пропущенных значений в TotalCharges нулями
df['TotalCharges'].fillna(0, inplace=True)
# Создание бинарной целевой переменной
df['Churn'] = (df['Churn'] == 'Да').astype(int)
# Разработка признаков: создание полезных признаков
df['MonthlyChargesPerService'] = df['MonthlyCharges'] / (df['PhoneService'].str.count('Да') + 1)
df['AvgMonthlyCharges'] = df['TotalCharges'] / (df['tenure'] + 1)
df['IsHighValue'] = ((df['MonthlyCharges'] > df['MonthlyCharges'].quantile(0.75)) & 
                      (df['tenure'] > df['tenure'].median())).astype(int)
# Разделение признаков и целевой переменной
X = df.drop(['customerID', 'Churn'], axis=1)
y = df['Churn']
print(f"Форма признаков: {X.shape}")
print(f"Распределение целевой переменной:\n{y.value_counts()}")

Шаг 4: Обработка категориальных переменных

Модели машинного обучения нуждаются в числах. Вот как мы преобразуем категориальные данные:

# Идентификация категориальных и числовых столбцов
categorical_cols = X.select_dtypes(include=['object']).columns.tolist()
numerical_cols = X.select_dtypes(include=['int64', 'float64']).columns.tolist()
print(f"Категориальные столбцы: {categorical_cols}")
print(f"Числовые столбцы: {numerical_cols}")
# Создание конвейера предварительной обработки
from sklearn.preprocessing import OneHotEncoder
preprocessor = ColumnTransformer(
    transformers=[
        ('num', StandardScaler(), numerical_cols),
        ('cat', OneHotEncoder(drop='first', sparse_output=False), categorical_cols)
    ]
)
# Применение предварительной обработки
X_processed = preprocessor.fit_transform(X)
print(f"Форма обработанных признаков: {X_processed.shape}")

Шаг 5: Разделение на обучающую и тестовую выборки и обработка дисбаланса классов

Теперь нам нужно тщательно разделить данные и решить проблему дисбаланса:

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
    X_processed, 
    y, 
    test_size=0.2, 
    random_state=42,
    stratify=y
)
print(f"Размер обучающей выборки: {X_train.shape}")
print(f"Размер тестовой выборки: {X_test.shape}")
print(f"Процент оттока в обучающей выборке: {y_train.mean():.2%}")
# Обработка дисбаланса классов с помощью SMOTE
smote = SMOTE(random_state=42)
X_train_balanced, y_train_balanced = smote.fit_resample(X_train, y_train)
print(f"\nПосле SMOTE:")
print(f"Размер обучающей выборки: {X_train_balanced.shape}")
print(f"Процент оттока: {y_train_balanced.mean():.2%}")

Шаг 6: Обучение модели XGBoost

Вот и звезда шоу:

# Инициализация модели XGBoost
model = XGBClassifier(
    n_estimators=100,
    max_depth=6,
    learning_rate=0.1,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42,
    eval_metric='logloss',
    verbosity=1
)
# Обучение модели
model.fit(
    X_train_balanced, 
    y_train_balanced,
    eval_set=[(X_test, y_test)],