Почему предсказание оттока клиентов важнее вашего утреннего кофе
Давайте признаем — терять клиентов всё равно что быть брошенным после отличного первого свидания. Вы думали, что всё идёт гладко, а потом — бац! — они исчезают без объяснений. В мире бизнеса мы называем это «оттоком», и это тихий убийца потоков доходов. Я узнал это на собственном горьком опыте, когда моя любимая кофейня внезапно закрылась, потому что они не смогли предсказать, какие клиенты уйдут к новому ремесленному заведению на углу.
Именно поэтому сегодня мы создаём проверенную систему прогнозирования оттока, используя XGBoost — алгоритм, который выиграл больше соревнований по машинному обучению, чем я успел поужинать. Мы пройдёмся по всему процессу от сырых данных до практических выводов, с примерами кода и визуализациями. К концу вы сможете предсказывать, какие клиенты собираются уйти, быстрее, чем вы скажете «двойной эспрессо».
План: Наш конвейер прогнозирования оттока
Прежде чем погрузиться в код, давайте визуализируем нашу стратегию на поле боя. Каждый успешный проект по прогнозированию оттока следует этому рабочему процессу:
Это не просто академическая болтовня — каждый этап напрямую влияет на способность вашей модели выявлять утекающих клиентов. Пропустите этап разработки признаков? Ваша модель станет бесполезной, как дверь с сеткой на подводной лодке. Пренебрегёте настройкой гиперпараметров? Ваши прогнозы будут такими же точными, как прогнозы погоды моей тёти Мардж.
Шаг 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