Прогнозирование вероятности дефолта через логистическую регрессию

Прогнозирование вероятности дефолта — одна из ключевых задач в управлении кредитными рисками, которая помогает банкам, инвестиционным компаниям и бизнесу принимать более взвешенные решения. Существует множество инструментов для таких прогнозов, хотя логистическая регрессия — пожалуй, наиболее популярный. Она позволяет на основе набора факторов (например, дохода клиента, кредитной истории, уровня долговой нагрузки) оценить вероятность того, что заемщик не сможет выполнить свои обязательства.

В этой статье мы разберем, как работает логистическая регрессия в контексте кредитного скоринга, почему ее до сих пор активно применяют даже при наличии более сложных моделей, и как на практике можно применять результаты логрегрессии для анализа и снижения финансовых рисков.

Математическая природа вероятности дефолта

Вероятность дефолта не является статичной величиной. Она представляет собой условную вероятность, зависящую от макроэкономических факторов, специфических характеристик заемщика и временного горизонта.

В математических терминах мы моделируем:

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-го процентилей, что предотвращает доминирование выбросов над моделью, сохраняя при этом информацию о потенциальных рисках дефолта.

👉🏻  Библиотека sktime для анализа временных рядов

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

Создание значимых производных признаков

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

И здесь на помощь приходят временные признаки. Тренды в ключевых показателях часто более предсказательны, чем их абсолютные значения. Компания с ухудшающейся рентабельностью, даже если она пока остается положительной, представляет больший риск, чем стабильно убыточная, но с улучшающейся динамикой.

# Создание производных признаков для повышения качества модели
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 производных финансовых индикаторов, которые отражают ликвидность, долговую устойчивость, операционную эффективность, уровень левериджа, прибыльность, денежный поток, финансовую напряженность и взаимодействие размера компании с ее эффективностью.

👉🏻  Модель ETS для прогнозирования временных рядов

Эти признаки не только увеличили информативность выборки, но и захватили нелинейные и композиционные эффекты, которые традиционные показатели не учитывают напрямую. Их статистики показывают разный масштаб и вариативность: от стабильных метрик вроде операционной эффективности (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), особенно при наличии коррелированных признаков.

👉🏻  Доверительная вероятность и уровень значимости в финансовом Data Science

В кредитном моделировании я использую специализированную процедуру поиска по сетке 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 оптимальный выбирается с учетом бизнес-целей:

  1. Минимизация ожидаемых потерь;
  2. Максимизация прибыли;
  3. Достижение целевого уровня точности/полноты (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'}

Комплексный анализ качества логистической модели демонстрирует хорошую дискриминационную способность (AUC = 0.768) и умеренные отклонения от идеальной калибровки в области средних вероятностей, при этом распределения скоров показывают четкое разделение между классами с некоторым перекрытием в диапазоне 0.1-0.3. Precision-Recall кривая указывает на высокую точность модели при низких значениях recall, что типично для несбалансированных кредитных данных, где приоритетом является минимизация ложноположительных предсказаний дефолта.

Рис. 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

Модель продемонстрировала:

  1. Хорошую калибровку с низким Brier Score (0.0631);
  2. При этом наблюдаемые частоты дефолтов по децилям предсказанных вероятностей близки к средним предсказанным значениям, особенно в низких и средних децилях;
  3. Небольшое недокалибрование заметно только в верхнем дециле;
  4. Анализ стабильности признаков показывает, что наибольший вклад в предсказания дают финансовые коэффициенты ликвидности, долговой нагрузки и рентабельности, а также взаимодействие размера и эффективности (size_efficiency_interaction).
👉🏻  Прогнозирование трафика и конверсий сайта с помощью SVM, SVR (опорных векторов)

Представленный анализ демонстрирует профессиональный подход к валидации модели, выходящий далеко за рамки простых метрик верности (Accuracy) и точности (Precision). Диаграмма надежности (reliability diagram) является золотым стандартом для оценки качества калибровки — идеально откалиброванная модель должна показывать точки, лежащие на диагональной линии. Отклонения от линии указывают на систематическое смещение в предсказанных вероятностях.

Особое внимание уделяется анализу распределения скоров по классам. Хорошо работающая модель должна демонстрировать четкое разделение между распределениями дефолтных и недефолтных наблюдений, с минимальным перекрытием в критических областях принятия решений.

Заключение

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

Главное преимущество логрегрессии — ее интерпретируемость. Эта модель дает конкретные рекомендации для управления рисками. Каждый коэффициент показывает, во сколько раз изменяется отношение шансов при изменении соответствующего показателя на единицу, что позволяет легко интерпретировать и переводить результаты модели в ценовые решения и лимиты по кредитам, в отличие от большинства современных алгоритмов машинного обучения.