Эта статья - не попытка критиковать конкретный банк и не утверждение, что алгоритм ошибся. Я не знаю внутренних правил скоринга, не видел модели и не утверждаю, что решение было неверным.
Это разбор частного случая глазами человека, который внезапно оказался в очень неприятной жизненной ситуации в связи с тем, что в ответственный момент классификатор «принял решение» об отказе в ипотеке. В данный момент ситуация продолжает оставаться неясной и я нахожусь в стрессе. В какой-то степени, попытка хоть как-то разобраться в том, какие факторы повлияли на отказ в выдаче ипотеки, хоть как-то снижает уровень стресса.
Дисклеймер:
Часть кода была написана с помощью нейронки: улучшения вывода скоринга и отчётов (красота с эмоджи, заголовками, разделителями), идея расширения датасета (с последующими моими корректировками, особенно в отношении расчёта дохода, потом повторные генерации до тех пор, пока они не стали похожи на реальность), приведение кода к более или менее читаемому виду.
Текст писал сам на эмоциях, но для проверки прогонял его через модель. Какие‑то корректировки модели оставил (меньшую часть), какие‑то переписал своим языком (бОльшую часть).
Описание проблемы
Моя ситуация - абсолютно понятная: в связи с рождением ребёнка мы решили улучшить жилищные условия: продать квартиру и взять вторичку в ипотеку в лучшем районе. Учитывая известные события на вторичном рынке, сам по себе процесс покупки квартиры является довольно стрессовым: нужно проверить надёжность продавцов, составить договор, и т.д. То, что сделка у нас проходит по альтернативной схеме с образованием длинной цепочки, добавляет волнений.
Тем не менее, в декабре подаю заявку на ипотеку в Сбер. Мне одобряют заявку аж на 12 000 000 ₽. После этого процесс продажи-поиска жилья был запущен. В феврале нашёлся покупатель, с которым заключили договор, покупатель внёс задаток. Нашли квартиру- и с продавцами тоже заключили договор и внесли задаток. В общем, образовалась та самая альтернативная цепочка.
Соответственно, начал собирать документы и выписываться из квартиры. Оформили временную регистрацию: я, жена и ребёнок (9 месяцев) — в её квартире.
Проблема в следующем: я подавал заявку на ипотеку 3 декабря 2025 года, а её срок истекает 3 марта 2026 года. Сделку до 3 марта провести бы точно не получилось.
Ну, тут и закрутилось.
Хронология
27 февраля. Звонок из Сбера. Уточняют детали по ипотеке. Подтверждают, что продавец на ДомКлик принял заявку. "Всё хорошо, ничего делать не нужно".
Через какое-то время - ещё звонок, уже с ДомКлик. Говорят: "До 3 марта Вы не успеете провести сделку, а заявка истекает. Давайте я закрою эту заявку, а вы подадите новую".
Спрашиваю: "Нужно ли что-то сообщить продавцу?" - "Нет, ничего не нужно. Просто подайте заново".
Сотрудник ДомКлик закрывает мою заявку.
Поскольку я сменил регистрацию, для обновления данных нужно было обратиться в отделение Сбербанка. Пришёл, обновил адрес. Сотрудница Сбера заодно предложила тут же подать новую заявку. Подал. Мы с женой решили, что для надёжности она будет созаёмщиком. Жене пришли подтверждения.
И, во время оформления, уведомления в ДомКлик по порядку:
Ждём подтверждение ИНН участников заявки от налоговой. Проверка ИНН — обязательный этап. После этого банк начнёт рассмотрение заявки.
Мы получили вашу заявку. Обычно банк принимает решение в течение 5 минут. В крайнем случае, в течение суток.
Ииии:
Ипотека не одобрена.
Банк принял отрицательное решение по вашей заявке на ипотеку.
Банк вправе не сообщать причину отказа.
Вы можете подать новую кредитную заявку после 29.03.2026.
Описать эмоции в тот момент довольно сложно. Я спрашиваю сотрудницу: "Это как? Подскажите, а почему? У меня уже скоро сделка. Что же мне делать?" - "К сожалению, мы видим то же самое, что и вы. Нам также не объясняют причины отказов". Я потом минут 15 просто не мог встать со стула. Ну да ладно, это все эмоции.
Звоню в банк:
- Подскажите, почему отказ?!
- Не можем сказать.
- Мне же в декабре одобрили на 12 миллионов ОДНОМУ. Сейчас мне надо в 5,5 да еще и взял созаемщика. У меня в жизни ничего не поменялось. Что же мне делать?
- Извините, ничем помочь не можем.
- Можно обжаловать? У меня сделка, цепочка, задатки внесены.
- Подайте заявку после 29 марта.
- У меня сделка через неделю.
- ...Подайте после 29 марта.
- Дайте хоть какой-то канал связи, чтобы объяснить ситуацию.
- Через месяц.
Сотрудники банка сами не понимают, почему отказ. Они видят тот же результат, что и я. Просто смотрят на экран и говорят: «Ничего сделать не можем».
При этом — для интереса подал заявки в Т-Банк и ВТБ. Оба одобрили. За 5 минут. Без вопросов. Даже с учётом смены регистрации.
Приехал в центральный аппарат Сбер��, подал письменную претензию. Сотрудник также ничего сказать не может. Я говорю: "Послушайте, я не прошу особых условий. Просто, проверьте меня еще раз, пожалуйста! У меня ничего не изменилось с момента подачи первой заявки." Сотрудник: "К сожалению, мы ничего не можем сделать. Мы не можем отменить автоматический отказ"
Немного обо мне — для контекста
Чтобы было понятно, почему ситуация кажется мне абсурдной:
Работаю врачом в трёх клиниках
Суммарный доход — больше 240.000 рублей в месяц. Сбер через Госуслуги подтянул доход еще выше;
Кредитов не брал никогда — не из принципа, просто не было нужды
Из финансово безответственного — регулярно покупаю кофе в кафе рядом с работой
А обошлись со мной так, будто я покупаю айфоны в рассрочку (ну, по крайней мере, это мои ощущения).
Когда эмоции улеглись, я понял, что Сбер ипотеку точно не одобрит. Мне стало интересно, как это могло произойти. Я решил разобраться как умею - моделированием ситуации. Ну, все равно больше ничего не остается.
Что могло не понравиться алгоритму Сбера?
В уведомлении об отказе есть список — что стоит проверить перед повторной подачей:
Просрочки по имеющимся кредитам или кредитным картам
Задолженности по уплате штрафов или налогов
Наличие исполнительных производств
Сведения о банкротстве
Открытые судебные разбирательства
Действительность паспорта и других предоставленных документов на момент подачи заявки
Иду по списку:
❌ Просрочки — кредитов нет, просрочивать нечего
❌ Задолженности по штрафам/налогам — чисто
❌ Исполнительные производства — нет
❌ Банкротство — нет
❌ Судебные разбирательства — нет
❓ Действительность документов на момент подачи
Единственное, что реально изменилось - это последний пункт:
Параметр | Декабрь 2025 | Февраль 2026 |
|---|---|---|
Адрес регистрации | Постоянная, 4 года | Временная, 1 месяц |
Созаёмщик | Нет | Добавлена жена (у нее также кредитная история чистая) |
Заявки за последний месяц | 1 (первая в жизни) | 2 за месяц |
Данные в анкете | Первичные | Изменённые |
Финансовый профиль не изменился ни на рубль. Но набор "административных" данных изменился полностью.
Гипотеза
Мне не объяснили причину. Но я могу предположить: алгоритм среагировал не на финансы, а на комбинацию вторичных признаков, которые статистически ассоциируются с проблемными заёмщиками.
Короткий срок по адресу. Несколько заявок подряд. Изменения данных между подачами. Добавление созаёмщика.
Каждый фактор по отдельности навряд ли мог оказать серьезное влияние на отказ. Но если в исторических данных люди с финансовыми проблемами чаще демонстрировали такую комбинацию поведения, модель может посчитать это подозрительным. даже если в конкретном случае за всем стоит обычная бытовая логика: продаём квартиру, выписываемся, переезжаем.
Я решил попробовать это проверить. Хотя бы на уровне «насколько это вообще правдоподобно».
Техническая часть: моделирование на открытых данных
Оговорка
Я не знаю, какую модель использует Сбер. Не знаю весов, архитектуры, порогов. Всё, что ниже — это попытка воспроизвести общую логику кредитного скоринга на публичном датасете и посмотреть, могут ли административные факторы заметно сдвинуть оценку при неизменном финансовом профиле.
Данные
Датасет Give Me Some Credit содержит 150 000 записей, целевая переменная "SeriousDlqin2yrs " - факт серьёзной просрочки в течение 2 лет.
Загружаем данные:
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.model_selection import train_test_split from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, roc_auc_score, confusion_matrix import warnings warnings.filterwarnings('ignore') # Загружаем датасет url = "https://raw.githubusercontent.com/DrIanGregory/Kaggle-GiveMeSomeCredit/refs/heads/master/data/GiveMeSomeCredit-training.csv" df = pd.read_csv(url, index_col=0) print("Размер датасета:", df.shape) print("\nПервые строки:") df.head()
Размер датасета: (150000, 11) Первые строки: SeriousDlqin2yrs RevolvingUtilizationOfUnsecuredLines age NumberOfTime30-59DaysPastDueNotWorse DebtRatio MonthlyIncome NumberOfOpenCreditLinesAndLoans NumberOfTimes90DaysLate NumberRealEstateLoansOrLines NumberOfTime60-89DaysPastDueNotWorse NumberOfDependents 1 1 0.766127 45 2 0.802982 9120.0 13 0 6 0 2.0 2 0 0.957151 40 0 0.121876 2600.0 4 0 0 0 1.0 3 0 0.658180 38 1 0.085113 3042.0 2 1 0 0 0.0 4 0 0.233810 30 0 0.036050 3300.0 5 0 0 0 0.0 5 0 0.907239 49 1 0.024926 63588.0 7 0 1 0 0.0
Для наглядности переведу признаки на русский:
column_names_ru = { 'SeriousDlqin2yrs': 'Дефолт_90_дней', # Целевая: были ли серьёзные просрочки 'RevolvingUtilizationOfUnsecuredLines': 'Использование_кредитных_линий', 'age': 'Возраст', 'NumberOfTime30-59DaysPastDueNotWorse': 'Просрочки_30_59_дней', 'DebtRatio': 'Долговая_нагрузка', 'MonthlyIncome': 'Месячный_доход', 'NumberOfOpenCreditLinesAndLoans': 'Количество_кредитов', 'NumberOfTimes90DaysLate': 'Просрочки_90_дней', 'NumberRealEstateLoansOrLines': 'Ипотечные_кредиты', 'NumberOfTime60-89DaysPastDueNotWorse': 'Просрочки_60_89_дней', 'NumberOfDependents': 'Иждивенцы' } print("Описание признаков:") for eng, rus in column_names_ru.items(): print(f" {rus}: {eng}")
Описание признаков: Дефолт_90_дней: SeriousDlqin2yrs Использование_кредитных_линий: RevolvingUtilizationOfUnsecuredLines Возраст: age Просрочки_30_59_дней: NumberOfTime30-59DaysPastDueNotWorse Долговая_нагрузка: DebtRatio Месячный_доход: MonthlyIncome Количество_кредитов: NumberOfOpenCreditLinesAndLoans Просрочки_90_дней: NumberOfTimes90DaysLate Ипотечные_кредиты: NumberRealEstateLoansOrLines Просрочки_60_89_дней: NumberOfTime60-89DaysPastDueNotWorse Иждивенцы: NumberOfDependents
# Обработаем пропуски df_clean = df.copy() # Заполняем медианой df_clean['MonthlyIncome'].fillna(df_clean['MonthlyIncome'].median(), inplace=True) df_clean['NumberOfDependents'].fillna(df_clean['NumberOfDependents'].median(), inplace=True) # На свякий случай, проверим на выборсы и удалим df_clean = df_clean[(df_clean['age'] > 18) & (df_clean['age'] < 100)] print(f"{len(df_clean)} записей")
149986 записей
Сначала опишу себя с точки зрения данного датасета:
# === ДЕКАБРЬ 2025: Идеальный заёмщик === december_applicant = pd.Series({ # Стандартные признаки 'RevolvingUtilizationOfUnsecuredLines': 0.0, # кредитами не пользовался и кредитные карты не открывал 'age': 40, # 40 лет 'NumberOfTime30-59DaysPastDueNotWorse': 0, # Просрочек за последние n не было 'DebtRatio': 0.0, # долгов не было 'MonthlyIncome': df_clean['MonthlyIncome'].median() * 2, # Месячный доход у меня в рублях, ну немного выше среднего (все-таки, работаю на трех работах: полная ставка на одной, а на двух других совместитель), я думаю, соответственно прикинем как в данном датасете в 2 раза больше медианного 'NumberOfOpenCreditLinesAndLoans': 0, # Кредиты не брал 'NumberOfTimes90DaysLate': 0, # просрочек не было 'NumberRealEstateLoansOrLines': 0, # Ипотечных кредитов ранее не брал 'NumberOfTime60-89DaysPastDueNotWorse': 0, # просрочек не было 'NumberOfDependents': 1, # 1 ребенок # "Секретные" признаки — всё стабильно 'months_at_address': 48, # 4 года по адресу 'recent_address_change': 0, # Не менял 'recent_credit_inquiries': 0, # Не подавал заявок 'data_changed_from_previous': 0, # Первая заявка 'amount_ratio_to_previous': 1.0, # Не применимо 'coborrower_added': 0 # Без созаёмщика }) # === ФЕВРАЛЬ 2026: После всех изменений === february_applicant = december_applicant.copy() # Что изменилось: february_applicant['months_at_address'] = 1 # ТОЛЬКО ЧТО сменил регистрацию! february_applicant['recent_address_change'] = 1 # Флаг: да, менял february_applicant['recent_credit_inquiries'] = 2 # Две заявки (старая + новая) february_applicant['data_changed_from_previous'] = 1 # Данные изменились! february_applicant['amount_ratio_to_previous'] = 0.46 # 5.5M / 12M = просит МЕНЬШЕ february_applicant['coborrower_added'] = 1 # Добавил жену february_applicant['NumberOfDependents'] = 1 # Ребёнок (без изменений)
Посмотрю абстрактно, где я нахожусь на данном датасете
feature_labels_ru = { 'age': 'Возраст', 'MonthlyIncome': 'Доход (мес.)', 'DebtRatio': 'Долговая нагрузка', # отношение платежей к доходу 'NumberOfOpenCreditLinesAndLoans': 'Открытых кредитов', # НЕ заявок, а уже действующих 'NumberOfDependents': 'Иждивенцы', # 1 ребенок 'RevolvingUtilizationOfUnsecuredLines': 'Использование лимита' # кредитами не пользовался и кредитные карты не открывал } features_to_plot = [ 'age', 'MonthlyIncome', 'DebtRatio', 'NumberOfOpenCreditLinesAndLoans', 'NumberOfDependents', 'RevolvingUtilizationOfUnsecuredLines' ] # === Графики стандартных финансовых признаков === fig, axes = plt.subplots(2, 3, figsize=(15, 10)) fig.suptitle('Распределение признаков: где я нахожусь?', fontsize=14, fontweight='bold') for ax, feature in zip(axes.flatten(), features_to_plot): # Ограничиваем выбросы для визуализации data = df_extended[df_extended[feature] < df_extended[feature].quantile(0.95)] # Гистограммы data[data['SeriousDlqin2yrs']==0][feature].hist( ax=ax, alpha=0.5, label='Надёжные', bins=30, color='green', density=True ) data[data['SeriousDlqin2yrs']==1][feature].hist( ax=ax, alpha=0.5, label='Дефолт', bins=30, color='red', density=True ) # Моя позиция в ДЕКАБРЕ dec_value = december_applicant[feature] ax.axvline(dec_value, color='blue', linestyle='--', linewidth=2.5, label=f'Декабрь: {dec_value:.1f}') # Моя позиция в ФЕВРАЛЕ (если изменилось) feb_value = february_applicant[feature] if dec_value != feb_value: ax.axvline(feb_value, color='orange', linestyle='--', linewidth=2.5, label=f'Февраль: {feb_value:.1f}') # Русское название из словаря ax.set_title(feature_labels_ru[feature], fontsize=11, fontweight='bold') ax.legend(fontsize=8) plt.tight_layout() plt.savefig('features_standard_with_me.png', dpi=150) plt.show()

Как видим, в данном датасете моя позиция не менялась ни в декабре и в феврале из-за того, что этот датасет не содержит признаков, которыми оперирует Сбер. Давайте попробуем добавить синететические признаки, чтобы провести анализ
Я добавлю 6 дополнительных признаков
Признак | Как генерируется | Почему именно так |
|---|---|---|
|
| Экспоненциальное распределение - большинство людей живут по адресу относительно недолго, но есть "хвост" с 10-20 годами. |
| Производный: | Бинарный флаг "только что переехал". Моя история |
|
| Пуассон для счётных событий. Среднее 0.5, большинство = 0, но некоторые = 1-2, редко ≥ 3. |
|
| 10% подающих повторную заявку меняют данные. Флаг нестабильности. |
|
| Отношение новой запрашиваемой суммы к ранее одобренной. У меня = 5.5M / 12M ≈ 0.46. А вдруг это тоже влияет. по крайней мере, Сбер это видит |
|
| Ну, тут взял грубо, может быть, таких как я 15%, которые добавляют созаёмщика при повторной подаче. Хотя, это +- |
# Создаём расширенный датасет с "банковскими" признаками df_extended = df_clean.copy() np.random.seed(42) n = len(df_extended) # === ДОБАВЛЯЕМ ПРИЗНАКИ, КОТОРЫЕ ЕСТЬ У СБЕРА === # 1. Срок проживания по текущему адресу (в месяцах) # Чем меньше — тем подозрительнее df_extended['months_at_address'] = np.random.exponential(scale=36, size=n).astype(int) df_extended['months_at_address'] = df_extended['months_at_address'].clip(1, 240) # 2. Смена адреса за последние 3 месяца (бинарный) df_extended['recent_address_change'] = (df_extended['months_at_address'] < 3).astype(int) # 3. Количество кредитных заявок за последние 30 дней df_extended['recent_credit_inquiries'] = np.random.poisson(lam=0.5, size=n) # 4. Изменение данных между заявками (флаг нестабильности) # Если человек подаёт повторную заявку с изменёнными данными — это триггер df_extended['data_changed_from_previous'] = np.random.binomial(1, p=0.1, size=n) # 5. Отношение запрашиваемой суммы к ранее одобренной # Если просишь меньше, чем одобряли раньше — вроде норм, но... # ...если ДАННЫЕ изменились — это подозрительно df_extended['amount_ratio_to_previous'] = np.random.uniform(0.3, 1.5, size=n) # 6. Добавление созаёмщика (которого не было раньше) df_extended['coborrower_added'] = np.random.binomial(1, p=0.15, size=n) # === КОРРЕЛЯЦИЯ С ДЕФОЛТОМ === # Люди со свежей сменой адреса + изменением данных чаще дефолтят # (это статистика, не причинно-следственная связь!) # Увеличиваем вероятность дефолта для "подозрительных" профилей suspicious_mask = ( (df_extended['recent_address_change'] == 1) | (df_extended['data_changed_from_previous'] == 1) | (df_extended['recent_credit_inquiries'] > 2) ) # Переопределяем target для обучения (грубо, но показательно) # В реальности банк обучает модель на исторических данных с этими признаками df_extended.loc[suspicious_mask, 'SeriousDlqin2yrs'] = np.where( np.random.random(suspicious_mask.sum()) < 0.3, # 30% из подозрительных — дефолт 1, df_extended.loc[suspicious_mask, 'SeriousDlqin2yrs'] ) print(f"Добавлено признаков: 6") print(f"Доля 'подозрительных' профилей: {suspicious_mask.mean()*100:.1f}%") print(f"Новая доля злостных неплательщиков: {df_extended['SeriousDlqin2yrs'].mean()*100:.2f}%")
Добавлено признаков: 6 Доля 'подозрительных' профилей: 18.3% Новая доля злостных неплательщиков: 11.76%
Ну, давайте теперь нарисуем графики с учетом синтетических данных
# ПРОДУБЛИРУЮ МОИ ПАРАМЕТРЫ: # === ДЕКАБРЬ 2025: Идеальный заёмщик === december_applicant = pd.Series({ # Стандартные признаки 'RevolvingUtilizationOfUnsecuredLines': 0.0, # кредитами не пользовался и кредитные карты не открывал 'age': 40, # 40 лет 'NumberOfTime30-59DaysPastDueNotWorse': 0, # Просрочек за последние n не было 'DebtRatio': 0.0, # долгов не было 'MonthlyIncome': df_clean['MonthlyIncome'].median() * 2, # Месячный доход у меня в рублях, ну немного выше среднего (все-таки, работаю на трех работах: полная ставка на одной, а на двух других совместитель), я думаю, соответственно прикинем как в данном датасете в 2 раза больше медианного 'NumberOfOpenCreditLinesAndLoans': 0, # Кредиты не брал 'NumberOfTimes90DaysLate': 0, # просрочек не было 'NumberRealEstateLoansOrLines': 0, # Ипотечных кредитов ранее не брал 'NumberOfTime60-89DaysPastDueNotWorse': 0, # просрочек не было 'NumberOfDependents': 1, # 1 ребенок # "Секретные" признаки — всё стабильно 'months_at_address': 48, # 4 года по адресу 'recent_address_change': 0, # Не менял 'recent_credit_inquiries': 0, # Не подавал заявок 'data_changed_from_previous': 0, # Первая заявка 'amount_ratio_to_previous': 1.0, # Не применимо 'coborrower_added': 0 # Без созаёмщика }) # === ФЕВРАЛЬ 2026: После всех изменений === february_applicant = december_applicant.copy() # Что изменилось: february_applicant['months_at_address'] = 1 # ТОЛЬКО ЧТО сменил регистрацию! february_applicant['recent_address_change'] = 1 # Флаг: да, менял february_applicant['recent_credit_inquiries'] = 2 # Две заявки (старая + новая) february_applicant['data_changed_from_previous'] = 1 # Данные изменились! february_applicant['amount_ratio_to_previous'] = 0.46 # 5.5M / 12M = просит МЕНЬШЕ february_applicant['coborrower_added'] = 1 # Добавил жену february_applicant['NumberOfDependents'] = 1 # Ребёнок (без изменений) #-------------- # === СДЕЛАЕМ ГРАФИКИ === # Теперь мы увидим, что ему не понравилось fig, axes = plt.subplots(2, 3, figsize=(15, 10)) fig.suptitle('"Расширенные" банковские признаки и оценка динамики ДЕК-ФЕВ:', fontsize=14, fontweight='bold') secret_features = [ 'months_at_address', 'recent_address_change', 'recent_credit_inquiries', 'data_changed_from_previous', 'amount_ratio_to_previous', 'coborrower_added' ] feature_labels = { 'months_at_address': 'Месяцев по адресу', 'recent_address_change': 'Смена адреса (0/1)', 'recent_credit_inquiries': 'Заявок за 30 дней', 'data_changed_from_previous': 'Данные изменены (0/1)', 'amount_ratio_to_previous': 'Отношение к прошлой сумме', 'coborrower_added': 'Созаёмщик добавлен (0/1)' } for ax, feature in zip(axes.flatten(), secret_features): data = df_extended.copy() # Для бинарных признаков — bar chart if feature in ['recent_address_change', 'data_changed_from_previous', 'coborrower_added']: # Считаем доли дефолтов для каждого значения grouped = data.groupby(feature)['SeriousDlqin2yrs'].agg(['sum', 'count']) grouped['default_rate'] = grouped['sum'] / grouped['count'] x_pos = [0, 1] bars = ax.bar(x_pos, grouped['default_rate'], color=['green', 'red'], alpha=0.7) # Моя позиция dec_val = int(december_applicant[feature]) feb_val = int(february_applicant[feature]) # Отмечаем декабрь ax.scatter([dec_val], [grouped.loc[dec_val, 'default_rate'] + 0.02], marker='v', s=200, color='blue', zorder=5, label='Декабрь') # Отмечаем февраль (если изменилось) if dec_val != feb_val: ax.scatter([feb_val], [grouped.loc[feb_val, 'default_rate'] + 0.02], marker='v', s=200, color='orange', zorder=5, label='Февраль') # Стрелка перехода ax.annotate('', xy=(feb_val, grouped.loc[feb_val, 'default_rate']), xytext=(dec_val, grouped.loc[dec_val, 'default_rate']), arrowprops=dict(arrowstyle='->', color='purple', lw=2)) ax.set_xticks([0, 1]) ax.set_xticklabels(['Нет (0)', 'Да (1)']) ax.set_ylabel('Доля неплательщиков') else: # Для непрерывных — гистограмма data[data['SeriousDlqin2yrs']==0][feature].hist( ax=ax, alpha=0.5, label='Надёжные', bins=30, color='green', density=True ) data[data['SeriousDlqin2yrs']==1][feature].hist( ax=ax, alpha=0.5, label='Неплательщики', bins=30, color='red', density=True ) # Моя позиция dec_value = december_applicant[feature] feb_value = february_applicant[feature] ax.axvline(dec_value, color='blue', linestyle='--', linewidth=2.5, label=f'Дек: {dec_value:.1f}') if abs(dec_value - feb_value) > 0.01: ax.axvline(feb_value, color='orange', linestyle='--', linewidth=2.5, label=f'Фев: {feb_value:.1f}') # Стрелка перехода y_max = ax.get_ylim()[1] * 0.8 ax.annotate('', xy=(feb_value, y_max), xytext=(dec_value, y_max), arrowprops=dict(arrowstyle='->', color='purple', lw=2)) ax.set_title(feature_labels.get(feature, feature), fontsize=11, fontweight='bold') ax.legend(fontsize=8, loc='upper right') plt.tight_layout() plt.savefig('features_secret_with_me.png', dpi=150) plt.show()

Ну, теперь видим различия в заявках:
Предварительно, скорингу могли не понравиться:
"Месяцев по адресу" - видим, что изменение адреса МОГЛО сместить меня в сторону злостных неплательщиков. Вроде, это логично, потому что если человеку негде жить, он будет испытывать затруднения с кредитом
"Смена адреса (да/нет)" - видим аналогичную ситуацию, как меня перевели в рисковую группу неплательщиков
"Заявок за 30 дней" - В декабре ни одной предыдущей заявки на ипотеку, а в феврале уже 2 заявки (закрытая + новая). Множественные запросы — классический "красный флаг" в скоринге: выглядит как человек, который судорожно ищет деньги
"Данные изменены (не менялись/менялись)" - С точки зрения модели, нестабильность анкетных данных — тревожный сигнал
"Отношение к прошлой сумме" - по логике, само по себе снижение суммы кредита хороший знак. Но в комбинации с остальными изменениями модель может интерпретировать это как часть подозрительного паттерна: «сменил адрес, изменил данные, теперь просит другую сумму»
"Созаёмщик добавлен (0/1)" - напомню: декабрь подавал один (0), а февраль, для надежности решил с женой (1). Может быть, добавление созаёмщика при повторной подаче статистически связано с чуть более высокой долей дефолтов. Возможная логика модели: если в первый раз справлялся сам, а теперь вдруг нужен созаёмщик — возможно, финансовая ситуация ухудшилась
Как видим, в теории комплекс факторов мог сильно ухудшить мой рейтинг и сдвинуть решение алгоритма Сбера в сторону отказа. По крайней мере, выглядит логично.
Ну, наконец, давайте обучим собственную модель скоринга и посмотрим, как она принимает решения
Модель в колабе учится где-то 1-2 минуты, так что придется ждать ...
# train|test данные X_ext = df_extended.drop('SeriousDlqin2yrs', axis=1) y_ext = df_extended['SeriousDlqin2yrs'] X_train_ext, X_test_ext, y_train_ext, y_test_ext = train_test_split( X_ext, y_ext, test_size=0.2, random_state=42, stratify=y_ext ) # Скорее всего, Сбер использует ее, потому что ее результат наиболее интерпретируемый model_extended = GradientBoostingClassifier( n_estimators=100, max_depth=6, random_state=42, min_samples_leaf=50 ) model_extended.fit(X_train_ext, y_train_ext) y_pred_ext = model_extended.predict_proba(X_test_ext)[:, 1] print(f"\n📊 ROC-AUC (расширенная модель): {roc_auc_score(y_test_ext, y_pred_ext):.4f}")
📊 ROC-AUC (расширенная модель): 0.8620
Давайте посмотрим, какие признаки важны для нашей скоринговой модели при принятии решения в отказе/одобрении заявки
feature_importance_ext = pd.DataFrame({ 'Признак': X_ext.columns, 'Важность': model_extended.feature_importances_ }).sort_values('Важность', ascending=False) print("\nВАЖНОСТЬ ПРИЗНАКОВ:") print("=" * 60) for _, row in feature_importance_ext.head(15).iterrows(): bar = "█" * int(row['Важность'] * 100) # Подсвечиваем "скрытые" признаки marker = "🔴" if row['Признак'] in [ 'recent_address_change', 'data_changed_from_previous', 'months_at_address', 'recent_credit_inquiries', 'coborrower_added', 'amount_ratio_to_previous' ] else " " print(f"{marker} {row['Признак']:40} {row['Важность']:.4f} {bar}") print("\n🔴 = Признаки, которых нет в публичных датасетах")
ВАЖНОСТЬ ПРИЗНАКОВ: ============================================================ NumberOfTimes90DaysLate 0.2388 ███████████████████████ 🔴 data_changed_from_previous 0.2126 █████████████████████ 🔴 recent_address_change 0.1204 ████████████ RevolvingUtilizationOfUnsecuredLines 0.1000 █████████ NumberOfTime30-59DaysPastDueNotWorse 0.0644 ██████ 🔴 months_at_address 0.0629 ██████ NumberOfTime60-89DaysPastDueNotWorse 0.0418 ████ 🔴 recent_credit_inquiries 0.0350 ███ DebtRatio 0.0329 ███ 🔴 amount_ratio_to_previous 0.0215 ██ age 0.0213 ██ MonthlyIncome 0.0212 ██ NumberOfOpenCreditLinesAndLoans 0.0142 █ NumberRealEstateLoansOrLines 0.0076 NumberOfDependents 0.0050 🔴 = Признаки, которые я добавил
Делаем скоринг по моим параметрам в декабре и феврале
def get_risk_extended(applicant_data): return model_extended.predict_proba(applicant_data.values.reshape(1, -1))[0][1] def credit_decision_extended(risk_score, threshold=0.12): "Порог одобрения" if risk_score < threshold: return "✅ ОДОБРЕНО", "green" elif risk_score < threshold * 1.3: return "⚠️ РУЧНАЯ ПРОВЕРКА", "orange" else: return "❌ ОТКАЗАНО", "red" # === ДЕКАБРЬ 2025: Идеальный заёмщик === december_applicant = pd.Series({ # Стандартные признаки 'RevolvingUtilizationOfUnsecuredLines': 0.0, # кредитами не пользовался и кредитные карты не открывал 'age': 40, # 40 лет 'NumberOfTime30-59DaysPastDueNotWorse': 0, # Просрочек за последние n не было 'DebtRatio': 0.0, # долгов не было 'MonthlyIncome': df_clean['MonthlyIncome'].median() * 2, # Месячный доход у меня в рублях, ну немного выше среднего (все-таки, работаю на трех работах: полная ставка на одной, а на двух других совместитель), я думаю, соответственно прикинем как в данном датасете в 2 раза больше медианного 'NumberOfOpenCreditLinesAndLoans': 0, # Кредиты не брал 'NumberOfTimes90DaysLate': 0, # просрочек не было 'NumberRealEstateLoansOrLines': 0, # Ипотечных кредитов ранее не брал 'NumberOfTime60-89DaysPastDueNotWorse': 0, # просрочек не было 'NumberOfDependents': 1, # 1 ребенок # "Секретные" признаки — всё стабильно 'months_at_address': 48, # 4 года по адресу 'recent_address_change': 0, # Не менял 'recent_credit_inquiries': 0, # Не подавал заявок 'data_changed_from_previous': 0, # Первая заявка 'amount_ratio_to_previous': 1.0, # Не применимо 'coborrower_added': 0 # Без созаёмщика }) # === ФЕВРАЛЬ 2026: После всех изменений === february_applicant = december_applicant.copy() # Что изменилось: february_applicant['months_at_address'] = 1 # ТОЛЬКО ЧТО сменил регистрацию! february_applicant['recent_address_change'] = 1 # Флаг: да, менял february_applicant['recent_credit_inquiries'] = 2 # Две заявки (старая + новая) february_applicant['data_changed_from_previous'] = 1 # Данные изменились! february_applicant['amount_ratio_to_previous'] = 0.46 # 5.5M / 12M = просит МЕНЬШЕ february_applicant['coborrower_added'] = 1 # Добавил жену february_applicant['NumberOfDependents'] = 1 # Ребёнок (без изменений) # Считаем риски risk_dec = get_risk_extended(december_applicant) risk_feb = get_risk_extended(february_applicant) decision_dec, _ = credit_decision_extended(risk_dec) decision_feb, _ = credit_decision_extended(risk_feb) print("=" * 70) print("📅 ДЕКАБРЬ 2025 — ПЕРВИЧНАЯ ЗАЯВКА") print("=" * 70) print(f" Срок по адресу: 48 месяцев") print(f" Смена адреса: НЕТ") print(f" Созаёмщик: НЕТ") print(f" Изменение данных: НЕТ (первая заявка)") print("-" * 70) print(f" 🎯 Риск дефолта: {risk_dec:.4f} ({risk_dec*100:.2f}%)") print(f" 📋 Решение: {decision_dec}") print("=" * 70) print() print("=" * 70) print("📅 ФЕВРАЛЬ 2026 — ПОВТОРНАЯ ЗАЯВКА") print("=" * 70) print(f" Срок по адресу: 1 месяц (ТОЛЬКО СМЕНИЛ!)") print(f" Смена адреса: ДА") print(f" Созаёмщик: ДОБАВЛЕН") print(f" Изменение данных: ДА") print(f" Недавние заявки: 2") print("-" * 70) print(f" 🎯 Риск: {risk_feb:.4f} ({risk_feb*100:.2f}%)") print(f" 📋 Решение: {decision_feb}") print("=" * 70) print(f"\n📈 Изменение риска: {risk_dec:.2%} → {risk_feb:.2%}") print(f" Рост в {risk_feb/risk_dec:.1f} раз!")
====================================================================== 📅 ДЕКАБРЬ 2025 — ПЕРВИЧНАЯ ЗАЯВКА ====================================================================== Срок по адресу: 48 месяцев Смена адреса: НЕТ Созаёмщик: НЕТ Изменение данных: НЕТ (первая заявка) ---------------------------------------------------------------------- 🎯 Риск дефолта: 0.0188 (1.88%) 📋 Решение: ✅ ОДОБРЕНО ====================================================================== ====================================================================== 📅 ФЕВРАЛЬ 2026 — ПОВТОРНАЯ ЗАЯВКА ====================================================================== Срок по адресу: 1 месяц (ТОЛЬКО СМЕНИЛ!) Смена адреса: ДА Созаёмщик: ДОБАВЛЕН Изменение данных: ДА Недавние заявки: 2 ---------------------------------------------------------------------- 🎯 Риск: 0.2694 (26.94%) 📋 Решение: ❌ ОТКАЗАНО ====================================================================== 📈 Изменение риска: 1.88% → 26.94% Рост в 14.3 раз!
Как видите, модель также отказала мне в ипотеке. Разуеется, работа сделана на основании тех изменений, которые я смог добыть из текста отказа.
Давайте посмотрим, насколько каждый признак не понравился модели
print("\n" + "=" * 70) print("ЧТО ИМЕННО ПОВЛИЯЛО?") print("=" * 70) baseline = get_risk_extended(december_applicant) print(f"\nБазовый риск (декабрь): {baseline:.4f}\n") changes = { 'months_at_address': (48, 1, "Срок по адресу"), 'recent_address_change': (0, 1, "Флаг смены адреса"), 'recent_credit_inquiries': (0, 2, "Недавние заявки"), 'data_changed_from_previous': (0, 1, "Изменение данных"), 'coborrower_added': (0, 1, "Добавление созаёмщика"), } impacts = [] for param, (old_val, new_val, description) in changes.items(): test = december_applicant.copy() test[param] = new_val new_risk = get_risk_extended(test) delta = (new_risk - baseline) * 100 impacts.append((description, param, old_val, new_val, delta)) # Сортируем по влиянию impacts.sort(key=lambda x: -abs(x[4])) for desc, param, old, new, delta in impacts: direction = "📈" if delta > 0 else "📉" bar = "█" * int(abs(delta) * 5) print(f"{direction} {desc}") print(f" {old} → {new}") print(f" Влияние: {delta:+.2f}% {bar}") print() # Кумулятивный эффект print("-" * 70) print("КУМУЛЯТИВНЫЙ ЭФФЕКТ (все изменения вместе):") print(f" Было: {baseline:.4f} ({baseline*100:.2f}%)") print(f" Стало: {risk_feb:.4f} ({risk_feb*100:.2f}%)") print(f" Итого: +{(risk_feb - baseline)*100:.2f} процентных пунктов")
====================================================================== ЧТО ИМЕННО ПОВЛИЯЛО? ====================================================================== Базовый риск (декабрь): 0.0188 📈 Изменение данных 0 → 1 Влияние: +37.81% █████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ 📈 Флаг смены адреса 0 → 1 Влияние: +11.29% ████████████████████████████████████████████████████████ 📈 Срок по адресу 48 → 1 Влияние: +3.80% ███████████████████ 📈 Недавние заявки 0 → 2 Влияние: +0.13% 📉 Добавление созаёмщика 0 → 1 Влияние: +0.00% ---------------------------------------------------------------------- КУМУЛЯТИВНЫЙ ЭФФЕКТ (все изменения вместе): Было: 0.0188 (1.88%) Стало: 0.2694 (26.94%) Итого: +25.06 процентных пунктов
Ну, моей модели очень не понравилась смена адреса (а я грешил на супругу). Эх... Добавление созаемщика никак не влияло на решение модели в отказе.
Реконструкция событий
Скорее всего, отказали из-за того, что я сменил регистрацию и сделал 2 заявки за короткий период времени. Видимо, модель "решила", что я судорожно ищу деньги в условиях проблем с жильем.
На деле же у меня просто обычная АЛЬТЕНАТИВНАЯ сделка. Мне просто не повезло со сроками заявки на ипотеку.
Ирония ситуации
В декабре одобрили мне ОДНОМУ 12.000.000
В феврале, когда уже все готово, когда я в стрессе, собираю документы, мне отказывают в 5.500.000
Для самого себя я стал более надежным заемщиком
Для алгоритма Сбера я стал токсичным подозрительным типом
Выводы
Что показало моделирование
Даже на синтетических данных видно: комбинация нескольких небольших изменений одновременно может резко сдвинуть скоринговую оценку при абсолютно неизменном финансовом профиле. Ни один из факторов по отдельности не выглядит критичным. Но совокупность факторов: смена регистрации, повторная заявка, изменение данных, добавление созаёмщика — формирует паттерн, который статистически ассоциируется с проблемными заёмщиками.
Модель не знает, почему я сменил адрес. Она знает только, что люди, которые так делают, исторически чаще не платят. И этого ей достаточно.
Что не так с процессом
Я не утверждаю, что алгоритм ошибся. Возможно, с точки зрения минимизации рисков на выборке из миллионов заёмщиков, отказ мне является оправданным решением. Лучше, из 1000 человек, которые не будут платить по кредиту, пострадаю один я, чем банк лишится денег или будет заниматься должниками.
Но в данной ситуации, у меня конкретная сделка, конкретная цепочка, конкретный ребёнок, которому нужна квартира. И вот здесь начинается проблема.
Проблема не в том, что модель отказала. Проблема в том, что после отказа - пустота.
Сотрудники банка видят ровно то же, что и я: «Отказано». Без объяснений, без деталей, без возможности что-либо сделать.
Канала коммуникации просто нет. Апелляции нет. Возможности предоставить дополнительные документы - нет.
Единственный совет: «Подайте через месяц». При том, что сделка — через неделю.
При этом ДомКлик сам предложил закрыть старую заявку и подать новую. То есть процедура, которая привела к отказу, была инициирована самим банком.
Т-Банк и ВТБ одобрили заявку за 5 минут. С теми же данными, с той же сменой регистрации, с тем же созаёмщиком. Это не доказывает, что Сбер ошибся - у банков разные модели и разный аппетит к риску. Но это показывает, что мой профиль не является объективно "отказным".
Что можно было бы сделать лучше
Сбер позиционирует себя как технологическую компанию. Выступает на конференциях по AI, публикует исследования, развивает GigaChat, проводит AI Journey. Это вызывает уважение. Но технологичность - это не только модели и инфраструктура. Это ещё и то, как система взаимодействует с человеком в пограничных случаях.
Несколько вещей, которые кажутся мне разумными:
1. Контекстный классификатор для ручной проверки. Если модель видит, что клиент с чистой кредитной историей, подтверждённым доходом и недавним одобрением внезапно получает отказ - может быть, стоит не отказывать автоматически, а направить на ручную проверку? Это не «сделать исключение». Это выделить случаи, где автоматическое решение может быть ненадёжным.
2. Объяснимость решения. Хотя бы на уровне категории: «отказ связан с изменением анкетных данных» или «отказ связан с кредитной историей». Не нужно раскрывать веса модели. Но когда человек не понимает, что произошло, и сотрудники банка тоже не понимают - это не безопасность, это дефект процесса.
3. Механизм апелляции. Возможность предоставить документы, объясняющие контекст. Договор купли-продажи. Справку о временной регистрации в связи с переездом. Свидетельство о рождении ребёнка. Что-нибудь, что позволит системе учесть реальность, а не только статистику.
4. Согласованность внутренних процессов. Если ДомКлик рекомендует закрыть заявку и подать новую, то процесс повторной подачи не должен приводить к автоматическому отказу из-за факта повторной подачи. Левая рука должна знать, что делает правая.
Послесловие
Эта статья - не жалоба и не попытка давления. Ситуация в процессе разрешения: жалоба подана в ЦБ, письменная претензия - в Сбер. Ипотеку, вероятно, возьму в другом банке - одобрения уже есть.
Но мне хотелось зафиксировать этот случай, потому что он хорошо иллюстрирует разрыв между моделью на слайде и моделью в жизни. Когда читаешь про precision и recall в учебнике - это абстракция. Когда ты сам оказываешься тем самым false negative - абстракция заканчивается.
Хороший алгоритм - это не тот, который никогда не ошибается. Это тот, который предусматривает, что он может ошибиться. И оставляет человеку выход.
Colab опубликую попозже, как почищу.
Спасибо за внимание, надеюсь никого не задел.
