Определение вероятности невозврата кредита

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

kredit.jpg

Предисловие

В рамках курса для магистов ВМК МГУ мы участвуем в решении задач анализа данных на соревновательных платформах. «Мы» — это потому, что я вместе со студентами решаю задачи, чтобы

  • быть в курсе всех аспектов решения задачи,
  • показать «как надо», если что-то не будет получаться,
  • задать бенчмарки, которые студенты должны преодолеть для получения определённых баллов.

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

Ещё один плюс местных соревнований: правила конкурсов позволяют использовать их в учебном процессе (кстати, а часто и правил-то нет:). Скажем, может быть запрещено использовать два акаунта, обмениваться кодом… и всё, а значит, не запрещено обсуждать задачу, закономерности в данных, подходы к решению. А правила мы не нарушаем, поскольку об этом всё равно станет рано или поздно известно (мир дата-аналитиков очень тесный).

«Прогнозирование» вероятности

Конкурс, о котором пойдёт речь, был идеален практически во всём:

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

кроме одного — названия «Прогнозирование вероятности невозврата кредита». Вероятность обычно не прогнозируют: её оценивают или вычисляют. Прогнозируют наступление события. Например, если я через минуту буду бросать игральную кость, то вероятность выпадения «5» равна 1/6. Событие БУДЕТ, а вероятность УЖЕ ЕСТЬ и равна конкретному числу… вот если у кубика постоянно стёсываются грани и вероятности выпадения отдельных числе меняются, то можно уже говорить про «прогнозирование вероятности»… хотя и здесь, как правило, говорят об «оценке вероятности в момент t».

Задача конкурса, кроме учебных целей, ещё пригодилась при собеседованиях на работу… мы дали задание группе кандидатов поучаствовать в конкурсе, лишь один человек реализовал здравое решение, которое было в топ-10 (в начале соревнования). Интересно, что некоторые даже не поняли условие задачи, несмотря на описание организаторов.

Задача

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

bki
Рис. 1. Таблица с данными.

Здесь ID — номер клиента, который обратился в банк в день SK_DATE_DECISION. Клиенту может соответствовать несколько строк, поскольку строка содержит информацию об одном из его кредитов в прошлом. Доступны признаки:

  • NUM_SOURCE – номер источника данных
  • CREDIT_ACTIVE – статус кредитного договора (0 – закрыт, 1 – активен, 2 – продан, …)
  • CREDIT_COLLATERAL – признак погашения за счет обеспечения / поручительства (0 или 1)
  • CREDIT_CURRENCY – валюта кредита (RUR, USD, EUR, CHF)
  • DTIME_CREDIT – дата выдачи кредита
  • CREDIT_DAY_OVERDUE – текущая просроченная задолженность, дни
  • DTIME_CREDIT_ENDDATE – планируемая дата окончания кредита
  • DTIME_CREDIT_ENDDATE_FACT – фактическая дата окончания кредита
  • CREDIT_FACILITY – семантика поля неизвестна
  • AMT_CREDIT_MAX_OVERDUE – максимальная просроченная задолженность за все время жизни
  • CNT_CREDIT_PROLONG – число пролонгаций кредита
  • AMT_CREDIT_SUM – сумма кредита
  • AMT_CREDIT_SUM_DEBT – сумма оставшегося долга
  • AMT_CREDIT_SUM_LIMIT – лимит (для карт)
  • AMT_CREDIT_SUM_OVERDUE – текущая просроченная задолженность, сумма
  • CREDIT_SUM_TYPE – тип задолженностей (0 или 1)
  • CREDIT_TYPE – тип договора (0 – неизвестный, 1 – на автомобиль, 2 – лизинг, 3 – ипотека, …)
  • DTIME_CREDIT_UPDATE – дата последнего обновления информации в источнике
  • CREDIT_DELAY30 – число просроченных на 6..30 дней платежей
  • CREDIT_DELAY5 – число просроченных на не более 5 дней платежей
  • CREDIT_DELAY60 – число просроченных на 31..60 дней платежей
  • CREDIT_DELAY90 – число просроченных на 61..90 дней платежей
  • CREDIT_DELAY_MORE – число просроченных на 90+ дней платежей
  • AMT_REQ_SOURCE_HOUR – число запросов к источнику за последний час
  • AMT_REQ_SOURCE_DAY – число запросов к источнику за последний день
  • AMT_REQ_SOURCE_WEEK – число запросов к источнику за последнюю неделю
  • AMT_REQ_SOURCE_MON – число запросов к источнику за последний месяц
  • AMT_REQ_SOURCE_QRT – число запросов к источнику за последний квартал
  • AMT_REQ_SOURCE_YEAR – число запросов к источнику за последний год
  • AMT_ANNUITY – сумма ежемесячного платежа
  • TEXT_PAYMENT_DISCIPLINE – платежная строка, вектор статусов платежей по кредиту:
    0 – своевременный платеж
    1 – просрочка 1..30 дней
    2 – просрочка 31..60 дней
    3 – просрочка 61..90 дней
    4 – просрочка 91..120 дней
    5 – просрочка 121+ дней, передан коллекторам, продан, списан
    X – статус неизвестен
    C – договор закрыт

Целевой признак называется DEF и принимает одно и то же значение для строк с одним ID. По смыслу это «перестал ли клиент выплачивать кредит» (1 — перестал, 0 — нет). Нужно построить алгоритм, который выдаёт оценку (вероятности) события невозврата кредита. Решения оцениваются по AUC ROC. После загрузки решения — ответов на котрольной выборке — показывается качество на её части, для которой всё известно на момент начала соревнования. На остальной части решения оценивались после завершения соревнования (правильные ответы в начале конкурса не были доступны физически). Интересно, что итоговый результат оценивался по всей тестовой выборке (а не по последней части, как это обычно принято).

Картинки

Сначала, как я писал ранее, посмотрим на визуализации данных, например, нет ли ликов, связанных с использованием информации из будущего. На рис. 2 показана диаграмма рассеивания (дата запроса кредита в нашем банке, дата открытия кредита из истории). Ясно, что невозможно открыть предыдущий кредит после того, как пытаешься открыть настоящий (иначе какой же он «предыдущий»?) и на рис. 2 это видно: точки не вылезают за «линию настоящего» (на которой эти даты равны).

credit.png
Рис. 2. Даты визита в банк и открытия предыдущего кредита.

С закрытием эта логика уже не проходит (когда клиент приходит в банк, предыдущий кредит может быть не закрыт), рис. 3 это подтверждает. Сразу отмечаем использование каких-то аномальных (но фиксированных) значений для обозначения закрытия определённых кредитов (хотя 2107 год, например, может быть стандартной опечаткой при наборе 2017). Но интересно, что одно из этих значений появляется незадолго до деления на обучение и тест (см. на розовую линию с синим кончиком слева). Вот почему плохо использовать время как признак и «затачиваться» на определённые значения! И вот почему я так много на курсах говорю про визуализацию, генерацию признаков и отдельные признаки: на реальных данных сразу видно, что разумно, а что нет при построении модели.

credit2.png
Рис. 3. Даты визита в банк и закрытия предыдущего кредита.

Следующая иллюстрация заставляет ещё больше бояться теста: он совсем не похож на обучающую выборку. Так, среднее число запросов к источнику за последнюю неделю резко подскакивает в апреле 2017 года (подобного поведения нет на обучении). Вообще, признаки, связанные с числом запросов очень подозрительные и не согласуются друг с другом…

april.png
Рис. 4. Среднее число запросов к источнику за последнюю неделю.

Вероятности просрочек медленно, но увеличиваются со временем, как показано на рис. 5. По-хорошему, это надо учитывать в алгоритме (и я даже обращаю внимание студентов на то, как и когда это сделать), но сам при решении это не использовал — просто настроил бустинг на простых признаках…

delays.png
Рис. 5. Среднее (по дням) число просроченных на 61..90 дней платежей.

Анализ легко даёт понять, чем различаются разные источники. Например, задержки на 5 дней отмечаются только в 1м и 4м. На рис. 6 показан процент договоров вида «кредит на авто» — как он меняется со временем. Видим, что во втором источнике таких кредитов нет вообще (аналогично можно составить наполненность видами кредитов по всем источникам).

auto.png
Рис. 6. Средний процент кредитов на авто.

Ниже аналогичный график по продукту «Кредитные карты», опять видим особенность второго источника. И здесь совсем заметен тренд в средних значения одного из признаков, который, по-хорошему, надо учитывать в модели.

credits
Рис. 7. Средний процент оформления кредитных карт.

Идея решения

Понятно, что для решения придётся перейти к признаковому описанию клиента и как-то сагрегировать информацию по всем его кредитам… есть подход настроить классификатор на отдельных кредитах и затем агрегировать его ответы, но он заведомо провальный (если не использовать его в блендинге!). Что я делал с признаками…

Временные признаки
  • приводил к нужному типу,
  • создавал признаков вида «день недели», «день месяца»,
  • брал разности времён (например, закрытие кредита — открытие кредита),
  • создавал относительные признаки (например, на каком проценте планируемого отрезка жизни кредита он был реально закрыт).
Строковые признаки

были перекодированы в числа, мелкие категории объединены в одну.

Пропуски

заполнены фиксированным значением, созданы отдельные dummy-признаки «наличие пропуска»

После этого по всем признакам проводилась агрегация…

по бинарным признакам

просто бралось среднее значение по всем предыдущим кредитам

по категориальным признакам

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

categories.png
Рис. 8. Преобразование категориального признака.
по вещественным признакам

брались различные статистики, см. рис. 9

real_feature
Рис. 9. Агрегация вещественного признака.
по строкам состояний

т.е. строкам вида «010X00XXXXXXX0X00XX00X0X00X00XX0X» строились признаки вида:

  • число отдельных символов,
  • длина строки,
  • весовые схемы (~ центр масс каждой буквы),
  • первая (самая свежая) буква.

Выбор метода решения

Как было показано в разделе про визуализацию, в данных много особенностей, которые непременно повлияют на устойчивость модели, если её просто настроить на обучающей выборке. Тем не менее, моя модель именно «просто настроена»… почему? Во-первых, изначально я решал задачу просто для написания бенчмарков для студентов. Первый бенчмарк писался за несколько минут, над вторым пришлось немного посидеть — но он оказался в топ-10. Грех было не поучаствовать в соревновании, если так быстро получается сделать качественное решение, но я решил участвовать «не переусердствуя». Во-вторых, если решать задачу по-серьёзному, пришлось бы использовать некоторые ноу-хау, которые мы применяем при решении задач скоринга на заказ. В-третьих, мне казалось, что никто из участников и не будет «решать задачу правильно» — просто «нагенерят» признаков и «засунут» в ансамбль бустингов (так в итоге и произошло).

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

bank
Рис. 10. Схлопывание информации об одном кредите.

Бустинг настаивался с помощью библиотек

Фиксировалась глубина дерева (или число листьев), остальные параметры настраивались с помощью скользящего и out-of-time-контроля. Формально ансамбль состоял из 15 усреднений смеси 4-х XG-бустов и 4-х Light-гбмов, но без ухудшения качества можно использовать линейную комбинацию всего 2х алгоритмов.

При посылке решений обнаружился интересный эффект: если увеличить число деревьев в бустинге, то качество повышается. Учитывая, что контрольная выборка описывала вкладчиков, которые пришли после тех, что были в обучающей, это достаточно удивительно. Можно объяснить, почему качество вырастает при уменьшении числа деревьев, а вот при увеличении — не понятно… было ещё несколько вопросов, которые заставляли с опаской смотреть на контрольную выборку, тем не менее, для финальной посылки я выбрал именно переобученное на публичный контроль решение (с увеличенным числом деревьев), это обеспечило 1е место в предварительном рейтинге (0.7244 AUC ROC) и потенциальное падение качества при итоговом подсчёте (что и произошло — 0.7117 AUC ROC).

Итоги

Финальная турнирная таблица показана на рис. 11. В конкурсе зарегистрировались 418 участников, но в финальной таблице показано лишь 145. Мои студенты выступили не очень блестяще: 5, 14, 17 и т.д. места (хотя за попадание в тройку было обещано 5 автоматом). Видео с решениями победителей можно посмотреть на странице SAS вконтакте. Пользуясь случаем, поздравляю всех победителей!

lb
Рис. 11. Турнирная таблица конкурса: топ-5.

П.С. Все, кто дочитал до конца, могут посмотреть и код😉

Определение вероятности невозврата кредита: 13 комментариев

  1. Я думал, что уже воркфлоу: «просто «нагенерят» признаков и «засунут» в ансамбль бустингов» является правильным 😦

    • В определённом смысле, да. Только 1) надо не просто генерить, а стараться придумать признаки, не зависящие от времени 2) ансамблировать лучше разнородные алгоритмы (не только бустинги), а для этого необходимо создавать разные признаки (например, OHE-кодировка хороша для логистической регрессии, но плоха для бустинга)

  2. И вас тоже поздравляем, Александр.
    Зря вы не пришли с докладом, мы раскритиковали там оргов, а вы бы немного умаслили 🙂

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

    • Спасибо. Как ни странно, но к организаторам с докладом я все-таки приду — они очень просят. У меня было два признаковых пространства. Когда я строил первое, я действительно никак не агрегировал. Когда я строил второе, то просто брал max-агрегацию по всем записям одного и того же вклада. Возможно, именно поэтому Ваше решение оказалось лучше: насколько я понял, Вы даже учитывали свежесть информации из разных источников. Можно ещё много чего было сделать и учесть, но меня смущал скрытый контроль и не хотелось стараться в условиях потенциального шафла лидерборда.

  3. Александр, спасибо за полезный пост!
    Что такое def (1/0)?
    «По смыслу это «перестал ли клиент выплачивать кредит» (1 — перестал, 0 — нет).» — когда перестал? в будущем в течение n дней?

  4. Спасибо за пост. Александр, в одном из предыдущих комментариев вы написали «например, OHE-кодировка хороша для логистической регрессии, но плоха для бустинга». Поясните пожалуйста, почему OHE плоха для бустинга? Когда 2 категориальных значения, я привык использовать labelencoder, но если больше, то пользовался OHE. Про другие методы аля mean encoding итд в курсе, просто интересно конкретно про OHE понять.

    • Правильнее написать так: может быть очень плоха для бустинга. Смотрите, когда Вы делаете OHE, у Вас один признак заменяется на q бинарных, которые, по сути, отражают информацию этого одного. Если (например, по соображениям скорости) Вы выбираете случайное подмножество признаков при построении ветвления в дереве, то получается, что вещественные признаки выбираются реже, чем OHE-признаки категориальных с огромным числом категорий. Интуитивно это очень плохо. И вообще плохо, когда информация «размазывается по признакам». Если категорий мало, категориальных признаков мало, если как раз в них и содержатся основные закономерности, то OHE-кодировка никак не вредит. Но как подсказывает опыт (в том числе кэглеров), mean target coding или приёмы из catboost всё-таки лучше;)

      • Спасибо, хорошие аргументы. Позвольте еще один вопрос. В одной из предыдущих задач я использовал кодирование категориальных признаков целыми числами по убыванию в зависимости от частоты категории. И кроме этого для каждого категориального признака создавал соответствующий признак с кодировкой smoothed mean_target. То есть в датасет добавилось еще порядком 30 признаков, равное кол-ву категориальных фичей. Есть ли в этом случае размывание информации по признакам? Или стоило просто кодировать исходные признаки сглаженным средним без создания новых фичей? Заранее спасибо.

      • На самом деле, я примерно так всегда по умолчанию и делаю. Если на каждый категориальный признак 2-3 добавляется, то это нормально.

  5. Коллеги нет возможности пошарить данные? Хотел покрутить готовые решения, но данных нет. Регистрация на SAS не открывает доступ к данным, там уже все убрали

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s