Почему предсказание оттока клиентов важнее вашего утреннего кофе

Давайте признаем — терять клиентов всё равно что быть брошенным после отличного первого свидания. Вы думали, что всё идёт гладко, а потом — бац! — они исчезают без объяснений. В мире бизнеса мы называем это «оттоком», и это тихий убийца потоков доходов. Я узнал это на собственном горьком опыте, когда моя любимая кофейня внезапно закрылась, потому что они не смогли предсказать, какие клиенты уйдут к новому ремесленному заведению на углу.

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

План: Наш конвейер прогнозирования оттока

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

graph TD A[Сырые данные о клиентах] --> B[Предварительная обработка данных] B --> C[Разработка признаков] C --> D[Разделение на обучение и тестирование] D --> E[Обучение модели XGBoost] E --> F[Настройка гиперпараметров] F --> G[Оценка модели] G --> H[Прогнозы оттока] H --> I[Действия по удержанию]

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

Шаг 1: Подготовка данных — фундамент

Все отличные модели начинаются с грязных, реальных данных. Мы будем использовать набор данных Telco Customer Churn — это как «Hello World» для прогнозирования оттока. Сначала давайте загрузим и изучим наши данные:

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

Критические шаги по очистке данных:

  • Преобразовать TotalCharges в числовой формат (с ошибками)
  • Кодировать категориальные признаки (yes/no → 1/0)
  • Обработать пропущенные значения (импутация или удаление)
  • Сбалансировать классы с помощью SMOTE при необходимости
# Конвейер очистки данных
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df.fillna(df['TotalCharges'].median(), inplace=True)
# Кодирование категориальных признаков
cat_cols = ['gender', 'Partner', 'Dependents', 'PhoneService', 'PaperlessBilling']
df = pd.get_dummies(df, columns=cat_cols, drop_first=True)
# Преобразование целевого признака в двоичный
df['Churn'] = df['Churn'].map({'No': 0, 'Yes': 1})

Шаг 2: Разработка признаков — ваше секретное оружие

Сырые данные — это как необжаренные кофейные зёрна — полны потенциала, но бесполезны, пока не обработаны. Здесь мы извлекаем предсказательную ценность:

# Создание групп по длительности обслуживания
df['TenureGroup'] = pd.cut(df['tenure'], bins=[0, 12, 24, 48, float('inf')], 
                           labels=['Новый', 'Растущий', 'Устоявшийся', 'Ветеран'])
# Расчёт разнообразия услуг
services = ['PhoneService', 'InternetService', 'OnlineSecurity', 'StreamingMovies']
df['ServiceDiversity'] = df[services].sum(axis=1)
# Сегментация по денежной стоимости
df['MonetaryGroup'] = pd.qcut(df['MonthlyCharges'], q=4, labels=['Низкий', 'Средний', 'Высокий', 'VIP'])
# Кодирование новых признаков
df = pd.get_dummies(df, columns=['TenureGroup', 'MonetaryGroup'])

Совет профессионала: Всегда создавайте признаки, отражающие реальность бизнеса. Показатель «ServiceDiversity»? Появился благодаря осознанию, что клиенты с несколькими услугами уходят на 30% реже — чистая правда!

Шаг 3: Построение модели XGBoost — где происходит волшебство

Теперь к главному событию. XGBoost отлично справляется с прогнозированием оттока, поскольку обрабатывает несбалансированные данные и выявляет сложные закономерности:

import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
# Подготовка данных
X = df.drop(['customerID', 'Churn'], axis=1)
y = df['Churn']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
# Инициализация базовой модели
model = xgb.XGBClassifier(
    objective='binary:logistic',
    eval_metric='logloss',
    use_label_encoder=False,
    early_stopping_rounds=50
)
# Обучение с оценочным набором
model.fit(X_train, y_train,
          eval_set=[(X_test, y_test)],
          verbose=False)

Шаг 4: Настройка гиперпараметров — секретный соус

Параметры по умолчанию — это как обычный кофе — пить можно, но ничего особенного. Давайте улучшим нашу модель:

from sklearn.model_selection import RandomizedSearchCV
# Определение сетки параметров
param_grid = {
    'max_depth': [3, 5, 7],
    'learning_rate': [0.01, 0.1, 0.2],
    'subsample': [0.6, 0.8, 1.0],
    'colsample_bytree': [0.6, 0.8, 1.0],
    'gamma': [0, 0.1, 0.3],
    'reg_alpha': [0, 0.5, 1],
    'reg_lambda': [1, 1.5, 2]
}
# Случайный поиск
search = RandomizedSearchCV(
    estimator=model,
    param_distributions=param_grid,
    n_iter=50,
    scoring='f1',
    cv=3,
    verbose=1,
    random_state=42
)
search.fit(X_train, y_train)
# Лучшая модель
tuned_model = search.best_estimator_

Почему это работает: Мы фокусируемся на F1-оценке, а не на точности, потому что нам нужно сбалансировать точность (правильные прогнозы оттока) и полноту (выявление всех потенциальных утекающих). Пропустить потенциального утекающего клиента обходится дороже, чем ложная тревога!

Шаг 5: Оценка модели — проверка реальности

Модель бесполезна, если она не принимает более качественные решения, чем Magic 8-Ball. Давайте проверим как следует:

from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
# Генерация прогнозов
y_pred = tuned_model.predict(X_test)
y_proba = tuned_model.predict_proba(X_test)[:, 1]
# Отчёт о классификации
print(classification_report(y_test, y_pred))
# Матрица ошибок
cm = confusion_matrix(y_test, y_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm)
disp.plot(cmap='Blues')
plt.title('Матрица ошибок прогнозирования оттока')
plt.show()
# Важность признаков
xgb.plot_importance(tuned_model, importance_type='weight')
plt.title('Важность признаков')
plt.show()

Шаг 6: Интерпретация результатов — за пределами чёрного ящика

Настоящая ценность не только в прогнозах — важно понимать, ПОЧЕМУ клиенты уходят. Ввод SHAP-значений:

import shap
# Инициализация JS для визуализаций
shap.initjs()
# Объяснение прогнозов модели
explainer = shap.TreeExplainer(tuned_model)
shap_values = explainer.shap_values(X