- BrainTools - https://www.braintools.ru -

Когда A-B-тест провести сложно: оцениваем эффект предодобренных кредитов с помощью PSM

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

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

Когда я искала способ решить эту задачу, нашла на Хабре статью Алексея Терентьева про Propensity Score Matching [1]. Там предлагается подход к решению подобных задач с помощью PSM, есть как практические рекомендации так и теоретическая часть. Этот материал помог мне при внедрении, если вы не знакомы с методом, рекомендую тоже почитать.

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

На что влияет предодобрение?

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

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

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

Как оценить эффект?

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

Представим, что предложение получили 20 тысяч клиентов. За 120 дней кредит в банке оформили 1 000 человек – конверсия составила 5%. Среди 100 тысяч клиентов без предложения было 1 500 выдач, или 1,5%. Разница – 3,5 процентного пункта. Если умножить её на число получателей предложения, получится 700 «дополнительных» выдач.

Но напрямую сравнивать эти группы некорректно. Кавычки здесь нужны: пока мы смешали эффект предодобрения с результатом отбора клиентов. Предодобренные предложения получают пользователи из определённого сегмента, которые могут отличаться от остальных клиентов банка. У человека со стабильным доходом, накоплениями и признаками интереса [2] к кредиту вероятность оформления может быть выше и без предодобрения. Если таких людей больше среди получателей, часть разницы появилась бы без нашего участия.

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

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

Подготовка к построению оценки

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

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

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

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

Теперь определим дату тритмента. Для тестовой группы это дата, когда у пользователя появилось предодобренное предложение. Для потенциального контроля нет такого естественного события, как получение предодобренного предложения. И в целом это не требуется. Но, по моему опыту [4], проще подобрать сопоставимых клиентов, если взять пользователей, которые в том же период получали другие коммуникации банка, не связанные с предодобрением, а дату такой коммуникации использовать как точку отсчёта для измерения эффекта.

Зафиксирую такой дизайн:

Элемент

Определение в разборе

Единица анализа

Клиент

Дата тритмента (t0)

Для теста – дата появления доступного предодобренного предложения;
Для потенциального контроля – дата другой подходящей коммуникации в том же календарном месяце.

Тестовая группа

Клиентам назначили персональное предложение и коммуникацию

Потенциальная контрольная группа

Подходящие клиенты той же волны без назначения предложения на t0

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

После этого мы можем переходить к построению PSM.

Как обучать PSM?

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

Вероятность попадания в тест

PSM формирует эту группу через propensity score – оценку вероятности получить предодобренное предложение (попадания в тест) при известных характеристиках клиента. Это вероятность назначения предложения, а не вероятность взять кредит. Модели могут использовать похожие данные, но отвечают на разные вопросы.

Для оценки этой вероятности строим модель на объединённой выборке теста и потенциального контроля. Целевая переменная – получил ли клиент предодобренное предложение, а признаки – его характеристики до начала воздействия.

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

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

Итого, для оценки propensity score используем CatBoostClassifier. Обучаем его на объединённой выборке теста и потенциального контроля: целевая переменная – получил ли клиент предодобренное предложение, признаки – его характеристики до начала воздействия.

Подбор пар

После построения модели получаем propensity score для каждого пользователя теста и потенциального контроля. Затем проводим матчинг: для каждого пользователя из теста ищем пользователя без предодобрения с близким значением score.

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

Просто взять ближайшего соседа недостаточно. Ближайший клиент может оказаться совсем непохожим, если подходящего контроля нет. Поэтому задаём максимальное допустимое расстояние – caliper. Если ближайший кандидат находится за его пределами, оставляем клиента теста без пары. Расстояние будем считать по логиту propensity score: log(p / (1 - p)). Начальное значение caliper возьмём равным 0,2 стандартного отклонения логита score. Это распространённый ориентир, которая подойдёт любой задаче. После подбора проверим баланс признаков и долю клиентов, которым нашлась пара.

Пример кода

Ниже приведу пример кода для иллюстрации подхода. В df одна строка соответствует одному клиенту. treatment равен 1 для получателей предодобрения и 0 для потенциального контроля, t0 – выбранная дата начала наблюдения. Для контроля это дата другой коммуникации, определённая по заранее заданному правилу, например первой подходящей коммуникации в месяце. product обозначает продукт, эффект которого оцениваем. Все клиентские признаки рассчитаны до его t0. Набор признаков условный, выбран для примера.

Сначала получим оценки CatBoost. Используем out-of-fold предсказания: score каждого клиента рассчитывает модель, которая не видела его при обучении [5]. Это снижает риск того, что матчинг будет опираться на запомненные моделью особенности отдельных клиентов.

import numpy as np
import pandas as pd
from catboost import CatBoostClassifier
from sklearn.model_selection import StratifiedKFold

data = df.copy().reset_index(drop=True)
assert data["client_id"].notna().all()
assert data["client_id"].is_unique
assert set(data["treatment"].unique()) == {0, 1}

data["t0"] = pd.to_datetime(data["t0"])
assert data[["t0", "product"]].notna().all().all()
data["index_month"] = data["t0"].dt.to_period("M").astype(str)

num_features = [
    "income", "balance", "debt_load", "tenure_months",
    "app_active_days_30d", "calculator_uses_90d",
]
cat_features = ["region", "salary_client", "product", "index_month"]
features = num_features + cat_features

for col in cat_features:
    data[col] = data[col].fillna("__MISSING__").astype(str)

params = dict(
    iterations=300,
    depth=4,
    learning_rate=0.05,
    l2_leaf_reg=5,
    loss_function="Logloss",
    random_seed=42,
    thread_count=2,
    verbose=False,
    allow_writing_files=False,
)

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
data["ps"] = np.nan
for train_idx, valid_idx in cv.split(data[features], data["treatment"]):
    model = CatBoostClassifier(**params)
    model.fit(
        data.loc[train_idx, features], data.loc[train_idx, "treatment"],
        cat_features=cat_features,
    )
    data.loc[valid_idx, "ps"] = model.predict_proba(
        data.loc[valid_idx, features]
    )[:, 1]

p = data["ps"].clip(1e-6, 1 - 1e-6)
data["logit_ps"] = np.log(p / (1 - p))
caliper = 0.2 * data["logit_ps"].std(ddof=1)

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

Теперь подберём пары. Внутри каждого месяца и продукта сортируем контроль по логиту score. Для каждого клиента теста ищем ближайшего из ещё не использованных кандидатов. Начинаем с клиентов с высоким score. Это один из вариантов жадного подбора; порядок обхода может влиять на результат.

from bisect import bisect_left


def match_1to1(data, caliper):
    pairs = []

    for _, group in data.groupby(["index_month", "product"], sort=True):
        control = group[group["treatment"].eq(0)].sort_values("logit_ps")
        scores = control["logit_ps"].tolist()
        indices = control.index.tolist()

        treated = group[group["treatment"].eq(1)].sort_values(
            "logit_ps", ascending=False
        )

        for row in treated.itertuples():
            position = bisect_left(scores, row.logit_ps)
            candidates = [
                k for k in (position - 1, position)
                if 0 <= k < len(scores)
            ]
            if not candidates:
                continue

            nearest = min(
                candidates,
                key=lambda k: abs(scores[k] - row.logit_ps),
            )
            distance = abs(scores[nearest] - row.logit_ps)

            if distance <= caliper:
                pairs.append((row.Index, indices.pop(nearest), distance))
                scores.pop(nearest)

    return pd.DataFrame(
        pairs, columns=["treated_idx", "control_idx", "distance"]
    )

pairs = match_1to1(data, caliper)
coverage = len(pairs) / data["treatment"].sum()
print(f"Пар: {len(pairs):,}; сохранилось {coverage:.1%} теста")

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

Проверка сопоставимости

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

Для начала сравним исходные признаки до и после матчинга через стандартизованную разницу средних SMD. Для категориальных признаков проверяем долю каждой категории. В примере используем объединённое стандартное отклонение исходных групп и сохраняем этот знаменатель при расчёте после подбора.

diagnostic = data[features].copy()
for col in num_features:
    diagnostic[col + "_missing"] = diagnostic[col].isna().astype(float)
    diagnostic[col] = diagnostic[col].fillna(
        diagnostic[col].median()
    ).fillna(0)

diagnostic = pd.get_dummies(
    diagnostic, columns=cat_features, dtype=float
)

initial_t = diagnostic.loc[data["treatment"].eq(1)]
initial_c = diagnostic.loc[data["treatment"].eq(0)]
scale = np.sqrt((initial_t.var(ddof=1) + initial_c.var(ddof=1)) / 2)


def absolute_smd(treated, control):
    difference = (treated.mean() - control.mean()).abs()
    result = difference / scale.replace(0, np.nan)
    result = result.mask(scale.eq(0) & difference.eq(0), 0)
    return result.mask(scale.eq(0) & difference.ne(0), np.inf)


balance = pd.DataFrame({
    "before": absolute_smd(initial_t, initial_c),
    "after": absolute_smd(
        diagnostic.loc[pairs["treated_idx"]],
        diagnostic.loc[pairs["control_idx"]],
    ),
}).sort_values("after", ascending=False)

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

Условный пример диагностики:

Признак

Абсолютный SMD до подбора

После подбора

Доход

0,52

0,04

Кредитная нагрузка

0,27

0,03

Доля зарплатных клиентов

0,41

0,04

Использование кредитного калькулятора

0,64

0,06

В реальной оценке проверяем все наиболее важные признаки. Например то 20% среди наиболее важных признаков из модели propensity score + наиболее важные продуктовые фичи. Если после матчинга группы всё ещё заметно различаются по доходу или предварительному интересу к кредиту, возвращаемся к модели и настройкам подбора. При этом не выбираем настройки по тому, где эффект получился больше. Сначала фиксируем сопоставимые группы, затем смотрим на выдачи.

По этой же причине высокий ROC-AUC модели не является главным критерием качества PSM. Модель может прекрасно отличать получателей предодобрения от остальных, но подходящих пар при этом почти не окажется. Нам нужен баланс исходных характеристик, а не рекорд качества классификации.

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

Оцениваем эффекты

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

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

Сравнение

Получатели предложения

Клиенты без предложения

Разница конверсий

До подбора

1 000 из 20 000 — 5,0%

1 500 из 100 000 — 1,5%

3,5 п. п.

После подбора

864 из 18 000 — 4,8%

648 из 18 000 — 3,6%

1,2 п. п.

Разница после подбора – 1,2 процентного пункта. Для оставшихся 18 тысяч получателей это даёт оценку в 216 дополнительных выдач: 18 000 × 0,012. Относительно конверсии подобранного контроля прирост составляет около 33%: 4,8 / 3,6 − 1.

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

Заключение

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

Надеюсь, материал будет вам полезен. Буду рада ответить на ваши вопросы в комментариях!

Полезные ссылки:

Автор: ar_daria

Источник [10]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/37794

URLs in this post:

[1] статью Алексея Терентьева про Propensity Score Matching: https://habr.com/ru/articles/887276/

[2] интереса: http://www.braintools.ru/article/4220

[3] логично: http://www.braintools.ru/article/7640

[4] опыту: http://www.braintools.ru/article/6952

[5] обучении: http://www.braintools.ru/article/5125

[6] CatBoostClassifier: документация: https://catboost.ai/docs/en/concepts/python-reference_catboostclassifier

[7] Austin: выбор caliper: https://pubmed.ncbi.nlm.nih.gov/20925139/

[8] MatchIt: проверка баланса: https://kosukeimai.github.io/MatchIt/articles/assessing-balance.html

[9] MatchIt: оценка эффекта после матчинга: https://kosukeimai.github.io/MatchIt/articles/estimating-effects.html

[10] Источник: https://habr.com/ru/articles/1101732/?utm_campaign=1101732&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Rambler's Top100