Корреляция и причинность: как проверить связь до того, как назвали её причиной
Разбор на учебной базе курса: связь «создал отчёт — дольше остаётся» выглядит убедительно и статистически значима, но полностью объясняется каналом привлечения. Как это увидеть в SQL и что делать дальше.
Содержание статьи
Почти в каждом продукте находится действие, после которого пользователи выглядят лучше: включили уведомления, создали первый отчёт, пригласили коллегу. Разница в удержании между теми, кто это сделал, и остальными обычно большая и легко считается одним запросом. Дальше происходит подмена: связь называют эффектом и ставят задачу «довести до этого действия всех». Ниже — разбор одной такой связи на учебной базе SQL-курса, где видно, откуда берётся разница и как она исчезает.
Коротко
Корреляция говорит, что две величины меняются вместе. Причинность требует ответа на другой вопрос: что было бы с теми же пользователями без этого действия. Данные наблюдений на этот вопрос сами по себе не отвечают.
- Статистическая значимость не спасает: разница может быть надёжной и при этом целиком созданной третьей переменной.
- Первый шаг проверки — не регрессия, а разбивка по составу групп: кто в них попал и в каких долях.
- Люди выбирают действия не случайно, поэтому сравнение «сделал / не сделал» почти всегда сравнивает разных людей.
- Контроль переменных помогает не всегда: некоторые контроли создают смещение, а не убирают его.
- Если решение дорогое, нужен эксперимент или квазиэкспериментальный дизайн, а не более сложная модель на тех же данных.
Связь, которая выглядит убедительно
В учебной базе курса 5000 пользователей и события первых трёх недель. Возьмём привычную продуктовую гипотезу: пользователи, создавшие первый отчёт, активнее остальных. Мерить будем числом разных дней, в которые человек открывал приложение.
Запрос простой, и результат ровно такой, какого ждёт продуктовая команда: 11,37 активного дня против 10,33. Разница — один день на пользователя, десять процентов сверху.
На этом месте обычно и появляется вывод «нужно довести до первого отчёта всех новых пользователей». Он кажется тем более обоснованным, что данные не выборочные, а полные: посчитаны все 5000 человек.
| Группа | Пользователей | Активных дней в среднем |
|---|---|---|
| Создали отчёт | 2001 | 11,37 |
| Не создали | 2999 | 10,33 |
| Разница | — | +1,04 дня (+10,0%) |
WITH flags AS (
SELECT
u.user_id,
u.channel,
max(CASE WHEN e.event_name = 'report_created' THEN 1 ELSE 0 END) AS made_report,
count(DISTINCT CASE WHEN e.event_name = 'app_open'
THEN date_trunc('day', e.event_time) END) AS active_days
FROM users u
LEFT JOIN events e USING (user_id)
GROUP BY 1, 2
)
SELECT
made_report,
count(*) AS users,
round(avg(active_days), 2) AS avg_active_days
FROM flags
GROUP BY 1
ORDER BY 1;Статистика подтверждает разницу — и это ничего не меняет
Первое, что делают с такой находкой, — проверяют, не случайна ли она. Проверка проходит с запасом: при стандартных отклонениях 3,35 и 3,57 дня стандартная ошибка разницы равна 0,099, то есть t ≈ 10,5, а 95% доверительный интервал — от +0,84 до +1,23 дня. Ни о какой случайности речи нет.
Именно здесь чаще всего и теряется осторожность. Значимость отвечает на вопрос «могла ли такая разница возникнуть из случайного шума». Она ничего не говорит о том, почему группы отличаются. Тест не знает, что группы собраны не жребием, а решением самих пользователей, и на это нечувствителен.
Полезная привычка: увидев маленькое p-value в наблюдательном сравнении, спрашивать не «надёжна ли разница», а «чем ещё эти две группы отличаются друг от друга».
p-value отвечает на вопрос о случайности, а не о причине. Систематическое различие между группами оно только подтвердит — тем увереннее, чем больше данных.
Разбивка по каналу: разница исчезает
Прежде чем строить модели, стоит посмотреть на состав групп. В базе четыре канала привлечения, и они сильно различаются по вовлечённости: пользователи из referral активны в среднем 13,67 дня, из organic — 12,68, из partner — 10,14, из paid_search — всего 6,84.
Те же каналы различаются и по доле создавших отчёт: referral — 51,3%, organic — 45,5%, partner — 40,3%, paid_search — 25,9%. Получается, что канал влияет и на действие, и на результат. Это и есть третья переменная в чистом виде.
Теперь сравним внутри каждого канала — то есть среди людей, пришедших одинаковым путём. Разница не просто уменьшается: она пропадает и слегка меняет знак. В referral 13,61 против 13,72, в organic 12,64 против 12,72, в partner 10,11 против 10,16, в paid_search 6,79 против 6,86.
Ни в одном канале создание отчёта не связано с большей активностью. Весь наблюдавшийся плюс был разницей состава: среди создавших отчёт 39,8% пришли из organic и 25,6% из referral, а среди остальных — 31,8% и 16,2%, зато 37,0% из слабого paid_search против 19,4%.
| Канал | Пользователей | С отчётом | Без отчёта | Разница |
|---|---|---|---|---|
| referral | 1000 | 13,61 | 13,72 | −0,11 |
| organic | 1750 | 12,64 | 12,72 | −0,08 |
| partner | 750 | 10,11 | 10,16 | −0,05 |
| paid_search | 1500 | 6,79 | 6,86 | −0,07 |
Каналы различаются вдвое. Группа «создали отчёт» просто состоит из более сильных каналов.
Как посчитать разницу при одинаковом составе
Чтобы не читать четыре строки таблицы глазами, разницу сводят в одно число: считают её внутри каждого канала, а затем усредняют с весами по размеру канала. Приём называется прямой стандартизацией и отвечает на вопрос «какой была бы разница, если бы обе группы имели одинаковый набор каналов».
Для нашего примера получается −0,08 дня: 10,70 против 10,78. Исходный плюс в один день исчез полностью. Вывод «первый отчёт удерживает» не подтверждается — по крайней мере, этими данными.
Стандартизация не превращает наблюдение в эксперимент. Она убирает влияние только того фактора, который вы назвали и измерили. Если бы вместе с каналом действовала мотивация, устройство или тариф, поправка на канал их бы не тронула.
Δ = Σ nᵢ · (aᵢ − bᵢ) / Σ nᵢaᵢ и bᵢ — средние в группах внутри слоя i, nᵢ — размер слоя. Веса берут из общей популяции, а не из одной группы.
WITH flags AS (
SELECT
u.user_id,
u.channel,
max(CASE WHEN e.event_name = 'report_created' THEN 1 ELSE 0 END) AS made_report,
count(DISTINCT CASE WHEN e.event_name = 'app_open'
THEN date_trunc('day', e.event_time) END) AS active_days
FROM users u
LEFT JOIN events e USING (user_id)
GROUP BY 1, 2
),
per_channel AS (
SELECT
channel,
count(*) AS n,
avg(CASE WHEN made_report = 1 THEN active_days END) AS with_report,
avg(CASE WHEN made_report = 0 THEN active_days END) AS without_report
FROM flags
GROUP BY 1
)
SELECT round(sum(n * (with_report - without_report)) / sum(n), 2) AS adjusted_diff
FROM per_channel;Три способа получить связь без причины
Разобранный случай — самый частый, но не единственный. Полезно держать в голове три механизма и проверять их по очереди, потому что лечатся они по-разному.
Третья переменная действует на оба конца связи, как канал в примере выше. Обратная причинность переставляет местами причину и следствие: не приглашения удерживают команду, а те, кто уже собирается остаться, зовут коллег. Отбор в выборку создаёт связь там, где её нет вовсе: если смотреть только на активных пользователей, любые два фактора успеха начнут выглядеть враждебными друг другу — слабость по одному компенсируется силой по другому, иначе человек в выборку не попал бы.
Третий механизм обычно самый неожиданный, потому что он возникает не из данных, а из фильтра в запросе. Условие вида «только оплатившие» или «только дожившие до 30-го дня» — уже отбор, и он способен перевернуть знак связи.
- Третья переменная: и действие, и результат зависят от общего фактора. Лечится разбивкой или поправкой — если фактор известен и измерен.
- Обратная причинность: следствие принято за причину. Проверяется временем — что случилось раньше и с каким лагом.
- Отбор в выборку: фильтр в запросе связывает независимые вещи. Лечится только сменой выборки, поправка тут не помогает.
Почему «добавить контроль» — не универсальное лекарство
После первой встречи с третьей переменной возникает соблазн добавить в регрессию все доступные признаки: чем больше контролей, тем чище оценка. Это неверно, и два случая портят результат систематически.
Первый — переменная после воздействия. Если мы изучаем влияние онбординга на оплату и добавляем контролем «дошёл до второго экрана», то отбираем у онбординга ровно тот путь, которым он и работает. Оценка эффекта становится меньше настоящей, притом выглядит аккуратнее.
Второй — общее следствие. Контроль по переменной, на которую влияют и причина, и результат, создаёт связь между ними, даже если её не было. Именно так фильтр «только активные» превращает независимые факторы в отрицательно связанные.
Отсюда рабочее правило: контроли выбирают до расчёта и по смыслу — что произошло раньше воздействия и могло повлиять на обе стороны. Список признаков, доступных в таблице, таким критерием не является.
| Переменная | Когда возникла | Действие |
|---|---|---|
| Канал, устройство, тариф на входе | до действия | контролировать или разбивать |
| Шаг внутри воронки | после действия | не контролировать, смотреть отдельно |
| Признак активности, факт оплаты | следствие обоих | не фильтровать по ней выборку |
| Неизмеренная мотивация | до действия | поправкой не берётся — нужен эксперимент |
Что меняет рандомизация
В той же базе есть эксперимент с онбординговым чеклистом, и разница между ним и предыдущим сравнением — не в математике, а в том, кто распределял людей по группам. Здесь это делал жребий, а не сами пользователи.
Проверить работу жребия можно тем же способом, каким мы нашли проблему: посмотреть состав групп. Из organic в контроле 598 человек, в тестовой 573; из paid_search — 499 и 488; из referral — 329 и 335; из partner — 230 и 267. Каналы разложились ровно, и это не совпадение, а свойство случайного распределения.
Конверсия в активацию: 18,60% в контроле (308 из 1656) против 29,16% с чеклистом (485 из 1663). Разница 10,6 п.п., z ≈ 7,1, 95% интервал — от 7,7 до 13,4 п.п. Формально это такая же значимая разница, как и в первом сравнении. Содержательно — совсем другая: здесь мы знаем, что группы отличались только чеклистом, потому что всё остальное распределял случай.
Разница в том, что нужно допустить. Для эксперимента — что сплит корректен и метрика измерена одинаково; это проверяемо. Для наблюдения — что не осталось ни одного неучтённого фактора; это не проверяется в принципе.
Учебный эксперимент той же базы. Каналы в группах сбалансированы жребием, поэтому разницу можно отнести к чеклисту.
Если эксперимент невозможен
Эксперимент ставят не всегда: изменение может быть общим для всех, слишком дорогим или уже случившимся. Тогда работают с дизайном наблюдения, и каждый вариант держится на своём допущении, которое нужно называть вслух.
Сравнение до и после у затронутой и незатронутой групп снимает общий тренд, но требует, чтобы без вмешательства группы двигались параллельно — это видно на истории до события и проверяется глазами. Резкий порог в правиле (скидка от определённой суммы, лимит по возрасту аккаунта) позволяет сравнить тех, кто оказался чуть выше и чуть ниже границы: рядом с порогом попадание в группу почти случайно. Внешний толчок вроде поэтапной раскатки по регионам иногда заменяет жребий.
Эти дизайны сильнее простого сравнения групп, но слабее эксперимента, и их результат корректно подавать вместе с допущением: «при условии, что без редизайна обе когорты двигались бы одинаково, эффект составил…». Такая оговорка не ослабляет вывод, а показывает, где он сломается.
- До/после с контрольной группой — допущение о параллельных трендах, проверяется по истории.
- Разрыв на пороге правила — допущение о непрерывности всего остального в точке порога.
- Поэтапная раскатка — допущение, что порядок этапов не связан с результатом.
- Поправка на наблюдаемые факторы — допущение, что неучтённых больше нет; самое сильное и самое частое.
Как написать вывод, который не превратится в дорогое решение
Разница между честной и опасной формулировкой — не в осторожности тона, а в том, названо ли сравнение. «Первый отчёт повышает удержание на 10%» — утверждение об эффекте. «Пользователи, создавшие отчёт, активнее на один день, но это различие каналов: внутри каждого канала разницы нет» — описание данных, из которого сразу видно следующий шаг.
Хорошая запись держит три части: что сравнивали, что осталось от разницы после поправки на известные факторы и какой шаг даст ответ на причинный вопрос. Третья часть важнее первых двух: она превращает «мы не знаем» в план.
Для нашего примера это выглядит так: «Связь между первым отчётом и активностью полностью объясняется каналом привлечения — внутри каналов разница нулевая. Продвижение отчёта на онбординге не обосновано этими данными. Проверить эффект можно за две недели, показав подсказку про отчёт половине новых пользователей».
| Опасно | Точнее |
|---|---|
| Отчёт повышает удержание на 10% | В группе с отчётом активность выше на 1 день |
| Метрика значима, эффект доказан | Разница устойчива, но группы различаются по каналам |
| Мы контролировали все факторы | Поправка сделана на канал; мотивация не измерена |
| Нужно довести до отчёта всех | Нужен тест подсказки на половине новых пользователей |
Проверка перед выводом
Список короткий и проходится за один заход. Он не заменяет причинный анализ, но отсекает большую часть выводов, которые разваливаются на первом же вопросе.
- Кто попал в группы и по какому решению — своему или чужому.
- Чем ещё группы отличаются: канал, устройство, тариф, дата прихода.
- Что было раньше по времени — действие или результат.
- Есть ли в запросе фильтр, который сам создаёт отбор.
- Что останется от разницы после разбивки по главному фактору.
- Какой эксперимент ответил бы на вопрос и сколько он стоит.
Материалы по теме
Регрессия к среднему: почему после плохого дня метрика часто улучшается сама
Что такое regression to the mean и как она искажает оценку релизов, маркетинговых кампаний и ручных вмешательств в продуктовой аналитике.

Доверительный интервал: как показать, насколько точна ваша цифра
Что означает 95% доверительный интервал для конверсии и разницы конверсий, как его посчитать, почему он важнее p-value и какая формулировка про него неверна.

Статистическая мощность: почему тест не находит эффект, который есть
Что такое мощность A/B-теста, как она связана с размером выборки и MDE, как посчитать нужное число участников и почему «незначимо» на слабом тесте ничего не доказывает.