← Назад к вопросам

ML: Построить модель скоринга клиентов

2.7 Senior🔥 131 комментариев
#Машинное обучение#Опыт и проекты

Условие

Вам нужно построить модель кредитного скоринга для оценки вероятности дефолта клиента.

Опишите:

  1. Какие признаки использовать
  2. Как обработать данные (пропуски, выбросы, категориальные признаки)
  3. Какую модель выбрать и почему
  4. Как интерпретировать результаты для бизнеса

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

1. Выбор признаков для скоринга

Демографические признаки:

  • Возраст — более молодые имеют выше риск, U-образное распределение
  • Пол — может быть предиктивным в некоторых случаях
  • Город/Регион — экономическое состояние региона влияет на платежеспособность
  • Семейный статус — женатые/замужние более надёжны
  • Дети — влияет на финансовые обязательства

Финансовые признаки:

  • Доход — основной предиктор (нужна валидация)
  • Стаж работы — стабильность дохода
  • Тип занятости — постоянная/временная
  • Существующие кредиты — отношение долгов к доходу (DTI)
  • История платежей — дефолты/задержки в прошлом
  • Размер заёмки — большие суммы рискованнее
  • Срок кредита — длинные сроки = выше риск

Поведенческие признаки:

  • Кредитная история — 3-5 лет истории
  • Количество открытых счётов — диверсификация кредитов
  • Возраст первого кредита
  • Среднее использование кредитного лимита — финансовая ответственность

Альтернативные источники (для новых клиентов):

  • Цифровой след (социальные сети, интернет-активность)
  • Телефонные счета — историю платежей
  • Коммунальные платежи
  • Поведение в приложении (время заполнения анкеты, аккуратность)

2. Обработка данных

import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler, LabelEncoder, OneHotEncoder
from sklearn.impute import SimpleImputer
from scipy.stats import zscore

# Пример датасета
df = pd.read_csv('credit_data.csv')

print(f"Исходный размер: {df.shape}")
print(f"\nПропущенные значения:\n{df.isnull().sum()}")

# === 2.1 ПРОПУСКИ ===

# Пропуски в численных признаках: медиана
num_imputer = SimpleImputer(strategy='median')
df[['age', 'income', 'job_years']] = num_imputer.fit_transform(df[['age', 'income', 'job_years']])

# Пропуски в категориальных: мода или отдельная категория
df['city'].fillna(df['city'].mode()[0], inplace=True)
df['marital_status'].fillna('unknown', inplace=True)

# Пропуски в истории платежей: предположим 0 дефолтов (консервативно)
df['past_defaults'].fillna(0, inplace=True)

print(f"\nПосле заполнения пропусков: {df.isnull().sum().sum()} пропусков")

# === 2.2 ВЫБРОСЫ ===

# Метод IQR для численных признаков
def remove_outliers_iqr(df, columns, iqr_multiplier=1.5):
    for col in columns:
        Q1 = df[col].quantile(0.25)
        Q3 = df[col].quantile(0.75)
        IQR = Q3 - Q1
        
        lower_bound = Q1 - iqr_multiplier * IQR
        upper_bound = Q3 + iqr_multiplier * IQR
        
        mask = (df[col] >= lower_bound) & (df[col] <= upper_bound)
        print(f"{col}: {(~mask).sum()} выбросов удалено")
        df = df[mask]
    
    return df

df = remove_outliers_iqr(df, ['age', 'income', 'loan_amount'])

# Альтернатива: преобразование (log) вместо удаления
df['income'] = np.log1p(df['income'])  # логарифмическое преобразование
df['loan_amount'] = np.log1p(df['loan_amount'])

print(f"\nПосле обработки выбросов: {df.shape}")

# === 2.3 КАТЕГОРИАЛЬНЫЕ ПРИЗНАКИ ===

# OneHotEncoder для низкокардинальных (мало уникальных значений)
encoder = OneHotEncoder(sparse_output=False, drop='first')
categorical_cols = ['city', 'job_type', 'marital_status']
encoded = encoder.fit_transform(df[categorical_cols])
df_encoded = pd.DataFrame(encoded, columns=encoder.get_feature_names_out(categorical_cols))

# Объединяем с остальными данными
df = pd.concat([df.drop(categorical_cols, axis=1), df_encoded], axis=1)

# Или LabelEncoder для высококардинальных (много уникальных) — используй осторожно
le = LabelEncoder()
df['employment_type_encoded'] = le.fit_transform(df['employment_type'])

print(f"\nПосле кодирования категорий: {df.shape}")
print(f"\nЦветные признаки {df.columns.tolist()[:10]}...")

# === 2.4 МАСШТАБИРОВАНИЕ ===

# Стандартизация для алгоритмов, чувствительных к масштабу (логрег, SVM, KNN)
scaler = StandardScaler()
feature_columns = [col for col in df.columns if col != 'target']
df[feature_columns] = scaler.fit_transform(df[feature_columns])

print(f"\nПосле масштабирования: среднее={df[feature_columns].mean().mean():.3f}, std={df[feature_columns].std().mean():.3f}")

# === 2.5 ИНЖЕНЕРИЯ ПРИЗНАКОВ ===

# Новые признаки из существующих
df['debt_to_income_ratio'] = df['total_debt'] / (df['income'] + 1e-6)  # DTI
df['credit_utilization'] = df['used_credit'] / (df['credit_limit'] + 1e-6)  # % используемого кредита
df['age_group'] = pd.cut(df['age'], bins=[0, 25, 35, 50, 65, 100], labels=['18-25', '26-35', '36-50', '51-65', '65+'])
df['loan_to_income'] = df['loan_amount'] / (df['income'] + 1e-6)  # LTI
df['employment_stability'] = df['job_years'] / (df['age'] - 18)  # стабильность работы

print(f"\nПосле инженерии признаков: {df.shape}")
print(f"\nОсновная статистика новых признаков:")
print(df[['debt_to_income_ratio', 'credit_utilization', 'employment_stability']].describe())

3. Выбор модели

3.1 Почему Logistic Regression?

Плюсы:

  • Интерпретируемость — коэффициенты показывают влияние каждого признака
  • Скорость — быстро обучается и предсказывает
  • Вероятности — выдаёт вероятность дефолта (0-1)
  • Стандарт в банках — регуляторы требуют понимания модели
  • Стабильность — малая вероятность переобучения

Минусы: Не учитывает нелинейные связи

3.2 Построение модели

from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import roc_auc_score, roc_curve, precision_recall_curve, confusion_matrix, classification_report
import matplotlib.pyplot as plt

# Разделение данных
X = df.drop('default', axis=1)  # признаки
y = df['default']  # целевая переменная (0/1)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"Размер обучающей выборки: {X_train.shape}")
print(f"Распределение целевой переменной (train): \n{y_train.value_counts(normalize=True)}")

# === Модель 1: Logistic Regression ===

lr_model = LogisticRegression(
    max_iter=1000,
    class_weight='balanced',  # учитываем дисбаланс классов
    random_state=42,
    penalty='l2',  # регуляризация
    C=1.0  # сила регуляризации
)

lr_model.fit(X_train, y_train)

# Кроссвалидация
cv_scores = cross_val_score(lr_model, X_train, y_train, cv=StratifiedKFold(n_splits=5), scoring='roc_auc')
print(f"\nLogistic Regression AUC-ROC (5-fold CV): {cv_scores.mean():.4f} (+/- {cv_scores.std():.4f})")

# === Модель 2: Random Forest (для сравнения) ===

rf_model = RandomForestClassifier(
    n_estimators=100,
    max_depth=10,
    class_weight='balanced',
    random_state=42,
    n_jobs=-1
)

rf_model.fit(X_train, y_train)

cv_scores_rf = cross_val_score(rf_model, X_train, y_train, cv=StratifiedKFold(n_splits=5), scoring='roc_auc')
print(f"Random Forest AUC-ROC (5-fold CV): {cv_scores_rf.mean():.4f} (+/- {cv_scores_rf.std():.4f})")

# === Модель 3: Gradient Boosting (обычно лучший, но меньше интерпретируемый) ===

gb_model = GradientBoostingClassifier(
    n_estimators=100,
    learning_rate=0.05,
    max_depth=5,
    random_state=42
)

gb_model.fit(X_train, y_train)

cv_scores_gb = cross_val_score(gb_model, X_train, y_train, cv=StratifiedKFold(n_splits=5), scoring='roc_auc')
print(f"Gradient Boosting AUC-ROC (5-fold CV): {cv_scores_gb.mean():.4f} (+/- {cv_scores_gb.std():.4f})")

# Выбираем лучшую
best_model = lr_model  # в банках обычно логрег за интерпретируемость

print(f"\nВыбранная модель: Logistic Regression")

3.3 Оценка на тестовой выборке

# Предсказания
y_pred = best_model.predict(X_test)
y_pred_proba = best_model.predict_proba(X_test)[:, 1]

# ROC-AUC (главная метрика для кредитного скоринга)
auc_score = roc_auc_score(y_test, y_pred_proba)
print(f"\nTest AUC-ROC: {auc_score:.4f}")

# Матрица ошибок
cm = confusion_matrix(y_test, y_pred)
print(f"\nМатрица ошибок:")
print(f"  TN={cm[0,0]}, FP={cm[0,1]}")
print(f"  FN={cm[1,0]}, TP={cm[1,1]}")

# Бизнес-метрики
precision = cm[1,1] / (cm[1,1] + cm[0,1])  # из предсказанных дефолтов, сколько реальных
recall = cm[1,1] / (cm[1,1] + cm[1,0])  # из реальных дефолтов, сколько мы поймали
print(f"\nPrecision: {precision:.4f} (из 100 выданных кредитов высокого риска, ~{int(precision*100)} действительно дефолтирует)")
print(f"Recall: {recall:.4f} (мы ловим {int(recall*100)}% реальных дефолтов)")

# ROC кривая
fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba)
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'ROC curve (AUC = {auc_score:.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random classifier')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
plt.savefig('roc_curve.png')

print(f"\nROC кривая сохранена в roc_curve.png")

4. Интерпретация результатов для бизнеса

4.1 Коэффициенты модели

# Коэффициенты логистической регрессии
feature_importance = pd.DataFrame({
    'feature': X.columns,
    'coefficient': best_model.coef_[0]
}).sort_values('coefficient', key=abs, ascending=False)

print(f"\nТоп признаков по влиянию на риск:")
print(feature_importance.head(10))

# Интерпретация
print(f"\nИнтерпретация (пример):")
print(f"- coefficient=0.5 для 'age' означает: увеличение возраста на 1 стандартное отклонение")
print(f"  увеличивает odds дефолта в exp(0.5)={np.exp(0.5):.2f}x (на {(np.exp(0.5)-1)*100:.1f}%)")

4.2 Скор-карты для принятия решений

# Преобразование вероятности в скор (обычно 300-900 баллов)
def proba_to_score(proba, score_min=300, score_max=900):
    # Линейное отображение: 0.5 -> 600 баллов
    return score_min + (score_max - score_min) * (1 - proba)

df_test = X_test.copy()
df_test['default_probability'] = y_pred_proba
df_test['credit_score'] = proba_to_score(y_pred_proba)
df_test['actual_default'] = y_test.values

print(f"\nПримеры скоров:")
print(df_test[['default_probability', 'credit_score', 'actual_default']].head(10))

# Сегментирование клиентов
print(f"\nСегментирование по риску:")
df_test['risk_segment'] = pd.cut(df_test['credit_score'], 
                                   bins=[0, 400, 600, 800, 900], 
                                   labels=['HIGH', 'MEDIUM', 'LOW', 'VERY_LOW'])

for segment in ['HIGH', 'MEDIUM', 'LOW', 'VERY_LOW']:
    subset = df_test[df_test['risk_segment'] == segment]
    default_rate = subset['actual_default'].mean()
    print(f"{segment:10} ({len(subset):4} клиентов): {default_rate:.2%} дефолтов")

4.3 Рекомендации для бизнеса

📊 DASHBOARD ДЛЯ РУКОВОДСТВА:

1. РАСПРЕДЕЛЕНИЕ РИСКА:
   - HIGH (скор < 400): отклонить 90% заявок
   - MEDIUM (400-600): выдать с условиями (повышенный %)
   - LOW (600-800): стандартные условия
   - VERY_LOW (> 800): премиальные продукты

2. КЛЮЧЕВЫЕ РЫЧАГИ РИСКА:
   - Debt-to-Income > 0.5: дефолт вероятен в 35%
   - История дефолтов: игнорировать рискованно
   - Job tenure < 2 лет: нестабильный доход

3. ЭКОНОМИЧЕСКАЯ ЦЕЛЕСООБРАЗНОСТЬ:
   - Если процент по кредиту = 15%, убыток от дефолта = 85% суммы
   - Приемлемый дефолт на сегмент MEDIUM: < 25%
   - ROI модели: (выданные кредиты - убытки от дефолтов) / затраты на разработку

4. МОНИТОРИНГ В PRODUCTION:
   - Ежемесячное отслеживание: AUC > 0.75?
   - Drift detection: изменился ли дефолт rate?
   - Переобучение модели: при изменении экономики

Итоги

Лучший подход для кредитного скоринга:

  1. Начните с Logistic Regression — интерпретируемо, стандарт в банках
  2. Обработайте данные тщательно — пропуски, выбросы, масштабирование
  3. Используйте доменные знания — финансовые признаки важнее случайных
  4. Валидируйте кроссвалидацией — AUC-ROC > 0.7 = хороший скоринг
  5. Интерпретируйте для бизнеса — скор-карты, сегменты, рекомендации
  6. Мониторьте в production — модель дрейфует, нужны переобучение