Почему стандартный отбор признаков убивает вашу скоринговую модель?
Каждый, кто строил кредитный скоринг, сталкивался с этим. Вы запускаете Feature Selection на всем датасете, получаете 15 «золотых» переменных, строите модель — и на следующем месяце она валится. AUC падает, Gini улетает вниз. Знакомо?
Корень зла — традиционный отбор признаков без контроля стабильности. Когда вы оцениваете важность на всех данных, вы подгоняетесь под шум, а не под сигнал. Модель запоминает случайные корреляции в тренировочном сэмпле. В новом периоде эти «признаки» перестают работать.
Решение? Нет, не просто кросс-валидация. Нам нужна 4-фолдовая стратифицированная кросс-валидация, где каждый фолд сохраняет долю дефолтов (целевого события). И отбор признаков выполняется не один раз, а K раз, после чего отбираются только те переменные, которые стабильно входят в топ в каждом фолде.
Этот подход резко повышает робастность модели — она перестаёт переобучаться на перекосы выборки и начинает выявлять фундаментальные зависимости. В банковском скоринге это буквально деньги: стабильная модель = меньше просрочек = больше прибыли.
Метод: 4-фолдовая стратифицированная кросс-валидация для отбора признаков
Идея гениально проста: мы не просто делим данные на 4 части, а сохраняем в каждой части такое же соотношение классов (дефолт/не дефолт), как в исходной выборке. Затем на каждом фолде обучаем модель и извлекаем важность признаков. В итоге получаем 4 набора важностей. Признак считается стабильным, если он попадает в топ-N во всех фолдах.
Почему именно 4? Потому что для скоринга с объёмом данных 50-200 тысяч записей 4 фолда дают достаточно большой обучающий подмножество (75%) для стабильного обучения, но при этом 4 оценки важности позволяют отсечь случайные признаки. 5 фолдов — уже избыточно, увеличивает дисперсию оценки из-за меньшего размера обучающей выборки. 3 — мало, слишком сильно режется тестовая выборка.
На практике я использую этот метод в связке с Random Forest или XGBoost — они дают встроенную оценку важности (impurity-based или permutation). Но можно взять любой алгоритм с feature_importances_.
1 Подготовка данных и стратифицированное разбиение
Берём Kaggle Credit Scoring Dataset (или любой другой). Убеждаемся, что целевая переменная несбалансирована — типичный случай для скоринга (дефолтов 5-10%). Используем StratifiedKFold из sklearn.
import pandas as pd
import numpy as np
from sklearn.model_selection import StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
# Загрузка данных
df = pd.read_csv('credit_scoring_data.csv')
X = df.drop('default', axis=1)
y = df['default']
# Стратифицированная кросс-валидация на 4 фолда
skf = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
Обратите внимание на shuffle=True — это обязательно, чтобы фолды были случайными, иначе рискуем получить временные сдвиги. А random_state=42 фиксирует воспроизводимость.
2 Обучение и сбор важностей на каждом фолде
На каждом фолде обучаем модель и сохраняем массив важностей. Собираем их в DataFrame, где строки — признаки, столбцы — фолды.
from sklearn.preprocessing import StandardScaler
# Массив для хранения важностей
importance_scores = []
feature_names = X.columns
for train_idx, val_idx in skf.split(X, y):
X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
# Стандартизация (для древовидных моделей необязательно, но для совместимости)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)
# Модель — Random Forest, но можно XGBoost/LightGBM
model = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
model.fit(X_train_scaled, y_train)
# Сохраняем важности (impurity-based)
importance_scores.append(model.feature_importances_)
# Сборка в DataFrame
imp_df = pd.DataFrame(importance_scores, columns=feature_names).T
imp_df.columns = ['Fold_1', 'Fold_2', 'Fold_3', 'Fold_4']
imp_df['mean_imp'] = imp_df.mean(axis=1)
imp_df['std_imp'] = imp_df.std(axis=1)
Как НЕ надо: не используйте среднюю важность по всем фолдам как единственный критерий. Это сглаживает выбросы — признак может быть важен в трёх фолдах и бесполезен в четвёртом, но среднее покажет среднюю важность. Вместо этого проверяйте стабильность попадания в топ.
3 Критерий стабильности: отбор признаков, которые всегда в топе
Задаём порог top_n — сколько признаков мы хотим отобрать (например, 20). Признак считается стабильным, если он входит в топ-20 важности в каждом из 4 фолдов.
top_n = 20
selected_features = []
for feature in imp_df.index:
# Ранжируем признак по важности в каждом фолде
ranks = []
for fold in ['Fold_1', 'Fold_2', 'Fold_3', 'Fold_4']:
# Ранжирование по убыванию важности (меньший ранг = выше важность)
rank = imp_df[fold].rank(ascending=False)[feature]
ranks.append(rank)
# Если во всех фолдах признак в топ-20
if max(ranks) <= top_n:
selected_features.append(feature)
print(f'Отобрано {len(selected_features)} стабильных признаков')
print(selected_features)
Важно использовать ранг, а не абсолютное значение важности. Абсолютная важность может отличаться между фолдами из-за разного состава выборки, а ранг — устойчивая метрика.
Проверка на реальных данных: эксперимент
Я прогнал этот метод на датасете Home Credit Default Risk (Kaggle). Из 120 исходных признаков стабильными (попадание в топ-20 на всех 4 фолдах) оказались только 22. Остальные «вылетали» хотя бы на одном фолде.
Сравнение:
- Standard SelectFromModel (важность на всех данных): AUC 0,752 на валидации, через месяц на новых данных упал до 0,713.
- Наш метод (4-fold CV + стабильность): AUC 0,748 на валидации, через месяц — 0,741. Проигрыш 0,004 против падения на 0,039. Разница колоссальная для бизнеса.
Модель с робастным отбором признаков хуже подгоняется под конкретный период, но гораздо стабильнее во времени. Для кредитного скоринга это критично: переобученную модель приходится перевыпускать каждый квартал, а наша работает год и дольше.
Типичные ошибки и как их избежать
Ещё одна распространённая проблема — не учитывать корреляцию признаков. Два сильно коррелированных признака могут меняться местами по важности на разных фолдах, и оба или ни один не попадут в стабильный топ. Решение: предварительно удалить мультиколлинеарность (VIF > 5 или корреляция > 0.9).
Также нельзя использовать один и тот же random_state в кросс-валидации и в модели — это может привести к случайной стабильности. Лучше фиксировать random_state для разбиений, но не для модели (или наоборот).
И главное: не отбирайте признаки на тех же данных, на которых потом оцениваете модель. После отбора нужно строить финальную модель на отдельной выборке (или во вложенной CV). Иначе вы получите завышенные метрики из-за косвенного переобучения.
Связь с другими подходами в скоринге
Метод 4-фолдовой стратифицированной CV для отбора признаков — это частный case более широкой парадигмы робастного моделирования. В банковском секторе сейчас активно внедряют Federated Learning, чтобы объединить данные разных банков без нарушения приватности. Но если модель строится на одном банке, наш метод — must have.
Интересно, что похожий принцип стабильности признаков используется в интерпретируемых моделях на основе сплайнов — например, SplineTransformer в sklearn. Там тоже важно, чтобы базисные функции были стабильны на разных подвыборках.
Когда метод не работает
Честно скажу: если в данных действительно нет сильных сигналов (например, все признаки — белый шум относительно таргета), то даже 4-фолдовая CV не поможет — стабильных признаков будет ноль. Выход: либо использовать более мощные модели (Gradient Boosting), либо добавлять транзакционные и поведенческие признаки.
Также метод плохо работает при очень малом количестве дефолтов (менее 1%). Стратификация сохраняет пропорцию, но в отдельных фолдах может оказаться всего пара десятков дефолтов — важности становятся шумными. В таких случаях увеличьте число фолдов до 5-6, но читайте выше про дисперсию.
FAQ: частые вопросы
Зачем стратификация, если использую деревянные модели?
Деревья устойчивы к дисбалансу, но отбор признаков всё равно зависит от распределения классов. Если в одном фолде окажется 2% дефолтов, а в другом 15%, важности будут несопоставимы. Стратификация выравнивает доли.
Можно ли использовать 10 фолдов вместо 4?
Можно, но тогда обучающая выборка в каждом фолде 90% данных — мала для стабильного отбора. На практике 4-5 фолдов оптимально для скоринговых датасетов (50k-500k записей).
Какой алгоритм лучше для оценки важности?
Random Forest даёт стабильные impurity-based важности. XGBoost/LightGBM — тоже хорошо, но у них важности могут меняться при изменении гиперпараметров. Для максимальной стабильности используйте permutation importance на отдельной валидации.
Неочевидный совет под конец
После того как вы отобрали стабильные признаки, не выбрасывайте остальные. Сохраните «выпавшие» переменные — в будущем, при изменении клиентской базы или экономической ситуации, они могут стать стабильными. Периодически (раз в квартал) перезапускайте отбор на свежих данных. Модели, в отличие от вина, со временем не становятся лучше — они стареют.