Всем привет! С вами дата-сайентисты банка «Открытие» Иван Кондраков, Константин Грушин, Станислав Арешин и Алексей Дьяков. Часто даже самые хорошие произведения, будь то фильмы, книги или компьютерные игры, остаются без сиквела. А еще чаще сиквел просто не дотягивает до оригинала… К счастью, это не наш случай! Мы возвращаемся с прямым продолжением нашей статьи о программной генерации длинного списка факторов. И, поверьте, мы следовали всем правилам хорошего сиквела: наш сиквел держит планку качества, продолжает идеи оригинала, при этом полезной информации в нем еще больше!

Наше итоговое решение можно разделить на три блока: факторы, основанные на формулах, основанные на агрегатах и срезах, и кросс-факторы:

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

В предыдущей серии… Первым делом напомним вводные. Построение моделей на не выверенных факторах — не лучшая практика. Например, неоднозначная логика расчета, ошибки в этой самой логике, в специальных значениях приводят к тому, что фактор в модели работает не так, как это было задумано разработчиком. Помножим это на то, что ранее такой список был далеко не один, факторы в них могли пересекаться или незначительно отличаться. Поэтому уследить за всеми потенциальными проблемами если и возможно, то очень сложно. Наша цель — получить один файл-справочник со всеми нашими наработками в едином формате, да еще и таким образом, чтобы все факторы были выверены. Для этого решено реализовать условный парсер языка, который компактно отражает все факторы, а его функционал может избавить пользователя от ручного труда при формировании такого справочника. Теперь переходим к сути!

Факторы, основанные на формулах. Финансовые факторы

Этот раздел также опубликован в: Кондраков И., Грушин К., Арешин С. Бережем время, деньги, нервы: опыт улучшения справочника факторов для ML-моделей оценки риска // Риск менеджмент в кредитной организации. 2023, №1.

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

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

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

Для генерации справочника финансовых факторов достаточно задать список, в котором за каждый фактор отвечает кортеж. Пример входных данных приведен ниже:

# список на вход
fin_features_lst = [
(
    Формула,
   	Блок фин. факторов,
 	Ожидаемая бизнес логика,
 	Специальные правила,
    Диапазон значений
),
    ...
]

­­Далее мы направляем список факторов в функцию, внутри которой реализовано составление наименования и текстового описания фактора на основе трактовок и полученных формул.

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

Из формулы парсится первая цифра каждой используемой строки финансовой отчетности, единица в данном случае отвечает за первую форму, двойка — за вторую. Также в формуле могут присутствовать символы «нг» и «кг», которые означают начало года и конец года, это свойственно показателям динамики. Если встречается «нг» после показателя, то необходима соответствующая показателю форма отчетности за период (T - 1). С «кг» после показателя, мы думаем, все понятно: по факту это то же самое, если бы «кг» и вовсе не было, то есть необходима форма отчетности за период T????

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

В заключение приведем код и результат генерации для одного фактора:

# список на вход
fin_features_lst = [
(
    '(1600-1400-1500-1110)/(1510)', # формула
    'Финансовая устойчивость', # блок
   	1, # бизнес-логика [1 – чем больше, тем лучше]
    '+999999: знаменатель = 0 & числитель > 0; -999999: числитель <= 0', # спец. значения
    '[0, inf) | специальные значения' # диапазон
)
]
for formula, block, bl, spec_rule, value_range in fin_features_lst:
    # проставляем бизнес-логику
    business_logic = 'Не определено'
    if bl == 1:
        business_logic = 'Чем больше, тем лучше'
    if bl == -1:
        business_logic = 'Чем меньше, тем лучше'
        
    name, descr, calc_logic = generate_from_formula(formula, block)#название, описание,логика расчета
    mask = generate_mask(formula)# генерация маски
    # тип переменной
    value_type = ' float32 | null'
    if block == 'Флаг':
        value_type = 'bool'

Факторы, основанные на агрегатах и срезах

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

Отличительной чертой этой группы факторов является довольно простая логика формирования. Если описывать фактор такого типа, получится, например, «количество встреч в зуме, сидя в кресле, за последний месяц». Однако процесс их программной генерации чуть сложнее.

Для лучшего понимания приведем пример. Допустим, у нас есть следующие доступные агрегаты и срезы:

  • операция для агрегации (например, количество или сумма);

  • срез по типу информации — встречи в зуме, добавим сюда звонки в телеграмме, созвоны в WhatsApp;

  • фильтр (или ограничение) — сидя в кресле, лежа на диване, по пути в магазин;

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

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

  • 30 дней;

  • 31 день;

  • объект 1 month из какой-нибудь библиотеки для работы со временем;

  • 365 или 366 (високосный год) / 12;

  • включая, левую/правую границу временного среза;

  • не включая, левую/правую границу временного среза.

Крайне полезно продумывать эти нюансы на стадии формирования длинного списка.

Таким образом, название фактора из начального примера трансформируется в CNT_ZOOM_ARMCHAIR_1M. Количество факторов, которые могут быть сгенерированы из примера: 2 (операции для агрегации)*3 (срез по типу информации)*3 (фильтр)*4 (временной срез) = 72.

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

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

  • с возможными агрегатами (как правило, их не так много и они довольно простые: количество, сумма, максимум, минимум);

  • с временными срезами: их количество ограничивают только фантазия разработчика, вычислительные ресурсы и доступное место в хранилище данных;

  • с двумя-тремя срезами для агрегации по типу информации — здесь заключена основная суть фактора;

  • с возможными ограничениями для фильтрации.

Ну что, допустим, мы сгенерировали ОГРОМНОЕ количество подобных факторов по разным источникам и срезам, с разными операциями агрегации и ограничениями. Их количество исчисляется десятками тысяч. Представьте, как трудно контролировать их в экселе. О версионировании этих эксель-файлов вообще можно забыть… Теперь же мы делаем это все автоматически. Как? Обсуждаем ниже! 

Контрактная история

Этот раздел также опубликован в: Кондраков И., Грушин К., Арешин С. Бережем время, деньги, нервы: опыт улучшения справочника факторов для ML-моделей оценки риска // Риск менеджмент в кредитной организации. 2023, №1.

Начинаем этот довольно обширный блок факторов с контрактных, которые описывают историю исполнения Принципалом или заключения Бенефициаром контрактов по 44, 223 ФЗ и 615 ПП.

Основные срезы в данном случае:

  • BEN, PR — отношение принципал/бенефициар;

  • 44FZ, 223FZ, 615FZ, ALLFZ — типы ФЗ;

  • 3M, 6M, 12M, 18M, 24M, 36M — срезы по времени;

  • SIGN, DONE — статусы контрактов.

Рассматриваемые агрегаты:

  • количество контрактов;

  • сумма контрактов;

  • средняя сумма/длительность/количество;

  • динамика среднего;

  • максимальная сумма/длительность;

  • флаги наличия;

  • доли контрактов.

Не забываем и про словари с трактовкой срезов наподобие того, как это было сделано в финансовом модуле.

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

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

input_list_cnt = [
    (
        ['BEN_'], ['44FZ_'], ['3M'], ['LAST_'], ['SIGN_'], ['ALL_']
    )
]
contract_features_list = []
for rels, fzs, months, months_add, status_types, restrict_types in input_list_cnt:
    for relative in rels: # по типам отношения
        for fz_type in fzs: # по типам ФЗ
            for month_slice in months: # по временным срезам
                for month_slice_add in months_add: # учитывая дополнительные временные срезы
                    contract_features_list += generate_cnt_agg(
                            relative, fz_type,\
 							month_slice, month_slice_add,\ 
                        	status_types, restrict_types)# генерация описания

Кредитная история

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

  • таблица в Excel на примерно 5 тысяч факторов;

  • пример ответа из кредитного бюро;

  • объемное руководство с описанием полей кредитного отчета из кредитного бюро.

Помимо стандартных операций для агрегаций, таких как количество, сумма, максимум, среднее, для данного модуля характерны некоторые уникальные агрегаты:

  • максимальная длина платежной строки;

  • средняя длина платежной строки;

  • количество месяцев с открытия/закрытия последнего договора.

Что касается срезов, группа факторов кредитной истории лидирует по этому показателю. Мы приведем основные рассматриваемые срезы:

  • по статусу договора;

  • по типу залога;

  • по типу продукта;

  • по типам отношения к договору;

  • по частоте платежей;

  • по времени / платежному периоду.

Функция генерации по своей логике схожа с тем, как это было сделано для группы контрактных факторов. Основной и самый трудоемкий шаг для при реализации функции — привести в соответствие срезы и агрегаты из названия фактора с их описанием, формулой расчета, бизнес-логикой, диапазоном допустимых значений. И все, что остается, написать код генерации:

# Лист агрегатов
AGG_LIST = ['CNT', 'PART'] 
# Лист статуса договора
STATUS_LIST = ['ACTCRED', 'CLOSECRED', 'OPENCRED', 'CURRCRED', 'IS_OWN', 'ALL'] 
# Лист типа залога
TYPE_LIST = [ 'COLLATERALCODE_AGREQUIP', 'COLLATERALCODE_ANTIQUES', 'COLLATERALCODE_APPLIANCES', 'COLLATERALCODE_AUTO',
              'COLLATERALCODE_BANKGUARANTEE', 'COLLATERALCODE_BILLOFEXCHANGE', 'COLLATERALCODE_BONDS', 'COLLATERALCODE_BUILDINGS',
              'COLLATERALCODE_COMGUARANTOR', 'COLLATERALCODE_COMREALESTATE', 'COLLATERALCODE_CURRENCY', 'COLLATERALCODE_GOODS', 
              'COLLATERALCODE_JEWELRY', 'COLLATERALCODE_OTHER', 'COLLATERALCODE_PRIVGUARANTOR', 'COLLATERALCODE_PRODEQUIP',
              'COLLATERALCODE_REALESTATE', 'COLLATERALCODE_STOCKS']
MONTH_LIST = ['6', '12', '24', '36', '48', '60', 'ALL_TIME'] 
feature_df = get_feature_df(AGG_LIST, STATUS_LIST, TYPE_LIST, MONTH_LIST, descr_features=descr_features)

Данный код сгенерирует 1512 факторов вида:

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

Факторы, основанные на транзакционных данных

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

Входные данные для задачи следующие:

  • лонг-лист транзакционных факторов с не интерпретируемыми названиями;

  • древний SQL-код и Excel-файл, на основе которого рассчитываются факторы;

  • дата-инженер, который запускал этот код;

  • коллега, который когда-то давно, кажется, видел разработчика этого кода.

В таком случае необходимо одновременно решать несколько связанных задач:

  • переработать названия и описание факторов;

  • погрузиться в код расчета факторов;

  • разобраться с возможными операциями, агрегатами, срезами и другими сущностями.

Расчет этой группы факторов основан на одном из трех прототипов:

  1. Идентичен подходу в модуле кредитной истории (то есть операция агрегирования, срез по типу информации, временной срез, например, MEAN_VALUE_RENT_1M);

  2. Отношение факторов из п.1. На этом этапе к типичным временным срезам типа «за последние N месяцев» добавляются более интересные временные интервалы. Например, SUM_SUMSALARY_L1Q_TO_SUM_SUMSALARY_L2Q - 'Зарплата - сумма суммы выплаченной зарплаты за последний квартал / сумма суммы выплаченной зарплаты за предпоследний квартал'. Временные срезы такого типа помогают отследить изменение фактора с течением времени.

  3. Разность факторов из п.2. Факторы этой группы могут отследить изменение динамики фактора с течением времени. Например, DYN_SUM_SUMSALARY_TO_SUM_SUMCOST_L1M_BY_L2M - Изменение суммы суммы выплаченной зарплаты / суммы суммы затрат за последний месяц относительно предпоследнего месяца').

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

Интересно отметить, что код для расчета самих факторов также автоматизирован. На вход поступает: номер прототипа, имя фактора, тип операции, срез по типу информации, фильтр, временной интервал — прямая аналогия с генератором факторов.

Для 2 и 3 пунктов преобразованный генератор на основе срезов продолжает работать. Внутри агрегатов продолжают сохраняться общая бизнес-логика, специальные правила и диапазоны значений. Код генерации практически не отличается. Так что по традиции приводим пример результата генерации одного фактора:

Судебная история

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

Всегда нужно отдавать себе отчет в том, нужен ли вообще этот фактор? Может ли он вообще быть отличным от 0 или не равен NaN? При генерации лонг-листа нашим способом очень легко нагенерировать сотню другую признаков, в которых вообще нет смысла. А ведь это время и ресурсы на расчет этих факторов и пространство для хранения данных.

Например, PRAVO_SUM_OP_ADMIN_NEG_36M - Сумма арбитражных дел в качестве ответчика открытых на дату заявки по административным спорам (налоги, штрафы) с негативным исходом (иск удовлетворен) за последние 36 мес. При беглом чтении может показаться, что все в порядке. Но на самом деле нет. Нет смысла считать количество открытых дел с негативным исходом. Дело еще открыто, по нему нет решения -> поле с решением не заполнено у всех открытых дел.

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

# Описание срезов и агрегатов факторов
NAME_BLOCK = 'PRAVO' # Блок факторов
part_type_list = ['DEF'] # Лист со статусом участия искомого ИНН
agg_list = ['CNT', 'SUM'] # Рассчитываемые агрегаты
case_stage_list = ['ALL', 'OP', 'CL'] # Лист статуса арбитражного дела на дату заявки
case_category_list = ['ANY', 'GUARANT', 'CREDIT', # Лист категорий дела
                      'BANKR', 'CORP', 'CONTR', 'ADMIN']
result_closed_list = ['', 'NEG'] # Лист с результатом исхода по закрытым делам
month_list = [3,6,12,18] # Лист ограничения по горизонту наблюдения для расчета фактора

feature_list = []
for part in part_type_list:
    for agg in agg_list:
        for stage in case_stage_list:
            for category in case_category_list:
                for result in result_closed_list:
                    for month in month_list:

                        # Негативный исход можно рассчитать только для закрытых дел
                        if (stage!='CL') & (result=='NEG'): continue
                         

                        feature = get_joined_name(NAME_BLOCK, agg, stage, category, result, 'PREV'+str(month)+'M')
                        description = get_joined_descr(agg, part, stage, category, result, month)
                        description = description.replace('за последние', 'за предшествующие')
                        business_logic = 'Тех. переменная'
                        source = 'TOP_SECRET'
                        flag_techical = 1
                        formula = get_formula(agg,stage,result,'prev')
                        values_range = values_limits_dict[agg]
                        spec_values = spec_values_dict[agg]
                        feature_list.append({
                            'part': part,
                            'feature': feature,
                            'description': description,
                            'business_logic': business_logic,
                            'source': source,
                            'flag_techical': flag_techical,
                            'formula': formula,
                            'values_range': values_range,
                            'spec_values': spec_values
                        })

feature_df = pd.DataFrame(feature_list)

А вот как выглядит один из факторов судебной истории:

Эпилог

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

  • огромное количество выверенных факторов, готовых к применению при построении моделей;

  • возможность версионирования файла-справочника через git;

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

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

Мы хотим прокачивать наше решение и дальше, всегда есть куда расти!

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

Титры.

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

Комментарии (0)