Все материалы
MLгайдсредний

Логистическая регрессия: сигмоида, отношение шансов и пример на Python

Логистическая регрессия простыми словами: сигмоида и логит, шансы и отношение шансов, интерпретация коэффициентов, лог-лосс. Модель оплаты на Python с ROC AUC и калибровкой.

КейсПрактика14 октября 2026 г.18 мин

Логистическая регрессия — модель, которая оценивает вероятность события с двумя исходами: заплатит пользователь или нет, уйдёт или останется. Рабочая ситуация: менеджер продукта спрашивает, можно ли по первым трём дням после регистрации понять, кто заплатит в первые три недели, и какие действия с оплатой связаны. Ниже — сигмоида, логит, шансы и лог-лосс с формулами, а затем модель на учебной базе «Маяк»: 3 207 пользователей, обучение через scipy, коэффициенты и отношения шансов, ROC AUC и калибровка. Итог неожиданный: канал привлечения говорит об оплате больше, чем все четыре действия вместе, а созданное рабочее пространство, которое в сырой таблице повышает шансы в 1,36 раза, после учёта канала почти ничего не добавляет.

Коротко

Логистическая регрессия считает вероятность события по нескольким признакам: p = 1 / (1 + e^(−z)), где z = β₀ + β₁x₁ + … + βₖxₖ.

  • Линейная часть та же, что в линейной регрессии, но её значение z проходит через сигмоиду и становится вероятностью от 0 до 1.
  • Коэффициент β — изменение логарифма шансов при росте признака на единицу. exp(β) — отношение шансов: во сколько раз меняются шансы при прочих равных.
  • Модель обучают методом максимального правдоподобия, то есть минимизируют лог-лосс. Готовой формулы, как у МНК, нет: минимум ищут численно.
  • Качество проверяют на отложенной части данных: ROC AUC показывает, как модель ранжирует людей, калибровка — сходятся ли обещанные вероятности с фактом.
  • Коэффициенты описывают связь, а не причину. Порог 0,5 — договорённость, а не свойство модели.

Логистическая регрессия — это что и когда она нужна

Её берут, когда исход бинарный: оплатил или нет, продлил подписку или нет, вернул товар, кликнул по письму, не вернул кредит. Единица наблюдения — тот, про кого спрашивают: пользователь, заказ, заявка. Ответ модели — вероятность для каждой единицы.

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

Метод относят к классификации, хотя выдаёт он число. Класс появляется только после порога: «вероятность выше 0,3 — звоним». Если размеченного исхода нет и нужно разбить пользователей на похожие группы, это другая задача — кластеризация k-means. Как выбрать метод под вопрос, разобрано в навигаторе по статистическим методам.

Почему линейная регрессия не годится для вероятности

Можно закодировать исход как 0 и 1 и провести через точки обычную прямую методом наименьших квадратов. Такую модель называют линейной вероятностной, и у неё три изъяна.

Первый — выход за границы. Допустим, база — 20%, а каждое из шести действий прибавляет по 15 пунктов: у человека, сделавшего всё, выйдет 110%. На «Маяке» с бинарными признаками прогнозы прямой остались в диапазоне 7,6–36,8%, но числовой признак вроде числа сессий выталкивает её за край.

Второй — сложение эффектов на шкале вероятности. Прямая прибавляет одни и те же пункты и при базе 2%, и при базе 90%, хотя с 95% нельзя вырасти на 15 пунктов.

Третий — разброс. Дисперсия бинарного исхода равна p(1 − p) и зависит от прогноза, а стандартные ошибки линейной регрессии предполагают одинаковый разброс. Логистическая регрессия обходит все три проблемы: линейной она делает не вероятность, а логарифм шансов.

Сигмоида и логит: формулы

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

Логит — обратная функция: логарифм отношения вероятности события к вероятности его отсутствия. Модель записывают так: логит вероятности равен линейной комбинации признаков. Слово logit ввёл Джозеф Берксон в статье 1944 года о применении логистической функции в биологических испытаниях, по аналогии с уже существовавшим probit.

Сигмоида и логит
p = σ(z) = 1 / (1 + e^(−z)); logit(p) = ln(p / (1 − p)) = z = β₀ + β₁x₁ + … + βₖxₖ

σ(0) = 0,5; σ(2) = 0,881; σ(−2) = 0,119. Логит переводит интервал (0; 1) во всю числовую прямую, поэтому справа можно складывать признаки без ограничений.

Сигмоида: вероятность p при разных значениях линейной части z

Шаг z на единицу меняет p на 23 пункта в середине (с 0,5 до 0,731) и на 3 пункта у края (с 0,953 до 0,982).

p = σ(z)

Шансы и отношение шансов

Шансы — вероятность события, делённая на вероятность его отсутствия. В выборке «Маяка» оплатили 756 человек из 3 207, это 23,6%. Шансы — 756 / 2 451 = 0,31: на одного плательщика приходится около трёх неплательщиков. Вероятность 50% — это шансы 1, вероятность 90% — шансы 9.

Отношение шансов, OR, сравнивает шансы двух групп: шансы тех, у кого признак есть, делят на шансы тех, у кого его нет. OR = 1 — разницы нет, больше единицы — шансы выше, меньше — ниже. Подробно о том, чем OR отличается от относительного риска и почему их нельзя подменять, — в статье об относительном риске и отношении шансов.

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

Одна и та же величина на трёх шкалах
Вероятность pШансы p / (1 − p)Логит ln(шансов)
0,100,111−2,197
0,236 (оплата в «Маяке»)0,309−1,175
0,5010
0,7531,099
0,9092,197

Как обучается модель: максимум правдоподобия и лог-лосс

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

Лог-лосс дорого наказывает уверенные промахи. Плательщику модель дала 0,3 — штраф −ln 0,3 = 1,20, неплательщику 0,3 — штраф 0,36, плательщику 0,01 — штраф 4,61. Поэтому такая модель стремится выдавать честные вероятности, а не только правильный порядок.

Формулы в одну строку, как у МНК, здесь нет: минимум ищут численно. Функция выпуклая, минимум у неё один. Градиент — Xᵀ(p − y) / n, признаки, взвешенные на ошибку прогноза. Классический способ — метод Ньютона (в пакетах его называют IRLS), в коде ниже — BFGS из scipy.

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

Лог-лосс (минус среднее логарифма правдоподобия)
L(β) = −(1/n) · Σ [yᵢ · ln pᵢ + (1 − yᵢ) · ln(1 − pᵢ)], pᵢ = σ(β₀ + β₁xᵢ₁ + … + βₖxᵢₖ)

yᵢ — 1, если пользователь оплатил, иначе 0. Модель, которая всем даёт среднюю долю 23,6%, получает около 0,55; это нижняя планка, которую нужно обойти.

Как собрать данные для модели в SQL

Строка будущей таблицы — пользователь. Признаки — только из первых трёх дней, то есть события раньше signup_date + 3: создал ли рабочее пространство, отчёт, отправил ли приглашение, сделал ли экспорт. В учебной базе эти события приходятся на дни 0–2, так что в окно попадают все. Плюс канал привлечения и устройство.

Исход — хотя бы одна оплата не позже 20-го дня после регистрации, то есть в первые 21 день. Первые оплаты в базе приходят на 3–20-й день, и признаки всегда собраны до исхода. В реальных данных это проверяют отдельно.

Оплаты записаны по 30 августа, поэтому берём регистрации по 9 августа включительно: у этих людей 21 день уже прошёл. Зарегистрированный 25 августа ещё не успел заплатить и стал бы в модели ложным нулём. Остаётся 3 207 человек, оплатили 756.

В ноутбуке таблицу на пользователя забирают целиком через pd.read_sql. Песочница отдаёт не больше 200 строк, поэтому запрос сворачивает её в счётчики: восемь строк «канал × устройство», в каждой десять ячеек «пользователей/оплатили» по комбинациям действий. Код 1101 — «пространство, отчёт, приглашения нет, экспорт». Комбинаций десять, а не шестнадцать: отчёт и экспорт бывают только у создавших пространство. Это пригодится в разговоре о мультиколлинеарности.

Признаки первых трёх дней и оплата за 21 день: счётчики по каналу, устройству и действиям
WITH features AS (
  SELECT
    u.user_id, u.signup_date, u.channel, u.device,
    max(CASE WHEN e.event_name = 'workspace_created' THEN 1 ELSE 0 END) AS workspace,
    max(CASE WHEN e.event_name = 'report_created' THEN 1 ELSE 0 END) AS report,
    max(CASE WHEN e.event_name = 'invite_sent' THEN 1 ELSE 0 END) AS invite,
    max(CASE WHEN e.event_name = 'export_completed' THEN 1 ELSE 0 END) AS exported
  FROM users AS u
  LEFT JOIN events AS e
    ON e.user_id = u.user_id
   AND e.event_time < CAST(u.signup_date + 3 AS timestamp)
  WHERE u.signup_date <= DATE '2026-08-09'
  GROUP BY u.user_id, u.signup_date, u.channel, u.device
),
labeled AS (
  SELECT
    f.*,
    concat(workspace, report, invite, exported) AS pattern,
    CASE WHEN EXISTS (
      SELECT 1 FROM payments AS p
      WHERE p.user_id = f.user_id AND p.paid_at <= f.signup_date + 20
    ) THEN 1 ELSE 0 END AS paid
  FROM features AS f
),
grid AS (
  SELECT c.channel, c.device, p.pattern
  FROM (SELECT DISTINCT channel, device FROM labeled) AS c
  CROSS JOIN (SELECT DISTINCT pattern FROM labeled) AS p
),
cells AS (
  SELECT g.channel, g.device, g.pattern,
    count(l.user_id) AS users, COALESCE(sum(l.paid), 0) AS payers
  FROM grid AS g
  LEFT JOIN labeled AS l
    ON l.channel = g.channel AND l.device = g.device AND l.pattern = g.pattern
  GROUP BY g.channel, g.device, g.pattern
)
SELECT
  channel, device,
  string_agg(CAST(users AS varchar) || '/' || CAST(payers AS varchar), ' ' ORDER BY pattern) AS cells
FROM cells
GROUP BY channel, device
ORDER BY channel, device;

Логистическая регрессия в Python: обучение через scipy

Блок разворачивает счётчики обратно в 3 207 строк и строит матрицу: константа, четыре действия, три канала и мобильное устройство. Одна категория каждого признака уходит в базу — organic и desktop, иначе столбцы канала в сумме повторят константу. Лог-лосс минимизирует scipy.optimize.minimize с явным градиентом.

Стандартные ошибки — из обратной матрицы Гессе XᵀWX, где на диагонали W стоят pᵢ(1 − pᵢ). Это приближение для больших выборок: оно предполагает независимых пользователей и верную форму модели. Для проверки мы пересчитали модель на 500 бутстреп-выборках: ошибки разошлись с гессианом не больше чем на 0,01.

В работе модель обычно строят библиотеками. sklearn.linear_model.LogisticRegression по умолчанию добавляет регуляризацию (C = 1,0): коэффициенты сжимаются к нулю, стандартных ошибок нет. Для прогноза это нормально, для чтения коэффициентов — нет. Logit из statsmodels печатает ошибки и p-value, но константу сам не добавляет — нужен add_constant. Числа ниже получены только scipy.

Результат запуска: 3 207 пользователей, база — organic, desktop, ни одного действия
ПризнакβSEOR = exp(β)95% для ORp
константа−1,0620,1010,350,28–0,42<0,001
рабочее пространство0,1810,1211,200,95–1,520,134
отчёт−0,0560,1090,950,76–1,170,612
приглашение0,1890,0961,211,00–1,460,049
экспорт−0,1870,1100,830,67–1,030,091
канал paid_search−1,0610,1480,350,26–0,46<0,001
канал partner−0,3430,1210,710,56–0,900,005
канал referral0,2100,1021,231,01–1,510,040
мобильное устройство−0,1050,0860,900,76–1,070,222
pythonЛогистическая регрессия на scipy: коэффициенты, ошибки по матрице Гессе, отношения шансов
import numpy as np
from scipy import optimize, stats

# Результат SQL-запроса выше: канал, устройство и десять ячеек «пользователей/оплатили»
# по комбинациям «пространство, отчёт, приглашение, экспорт» в порядке PATTERNS
PATTERNS = ['0000', '0010', '1000', '1001', '1010', '1011', '1100', '1101', '1110', '1111']
CELLS = """
organic desktop 170/48 55/14 75/30 34/5 23/7 15/4 139/40 97/30 54/15 29/6
organic mobile 141/29 49/18 72/12 30/8 20/6 15/6 106/31 67/11 47/10 20/7
paid_search desktop 105/8 35/5 13/2 10/2 2/1 4/0 21/5 7/1 9/1 8/1
paid_search mobile 198/11 56/10 29/6 11/0 11/0 3/0 40/7 34/6 16/1 9/0
partner desktop 126/20 39/11 29/6 20/2 10/3 6/2 62/14 42/9 13/2 13/0
partner mobile 81/13 23/6 21/7 15/1 7/2 0/0 36/7 20/7 16/4 8/4
referral desktop 73/26 22/6 65/22 36/11 23/7 10/5 102/31 48/9 35/14 23/8
referral mobile 58/20 26/10 43/15 23/6 12/4 4/1 68/21 46/9 12/0 12/7
"""
NAMES = ['const', 'workspace', 'report', 'invite', 'export',
         'paid_search', 'partner', 'referral', 'mobile']   # база: organic, desktop

# разворачиваем счётчики обратно в строку на пользователя
rows, y = [], []
for line in CELLS.strip().splitlines():
    channel, device, *cells = line.split()
    for pattern, cell in zip(PATTERNS, cells):
        users, payers = map(int, cell.split('/'))
        x = [1, *map(int, pattern), channel == 'paid_search', channel == 'partner',
             channel == 'referral', device == 'mobile']
        rows += [x] * users
        y += [1] * payers + [0] * (users - payers)
X, y = np.array(rows, dtype=float), np.array(y)


def log_loss(beta):                       # среднее −log правдоподобия
    z = X @ beta
    return np.mean(np.logaddexp(0, z) - y * z)


def gradient(beta):
    p = 1 / (1 + np.exp(-(X @ beta)))
    return X.T @ (p - y) / len(y)


fit = optimize.minimize(log_loss, np.zeros(X.shape[1]), jac=gradient, method='BFGS')
beta = fit.x
p = 1 / (1 + np.exp(-(X @ beta)))
hessian = X.T @ (X * (p * (1 - p))[:, None])     # Гессе для −log L по всем строкам
se = np.sqrt(np.diag(np.linalg.inv(hessian)))
z = stats.norm.ppf(0.975)

print(f'пользователей {len(y)}, оплатили {y.sum()} ({y.mean():.1%}), лог-лосс {fit.fun:.4f}')
print(f'{"признак":<12}{"β":>7}{"SE":>7}{"OR":>6}  95% для OR    p')
for name, b, s in zip(NAMES, beta, se):
    p_value = 2 * stats.norm.sf(abs(b / s))
    print(f'{name:<12}{b:>7.3f}{s:>7.3f}{np.exp(b):>6.2f}  '
          f'{np.exp(b - z * s):.2f}–{np.exp(b + z * s):.2f}  {p_value:.3f}')

Интерпретация коэффициентов логистической регрессии

Коэффициент читают через экспоненту. У канала referral β = 0,210, exp(0,210) = 1,23: при тех же действиях и устройстве шансы оплаты у пришедших по рекомендации в 1,23 раза выше, чем у органики. У paid_search β = −1,061, OR = 0,35: шансы втрое ниже. В процентах: (OR − 1) · 100% — шансы выше на 23% и ниже на 65%.

Чтобы перейти к вероятности, нужна конкретная точка. Пользователь из органики с десктопа, не сделавший ни одного действия: σ(−1,062) = 25,7%. Тот же профиль из платного поиска — σ(−1,062 − 1,061) = 10,7%, из рекомендаций — 29,9%. Приглашение с OR 1,21 поднимает органического пользователя с 25,7% до 29,5%, на 3,8 пункта. При базе 10,7% тот же коэффициент дал бы меньше пунктов: одинаковое OR — разные приросты вероятности.

Частая ошибка — прочитать OR 1,21 как «платят на 21% чаще». Доли оплаты у пригласивших и остальных — 26,2% и 22,7%, их отношение 1,15. OR больше относительного риска, когда исход не редкий, а 23,6% — не редкий.

Теперь интервалы. У приглашения нижняя граница — ровно 1,00, p = 0,049: связь на грани различимости. У пространства, отчёта и экспорта интервалы накрывают единицу; экспорт с OR 0,83 выглядит как минус, но интервал 0,67–1,03 не позволяет так сказать. Уверенно модель говорит только о каналах.

Коэффициент как отношение шансов
OR = шансы(x + 1) / шансы(x) = e^β; изменение шансов = (e^β − 1) · 100%

Верно для любого значения x и при фиксированных остальных признаках. Изменение вероятности от того же β зависит от исходной точки.

Таблица сопряжённости против модели: сырое и скорректированное отношение шансов

Проверим модель на самом простом расчёте. Таблица 2×2 по рабочему пространству: из 1 950 создавших оплатили 501 (25,69%, шансы 0,346), из 1 257 остальных — 255 (20,29%, шансы 0,254). Сырое отношение шансов 0,346 / 0,254 = 1,36, интервал 1,15–1,61. По этой таблице пространство выглядит заметным сигналом.

Логистическая регрессия с единственным бинарным признаком воспроизводит таблицу точно: exp(β) = 1,359. Добавляем канал и устройство — и OR падает до 1,08 с интервалом 0,90–1,29. Связь почти исчезла.

Дело в канале. Пришедшие по рекомендации создают пространство в 75,8% случаев и платят в 31,3%, из платного поиска — в 36,6% и 10,8%. Канал двигает обе величины сразу, и сырая таблица приписывает пространству то, что на деле принёс канал. С приглашением иначе: 1,21 в таблице и 1,21 в модели — с каналом оно почти не связано. Если сырое и скорректированное OR сильно расходятся, рядом стоит третья переменная; этот же эффект в крайней форме описывает парадокс Симпсона.

Отношение шансов оплаты: сырое и в моделях с поправками, 95%-й интервал
ПризнакТаблица 2×2+ канал и устройствоПолная модель
рабочее пространство1,36 (1,15–1,61)1,08 (0,90–1,29)1,20 (0,95–1,52)
приглашение1,21 (1,00–1,45)1,21 (1,00–1,46)1,21 (1,00–1,46)
Таблица 2×2: оплата за 21 день у создавших и не создавших рабочее пространство
WITH features AS (
  SELECT
    u.user_id, u.signup_date,
    max(CASE WHEN e.event_name = 'workspace_created' THEN 1 ELSE 0 END) AS workspace
  FROM users AS u
  LEFT JOIN events AS e
    ON e.user_id = u.user_id
   AND e.event_time < CAST(u.signup_date + 3 AS timestamp)
  WHERE u.signup_date <= DATE '2026-08-09'
  GROUP BY u.user_id, u.signup_date
),
labeled AS (
  SELECT
    f.workspace,
    CASE WHEN EXISTS (
      SELECT 1 FROM payments AS p
      WHERE p.user_id = f.user_id AND p.paid_at <= f.signup_date + 20
    ) THEN 1 ELSE 0 END AS paid
  FROM features AS f
)
SELECT
  workspace,
  count(*) AS users,
  sum(paid) AS payers,
  round(CAST(100.0 * sum(paid) / count(*) AS numeric(18, 6)), 2) AS paid_pct,
  round(CAST(1.0 * sum(paid) / (count(*) - sum(paid)) AS numeric(18, 6)), 3) AS odds
FROM labeled
GROUP BY workspace
ORDER BY workspace;

Как проверить качество: ROC AUC через ранги

Коэффициенты читают на всех данных, качество прогноза — на отложенных. Делим пользователей случайно 70 на 30 с фиксированным seed: 2 244 для обучения, 963 для проверки. Модель учится на первой части, оценивается на второй.

ROC AUC — вероятность того, что случайно взятый плательщик получил прогноз выше случайно взятого неплательщика. Значит, его можно посчитать через ранги, без scikit-learn: ранжируем все прогнозы, складываем ранги плательщиков и вычитаем минимально возможную сумму. Это тот же расчёт, что в критерии Манна — Уитни. У модели всего около 80 разных прогнозов — по числу комбинаций признаков, — поэтому ничьи неизбежны; scipy.stats.rankdata даёт им средний ранг, и это корректно. Что ещё показывает AUC и где он обманывает, разобрано в статье о метриках классификации.

Результат: AUC 0,607 на обучении и 0,631 на проверке. Модель только из канала даёт на проверке 0,627, только из действий и устройства — 0,560. Лог-лосс на проверке — 0,5267 против 0,5473 у константы. Проверка выше обучения — случайность разбиения: с seed 7 и 2026 на проверке выходит 0,649 и 0,624. На 963 людях AUC гуляет на пару сотых.

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

ROC AUC через ранги
AUC = (R₁ − n₁(n₁ + 1) / 2) / (n₁ · n₀)

R₁ — сумма рангов прогнозов у плательщиков, n₁ и n₀ — число плательщиков и неплательщиков. 0,5 — случайный порядок, 1 — все плательщики выше всех остальных.

pythonРазбиение 70/30, ROC AUC через ранги, калибровка по децилям и пороги
import numpy as np
from scipy import optimize, stats

# Те же данные, что в предыдущем блоке: канал, устройство и десять ячеек «пользователей/оплатили»
# по комбинациям «пространство, отчёт, приглашение, экспорт» в порядке PATTERNS
PATTERNS = ['0000', '0010', '1000', '1001', '1010', '1011', '1100', '1101', '1110', '1111']
CELLS = """
organic desktop 170/48 55/14 75/30 34/5 23/7 15/4 139/40 97/30 54/15 29/6
organic mobile 141/29 49/18 72/12 30/8 20/6 15/6 106/31 67/11 47/10 20/7
paid_search desktop 105/8 35/5 13/2 10/2 2/1 4/0 21/5 7/1 9/1 8/1
paid_search mobile 198/11 56/10 29/6 11/0 11/0 3/0 40/7 34/6 16/1 9/0
partner desktop 126/20 39/11 29/6 20/2 10/3 6/2 62/14 42/9 13/2 13/0
partner mobile 81/13 23/6 21/7 15/1 7/2 0/0 36/7 20/7 16/4 8/4
referral desktop 73/26 22/6 65/22 36/11 23/7 10/5 102/31 48/9 35/14 23/8
referral mobile 58/20 26/10 43/15 23/6 12/4 4/1 68/21 46/9 12/0 12/7
"""
NAMES = ['const', 'workspace', 'report', 'invite', 'export',
         'paid_search', 'partner', 'referral', 'mobile']   # база: organic, desktop

# разворачиваем счётчики обратно в строку на пользователя
rows, y = [], []
for line in CELLS.strip().splitlines():
    channel, device, *cells = line.split()
    for pattern, cell in zip(PATTERNS, cells):
        users, payers = map(int, cell.split('/'))
        x = [1, *map(int, pattern), channel == 'paid_search', channel == 'partner',
             channel == 'referral', device == 'mobile']
        rows += [x] * users
        y += [1] * payers + [0] * (users - payers)
X, y = np.array(rows, dtype=float), np.array(y)


def fit(X, y):                            # та же минимизация лог-лосса
    loss = lambda b: np.mean(np.logaddexp(0, X @ b) - y * (X @ b))
    grad = lambda b: X.T @ (1 / (1 + np.exp(-(X @ b))) - y) / len(y)
    return optimize.minimize(loss, np.zeros(X.shape[1]), jac=grad, method='BFGS').x


def auc(score, target):                   # ROC AUC через ранги, ничьи получают средний ранг
    ranks = stats.rankdata(score)
    n1, n0 = target.sum(), len(target) - target.sum()
    return (ranks[target == 1].sum() - n1 * (n1 + 1) / 2) / (n1 * n0)


def log_loss(p, target):
    return -np.mean(target * np.log(p) + (1 - target) * np.log(1 - p))


rng = np.random.default_rng(42)
order = rng.permutation(len(y))
train, test = order[:int(0.7 * len(y))], order[int(0.7 * len(y)):]
print(f'обучение {len(train)} ({y[train].mean():.1%} оплат), проверка {len(test)} ({y[test].mean():.1%})')

sets = {'все признаки': list(range(9)), 'только канал': [0, 5, 6, 7],
        'только действия и устройство': [0, 1, 2, 3, 4, 8]}
for label, cols in sets.items():
    beta = fit(X[train][:, cols], y[train])
    p_train = 1 / (1 + np.exp(-(X[train][:, cols] @ beta)))
    p_test = 1 / (1 + np.exp(-(X[test][:, cols] @ beta)))
    print(f'{label:<30} AUC обучение {auc(p_train, y[train]):.3f}, проверка {auc(p_test, y[test]):.3f}')
    if label == 'все признаки':
        p_full, y_test = p_test, y[test]

base = np.full(len(test), y[train].mean())       # модель «всем одна средняя доля»
print(f'лог-лосс на проверке: модель {log_loss(p_full, y_test):.4f}, константа {log_loss(base, y_test):.4f}')
print(f'прогнозы от {p_full.min():.3f} до {p_full.max():.3f}, не ниже 0,5: {(p_full >= 0.5).sum()}')

# калибровка: десять равных групп по возрастанию прогноза
decile = (stats.rankdata(p_full, method='ordinal') - 1) * 10 // len(p_full)
print('дециль  людей  прогноз  факт')
for d in range(10):
    m = decile == d
    print(f'{d + 1:>6}{m.sum():>7}{p_full[m].mean():>9.3f}{y_test[m].mean():>6.3f}')

# порог превращает вероятность в решение «звонить / не звонить»
for threshold in (0.5, 0.3, 0.25, 0.2):
    flagged = p_full >= threshold
    hits = (flagged & (y_test == 1)).sum()
    print(f'порог {threshold}: в списке {flagged.sum()}, из них оплатили {hits}, '
          f'полнота {hits / y_test.sum():.1%}')

Калибровка: совпадают ли вероятности с фактом

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

Здесь они идут рядом с поправкой на шум. В группе около 96 человек, стандартная ошибка доли около 23% — примерно 4,3 пункта. Самое большое расхождение — в третьем дециле: обещано 20,2%, оплатили 13,5%; прежде чем чинить, его проверяют на другом разбиении.

Главное другое: прогнозы лежат между 10% и 33%. Модель отличает плохую группу от хорошей, но не умеет назвать человека, который заплатит почти наверняка.

Калибровка на проверочной части, 963 пользователя: средний прогноз и факт по децилям, %
ДецильЛюдейПрогнозФакт
19710,28,2
29612,713,5
39620,213,5
49723,019,6
59624,928,1
69626,429,2
79727,625,8
89629,135,4
99630,529,2
109633,334,4
Калибровка по децилям прогноза: обещанная и фактическая доля оплаты, %

Проверочная часть, seed 42. Линии расходятся на 1–7 пунктов, ошибка доли в группе из 96 человек — около 4 пунктов.

Прогноз моделиФакт

Порог 0,5 и дисбаланс классов

Самый высокий прогноз на проверке — 0,364. При стандартном пороге 0,5 модель не назовёт плательщиком никого, и её доля верных ответов совпадёт с долей неплательщиков — 76,3%. Модель «никто не заплатит» даёт ровно столько же, ничего не считая.

Порог выбирают от задачи. Если у отдела продаж есть время на 170 звонков, берут порог 0,3: в списке 173 человека, 55 из них оплатят — 31,8% против 23,7% в среднем, но это лишь четверть всех будущих плательщиков. Порог 0,25 даёт 538 человек, 166 оплат и 72,8% полноты. Порог задают ёмкость команды и цена ошибки, а не красота числа.

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

Пороги на проверочной части: 963 пользователя, 228 оплатили
ПорогВ спискеИз них оплатилиТочностьПолнота
0,5000—0%
0,301735531,8%24,1%
0,2553816630,9%72,8%
0,2073619927,0%87,3%

Ошибки и ограничения: где модель легко прочитать неверно

Связь — не причина. Пригласившие платят чаще, OR 1,21. Это не значит, что подсказка «пригласите коллегу» поднимет оплату: приглашают те, кто пришёл с командой и задачей, и платят они по той же причине. Ответ на вопрос «что будет, если подтолкнуть» даёт только A/B-тест; подробнее — в разборе корреляции и причинности.

Мультиколлинеарность. Отчёт и экспорт бывают только у создавших пространство: корреляция пространства с отчётом — 0,645, с экспортом — 0,43. Когда они в модели вместе, коэффициент пространства означает «создал, но не сделал ни отчёта, ни экспорта» и вырастает с 1,08 до 1,20. Стандартная ошибка растёт с 0,091 до 0,121. Прогноз от этого не страдает, а отдельные коэффициенты читать трудно. Признаки, вложенные друг в друга, лучше объединить в одну ступенчатую переменную.

Утечка и окно. Признак должен быть известен к моменту прогноза. В «Маяке» оплаты начинаются с третьего дня, и окно 0–2 дня безопасно. В реальных данных так бывает не всегда: событие «ввёл карту» за день до оплаты сделает AUC прекрасным на истории и бесполезным в работе. Как это ловить и что происходит с моделью после запуска — в статье об утечке и дрейфе.

Незрелые когорты. 1 406 человек, зарегистрированных после 9 августа, мы не взяли: их 21 день ещё не прошёл. Включите их нулями — и доля оплат, и коэффициенты сдвинутся просто из-за календаря.

Линейность в логитах. Для числового признака модель считает, что каждая единица умножает шансы на одно и то же число. Число сессий или чек проверяют разбивкой на интервалы. Сами признаки удобно выбирать от первой ценности продукта — см. статью про activation rate.

Частые вопросы

Логистическая регрессия — это классификация или регрессия?

По устройству — регрессия: она оценивает вероятность как функцию признаков. По применению — чаще классификация: вероятность сравнивают с порогом и получают класс.

Чем логистическая регрессия отличается от линейной?

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

Как перевести коэффициент в проценты?

exp(β) — во сколько раз меняются шансы; (exp(β) − 1) · 100% — на сколько процентов. В изменение вероятности коэффициент переводят только для конкретного профиля: посчитайте σ(z) без признака и с ним.

Как сделать логистическую регрессию в Python?

Для прогноза — LogisticRegression из scikit-learn, помня о регуляризации по умолчанию. Для таблицы коэффициентов с ошибками — Logit из statsmodels с add_constant. Без этих библиотек — минимизация лог-лосса через scipy.optimize.minimize, как в примере выше.

Запросы из статьи выполняются в песочнице Симулятора SQL-аналитика на той же базе «Маяк»: можно поменять окно признаков, исход или канал и посмотреть, как сдвинутся счётчики.

Итог

Логистическая регрессия переводит линейную комбинацию признаков в вероятность через сигмоиду, коэффициенты отдаёт в логарифмах шансов: exp(β) — отношение шансов при прочих равных. Обучают её минимизацией лог-лосса, проверяют на отложенной части по ROC AUC и калибровке, порог выбирают под задачу.

На «Маяке» ответ скромный, но полезный: канал объясняет оплату лучше действий первых трёх дней, сырое OR пространства 1,36 после учёта канала сжимается до 1,08, а приглашение на грани различимости описывает связь, а не причину. Собрать такие признаки на продуктовых данных можно в Симуляторе SQL-аналитика.

Продолжить чтение
Вся библиотека