Раздел 42 · Аналитика данных с нуля

Модель оттока: логистическая регрессия, precision, recall и порог в деньгах

middle~20 мин

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

Мира · маркетолог магазина настолок «Кубик»

Сегмент «уходящие» хорош, но он про тех, кто уже ушёл. Я хочу ловить их раньше: отправить подарок, пока человек ещё с нами. Подарок с курьером стоит 500 ₽, всем подряд слать дорого. Кому отправлять?

В уроке про RFM «уходящих» нашло правило: давно не покупал, хотя раньше возвращался. Это взгляд назад. Мире нужен взгляд вперёд: кто уйдёт в ближайшие месяцы. В уроке про линейную регрессию модель предсказывала число. Сегодня она будет предсказывать вероятность события «ушёл», а мы научимся проверять такую модель и переводить её ответ в рубли.

Что значит «ушёл»

Модель не поможет, пока не договорились о словах. Определение берём из урока про отток: клиент «Кубика» ушёл, если 135 дней не сделал ни одного неотменённого заказа. 135 дней это 90-й перцентиль паузы между заказами: 90% повторных заказов приходят быстрее.

Теперь выбираем точку прогноза: 16 февраля 2026. От неё до конца данных ровно 135 дней: столько и нужно, чтобы узнать ответ. Смотрим на неё так, будто сейчас утро 16 февраля и будущего мы ещё не знаем:

  • кого прогнозируем: клиентов, у которых был неотменённый заказ за 135 дней до 16 февраля, то есть ещё не ушедших. Таких 1547;
  • признаки: всё, что известно к 16 февраля. Сколько дней с регистрации и с последнего заказа, сколько заказов, сколько денег за вычетом возвратов, сколько заказов с промокодом, сколько возвратов, канал привлечения;
  • ответ: churned = 1, если с 16 февраля по 30 июня у клиента нет ни одного неотменённого заказа. Июньский заказ может быть ещё в пути, поэтому, как и в уроке про отток, считаем любой неотменённый.

Ушли 946 клиентов из 1547, 61,2%. Магазин настолок не подписка: большинство покупает один-два раза.

Таблицу собрал один SQL-запрос по orders, order_items и returns, в задачах она лежит в customers.csv. Разбиение на обучающую и тестовую выборки из введения в ML уже сделано: 1082 клиента для обучения, 465 для проверки, доля ушедших в обеих одинаковая.

Признаки только из прошлого

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

Это утечка из введения в ML в чистом виде. Правило: у каждого признака должна быть дата, и эта дата раньше точки прогноза. Если признак слишком хорошо предсказывает, первым делом ищи, не из будущего ли он.

От прямой к вероятности

Линейная регрессия складывает признаки с весами и выдаёт любое число, хоть −3, хоть 140. Вероятность так не устроена: она живёт от 0 до 1. Логистическая регрессия берёт ту же взвешенную сумму z = b0 + b1 * x1 + b2 * x2 + ... и сжимает её в вероятность:

p = 1 / (1 + exp(-z))

При z = 0 получается 0,5. Большой плюс даёт вероятность около 1, большой минус около 0. В scikit-learn модель собирается так же, как в прошлом уроке, только класс другой:

import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

customers = pd.read_csv("customers.csv")
features = ["tenure_days", "recency_days", "orders", "revenue",
            "promo_orders", "returns", "channel"]
X = pd.get_dummies(customers[features], columns=["channel"], drop_first=True, dtype=float)
train = customers["part"] == "train"
test = customers["part"] == "test"

model = make_pipeline(StandardScaler(), LogisticRegression())
model.fit(X[train], customers.loc[train, "churned"])

model.predict(X[test])[:5]              # array([0, 1, 0, 0, 1])
model.predict_proba(X[test])[:5, 1]     # array([0.4041, 0.7775, 0.405 , 0.2153, 0.5658])

make_pipeline склеивает StandardScaler и модель в один объект: fit считает средние и отклонения только по обучающей выборке, а на тестовой применяет их же. Так масштабирование не подсматривает в тест.

predict отвечает классом, predict_proba вероятностью. Класс получается из вероятности по порогу 0,5, и этот порог scikit-learn выбрал за тебя. Мира же платит за каждую посылку, так что порог мы выберем сами, а для этого нужна вероятность.

Крайние клиенты теста выглядят так. У клиента 254 шесть заказов на 27 650 ₽, последний 29 дней назад: вероятность ухода 0,17. У клиента 764 один заказ на 650 ₽ с маркетплейса 129 дней назад: 0,90. Он и правда ушёл.

Веса модели после StandardScaler можно сравнивать между собой, как в прошлом уроке. Самый большой у давности, +0,55: чем дольше клиент не покупал, тем вероятнее уход. Дальше канал маркетплейс, +0,37 (оттуда уходят 74,6% клиентов против 50,3% у пришедших по приглашению; в уроке про отток мы видели, что маркетплейс испортила акция MEGA30), число заказов, −0,23, и сумма, −0,18. Знак веса говорит, в какую сторону признак двигает вероятность.

урок из курса

Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.