Обучение и валидация скоринговой карты¶
Обучаем скоринговую карту (SPEC §8) на синтетическом мире (SPEC §14), проверяем её на отложенной выборке и выгружаем в scorecard_v1.json — формат контракта Scorecard (docs/v2/CONTRACTS.md). Это наши собственные открытые веса: скоринговый модуль (T15) читает файл, банк может подставить свою модель в том же формате.
Запуск¶
python -m apps.ml --seed 42 --n 5000 --as-of 2026-10-01 --version scorecard_v1 --created-at 2026-10-24
Все параметры необязательные; мир — тот же, что python -m apps.world строит по умолчанию, --created-at по умолчанию — сегодня. Обучение и валидация на 5000 компаниях занимают около 10 секунд, numpy и scikit-learn не нужны. Результат — три файла в apps/ml/:
scorecard_v1.json— сама карта: интервалы, WoE и баллы каждого признака, базовые баллы, границы грейдов;scorecard_v1.training.json— паспорт обучения: версия набора признаков (feature_set_version), параметры мира, размер выборки, информационная ценность (IV), коэффициент и z-статистика каждого признака, почему признак не вошёл в карту, Gini на обучающей выборке. Версия набора признаков лежит здесь, потому что в контрактеScorecardдля неё нет поля;scorecard_v1.validation.json— валидация карты на отложенной выборке для страницы риск-менеджера (T32), см. «Валидация».
Одинаковый мир и одинаковая --created-at дают побайтно одинаковые файлы.
Когда переобучать¶
При любой правке config/features.json (поднимается его version), определений признаков в apps/api/features/, генератора мира или config/world_outcomes.json. Валидация пересчитывается той же командой. Тест test_committed_scorecard_is_what_training_gives переобучает карту и сравнивает все три файла с файлами в репозитории: если они устарели, он падает и подсказывает команду.
Как обучается¶
train.py:
- Выборка. Компании обучающей выборки мира (
sample == 'train'), признаки —extract_features(SourcePayloads.from_company(company), world.meta.as_of): тот же вызов, что у движка на живой заявке. Из скрытой правды мира обучение берёт только исход (default_12m) и выборку (sample). - Разбиение на интервалы и WoE (
binning.py). WoE интервала = ln(доля всех «хороших» в интервале / доля всех дефолтов в интервале): положительный WoE — интервал надёжнее среднего. - Числовой признак сначала режется на 20 квантильных классов. Потом соседние классы сливаются, пока в каждом интервале не окажется не меньше 5% компаний с данными и не меньше 10 дефолтов, пока доля дефолтов не станет монотонной по значению признака и пока интервалов не станет не больше пяти. Крайние интервалы открыты, поэтому любое значение попадает в какой-то интервал.
- Категориальный признак (группа риска ОКВЭД, правовая форма, флаги «да / нет»): каждая категория — свой интервал, маленькие сливаются с ближайшими по доле дефолтов. Категория, которой не было в обучении, всё равно получает интервал.
- У каждого признака есть интервал «нет данных» (SPEC §5.1). Если компаний без данных достаточно (те же 5% и 10 дефолтов), у него свой WoE; если нет — WoE пустой, а баллы нулевые, то есть средний риск: учиться не на чем.
- Отбор признаков. Признаки с IV меньше 0,02 не различают заёмщиков и отбрасываются. На остальных — логистическая регрессия
ln(odds_good) = b0 + сумма(b_i * WoE_i)методом Ньютона с небольшой L2-регуляризацией (logreg.py). Затем по одному убирается самый слабый признак, пока у каждого коэффициента z-статистика не станет не меньше 2. Отрицательный коэффициент значит, что признак в модели работает против своего WoE: обычно его информацию уже несёт другой признак. Такой признак тоже убирается, иначе баллы стали бы необъяснимыми («лучше значение — меньше баллов»). - Баллы (SPEC §8): 600 баллов = шансы 50:1, +20 баллов = шансы ×2.
factor = 20 / ln 2, offset = 600 − factor · ln 50
баллы интервала = factor · b_i · WoE интервала
базовые баллы = offset + factor · b0
PD = 1 / (1 + odds_good), odds_good = 50 · 2^((баллы − 600) / 20)
Баллы интервала показывают, насколько он надёжнее (+) или рискованнее (−) среднего. Сумма базовых баллов и баллов по признакам переводится в PD через Scorecard.pd_from_points. Баллы округляются до десятых.
5. Грейды — границы SPEC §8: A < 1,5%, B < 3%, C < 6%, D < 12%, E — остальные.
score.py — как карта читает признаки (find_bin, total_points). Скоринговый модуль (T15) должен давать те же баллы; проще всего импортировать их из apps.ml: пакет зависит только от контрактов, а обучение (apps.ml.train, ему нужен apps/world/) при этом не загружается.
Что получилось (scorecard_v1, зерно 42, 5000 компаний)¶
Обучающая выборка — 3492 компании, 234 дефолта (6,7%). В карту вошли 8 признаков: волатильность поступлений, доля снятия наличных, средний остаток в днях расходов, месяцы с выплатой зарплаты, худшая просрочка компании, использование лимитов, возраст бизнеса, маржинальность. Остальные отброшены, причина у каждого — в scorecard_v1.training.json.
| Gini карты | Gini истинной PD (потолок) | |
|---|---|---|
| Обучающая выборка | 0,56 | 0,66 |
| Отложенная выборка (1508 компаний, 111 дефолтов) | 0,46 | 0,61 |
Компании распределяются по грейдам примерно поровну (на отложенной выборке A — 23%, B — 19%, C — 19%, D — 19%, E — 20%), все «технические» компании попадают в E. Полная валидация — в разделе «Валидация».
Что стоит знать о результате:
- Группа риска ОКВЭД почти ничего не добавляет (IV 0,01), хотя в мире отрасль сдвигает вероятность дефолта. Поправки отраслей (от −0,2 до +0,5 к логарифму шансов) малы по сравнению с влиянием скрытого качества
q, а группы объединяют отрасли с разными поправками. - Кредитная история слабее поведения по счёту. В карту попала только худшая просрочка компании (62+ дня); просрочка собственника, запросы КИ и долг к выручке отброшены: их информацию уже несут признаки по счёту. Это свойство синтетического мира (
apps/world/README.md, «Какqпроявляется в кредитной истории»), а не метода. - Смена собственника или директора и отрицательный капитал не связаны с исходом (IV ≈ 0): в мире они от
qне зависят.
Валидация¶
validation.py — метрики и формат отчёта ValidationReport; как и score.py, он зависит только от контрактов, поэтому API может читать отчёт без мира. validate.py прогоняет карту по миру: признаки обучающей и отложенной выборки — тот же вызов extract_features, что у обучения и движка. Из скрытой правды мира валидация берёт исход (default_12m), выборку (sample) и, только для потолка Gini, истинную PD (pd_12m) — одним агрегированным числом, не по компаниям.
Отчёт проверяет три свойства, которые кейс (стр. 14) называет для рейтинговых моделей:
- Способность различать (
gini,cap). Gini на отложенной выборке и его 95%-й интервал по стандартной ошибке AUC (Hanley & McNeil). Рядом для сравнения — Gini на обучающей выборке и Gini истинной PD мира, то есть потолок, выше которого не поднимется никакая модель на этих данных. Кривая CAP (21 точка): отказав X% самых рискованных компаний, банк отсекает Y% всех дефолтов. - Точность (
calibration). По каждому грейду — число компаний, дефолты, средняя прогнозная PD, фактическая доля дефолтов и её 95%-й интервал Уилсона (в отличие от «p ± 1,96·σ» он не выходит за [0, 1] при малом числе дефолтов).predicted_vs_actual:within— прогноз внутри интервала,below— карта недооценивает риск,above— переоценивает. - Стабильность (
psi_grades,psi_points,psi_features). PSI = Σ (доля сейчас − доля при обучении) · ln(доля сейчас / доля при обучении). Считается по грейдам, по 10 диапазонам баллов, равным по размеру на обучающей выборке, и по интервалам каждого признака карты (индекс стабильности признака, CSI). Пороги общепринятые: меньше 0,1 —stable, 0,1–0,25 —watch, от 0,25 —shift(PSI_WATCH,PSI_SHIFTвvalidation.py). Пустой интервал считается с долей 0,01%, чтобы PSI не уходил в бесконечность.
В limitations отчёт перечисляет ограничения, которые страница должна показать рядом с метриками (раздел «Упрощения» ниже).
Что получилось (scorecard_v1)¶
Отложенная выборка — 1508 компаний, 111 дефолтов (7,4%).
| Значение | |
|---|---|
| Gini, отложенная выборка | 0,46 (95%: 0,35–0,57) |
| Gini, обучающая выборка | 0,56 |
| Gini истинной PD (потолок) | 0,61 |
| CAP: отказ 20% самых рискованных | отсекает 49% дефолтов |
| Грейд | Доля компаний | Прогнозная PD | Фактическая доля дефолтов | 95%-й интервал | Итог |
|---|---|---|---|---|---|
| A | 23% | 0,9% | 1,7% (6 из 343) | 0,8–3,8% | внутри |
| B | 19% | 2,2% | 3,8% (11 из 289) | 2,1–6,7% | внутри |
| C | 19% | 4,2% | 6,6% (19 из 288) | 4,3–10,1% | прогноз ниже |
| D | 19% | 8,6% | 7,4% (21 из 285) | 4,9–11,0% | внутри |
| E | 20% | 20,4% | 17,8% (54 из 303) | 13,9–22,5% | внутри |
Средняя прогнозная PD — 7,2% при фактических 7,4%. Карта откалибрована на долю дефолтов обучающей выборки (6,7%), а в отложенной дефолтов случайно больше, поэтому в грейдах A–C прогноз ниже факта; в C он на границе интервала. Перекалибровывать карту на отложенную выборку мы не стали: тогда она перестала бы быть отложенной.
PSI по грейдам — 0,004, по баллам — 0,007, по признакам — не больше 0,01: при случайном разбиении одного среза так и должно быть.
Упрощения¶
В коде помечены # SIMPLIFICATION:.
| Упрощение | Как было бы в реальности |
|---|---|
| Обучение на всех компаниях обучающей выборки, как будто банк выдал кредит всем заявителям, включая «технические» компании | Исходы известны только по выданным кредитам. По отказанным заявкам их восстанавливают (reject inference: экстраполяция, взвешивание, данные БКИ о кредитах в других банках). Отбирать компании по архетипу мы не можем: это скрытая правда мира |
| Выборка обучения — один срез мира, отложенная — случайная часть того же среза; Gini на ней поэтому ближе к обучающему, чем было бы в реальности | Обучение на нескольких поколениях выдач, проверка на более позднем периоде (out-of-time) |
| PSI сравнивает отложенную выборку с обучающей, а не поток заявок с обучающей выборкой | Ежемесячный мониторинг: PSI грейдов и признаков по новым заявкам, при shift — пересмотр модели |
Пороги PSI (0,1 и 0,25) и 95%-е интервалы — общепринятые, в validation.py, а не в config/ |
Пороги мониторинга и проверки калибровки (светофор по биномиальному тесту) утверждает валидационное подразделение |
| Модель обучена на синтетических данных: её качество доказывает работу конвейера обучения, а не реальный риск (SPEC §15) | Обучение на истории портфеля банка, независимая валидация |
| Параметры разбиения и отбора (5% и 10 дефолтов на интервал, до 5 интервалов, IV ≥ 0,02, z ≥ 2) взяты из учебной практики; на небольшом переборе (3–8% на интервал, 4–5 интервалов, 5–10 дефолтов) Gini на отложенной выборке менялся в пределах шума | Подбор на кросс-валидации, экспертный пересмотр интервалов андеррайтерами |
| Карта откалибрована на долю дефолтов обучающей выборки (6,7%) | Калибровка на долгосрочную среднюю долю дефолтов по циклу (through-the-cycle) |
Тесты¶
tests/test_ml.py:
- интервалы монотонные, не меньше 5% компаний и 10 дефолтов, покрывают все значения; маленький интервал «нет данных» — средний риск; категория, которой не было в обучении, получает интервал;
- логистическая регрессия восстанавливает известные коэффициенты; Gini на краевых случаях;
- карта проходит контракт; средняя PD по баллам равна доле дефолтов обучающей выборки; баллы интервала имеют знак его WoE;
- на отложенной выборке Gini больше 0,35 и меньше потолка истинной PD;
- PSI, интервалы Уилсона и Gini, кривая CAP, калибровка по грейдам и диапазоны баллов — на известных примерах;
- отчёт валидации проходит свою модель, сходится с выборками, PSI при случайном разбиении —
stable; модуль метрик не загружает мир; - обучение воспроизводимо, а
scorecard_v1.json, паспорт и отчёт валидации в репозитории совпадают с переобучением.