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

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

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

История болезни

Коротко о скоринге

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

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

Для анализа корпоративных клиентов была разработана многомодульная скоринговая модель. Каждый из модулей анализирует определенное направление деятельности клиента:

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

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

Внешние и внутренние данные

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

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

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

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

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

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

Реализация расчета факторов

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

Самый простой пример, который покажет эту проблему на практике — реализация расчета фактора «Общий долг/EBITDA». Данный фактор является распространенной метрикой для оценки долговой нагрузки клиента на основе его финансовой отчетности и широко используется финансовыми аналитиками. Существуют разные способы оценки как общего долга клиента, так и его EBITDA.

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

В скоринговой модели экспресс-кредитования помимо реализации таких простых факторов существует потребность расчета сложных факторов, включающих в себя большие объемы данных, например, транзакции клиентов банка за последние 2-3 года. При расчете таких факторов возникает еще одна проблема — проблема производительности при расчете факторов. Банк не может позволить себе многочасовой расчет факторов по одному клиенту. Поэтому при расчетах важно пользоваться оптимизированными алгоритмами на системах, позволяющих производить расчеты с большими объемами данных.

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

  • Прописывать методику расчета факторов так, чтобы она трактовалась однозначно;

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

Теперь — подробнее о том, как мы решали все эти проблемы.

Лечение болезни

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

Архитектура промышленного процесса

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

Рисунок 1: архитектура процесса скоринга для выдачи экспресс-кредитов
Рисунок 1: архитектура процесса скоринга для выдачи экспресс-кредитов

С целью проверки каждого этапа был разработан дублирующий проверочный скрипт, который запускался после этапа принятия решения, но до выдачи. Таким образом процесс, иллюстрированный на рисунке 1, был доработан следующим образом:

Рисунок 2: доработанный процесс скоринга для выдачи экспресс-кредитов
Рисунок 2: доработанный процесс скоринга для выдачи экспресс-кредитов

Технология реализации проверочного скрипта

Проверочный скрипт реализован на платформе Mlops в виде развернутого rest-сервиса, работающего по принципу API (запрос-ответ). Для этих целей развернут Docker с интеграцией между кредитным конвейером и внутренними базами данных. Подобный подход позволяет без дополнительных интеграций осуществлять обмен данными между системами (см. рисунок 3).

Рисунок 3: реализация дублирующего проверочного скрипта
Рисунок 3: реализация дублирующего проверочного скрипта

Рассмотрим каждый этап по отдельности, с выдержками кода, примерами и описаниями решений.

Формирование входных данных

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

Входящий JSON, состоящий из разных модулей данных, целесообразно формировать в следующей структуре:

  • RqUID — идентификатор файла для расчета;

  • DateTime — дата и время формирования файла для расчета;

  • app — информация по заявке:

    • Набор атрибутов для расчета по заявке;

    • spark_data — xml СПАРК;

    • pravo_data — список словарей с данными ПРАВО.ру;

    • bki_data — список словарей с данными бюро кредитных историй.

  • loan_member_express — список словарей с данными по клиенту и связанным лицам из кредитного конвейера для сравнения.

Также с целью упрощения передачи данных реализовано хранение пользовательских справочников в виде JSON, в которых содержится информация следующего рода:

  1. Списки отраслей с экспертными оценками;

  2. Перечень типов входных данных для проверки ошибок;

  3. Справочник с кодами факторов и стоп-факторов.

Кредитный конвейер по маршруту заявки генерирует запросы во внешние системы и записывает полученную информацию в JSON-файл, затем инициирует запрос к REST-API, который, в свою очередь, ссылается на нужные справочники и производит расчет скоринга и лимитов.

Технология реализации проверочного скрипта

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

Класс DataLoader

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

class DataLoader:
    def __init__(self, industry_classifier_path, stop_industry_list_path, types_path,
                 activity_type_path,
                 gz_stops_path,
                 gz_ec_map_path
                ):
        
        # Инициализация переменных класса, содержащих пути к файлам, содержащим таблицы и словари с параметрами модели
        self.stop_industry_list_path = stop_industry_list_path #20220524
        self.industry_classifier = industry_classifier_path
        self.types_path = types_path
        self.activity_type_path = activity_type_path
        self.gz_stops_path = gz_stops_path
        self.gz_ec_map_path = gz_ec_map_path

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

    def load_industry_classifier(self):
        """
        Загрузка стоп-отраслей ОКВЭД
        """
        df_bank_okved = pd.read_excel(self.industry_classifier, engine='openpyxl', sheet_name = 'ОКВЭД', 
                                         usecols = ['КОД', 'Отрасль', 'МБ'], skiprows=1, converters = {'КОД':str})
        df_bank_okved['МБ'] = df_bank_okved['МБ'].where(df_bank_okved['МБ'].isnull() == False, '1')
        df_bank_okved['МБ'] = df_bank_okved['МБ'].apply(lambda x: (x.strip()).capitalize())
        df_bank_okved = df_bank_okved[df_bank_okved['МБ']!='1']
        return df_bank_okved

Класс CheckInputData

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

class CheckInputData:
    def __init__(self, df_root, df_types): 
        
        self.df_types = df_types        
        self.df_root = df_root
        self.df = None
        self.RqUID = None
        self.DateTime = None

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

    def log_errors(self):
        root_columns = list(self.df_root)
        if 'RqUID' not in root_columns:
            result_calculation = json.dumps({'RqUID': None, 'DateTime': str(datetime.utcnow() + timedelta(hours=3)),
                'app': [{
                    'APP_ID': None,
                    'Code': "402",
                    'Status': "Error",
                    'Description': "Несоответствие перечня загруженных полей по модулю модели (отсутствует RqUID)"}]})
            result_json = json.loads(result_calculation)
            return result_json
        self.RqUID = self.df_root['RqUID'][0]

Пример ошибки, по которой проводится логирование: "Несоответствие перечня загруженных полей по модулю модели — отсутствует RqUID".

Класс ExtractData

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

class ExtractData:
    def __init__(self, df, industry_classifier):

Далее создаются основополагающие датафреймы, с которыми потом работаем в скрипте:

  • apps_df — датафрейм с информацией по заявке;

  • df_lm — часть входного json, соответствующая участникам сделки.

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

    def check_missing(self, field):
        if isinstance(field, str): #добавил проверку на строку, иначе падал при попытке привести float в нижний регистр
            if field.lower() in ('nat', 'null', 'nan'):
                field = np.nan
        return field

Функция заменяет "строковые пропущенные значения" вида ‘nat', 'null', 'nan' на np.nan.

Далее реализована функция def init_extractor(self, df, industry_classifier), которая осуществляет первоначальную загрузку данных для дальнейшего расчета факторов. В этой функции поочередно загружаются данные из входящего JSON для каждого модуля с предварительной обработкой пропущенных значений.

После загрузки и первичной обработки данных производится парсинг данных БКИ, СПАРК, ПРАВО.ру и др. Парсинг данных для каждого источника реализован как отдельная функция.

Парсинг данных БКИ включает в себя анализ платежной строки по полученным от сервиса CREA данным. Первоначально фильтруются нужные договоры клиента по условиям, заложенным в методологии на основе типа договора и срока действия, затем в каждой платежной строке производится поиск нужной информации, например, просроченной на 120+ дней задолженности, который обозначен символом «5».

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

# Удаляем символ подчеркивания в начале платежной строки
self.bki_df['PMTSTRING84M'] = self.bki_df['PMTSTRING84M'].\
                            apply(lambda x : str(x[1:]) if str(x) != 'nan' and '_' in x else x)

# 2.2) отбрасываются договора по которым неизвестна платежная строка (пустая)
self.bki_df = self.bki_df[(self.bki_df['PMTSTRING84M'].notnull())&\
                      (~self.bki_df['PMTSTRING84M'].str.lower().isin(self.missing_list))]

Парсинг данных СПАРК производится в два этапа — сначала определяется список связанных лиц, а затем производится загрузка информации, содержавшейся в методах СПАРК. Для анализа используется информация, содержащаяся в следующих методах API-СПАРК:

  • GetCompanyExtendedReport;

  • GetCompanyStructure;

  • ManagementCompanyINN;

  • GetCompanyListByPersonINN.

Данные по связанным лицам сохраняются в виде списка списков следующего формата: [ИНН, наименование, ИНН связанного лица на уровень ближе к заемщику, доля владения, роль связанного лица, OKATO, наименование страны].

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

Пример реализации парсинга данных-XML для извлечения OKOPF приведен ниже (остальные данные извлекаются аналогичным способом на основе описания каждого метода, изложенного в спецификации API-СПАРК).

if 'GetCompanyExtendedReport' in self.all_spark[inn].keys():
                        xml_root = self.all_spark[inn]['GetCompanyExtendedReport']
                        if not (xml_root.find('Data/Report/OKOPF') is None):
                            okopf_dict = xml_root.find('Data/Report/OKOPF')
                            okopf = okopf_dict.get('CodeNew')

Обработка данных ПРАВО.ру производится похожим методом с той лишь разницей, что необходимо обработать JSON вида {«КЛЮЧ»: «ЗНАЧЕНИЕ»: «»}:

def pravo_json_parse(self):
    if not self.root: return self.pravo_df
    
    for page in self.root:
        try: total_json = page['page']
        except KeyError: page_json = -1

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

Класс FeatureEngineering

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

В данном классе при инициализации загружаются датафреймы с распарсенными в классе FeatureExtract данными и производятся манипуляции, которые не выходят за рамки применения основных библиотек python для работы с датафреймами (pandas, datetime, numpy): def __init__(self, df_spark, df_apps, df_bki, df_pravo).

Разработка функций для расчета каждого фактора была осуществлена по следующей схеме:

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

Класс StopsProcess

В данном модуле производится проверка наличия стоп-факторов и проставляется отказ по заявке в случае их выявления. При инициализации задаются датафреймы с построенными в FeatureEngineering атрибутами. Используются данные по заявке, СПАРК, ЧС, БКИ, ПРАВО, транзакции и др.

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

def stops_from_pravo(self):
    self.stops_pravo = self.pravo
    self.stops_pravo['PRAVO_CntOpCaseBankrupt36m'] = int(self.pravo['PRAVO_CntOpCaseBankrupt36m'].sum()>0)
    comment = 'Наличие арбитражных дел, открытых на дату заявки, зарегистрированных на горизонте 36 мес. до даты заявки о банкротстве'
    if len(self.df_apps['client_inn'][0]) == 12:
        self.pravo_stops_comments = self.pravo_stops_comments + [comment] if self.pravo['PRAVO_CntOpCaseBankrupt36m'].sum()>0 else self.pravo_stops_comments
    return self.stops_pravo.copy()

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

Класс CalculateScore

В данном классе осуществляется расчет скорингового балла по клиенту. Рассчитываются флаги наличия данных в источниках.

Общая схема расчета скорингового балла выглядит одинаково:

  • Выгружаются значения нескольких факторов;

  • Задаются коэффициенты линейной модели и значения для трансформации в WOE;

  • Факторы бинаризируются (строковые в соответствии со списком, числовые атрибуты — по промежуткам) и проставляется значение WOE;

  • Скор по модулю получается линейной комбинацией значений WOE и 1.

Класс CalculateLimit

В данном классе производится расчет лимита. Аналогично классу FeatureEngineering осуществляется расчет факторов для расчета лимита: в каждую отдельную функцию выделен отдельный фактор. Затем каждая функция вызывается в итоговой функции расчета лимита.

Результаты и отчет о расхождениях

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

Для этих целей выделено три класса:

  • MakeReport — класс для построения отчета по итогам расчета;

  • MakeDiffList — класс формирования отчета о расхождениях;

  • ModelESDSWrapper — класс формирования выходного json, содержащего в себе два отчета, указанных выше.

При инициализации класса MakeRport задаются датафреймы с данными по заявке, все данные из внешних источников и трансакциям, а также скорам, стопам, и лимитам. Итогом работы класса является JSON со структурой: {«report»: [{«app_id»: НОМЕР; «client_name»: НАИМЕНОВАНИЕ; «Score»: СКОР; и т.д.}], «conn_participants»: [……]} Отчет содержит результаты по каждому модулю модели.

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

Удобство для клиентов и пользователей

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

С точки зрения пользователей процесса внутри банка тоже удалось добиться значительного снижения трудозатрат при выдаче экспресс-кредитов. Больше нет необходимости проверять каждую заявку в ручном режиме и принимать решения индивидуально. Благодаря REST-API кредитный конвейер самостоятельно инициирует запрос расчета в дублирующем скрипте, получает отчет и, в случае отсутствия расхождений, пропускает заявку дальше. Таким образом, аналитикам приходится рассматривать только небольшой перечень заявок, по которым возникают расхождения.

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