Прогнозирование вероятности дефолта — одна из ключевых задач в управлении кредитными рисками, которая помогает банкам, инвестиционным компаниям и бизнесу принимать более взвешенные решения. Существует множество инструментов для таких прогнозов, хотя логистическая регрессия — пожалуй, наиболее популярный. Она позволяет на основе набора факторов (например, дохода клиента, кредитной истории, уровня долговой нагрузки) оценить вероятность того, что заемщик не сможет выполнить свои обязательства.
В этой статье мы разберем, как работает логистическая регрессия в контексте кредитного скоринга, почему ее до сих пор активно применяют даже при наличии более сложных моделей, и как на практике можно применять результаты логрегрессии для анализа и снижения финансовых рисков.
Математическая природа вероятности дефолта
Вероятность дефолта не является статичной величиной. Она представляет собой условную вероятность, зависящую от макроэкономических факторов, специфических характеристик заемщика и временного горизонта.
В математических терминах мы моделируем:
P(D=1|X,t)
где:
- D — индикатор дефолта;
- X — вектор предикторов;
- t — время.
Логистическая регрессия решает эту задачу через логит-трансформацию, которая преобразует ограниченную область вероятностей [0,1] в неограниченное пространство действительных чисел. Приведенная ниже формула учитывает, что даже небольшие изменения в финансовых показателях могут привести к существенным изменениям в кредитном риске.
P(D=1) = 1/(1 + exp(-(β₀ + β₁X₁ + … + βₖXₖ)))
Ключевая особенность данного метода состоит в том, что коэффициенты βᵢ в логистической регрессии имеют прямую экономическую интерпретацию. Экспонента коэффициента exp(βᵢ) показывает, во сколько раз изменяется отношение шансов (odds ratio) при увеличении соответствующего предиктора на единицу. Это делает модель не только предсказательным инструментом, но и средством понимания драйверов кредитного риска.
Отличия от линейной регрессии и преимущества для моделирования дефолтов
Линейная регрессия плохо подходит для моделирования дефолта. Помимо очевидных проблем с интерпретацией (предсказанные «вероятности» могут быть отрицательными или больше единицы), линейная регрессия нарушает базовые предположения о распределении ошибок. Остатки в модели дефолта следуют биномиальному, а не нормальному распределению.
Логистическая регрессия элегантно решает эти проблемы через использование максимального правдоподобия вместо метода наименьших квадратов. Это позволяет корректно работать с биномиальным распределением и обеспечивает асимптотически эффективные оценки параметров.
Более того, логистическая функция имеет S-образную форму, которая лучше отражает реальное поведение вероятности дефолта — медленные изменения в зонах низкого и высокого риска, и быстрые изменения в промежуточной зоне.
Еще одним важным преимуществом является робастность к выбросам в предикторах. В отличие от линейной регрессии, где единичный экстремальный случай может существенно исказить всю модель, логистическая регрессия через логит-трансформацию «сжимает» влияние экстремальных значений на предсказанную вероятность.
Подготовка данных и инжиринг признаков
При создании модели логрегрессии я рекомендую делать сначала винсоризацию. Эта техника ограничивает экстремальные значения на заданном уровне, например, 1-го и 99-го процентилей.
В отличие от других техник удаления выбросов, винсоризация сохраняет информацию о том, что наблюдение было экстремальным, однако предотвращает доминирование единичных наблюдений над всей моделью. Для финансовых коэффициентов также эффективна логарифмическая трансформация, которая стабилизирует дисперсию и делает распределения более симметричными.
import numpy as np
import pandas as pd
pd.set_option('display.expand_frame_repr', False)
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import StratifiedKFold
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, classification_report
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(42)
# Генерация данных
n_samples = 10000
n_features = 15
# Корреляционная структура
correlation_structure = np.array([
[1.0, -0.6, 0.4, -0.3, 0.2, -0.1, 0.3, -0.2, 0.1, 0.0, -0.2, 0.1, 0.0, -0.1, 0.2],
[-0.6, 1.0, -0.5, 0.4, -0.3, 0.2, -0.4, 0.3, -0.1, 0.1, 0.3, -0.2, 0.1, 0.2, -0.3],
[0.4, -0.5, 1.0, -0.2, 0.1, 0.0, 0.2, -0.1, 0.0, -0.1, -0.1, 0.0, -0.1, 0.0, 0.1],
[-0.3, 0.4, -0.2, 1.0, -0.7, 0.3, -0.3, 0.2, 0.0, 0.1, 0.2, -0.1, 0.0, 0.1, -0.2],
[0.2, -0.3, 0.1, -0.7, 1.0, -0.4, 0.2, -0.1, 0.0, -0.1, -0.1, 0.1, 0.0, -0.1, 0.1],
[-0.1, 0.2, 0.0, 0.3, -0.4, 1.0, -0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.1, 0.0, -0.1],
[0.3, -0.4, 0.2, -0.3, 0.2, -0.1, 1.0, -0.5, 0.1, 0.0, -0.2, 0.1, 0.0, -0.1, 0.2],
[-0.2, 0.3, -0.1, 0.2, -0.1, 0.0, -0.5, 1.0, 0.0, 0.1, 0.2, -0.1, 0.0, 0.1, -0.1],
[0.1, -0.1, 0.0, 0.0, 0.0, 0.1, 0.1, 0.0, 1.0, -0.3, 0.0, 0.2, -0.1, 0.0, 0.0],
[0.0, 0.1, -0.1, 0.1, -0.1, 0.0, 0.0, 0.1, -0.3, 1.0, 0.1, -0.2, 0.1, 0.0, 0.0],
[-0.2, 0.3, -0.1, 0.2, -0.1, 0.1, -0.2, 0.2, 0.0, 0.1, 1.0, -0.4, 0.2, 0.1, -0.1],
[0.1, -0.2, 0.0, -0.1, 0.1, 0.0, 0.1, -0.1, 0.2, -0.2, -0.4, 1.0, -0.3, 0.0, 0.1],
[0.0, 0.1, -0.1, 0.0, 0.0, 0.1, 0.0, 0.0, -0.1, 0.1, 0.2, -0.3, 1.0, -0.2, 0.0],
[-0.1, 0.2, 0.0, 0.1, -0.1, 0.0, -0.1, 0.1, 0.0, 0.0, 0.1, 0.0, -0.2, 1.0, -0.1],
[0.2, -0.3, 0.1, -0.2, 0.1, -0.1, 0.2, -0.1, 0.0, 0.0, -0.1, 0.1, 0.0, -0.1, 1.0]
])
mean_values = np.array([15.2, 0.08, 1.4, 0.12, 0.25, 2.1, 0.35, 0.18, 45.2, 12.5, 0.42, 8.7, 0.15, 0.28, 1.8])
std_values = np.array([8.5, 0.15, 0.8, 0.18, 0.22, 1.2, 0.28, 0.12, 25.3, 8.2, 0.25, 4.2, 0.08, 0.15, 0.9])
# Многомерное нормальное распределение
raw_features = np.random.multivariate_normal(
mean_values,
np.outer(std_values, std_values) * correlation_structure,
n_samples
)
feature_names = [
'total_assets_log', 'debt_to_equity', 'current_ratio', 'roa', 'roe',
'interest_coverage', 'quick_ratio', 'gross_margin', 'days_sales_outstanding',
'inventory_turnover', 'asset_turnover', 'times_interest_earned', 'net_margin',
'debt_service_coverage', 'working_capital_ratio'
]
df_raw = pd.DataFrame(raw_features, columns=feature_names)
# Подрезаем отрицательные и нулевые значения перед логарифмом
df_raw['total_assets_log'] = df_raw['total_assets_log'].clip(lower=1e-3)
# Целевая переменная с долей дефолтов
logit_scores = (
-3.3 - np.log(4) +
-1.8 * df_raw['debt_to_equity'] +
1.2 * df_raw['roa'] +
0.8 * df_raw['current_ratio'] +
-0.6 * (df_raw['debt_to_equity'] ** 2) +
0.4 * df_raw['interest_coverage'] +
-0.3 * df_raw['days_sales_outstanding'] / 30 +
0.5 * (
0.3 * df_raw['roa'] +
0.2 * df_raw['current_ratio'] -
0.4 * df_raw['debt_to_equity'] +
0.1 * np.log(df_raw['total_assets_log'])
) +
np.random.normal(0, 0.3, n_samples) # шум
)
probabilities = 1 / (1 + np.exp(-logit_scores))
probabilities = np.clip(probabilities, 0, 1)
default_indicator = np.random.binomial(1, probabilities, n_samples)
df_raw['default'] = default_indicator
print(f"Размер датасета: {df_raw.shape}")
print(f"Доля дефолтов: {df_raw['default'].mean():.3f}")
# Винсоризация
def winsorize_features(df, columns, lower=0.01, upper=0.99):
df_processed = df.copy()
for col in columns:
lower_bound = df[col].quantile(lower)
upper_bound = df[col].quantile(upper)
df_processed[col] = np.clip(df[col], lower_bound, upper_bound)
return df_processed
features_to_winsorize = [col for col in df_raw.columns if col != 'default']
df_processed = winsorize_features(df_raw, features_to_winsorize)
print(f"\nСтатистика после винсоризации:")
print(df_processed[features_to_winsorize].describe())
Размер датасета: (10000, 16)
Доля дефолтов: 0.075
Статистика после винсоризации:
total_assets_log debt_to_equity current_ratio roa roe interest_coverage quick_ratio gross_margin days_sales_outstanding inventory_turnover asset_turnover times_interest_earned net_margin debt_service_coverage working_capital_ratio
count 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000
mean 15.343710 0.081235 1.390704 0.122098 0.251567 2.100822 0.349515 0.180087 45.581052 12.540763 0.423866 8.687735 0.149811 0.279994 1.790421
std 8.137397 0.147711 0.789399 0.174781 0.213747 1.154943 0.275118 0.118326 25.070311 8.084325 0.243724 4.128946 0.078343 0.150229 0.891202
min 0.001000 -0.272165 -0.406898 -0.293800 -0.246702 -0.602735 -0.301610 -0.101459 -13.121770 -6.989811 -0.159074 -0.864134 -0.034982 -0.081429 -0.297719
25% 9.541722 -0.019146 0.846100 -0.000808 0.104604 1.300259 0.164783 0.098312 28.513061 6.943660 0.256751 5.813461 0.096294 0.176961 1.180576
50% 15.251600 0.080655 1.391257 0.120752 0.252355 2.099945 0.348699 0.182548 45.424519 12.596320 0.421500 8.676622 0.151273 0.279310 1.790160
75% 20.892387 0.183095 1.942370 0.244196 0.396721 2.908824 0.538779 0.260661 62.603809 18.157629 0.593434 11.513833 0.203931 0.384743 2.398039
max 34.933807 0.421799 3.224088 0.530462 0.770434 4.859833 1.009755 0.459612 105.576739 31.669739 1.007291 18.704949 0.337978 0.636137 3.889395
Представленный код демонстрирует генерацию реалистичного датасета финансовых показателей с корреляционной структурой, характерной для реальных компаний. Винсоризация ограничивает экстремальные значения на уровне 1-го и 99-го процентилей, что предотвращает доминирование выбросов над моделью, сохраняя при этом информацию о потенциальных рисках дефолта.
Профессиональная обработка данных включает не только статистическую нормализацию, но и создание условий взаимодействий между переменными, которые могут выявить скрытые паттерны риска. Например, низкая ликвидность может быть опасной только при высокой долговой нагрузке, что математически выражается через произведение соответствующих коэффициентов.
Создание значимых производных признаков
Популярные финансовые коэффициенты в кредитном моделировании не всегда отражают полную картину. И гораздо более информативными оказываются их комбинации и производные метрики, отражающие различные аспекты финансового здоровья компании.
И здесь на помощь приходят временные признаки. Тренды в ключевых показателях часто более предсказательны, чем их абсолютные значения. Компания с ухудшающейся рентабельностью, даже если она пока остается положительной, представляет больший риск, чем стабильно убыточная, но с улучшающейся динамикой.
# Создание производных признаков для повышения качества модели
def create_advanced_features(df):
"""
Создание сложных финансовых индикаторов, используемых в профессиональных кредитных моделях
"""
df_enhanced = df.copy()
# Комплексные показатели ликвидности
df_enhanced['liquidity_buffer'] = (
df['current_ratio'] * df['quick_ratio'] - df['working_capital_ratio']
)
# Показатель долговой устойчивости (Debt Sustainability Score)
df_enhanced['debt_sustainability'] = (
df['interest_coverage'] / (1 + df['debt_to_equity']) -
df['debt_service_coverage'] * 0.5
)
# Операционная эффективность
df_enhanced['operational_efficiency'] = (
df['asset_turnover'] * df['gross_margin'] +
1 / (1 + df['days_sales_outstanding'] / 365)
)
# Показатель финансового левериджа (нелинейная зависимость)
df_enhanced['leverage_risk'] = np.where(
df['debt_to_equity'] > 1.0,
np.log(1 + df['debt_to_equity']**1.5),
df['debt_to_equity']
)
# Композитный индикатор прибыльности
df_enhanced['profitability_composite'] = (
0.4 * df['roa'] + 0.3 * df['roe'] + 0.3 * df['net_margin']
) / (1 + abs(df['debt_to_equity']))
# Показатель операционного денежного потока (аппроксимация)
df_enhanced['cash_flow_proxy'] = (
df['net_margin'] * df['asset_turnover'] +
df['interest_coverage'] / df['times_interest_earned']
)
# Индикатор финансовой напряженности
df_enhanced['financial_stress'] = np.maximum(
0, 2 - df['current_ratio'] - df['interest_coverage']/5
)
# Взаимодействие размера и эффективности
df_enhanced['size_efficiency_interaction'] = (
df['total_assets_log'] * df_enhanced['operational_efficiency']
)
return df_enhanced
# Применяем создание производных признаков
df_enhanced = create_advanced_features(df_processed)
# Разделяем на features и target
feature_columns = [col for col in df_enhanced.columns if col != 'default']
X = df_enhanced[feature_columns]
y = df_enhanced['default']
# Стандартизация признаков
scaler = StandardScaler()
X_scaled = pd.DataFrame(
scaler.fit_transform(X),
columns=X.columns,
index=X.index
)
print(f"Количество признаков после feature engineering: {X_scaled.shape[1]}")
print(f"\nНовые производные признаки:")
new_features = [col for col in df_enhanced.columns if col not in df_processed.columns]
for feature in new_features:
if feature != 'default':
print(f"- {feature}: {df_enhanced[feature].describe()['mean']:.3f} ± {df_enhanced[feature].describe()['std']:.3f}")
Количество признаков после feature engineering: 23
Новые производные признаки:
- liquidity_buffer: -1.263 ± 0.954
- debt_sustainability: 1.813 ± 1.089
- operational_efficiency: 0.975 ± 0.096
- leverage_risk: 0.081 ± 0.148
- profitability_composite: 0.150 ± 0.052
- cash_flow_proxy: 0.400 ± 11.763
- financial_stress: 0.434 ± 0.543
- size_efficiency_interaction: 14.751 ± 7.704
После запуска функции инжиниринга признаков мы расширили исходный набор данных до 23 признаков, добавив 8 производных финансовых индикаторов, которые отражают ликвидность, долговую устойчивость, операционную эффективность, уровень левериджа, прибыльность, денежный поток, финансовую напряженность и взаимодействие размера компании с ее эффективностью.
Эти признаки не только увеличили информативность выборки, но и захватили нелинейные и композиционные эффекты, которые традиционные показатели не учитывают напрямую. Их статистики показывают разный масштаб и вариативность: от стабильных метрик вроде операционной эффективности (0.975 ± 0.096) до сильно колеблющегося прокси-денежного потока (0.400 ± 11.763), что создает богатую основу для построения более устойчивой и точной кредитной модели.
Создание взаимодействующих признаков требует глубокого понимания бизнес-логики. Например, показатель долговой устойчивости учитывает не только способность компании обслуживать долг (коэффициент покрытия процентов), но и ее общую долговую нагрузку. Высокое покрытие процентов может быть обманчивым, если компания имеет критически высокий уровень заемных средств.
Особого внимания заслуживают нелинейные трансформации, такие как риск финансового левериджа. В реальности риск долговой нагрузки растет не линейно, а экспоненциально после определенного порога. Компания с коэффициентом заемного и собственного капитала 0.5 и 1.0 различаются не в два раза по рискованности — разница гораздо более существенная.
Построение и валидация базовой логистической модели
Стратифицированная k-fold cross-validation в кредитном моделировании
Правильная валидация кредитных моделей требует особого подхода, учитывающего специфику финансовых данных. Стандартная валидация методом random split может привести к утечке данных (data leakage, заглядыванию в будущее), особенно если в датасете присутствуют временные зависимости или кластеризация наблюдений по отраслям или регионам.
Стратифицированная k-блочная перекрестная проверка (stratified k-fold cross-validation) обеспечивает сохранение пропорций классов в каждом фолде, что особенно важно для несбалансированных данных о дефолтах. При этом я предпочитаю использовать k=10 для достижения баланса между смещением (bias) и разбросом (variance) оценок качества модели.
from sklearn.metrics import roc_auc_score, log_loss, brier_score_loss
from sklearn.model_selection import StratifiedKFold
from sklearn.linear_model import LogisticRegression
from joblib import Parallel, delayed
from tqdm import tqdm
import numpy as np
import pandas as pd
# Настройка кросс-валидации
cv_strategy = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
logistic_model = LogisticRegression(
penalty='l1',
solver='saga',
C=1.0,
random_state=42,
max_iter=1000
)
cv_auc_scores = []
cv_log_loss_scores = []
cv_brier_scores = []
feature_importances = []
# Запуск кросс-валидации
for fold_idx, (train_idx, val_idx) in enumerate(tqdm(cv_strategy.split(X_scaled, y), total=cv_strategy.get_n_splits(), desc="CV folds")):
X_train_fold = X_scaled.iloc[train_idx]
X_val_fold = X_scaled.iloc[val_idx]
y_train_fold = y.iloc[train_idx]
y_val_fold = y.iloc[val_idx]
logistic_model.fit(X_train_fold, y_train_fold)
y_pred_proba = logistic_model.predict_proba(X_val_fold)[:, 1]
cv_auc_scores.append(roc_auc_score(y_val_fold, y_pred_proba))
cv_log_loss_scores.append(log_loss(y_val_fold, y_pred_proba))
cv_brier_scores.append(brier_score_loss(y_val_fold, y_pred_proba))
feature_importances.append(logistic_model.coef_[0])
print(f"\nCross-validation результаты (10-fold):")
print(f"AUC: {np.mean(cv_auc_scores):.4f} ± {np.std(cv_auc_scores):.4f}")
print(f"Log Loss: {np.mean(cv_log_loss_scores):.4f} ± {np.std(cv_log_loss_scores):.4f}")
print(f"Brier Score: {np.mean(cv_brier_scores):.4f} ± {np.std(cv_brier_scores):.4f}")
# Финальная модель на всех данных
final_model = LogisticRegression(
penalty='l1',
solver='saga',
C=1.0,
random_state=42,
max_iter=500
)
final_model.fit(X_scaled, y)
feature_importance_df = pd.DataFrame({
'feature': X_scaled.columns,
'coefficient': final_model.coef_[0],
'abs_coefficient': np.abs(final_model.coef_[0])
}).sort_values('abs_coefficient', ascending=False)
print(f"\nТоп-10 наиболее важных признаков:")
print(feature_importance_df.head(10)[['feature', 'coefficient']].to_string(index=False))
# Бутстрап с параллельной обработкой
n_bootstrap = 200
def fit_bootstrap(i):
boot_idx = np.random.choice(len(X_scaled), len(X_scaled), replace=True)
X_boot = X_scaled.iloc[boot_idx]
y_boot = y.iloc[boot_idx]
model = LogisticRegression(penalty='l1', solver='saga', C=1.0, max_iter=500, random_state=i)
model.fit(X_boot, y_boot)
return model.coef_[0]
bootstrap_coefficients = Parallel(n_jobs=-1)(
delayed(fit_bootstrap)(i) for i in tqdm(range(n_bootstrap), desc="Bootstrap")
)
bootstrap_coefficients = np.array(bootstrap_coefficients)
# Доверительные интервалы
confidence_intervals = [
(np.percentile(bootstrap_coefficients[:, i], 2.5),
np.percentile(bootstrap_coefficients[:, i], 97.5))
for i in range(X_scaled.shape[1])
]
feature_importance_df['ci_lower'] = [ci[0] for ci in confidence_intervals]
feature_importance_df['ci_upper'] = [ci[1] for ci in confidence_intervals]
feature_importance_df['is_significant'] = (
(feature_importance_df['ci_lower'] > 0) |
(feature_importance_df['ci_upper'] < 0)
)
print(f"\nСтатистически значимые признаки (95% ДИ не содержит 0):")
significant_features = feature_importance_df[feature_importance_df['is_significant']]
print(significant_features[['feature', 'coefficient', 'ci_lower', 'ci_upper']].to_string(index=False))
CV folds: 0%| | 0/10 [00:00<!--?, ?it/s]
CV folds: 10%|█ | 1/10 [00:06<00:56, 6.29s/it]/usr/local/lib/python3.12/dist-packages/sklearn/linear_model/_sag.py:348: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
CV folds: 20%|██ | 2/10 [00:12<00:48, 6.07s/it]
CV folds: 30%|███ | 3/10 [00:13<00:26, 3.81s/it]/usr/local/lib/python3.12/dist-packages/sklearn/linear_model/_sag.py:348: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
CV folds: 40%|████ | 4/10 [00:20<00:30, 5.04s/it]/usr/local/lib/python3.12/dist-packages/sklearn/linear_model/_sag.py:348: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
CV folds: 50%|█████ | 5/10 [00:26<00:26, 5.32s/it]
CV folds: 60%|██████ | 6/10 [00:31<00:21, 5.48s/it]/usr/local/lib/python3.12/dist-packages/sklearn/linear_model/_sag.py:348: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
CV folds: 70%|███████ | 7/10 [00:37<00:16, 5.64s/it]
CV folds: 80%|████████ | 8/10 [00:44<00:11, 5.96s/it]
CV folds: 90%|█████████ | 9/10 [00:48<00:05, 5.32s/it]
CV folds: 100%|██████████| 10/10 [00:52<00:00, 5.24s/it]
Cross-validation результаты (10-fold):
AUC: 0.7625 ± 0.0329
Log Loss: 0.2330 ± 0.0095
Brier Score: 0.0633 ± 0.0019
Топ-10 наиболее важных признаков:
feature coefficient
current_ratio 0.724347
interest_coverage 0.437830
days_sales_outstanding -0.250014
roe -0.185569
debt_to_equity -0.165780
leverage_risk -0.165780
liquidity_buffer 0.151884
roa 0.107753
profitability_composite 0.085039
times_interest_earned 0.081423
Bootstrap: 0%| | 0/200 [00:00<?, ?it/s]
Bootstrap: 100%|██████████| 200/200 [08:33<00:00, 2.57s/it]
Статистически значимые признаки (95% ДИ не содержит 0):
feature coefficient ci_lower ci_upper
interest_coverage 0.437830 -0.270204 -0.109371
days_sales_outstanding -0.250014 0.557263 0.874547
leverage_risk -0.165780 0.159303 0.959806
profitability_composite 0.085039 -0.403359 -0.133533
debt_service_coverage -0.013893 -0.270204 -0.109371
На основе полученных результатов можно сделать следующие выводы. Модель логистической регрессии показала достаточно хорошее качество предсказаний:
- Среднее значение AUC равно 0.7625 с умеренной дисперсией (±0.0329), что указывает на хорошую способность модели различать классы;
- Значения Log Loss (0.2330) и Brier Score (0.0633) также подтверждают адекватную калибровку вероятностей и небольшую ошибку предсказаний;
- Наибольший вклад в прогноз вносят такие показатели, как current_ratio, interest_coverage и days_sales_outstanding. При этом результаты бутстрапа показывают, что часть коэффициентов не является статистически значимыми (их 95%-й доверительный интервал содержит ноль), что указывает на неопределенность влияния этих признаков;
- Важными показателями с высокой уверенностью оказались interest_coverage и days_sales_outstanding, что позволяет считать их ключевыми факторами риска или устойчивости компаний в рассматриваемой задаче.
Бутстрап-оценка доверительных интервалов дает более реалистичное представление о неопределенности коэффициентов по сравнению с асимптотическими стандартными ошибками. В реальных условиях я всегда проверяю стабильность знаков коэффициентов по бутстрап-выборкам — признак с часто меняющимся знаком указывает на нестабильность модели.
Интерпретация коэффициентов и экономический смысл
Профессиональная интерпретация логистической модели идет далеко за пределы простого ранжирования признаков по важности. Каждый коэффициент несет конкретную экономическую информацию, которую можно перевести в практические решения для риск-менеджмента.
Рассмотрим коэффициент debt-to-equity ratio. Если β = -1.8, это означает, что увеличение долговой нагрузки на одну единицу снижает логарифм отношения шансов недефолта к дефолту на 1.8. В терминах отношения шансов: exp(-1.8) = 0.165, то есть каждая дополнительная единица debt-to-equity снижает шансы избежать дефолта в 6 раз. Эта информация напрямую влияет на кредитные лимиты и ценовую политику.
Особую ценность представляют взаимодействия между признаками. Коэффициент при произведении текущей ликвидности и долговой нагрузки может показывать, что высокая ликвидность частично компенсирует негативное влияние долга, но только до определенного предела. Такие выводы невозможно получить с помощью простого одномерного анализа или корреляционных матриц.
Калибровка модели — это еще один важный элемент профессионального подхода. Предсказанные вероятности должны соответствовать фактической частоте дефолтов в разных группах компаний по скоринговым категориям. Например, если модель прогнозирует 5% вероятность дефолта для определенной группы, то в реальности примерно 5% компаний из этой группы должны действительно оказаться в дефолте за заданный период.
Оптимизация модели
Регуляризация
Выбор оптимального значения параметра регуляризации C — это всегда компромисс между смещением (Bias) и дисперсией (Variance) модели. Слишком сильная регуляризация (малое C) может привести к недообучению (Underfitting), когда модель игнорирует важные закономерности в данных. Слишком слабая регуляризация (большое C) создает риск переобучения (Overfitting), особенно при наличии коррелированных признаков.
В кредитном моделировании я использую специализированную процедуру поиска по сетке Grid Search, которая учитывает не только AUC, но и стабильность коэффициентов и качество калибровки. Еще иногда применяю Elastic Net — регуляризацию, комбинирующую L1 и L2.
Особое внимание стоит уделить использованию стратифицированной выборки (Stratified Sampling) в каждом фолде кросс-валидации при поиске по сетке. Такой подход гарантирует, что каждая подвыборка будет репрезентативной, и исключает ситуации, когда какой-то фолд содержит слишком много или слишком мало дефолтов.
Работа с несбалансированными данными
Данные по кредитам и дефолтам почти всегда имеют существенные дисбалансы классов — дефолты составляют обычно 3–15% от общего числа наблюдений. Простое игнорирование этого факта приводит к моделям, которые хорошо выглядят по стандартным метрикам, но практически бесполезны для управления рисками.
Логистическая регрессия с учетом веса классов (Class-Weighted Logistic Regression) автоматически корректирует функцию потерь для учета дисбаланса. При этом важно правильно подобрать веса — популярный подход «balanced» может быть слишком агрессивным для реальных кредитных портфелей. Я предпочитаю использовать веса, основанные на экономических потерях:
- weight_0 = 1,
- weight_1 = (average_loss_given_default / average_profit_per_performing_loan).
Альтернативный подход — использование различных порогов классификации (Decision Thresholds). Вместо стандартного порога 0.5 оптимальный выбирается с учетом бизнес-целей:
- Минимизация ожидаемых потерь;
- Максимизация прибыли;
- Достижение целевого уровня точности/полноты (Precision/Recall).
Метод SMOTE (Synthetic Minority Oversampling Technique) и его вариации тоже могут помочь модели лучше обучаться при сильном дисбалансе классов, однако он требует осторожного применения к финансовым данным. Следует помнить, что любая синтетическая генерация дефолтов должна осуществляться со здравым экономическим смыслом, что не всегда гарантируется при интерполяции в пространстве признаков.
Потенциальные проблемы валидации модели и их решения
Разная природа распределений на разных горизонтах данных
В классической валидации модели мы обычно предполагаем, что обучающая и тестовая выборки происходят из одного распределения данных. Однако в реальных условиях это не всегда так: новые данные могут отличаться от исторических из-за изменений в экономике, поведении клиентов, регуляторных требований или других факторов. Если модель обучена на данных, которые сильно отличаются от будущих данных, ее показатели на тестовой выборке могут быть завышены, и модель плохо работает в реальности.
Противоположная валидация (Adversarial Validation) — техника, позволяющая обнаружить систематические различия между обучающей и тестовой выборками. Для идентификации различий обучается бинарный классификатор. Если AUC этого классификатора существенно больше 0.5, это указывает на значительные смещения распределений, которые могут негативно повлиять на качество модели.
Выявление таких различий позволяет скорректировать выбор признаков или подготовку данных перед финальным обучением.
Устойчивость модели в периоды кризисов и экномической нестабильности
Очевидно, что в периоды кризисов и экономической нестабильности частота дефолтов (а значит и их вероятность) будут значительно выше обычных. Это делает особенно важным стресс-тестирование моделей (Stress Testing Models), позволяя оценить их поведение в экстремальных условиях и понять, насколько устойчивы прогнозы вероятностей дефолта к резким изменениям экономических показателей. Такие проверки помогают выявить уязвимости портфеля, скорректировать кредитные лимиты и подготовить меры по снижению потенциальных потерь.
Как правило, способность к дискриминации у логрегрессий существенно снижается во время кризисов из-за увеличенной корреляции между факторами риска и общего ухудшения кредитных портфелей. Вот почему прогнозы модели в таких сценариях должны показывать разумное увеличение в прогнозируемой вероятности дефолта.
Метод симуляций Монте-Карло позволяет оценить полное распределение прогнозов модели с учетом разных допущений о корреляции между факторами риска и их волатильности. Такой подход дает гораздо более полное представление об неопределенности модели (Model Uncertainty) по сравнению с точечными оценками, получаемыми из одиночных стресс-сценариев.
# Продвинутая валидация и диагностика модели
from sklearn.calibration import calibration_curve
from sklearn.model_selection import StratifiedKFold, ParameterGrid
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, brier_score_loss, roc_curve, precision_recall_curve
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from tqdm import tqdm
import warnings
warnings.filterwarnings('ignore')
# Дополнительная оптимизация модели с Elasticnet
def advanced_model_selection_tqdm(X, y):
"""
Комплексная оптимизация модели с учетом нескольких критериев и отображением прогресса
"""
param_grid = {
'C': [0.001, 0.01, 0.1, 0.5, 1.0, 2.0],
'l1_ratio': [0.1, 0.5, 0.7],
'penalty': ['elasticnet'],
'solver': ['saga'],
'max_iter': [2000]
}
best_score = -np.inf
best_params = None
best_model = None
for params in tqdm(list(ParameterGrid(param_grid)), desc="Grid Search"):
model = LogisticRegression(random_state=42, **params)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
fold_scores = []
for train_idx, val_idx in cv.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]
model.fit(X_train, y_train)
y_pred = model.predict_proba(X_val)[:, 1]
# Composite Score: AUC - Brier - Penalty на большие коэффициенты
auc = roc_auc_score(y_val, y_pred)
brier = brier_score_loss(y_val, y_pred)
coef_penalty = np.mean(np.abs(model.coef_)) * 0.01
composite_score = auc - brier - coef_penalty
fold_scores.append(composite_score)
mean_score = np.mean(fold_scores)
if mean_score > best_score:
best_score = mean_score
best_params = params
best_model = LogisticRegression(random_state=42, **params)
best_model.fit(X, y)
return best_model, best_params
# Применяем оптимизацию
optimal_model, best_params = advanced_model_selection_tqdm(X_scaled, y)
print(f"\nОптимальные параметры: {best_params}")
# Обучение финальной модели
optimal_model.fit(X_scaled, y)
y_pred_proba_optimal = optimal_model.predict_proba(X_scaled)[:, 1]
# Анализ калибровки модели
def plot_calibration_analysis(y_true, y_pred_proba, n_bins=10):
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
# Диаграмма надежности
fraction_of_positives, mean_predicted_value = calibration_curve(
y_true, y_pred_proba, n_bins=n_bins
)
axes[0, 0].plot([0, 1], [0, 1], 'k--', label='Perfect calibration')
axes[0, 0].plot(mean_predicted_value, fraction_of_positives, 's-',
color='black', label='Model calibration')
axes[0, 0].set_xlabel('Mean Predicted Probability')
axes[0, 0].set_ylabel('Fraction of Positives')
axes[0, 0].set_title('Reliability Diagram')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
# ROC-кривая
fpr, tpr, _ = roc_curve(y_true, y_pred_proba)
auc_score = roc_auc_score(y_true, y_pred_proba)
axes[0, 1].plot(fpr, tpr, color='black', linewidth=2,
label=f'ROC (AUC = {auc_score:.3f})')
axes[0, 1].plot([0, 1], [0, 1], 'k--', alpha=0.5)
axes[0, 1].set_xlabel('False Positive Rate')
axes[0, 1].set_ylabel('True Positive Rate')
axes[0, 1].set_title('ROC Curve')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)
# Кривая Precision-Recall
precision, recall, _ = precision_recall_curve(y_true, y_pred_proba)
axes[1, 0].plot(recall, precision, color='black', linewidth=2,
label='Precision-Recall curve')
axes[1, 0].set_xlabel('Recall')
axes[1, 0].set_ylabel('Precision')
axes[1, 0].set_title('Precision-Recall Curve')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)
# Распределение скоринга по классам
default_scores = y_pred_proba[y_true == 1]
non_default_scores = y_pred_proba[y_true == 0]
axes[1, 1].hist(non_default_scores, bins=30, alpha=0.7, color='lightblue',
label='Non-Default', density=True)
axes[1, 1].hist(default_scores, bins=30, alpha=0.7, color='lightcoral',
label='Default', density=True)
axes[1, 1].set_xlabel('Predicted Probability')
axes[1, 1].set_ylabel('Density')
axes[1, 1].set_title('Score Distributions')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Дополнительная статистика калибровки
print(f"\nКалибровка модели:")
print(f"Brier Score: {brier_score_loss(y_true, y_pred_proba):.4f}")
# Приближение Хосмера-Лемешоу (Hosmer-Lemeshow)
df_cal = pd.DataFrame({'predicted': y_pred_proba, 'actual': y_true})
df_cal['decile'] = pd.qcut(df_cal['predicted'], 10, labels=False, duplicates='drop')
calibration_table = df_cal.groupby('decile').agg({'predicted': ['mean', 'count'], 'actual': 'mean'}).round(4)
calibration_table.columns = ['Mean_Predicted', 'Count', 'Observed_Rate']
print(f"\nТаблица калибровки по децилям:")
print(calibration_table)
# Запуск анализа калибровки
plot_calibration_analysis(y, y_pred_proba_optimal)
# Анализ стабильности коэффициентов
print(f"\nАнализ стабильности признаков:")
final_features = pd.DataFrame({
'feature': X_scaled.columns,
'coefficient': optimal_model.coef_[0],
'abs_coefficient': np.abs(optimal_model.coef_[0])
}).sort_values('abs_coefficient', ascending=False)
# Отбираем значимые признаки
significant_threshold = 0.05
significant_features_final = final_features[
final_features['abs_coefficient'] > significant_threshold
]
print(f"Значимые признаки (|коэф.| > {significant_threshold}):")
print(significant_features_final[['feature', 'coefficient']].to_string(index=False))
Grid Search: 0%| | 0/18 [00:00<?, ?it/s]
Grid Search: 100%|██████████| 18/18 [06:15<00:00, 20.83s/it]
Оптимальные параметры: {'C': 0.01, 'l1_ratio': 0.1, 'max_iter': 2000, 'penalty': 'elasticnet', 'solver': 'saga'}

Рис. 1: Комплексный анализ качества логистической модели демонстрирует хорошую дискриминационную способность (AUC = 0.768) и умеренные отклонения от идеальной калибровки в области средних вероятностей, при этом распределения скоров показывают четкое разделение между классами с некоторым перекрытием в диапазоне 0.1-0.3. Precision-Recall кривая указывает на высокую точность модели при низких значениях recall, что типично для несбалансированных кредитных данных, где приоритетом является минимизация ложноположительных предсказаний дефолта.
Калибровка модели:
Brier Score: 0.0631
Таблица калибровки по децилям:
Mean_Predicted Count Observed_Rate
decile
0 0.0098 1000 0.009
1 0.0189 1000 0.024
2 0.0273 1000 0.021
3 0.0367 1000 0.028
4 0.0479 1000 0.048
5 0.0611 1000 0.057
6 0.0777 1000 0.067
7 0.1000 1000 0.092
8 0.1371 1000 0.126
9 0.2375 1000 0.282
Анализ стабильности признаков:
Значимые признаки (|коэф.| > 0.05):
feature coefficient
current_ratio 0.559963
debt_sustainability 0.204449
days_sales_outstanding -0.197975
interest_coverage 0.177588
roa 0.130286
debt_to_equity -0.120438
leverage_risk -0.120438
financial_stress -0.119793
roe -0.118478
liquidity_buffer 0.090420
size_efficiency_interaction 0.066226
Модель продемонстрировала:
- Хорошую калибровку с низким Brier Score (0.0631);
- При этом наблюдаемые частоты дефолтов по децилям предсказанных вероятностей близки к средним предсказанным значениям, особенно в низких и средних децилях;
- Небольшое недокалибрование заметно только в верхнем дециле;
- Анализ стабильности признаков показывает, что наибольший вклад в предсказания дают финансовые коэффициенты ликвидности, долговой нагрузки и рентабельности, а также взаимодействие размера и эффективности (size_efficiency_interaction).
Представленный анализ демонстрирует профессиональный подход к валидации модели, выходящий далеко за рамки простых метрик верности (Accuracy) и точности (Precision). Диаграмма надежности (reliability diagram) является золотым стандартом для оценки качества калибровки — идеально откалиброванная модель должна показывать точки, лежащие на диагональной линии. Отклонения от линии указывают на систематическое смещение в предсказанных вероятностях.
Особое внимание уделяется анализу распределения скоров по классам. Хорошо работающая модель должна демонстрировать четкое разделение между распределениями дефолтных и недефолтных наблюдений, с минимальным перекрытием в критических областях принятия решений.
Заключение
Прогнозирование вероятности дефолта через логистическую регрессию является эффективной техникой, которая способна конкурировать с гораздо более сложными методами машинного обучения. Однако надо учитывать, что успех модели на 80% зависит от качества предобработки данных и создания производных признаков, отражающих экономические взаимосвязи. Кроме того, нужно обязательно делать кросс-валидацию модели и проводить калибровку под стресс-сценарии.
Главное преимущество логрегрессии — ее интерпретируемость. Эта модель дает конкретные рекомендации для управления рисками. Каждый коэффициент показывает, во сколько раз изменяется отношение шансов при изменении соответствующего показателя на единицу, что позволяет легко интерпретировать и переводить результаты модели в ценовые решения и лимиты по кредитам, в отличие от большинства современных алгоритмов машинного обучения.