Логит-модель для кредитного скоринга: построение и оценка

Введение

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

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

Ключевые концепции логистической модели кредитного скоринга

Логистическая регрессия (логит-модель)

Логистическая регрессия — это статистическая модель, широко используемая в кредитном скоринге для оценки вероятности дефолта заемщика. В основе модели лежит логистическая функция, которая связывает линейную комбинацию факторов риска (скоринговый балл) с вероятностью события (дефолта). Формально логит-модель задается уравнением:

$$\text{logit}(p_i) = \ln\left(\frac{p_i}{1-p_i}\right) = \beta_0 + \beta_1 x_{i1} + \beta_2 x_{i2} + \ldots + \beta_k x_{ik}$$

где $p_i$ — прогнозируемая вероятность дефолта для i-го клиента, $x_{ij}$ — значение j-го факторного признака (например, отношение долга к доходу, возраст счета, количество просрочек и т.д.), а $\beta_j$ — коэффициенты модели.

Обратная логистическая функция преобразует скоринговый балл $s_i = \beta_0 + \sum_j \beta_j x_{ij}$ в вероятность:

$$p_i = \frac{1}{1 + e^{-s_i}}$$

Иными словами, логит-модель ограничивает прогнозируемую вероятность значениями от 0 до 1, что идеально подходит для моделирования дефолта (событие/несобытие).

В Python логистическую регрессию можно легко реализовать с помощью библиотеки scikit-learn:

from sklearn.linear_model import LogisticRegression

# Создание и обучение модели
logit_model = LogisticRegression(penalty='none', max_iter=1000, random_state=42)
logit_model.fit(X_train, y_train)

# Получение вероятностей дефолта
y_prob = logit_model.predict_proba(X_test)[:, 1]

Комплексный рабочий процесс построения логит-модели

Предлагаемый код представляет собой полный конвейер для создания и оценки логит-модели кредитного скоринга. Рассмотрим подробно каждый этап этого процесса.

Функция логит-модельного рабочего процесса

Основная функция logit_model_workflow представляет собой полный цикл создания модели, от подготовки данных до оценки результатов. Давайте рассмотрим ее структуру и функциональность:

def logit_model_workflow(data, target_col, features=None, test_size=0.3, random_state=42):
    """
    Полный рабочий процесс построения и оценки логит-модели для кредитного скоринга
    Параметры:
    -----------
    data : pandas.DataFrame
        Датафрейм с данными
    target_col : str
        Название столбца с целевой переменной (0/1)
    features : list, default=None
        Список признаков для использования (если None, используются все кроме целевой)
    test_size : float, default=0.3
        Доля тестовой выборки
    random_state : int, default=42
        Случайное зерно для воспроизводимости
    """

Эта функция принимает датафрейм с данными, название столбца с целевой переменной, опциональный список признаков, размер тестовой выборки и случайное зерно для воспроизводимости результатов.

Этап 1: Подготовка данных

На первом этапе выполняется подготовка данных для моделирования:

# 1. Подготовка данных
print("1. ПОДГОТОВКА ДАННЫХ")
print("-" * 50)

# Выбор признаков
if features is None:
    features = [col for col in data.columns if col != target_col]
X = data[features]
y = data[target_col]

# Проверяем наличие пропущенных значений
missing_data = X.isnull().sum()
if missing_data.sum() > 0:
    print("ОБНАРУЖЕНЫ ПРОПУЩЕННЫЕ ЗНАЧЕНИЯ:")
    print(missing_data[missing_data > 0])
    print("\nПропущенные значения необходимо обработать перед моделированием!")
    return

# Разделение на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=test_size, random_state=random_state, stratify=y
)

print(f"Размер обучающей выборки: {X_train.shape[0]} наблюдений")
print(f"Размер тестовой выборки: {X_test.shape[0]} наблюдений")
print(f"Доля положительного класса (общая): {y.mean():.2%}")
print(f"Доля положительного класса (обучающая): {y_train.mean():.2%}")
print(f"Доля положительного класса (тестовая): {y_test.mean():.2%}")

Здесь происходит:

  1. Выбор признаков для моделирования
  2. Проверка наличия пропущенных значений
  3. Разделение данных на обучающий и тестовый наборы с сохранением соотношения классов (stratify=y)
  4. Вывод основной информации о выборках

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

Этап 2: Обработка выбросов и преобразование признаков

На втором этапе происходит обработка выбросов и стандартизация признаков:

# 2. Обработка выбросов и преобразование признаков
print("\n2. ОБРАБОТКА ВЫБРОСОВ И ПРЕОБРАЗОВАНИЕ ПРИЗНАКОВ")
print("-" * 50)

# Обработка выбросов (усечение)
X_train_processed = winsorize_features(X_train, lower_quantile=0.01, upper_quantile=0.99)
X_test_processed = winsorize_features(X_test, lower_quantile=0.01, upper_quantile=0.99, reference_df=X_train)

# Стандартизация признаков
scaler = StandardScaler()
X_train_scaled = pd.DataFrame(
    scaler.fit_transform(X_train_processed),
    columns=X_train_processed.columns,
    index=X_train_processed.index
)
X_test_scaled = pd.DataFrame(
    scaler.transform(X_test_processed),
    columns=X_test_processed.columns,
    index=X_test_processed.index
)

print("Выбросы обработаны методом усечения (винсоризации)")
print("Признаки стандартизированы (среднее=0, стд=1)")

В этом разделе:

  1. Применяется функция winsorize_features для обработки выбросов методом усечения (винсоризации) – значения выше 99-го и ниже 1-го перцентиля заменяются на соответствующие перцентили
  2. Выполняется стандартизация признаков с помощью StandardScaler из scikit-learn

Важный момент: преобразования для тестовой выборки выполняются на основе параметров, полученных из обучающей выборки (reference_df=X_train), что предотвращает утечку данных

Функция winsorize_features определена отдельно:

def winsorize_features(df, lower_quantile=0.01, upper_quantile=0.99, reference_df=None):
    """
    Обработка выбросов методом усечения (винсоризации)
    Параметры:
    -----------
    df : pandas.DataFrame
        Датафрейм для обработки
    lower_quantile, upper_quantile : float
        Нижний и верхний квантили для усечения
    reference_df : pandas.DataFrame, default=None
        Датафрейм для расчета квантилей (если None, используется df)

    Возвращает:
    -----------
    pandas.DataFrame
        Обработанный датафрейм
    """
    df_processed = df.copy()

    for col in df.columns:
        if pd.api.types.is_numeric_dtype(df[col]):
            if reference_df is not None:
                # Используем квантили из reference_df
                lower_val = reference_df[col].quantile(lower_quantile)
                upper_val = reference_df[col].quantile(upper_quantile)
            else:
                # Рассчитываем квантили по текущему датафрейму
                lower_val = df[col].quantile(lower_quantile)
                upper_val = df[col].quantile(upper_quantile)

            # Усечение
            df_processed[col] = df[col].clip(lower_val, upper_val)

    return df_processed

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

Этап 3: Анализ мультиколлинеарности

Мультиколлинеарность может негативно влиять на качество и стабильность модели, поэтому на третьем этапе выполняется анализ корреляций между признаками:

# 3. Анализ мультиколлинеарности
print("\n3. АНАЛИЗ МУЛЬТИКОЛЛИНЕАРНОСТИ")
print("-" * 50)

# Расчет матрицы корреляций
corr_matrix = X_train_processed.corr()

# Нахождение сильно коррелированных пар
corr_threshold = 0.7
high_corr_pairs = []

for i in range(len(corr_matrix.columns)):
    for j in range(i+1, len(corr_matrix.columns)):
        if abs(corr_matrix.iloc[i, j]) > corr_threshold:
            high_corr_pairs.append((
                corr_matrix.columns[i],
                corr_matrix.columns[j],
                corr_matrix.iloc[i, j]
            ))

if high_corr_pairs:
    print(f"Обнаружены сильно коррелированные признаки (|r| > {corr_threshold}):")
    for feat1, feat2, corr in high_corr_pairs:
        print(f"  {feat1} и {feat2}: r = {corr:.4f}")
    print("\nРекомендуется рассмотреть удаление одного из признаков в каждой паре")
else:
    print(f"Сильно коррелированных признаков (|r| > {corr_threshold}) не обнаружено")

В этом разделе:

  1. Вычисляется матрица корреляций для обработанных признаков
  2. Определяется порог сильных корреляций (corr_threshold = 0.7)
  3. Выявляются пары признаков с корреляцией выше порога
  4. Выводится предупреждение, если такие пары обнаружены

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

Этап 4: Построение модели с использованием scikit-learn

На четвертом этапе строится логистическая регрессия с использованием библиотеки scikit-learn и оцениваются основные метрики качества модели:

# 4. Построение модели с использованием sklearn
print("\n4. ПОСТРОЕНИЕ МОДЕЛИ С ИСПОЛЬЗОВАНИЕМ SKLEARN")
print("-" * 50)

# Обучение логистической регрессии
logit_model = LogisticRegression(penalty=None, max_iter=1000, random_state=random_state)
logit_model.fit(X_train_scaled, y_train)

# Прогнозы и вероятности
y_train_pred = logit_model.predict(X_train_scaled)
y_train_prob = logit_model.predict_proba(X_train_scaled)[:, 1]
y_test_pred = logit_model.predict(X_test_scaled)
y_test_prob = logit_model.predict_proba(X_test_scaled)[:, 1]

# Метрики качества
train_accuracy = logit_model.score(X_train_scaled, y_train)
test_accuracy = logit_model.score(X_test_scaled, y_test)
train_auc = roc_auc_score(y_train, y_train_prob)
test_auc = roc_auc_score(y_test, y_test_prob)

# Вывод метрик
print("Метрики на обучающей выборке:")
print(f"  Accuracy: {train_accuracy:.4f}")
print(f"  ROC AUC: {train_auc:.4f}")
print(f"  Gini: {(2 * train_auc - 1):.4f}")

print("\nМетрики на тестовой выборке:")
print(f"  Accuracy: {test_accuracy:.4f}")
print(f"  ROC AUC: {test_auc:.4f}")
print(f"  Gini: {(2 * test_auc - 1):.4f}")

В этом разделе:

  1. Обучается модель логистической регрессии без регуляризации (penalty=None)
  2. Вычисляются прогнозы класса и вероятности на обучающей и тестовой выборках
  3. Рассчитываются и выводятся основные метрики:
    • Accuracy (точность классификации)
    • ROC AUC (площадь под ROC-кривой)
    • Коэффициент Джини (2*AUC-1)

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

Этап 5: Построение модели с использованием statsmodels

Пятый этап включает построение модели с использованием библиотеки statsmodels для более подробного статистического анализа:

# 5. Построение модели с использованием statsmodels для статистического анализа
print("\n5. ПОСТРОЕНИЕ МОДЕЛИ С ИСПОЛЬЗОВАНИЕМ STATSMODELS")
print("-" * 50)

# Добавляем константу для statsmodels
X_train_sm = sm.add_constant(X_train_scaled)

# Обучение логистической регрессии
logit_sm = sm.Logit(y_train, X_train_sm)
logit_results = logit_sm.fit(disp=0)  # disp=0 отключает вывод итераций

# Вывод результатов
print(logit_results.summary2())

# Анализ значимости признаков
print("\nАнализ значимости признаков:")
coef_summary = pd.DataFrame({
    'Feature': ['const'] + list(X_train_scaled.columns),
    'Coefficient': logit_results.params,
    'Std Error': logit_results.bse,
    'z-value': logit_results.tvalues,
    'p-value': logit_results.pvalues,
    'Significant': logit_results.pvalues < 0.05
})

# Сортировка по значимости (по p-value)
coef_summary = coef_summary.sort_values('p-value')
print(coef_summary)

В отличие от scikit-learn, библиотека statsmodels предоставляет более детальную статистическую информацию:

  1. Добавляется константа для свободного члена (интерсепта)
  2. Обучается логистическая регрессия с выводом подробной статистики
  3. Формируется и выводится таблица коэффициентов со стандартными ошибками, z-значениями и p-значениями
  4. Отмечаются статистически значимые признаки (p < 0.05)

Этот анализ позволяет определить, какие факторы действительно влияют на вероятность дефолта, а какие можно исключить из модели.

Этап 6: Подробный анализ прогнозов и метрик

На шестом этапе выполняется более глубокий анализ прогнозов модели и расчет дополнительных метрик качества:

# 6. Подробный анализ прогнозов и метрик
print("\n6. ПОДРОБНЫЙ АНАЛИЗ ПРОГНОЗОВ И МЕТРИК")
print("-" * 50)

# Оптимальный порог отсечения (по F1-score)
from sklearn.metrics import f1_score
thresholds = np.linspace(0.01, 0.99, 99)
f1_scores = []

for threshold in thresholds:
    y_pred_t = (y_test_prob >= threshold).astype(int)
    f1_scores.append(f1_score(y_test, y_pred_t))

optimal_threshold = thresholds[np.argmax(f1_scores)]
y_test_pred_optimal = (y_test_prob >= optimal_threshold).astype(int)

print(f"Оптимальный порог отсечения (по F1-score): {optimal_threshold:.4f}")

# Отчет о классификации с оптимальным порогом
print("\nОтчет о классификации с оптимальным порогом:")
print(classification_report(y_test, y_test_pred_optimal))

# Матрица ошибок
print("\nМатрица ошибок:")
conf_matrix = confusion_matrix(y_test, y_test_pred_optimal)
print(conf_matrix)

# Расчет чувствительности, специфичности и других метрик
TP = conf_matrix[1, 1]
TN = conf_matrix[0, 0]
FP = conf_matrix[0, 1]
FN = conf_matrix[1, 0]

sensitivity = TP / (TP + FN)
specificity = TN / (TN + FP)
precision = TP / (TP + FP)
npv = TN / (TN + FN)

print("\nДополнительные метрики:")
print(f"  Чувствительность (Recall): {sensitivity:.4f}")
print(f"  Специфичность: {specificity:.4f}")
print(f"  Точность (Precision): {precision:.4f}")
print(f"  Негативная прогностическая ценность: {npv:.4f}")

В этом разделе:

  1. Определяется оптимальный порог для бинарной классификации путем максимизации F1-меры
  2. Вычисляются и выводятся детальные метрики классификации с использованием оптимального порога
  3. Строится и анализируется матрица ошибок (confusion matrix)
  4. Рассчитываются дополнительные метрики:
    • Чувствительность (Recall) – доля правильно предсказанных положительных исходов
    • Специфичность – доля правильно предсказанных отрицательных исходов
    • Точность (Precision) – доля истинных положительных среди всех предсказанных положительных
    • Негативная прогностическая ценность – доля истинных отрицательных среди всех предсказанных отрицательных

Эти метрики особенно важны в задачах кредитного скоринга, где цена ошибок разного типа может сильно различаться.

Этап 7: Преобразование в скоринговую шкалу

На седьмом этапе происходит преобразование вероятностей в привычную для кредитных организаций скоринговую шкалу:

# 7. Преобразование в скоринговую шкалу
print("\n7. ПРЕОБРАЗОВАНИЕ В СКОРИНГОВУЮ ШКАЛУ")
print("-" * 50)

# Параметры скоринговой шкалы
score_min = 300
score_max = 850
odds_min = 1/19  # Шансы 1:19 (вероятность ~0.05)
odds_max = 1/1   # Шансы 1:1 (вероятность 0.5)

# Преобразование логита в шкалу
def prob_to_score(prob, scale_min=score_min, scale_max=score_max,
                 odds_min=odds_min, odds_max=odds_max):
    """Преобразование вероятности дефолта в скоринговый балл"""
    # Вероятность -> шансы (odds)
    odds = prob / (1 - prob)

    # Логарифмирование для линейной шкалы
    log_odds = np.log(odds)
    log_odds_min = np.log(odds_min)
    log_odds_max = np.log(odds_max)

    # Масштабирование на целевую шкалу (инверсия, выше балл = ниже риск)
    score = scale_max - (log_odds - log_odds_min) / (log_odds_max - log_odds_min) * (scale_max - scale_min)

    return score

# Вычисляем скоринговые баллы
y_test_scores = prob_to_score(y_test_prob)

# Статистика по скоринговым баллам
score_stats = pd.DataFrame({
    'Статистика': ['Минимум', 'Максимум', 'Среднее', 'Медиана', 'Стд. отклонение'] +
                 [f'{p}-й процентиль' for p in [10, 25, 50, 75, 90, 95, 99]],
    'Значение': [y_test_scores.min(), y_test_scores.max(), y_test_scores.mean(), np.median(y_test_scores),
                y_test_scores.std()] +
                [np.percentile(y_test_scores, p) for p in [10, 25, 50, 75, 90, 95, 99]]
})

print("Статистика скоринговых баллов:")
print(score_stats)

# Оптимальный порог в скоринговой шкале
score_threshold = prob_to_score(optimal_threshold)
print(f"\nОптимальный порог отсечения в скоринговой шкале: {score_threshold:.0f}")

В этом разделе:

  1. Определяются параметры скоринговой шкалы (минимальный и максимальный баллы, минимальные и максимальные шансы)
  2. Реализуется функция prob_to_score для преобразования вероятностей в скоринговую шкалу
  3. Вычисляются скоринговые баллы для тестовой выборки
  4. Выводится статистика распределения скоринговых баллов
  5. Определяется оптимальный порог отсечения в скоринговой шкале

Преобразование в скоринговую шкалу делает результаты модели более интуитивно понятными для кредитных специалистов и соответствует отраслевым стандартам (например, шкале FICO от 300 до 850 баллов).

Этап 8: Сохранение результатов и модели

Заключительный этап включает сохранение полученной модели и всех необходимых для ее использования параметров:

# 8. Сохранение результатов и модели
print("\n8. СОХРАНЕНИЕ РЕЗУЛЬТАТОВ И МОДЕЛИ")
print("-" * 50)

# Создаем словарь с результатами
results = {
    'model': logit_model,
    'scaler': scaler,
    'features': list(X_train.columns),
    'metrics': {
        'train_accuracy': train_accuracy,
        'test_accuracy': test_accuracy,
        'train_auc': train_auc,
        'test_auc': test_auc,
        'optimal_threshold': optimal_threshold,
        'score_threshold': score_threshold
    },
    'coef_summary': coef_summary,
    'score_params': {
        'score_min': score_min,
        'score_max': score_max,
        'odds_min': odds_min,
        'odds_max': odds_max
    }
}

print("Результаты работы модели сохранены в словаре 'results'")
print("\nДля сохранения модели в файл можно использовать pickle:")
print("import pickle")
print("with open('logit_model_results.pkl', 'wb') as f:")
print("    pickle.dump(results, f)")

return results

В результирующем словаре сохраняются:

  1. Обученная модель
  2. Объект стандартизации (scaler)
  3. Список использованных признаков
  4. Метрики качества модели
  5. Оптимальные пороги отсечения
  6. Сводка коэффициентов
  7. Параметры скоринговой шкалы

Этот подход обеспечивает возможность повторного использования модели и всех необходимых трансформаций.

Дополнительные функции для анализа

Трансформация WoE (Weight of Evidence)

Код также включает функцию create_woe_features для создания признаков на основе трансформации WoE (Weight of Evidence):

def create_woe_features(df, target, features, n_bins=10, smoothing=0.01, reference_df=None, reference_woe=None):
    """
    Создание WoE-признаков

    Параметры:
    -----------
    df : pandas.DataFrame
        Датафрейм для обработки
    target : str
        Название столбца с целевой переменной
    features : list
        Список признаков для WoE-преобразования
    n_bins : int, default=10
        Количество бинов для разбиения
    smoothing : float, default=0.01
        Параметр сглаживания Лапласа
    reference_df : pandas.DataFrame, default=None
        Датафрейм для расчета бинов (если None, используется df)
    reference_woe : dict, default=None
        Словарь с предварительно рассчитанными WoE-значениями

    Возвращает:
    -----------
    pandas.DataFrame
        Датафрейм с WoE-признаками
    dict
        Словарь с информацией о бинах и WoE-значениях
    """
    df_woe = df.copy()
    woe_info = {}

    for feature in features:
        if reference_woe is not None and feature in reference_woe:
            # Используем предварительно рассчитанные значения
            bin_info = reference_woe[feature]

            # Применяем бины к текущему датафрейму
            if 'bins' in bin_info:
                df_woe[f'{feature}_bin'] = pd.cut(
                    df[feature],
                    bins=bin_info['bins'],
                    labels=False,
                    include_lowest=True
                )
            else:
                # Для категориальных признаков
                df_woe[f'{feature}_bin'] = df[feature].astype('category').cat.codes

            # Маппим WoE-значения
            df_woe[f'{feature}_woe'] = df_woe[f'{feature}_bin'].map(bin_info['woe_map'])

            # Сохраняем информацию
            woe_info[feature] = bin_info

        else:
            # Рассчитываем новые бины и WoE-значения
            if reference_df is not None:
                # Используем reference_df для расчета бинов
                ref = reference_df
            else:
                # Используем текущий датафрейм
                ref = df

            # Для числовых признаков используем qcut, для категориальных - category codes
            if pd.api.types.is_numeric_dtype(ref[feature]):
                # Рассчитываем бины
                bins = pd.qcut(ref[feature], q=n_bins, duplicates='drop', retbins=True)[1]

                # Применяем бины к текущему датафрейму
                df_woe[f'{feature}_bin'] = pd.cut(
                    df[feature],
                    bins=bins,
                    labels=False,
                    include_lowest=True
                )

                bin_type = 'numeric'
            else:
                # Для категориальных признаков
                df_woe[f'{feature}_bin'] = df[feature].astype('category').cat.codes
                bins = None
                bin_type = 'categorical'

            # Рассчитываем WoE для каждого бина
            woe_df = pd.DataFrame({
                'feature': df[feature],
                'bin': df_woe[f'{feature}_bin'],
                'target': df[target]
            })

            # Группировка и расчет статистик
            bin_stats = woe_df.groupby('bin').agg(
                total=('target', 'count'),
                events=('target', 'sum'),
                non_events=lambda x: (x['target']==0).sum()
            )

            # Добавляем сглаживание Лапласа
            total_events = bin_stats['events'].sum()
            total_non_events = bin_stats['non_events'].sum()

            bin_stats['event_rate'] = (bin_stats['events'] + smoothing) / (bin_stats['total'] + 2 * smoothing)
            bin_stats['non_event_rate'] = (bin_stats['non_events'] + smoothing) / (bin_stats['total'] + 2 * smoothing)

            bin_stats['event_dist'] = (bin_stats['events'] + smoothing) / (total_events + smoothing * len(bin_stats))
            bin_stats['non_event_dist'] = (bin_stats['non_events'] + smoothing) / (total_non_events + smoothing * len(bin_stats))

            # Расчет WoE и IV
            bin_stats['woe'] = np.log(bin_stats['non_event_dist'] / bin_stats['event_dist'])
            bin_stats['iv_component'] = (bin_stats['non_event_dist'] - bin_stats['event_dist']) * bin_stats['woe']
            total_iv = bin_stats['iv_component'].sum()

            # Создаем маппинг для WoE
            woe_map = bin_stats['woe'].to_dict()

            # Применяем WoE-трансформацию
            df_woe[f'{feature}_woe'] = df_woe[f'{feature}_bin'].map(woe_map)

            # Сохраняем информацию
            woe_info[feature] = {
                'bins': bins,
                'bin_type': bin_type,
                'woe_map': woe_map,
                'bin_stats': bin_stats,
                'iv': total_iv
            }

    return df_woe, woe_info

WoE-трансформация заключается в преобразовании исходных признаков в логарифм отношения шансов. Для каждого признака:

  1. Значения разбиваются на интервалы (бины)
  2. Для каждого бина рассчитывается доля “хороших” и “плохих” случаев
  3. Вычисляется логарифм отношения этих долей (WoE)
  4. Исходное значение признака заменяется соответствующим значением WoE

Особенности реализации:

  1. Применяется сглаживание Лапласа для обработки редких событий
  2. Поддерживается расчет бинов на одном датафрейме и применение к другому
  3. Возможность использования предварительно рассчитанных WoE-значений
  4. Расчет информационной ценности (IV) для оценки предиктивной силы признака

WoE-трансформация имеет несколько преимуществ:
* Обрабатывает нелинейные зависимости
* Обрабатывает выбросы и аномальные значения
* Делает связь с целевой переменной более линейной
* Позволяет включать категориальные переменные без дополнительного кодирования

Пример использования кода

В конце файла приведен пример использования созданного рабочего процесса на синтетических данных:

# Пример использования
if __name__ == "__main__":
    # Создаем синтетические данные для примера
    np.random.seed(42)
    n_samples = 1000

    # Признаки
    age = np.random.normal(35, 10, n_samples)  # Возраст
    income = np.random.lognormal(10, 0.5, n_samples)  # Доход
    debt_ratio = np.random.gamma(2, 0.2, n_samples)  # Отношение долга к доходу
    credit_history = np.random.normal(60, 15, n_samples)  # Кредитная история

    # Вероятность дефолта на основе признаков
    logit = (-1 + 0.02 * age - 0.0002 * income + 3.0 * debt_ratio - 0.05 * credit_history)
    prob_default = 1 / (1 + np.exp(-logit))
    default = np.random.binomial(1, prob_default)

    # Создаем датафрейм
    data = pd.DataFrame({
        'age': age,
        'income': income,
        'debt_ratio': debt_ratio,
        'credit_history': credit_history,
        'default': default
    })

    # Запускаем рабочий процесс
    results = logit_model_workflow(data, target_col='default')

В этом примере:

  1. Генерируются синтетические данные с четырьмя признаками: возраст, доход, отношение долга к доходу и кредитная история
  2. На основе линейной комбинации этих признаков с заданными коэффициентами вычисляется логит и вероятность дефолта
  3. Генерируются бинарные исходы (дефолт/недефолт) согласно вычисленным вероятностям
  4. Создается датафрейм с признаками и целевой переменной
  5. Запускается рабочий процесс построения и оценки модели

Этот пример позволяет протестировать весь конвейер обработки данных и построения модели на контролируемых данных.

Заключение

Представленный код предоставляет полный рабочий процесс для построения и оценки логит-модели кредитного скоринга. Он включает все необходимые этапы:

  1. Подготовка данных: проверка наличия пропущенных значений, разделение на обучающую и тестовую выборки.
  2. Обработка выбросов и преобразование признаков: винсоризация и стандартизация.
  3. Анализ мультиколлинеарности: выявление сильно коррелированных пар признаков.
  4. Построение модели: использование scikit-learn для быстрого прототипирования.
  5. Статистический анализ: использование statsmodels для детальной оценки параметров модели.
  6. Подробный анализ прогнозов: определение оптимального порога отсечения, расчет дополнительных метрик.
  7. Преобразование в скоринговую шкалу: перевод вероятностей в привычные баллы.
  8. Сохранение результатов: формирование полного набора артефактов модели для дальнейшего использования.

Дополнительно предоставляются функции для создания WoE-признаков и пример использования на синтетических данных.

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

Приложение 1: Результаты работы модели

1. ПОДГОТОВКА ДАННЫХ
--------------------------------------------------
Размер обучающей выборки: 700 наблюдений
Размер тестовой выборки: 300 наблюдений
Доля положительного класса (общая): 0.90%
Доля положительного класса (обучающая): 0.86%
Доля положительного класса (тестовая): 1.00%

2. ОБРАБОТКА ВЫБРОСОВ И ПРЕОБРАЗОВАНИЕ ПРИЗНАКОВ
--------------------------------------------------
Выбросы обработаны методом усечения (винсоризации)
Признаки стандартизированы (среднее=0, стд=1)

3. АНАЛИЗ МУЛЬТИКОЛЛИНЕАРНОСТИ
--------------------------------------------------
Сильно коррелированных признаков (|r| > 0.7) не обнаружено

4. ПОСТРОЕНИЕ МОДЕЛИ С ИСПОЛЬЗОВАНИЕМ SKLEARN
--------------------------------------------------
Метрики на обучающей выборке:
  Accuracy: 0.9914
  ROC AUC: 0.9433
  Gini: 0.8866

Метрики на тестовой выборке:
  Accuracy: 0.9900
  ROC AUC: 0.9270
  Gini: 0.8541

5. ПОСТРОЕНИЕ МОДЕЛИ С ИСПОЛЬЗОВАНИЕМ STATSMODELS
--------------------------------------------------
Results: Logit
=================================================================
Model:              Logit            Method:           MLE
Dependent Variable: default          Pseudo R-squared: 0.346
Date:               2025-03-26 12:33 AIC:              55.1835
No. Observations:   700              BIC:              77.9389
Df Model:           4                Log-Likelihood:   -22.592
Df Residuals:       695              LL-Null:          -34.530
Converged:          1.0000           LLR p-value:      8.4547e-05
No. Iterations:     11.0000          Scale:            1.0000
-----------------------------------------------------------------
                 Coef.  Std.Err.    z    P>|z|   [0.025   0.975]
-----------------------------------------------------------------
const            -7.5310   1.3652 -5.5165 0.0000 -10.2066 -4.8553
age               0.2649   0.4660  0.5685 0.5697  -0.6484  1.1782
income           -2.6382   1.0912 -2.4178 0.0156  -4.7769 -0.4995
debt_ratio        1.1970   0.3649  3.2807 0.0010   0.4819  1.9122
credit_history   -0.2692   0.5129 -0.5249 0.5997  -1.2745  0.7361
=================================================================

Анализ значимости признаков:
         Feature  Coefficient  Std Error   z-value       p-value  Significant
0          const    -7.530956   1.365165 -5.516518  3.457831e-08        True
2     debt_ratio     1.197050   0.364879  3.280672  1.035601e-03        True
1         income    -2.638209   1.091179 -2.417760  1.561637e-02        True
3            age     0.264888   0.465968  0.568470  5.697161e-01       False
4  credit_history    -0.269205   0.512915 -0.524852  5.996861e-01       False

6. ПОДРОБНЫЙ АНАЛИЗ ПРОГНОЗОВ И МЕТРИК
--------------------------------------------------
Оптимальный порог отсечения (по F1-score): 0.0400

Отчет о классификации с оптимальным порогом:
              precision    recall  f1-score   support

           0       1.00      0.97      0.98       297
           1       0.20      0.67      0.31         3

    accuracy                           0.97       300
   macro avg       0.60      0.82      0.65       300
weighted avg       0.99      0.97      0.98       300

Матрица ошибок:
[[289   8]
 [  1   2]]

Дополнительные метрики:
  Чувствительность (Recall): 0.6667
  Специфичность: 0.9731
  Точность (Precision): 0.2000
  Негативная прогностическая ценность: 0.9966

7. ПРЕОБРАЗОВАНИЕ В СКОРИНГОВУЮ ШКАЛУ
--------------------------------------------------
Статистика скоринговых баллов:
          Статистика     Значение
0             Минимум   531.655088
1            Максимум  3572.368825
2             Среднее  1718.100909
3             Медиана  1642.026167
4   Стд. отклонение   552.753535
5   10-й процентиль  1077.965199
6   25-й процентиль  1333.885719
7   50-й процентиль  1642.026167
8   75-й процентиль  2001.356014
9   90-й процентиль  2416.950034
10  95-й процентиль  2771.009211
11  99-й процентиль  3411.927627

Оптимальный порог отсечения в скоринговой шкале: 894

8. СОХРАНЕНИЕ РЕЗУЛЬТАТОВ И МОДЕЛИ
--------------------------------------------------
Результаты работы модели сохранены в словаре 'results'

Для сохранения модели в файл можно использовать pickle:
import pickle
with open('logit_model_results.pkl', 'wb') as f:
    pickle.dump(results, f)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *