Всем привет, меня зовут Сергей Прощаев, я Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E‑commerce и преподаю на курсах разработки и архитектуры в ОТУС. В этой статье расскажу про задачу, которую любят давать продуктовым аналитикам на собеседованиях и которая в жизни приходит без предупреждения: метрика упала, у вас двадцать минут и созвон с директором.
Четверг, 09:40. Ночью пересчитался дашборд, и в рабочем чате сообщение от продакта: «Конверсия за среду 3,18%, во вторник была 3,70%. Минус 14% за ночь. Что случилось? В 11:00 созвон с директором, нужен ответ». Цены не менялись, ассортимент тот же.
Ниже все вводные, которые есть у аналитика в этот момент. Попробуйте сначала решить сами, а потом сверьтесь с разбором.

Условие
Интернет‑магазин продаёт через сайт и два мобильных приложения. Конверсия на дашборде считается просто: число событий purchase в продуктовой аналитике делим на число сессий. Строго говоря, это покупки на сессию, но в учебной модели сессия даёт не больше одного заказа, а исправный трекинг — одно событие на заказ, так что метрика совпадает с долей сессий с покупкой. Сравниваем полные сутки вторника и среды.
Что известно о среде:
в 06:00 вышел релиз iOS‑приложения 5.2, главное в нём — новый экран оплаты;
с утра маркетинг запустил посевы в Telegram‑каналах, ссылки ведут на сайт и в магазины приложений;
ошибок на бэкенде нет, время ответа обычное.
Таблица 1. Дашборд по платформам, конверсия по событиям purchase
Платформа |
Сессии, вт |
Покупки, вт |
CR, вт |
Сессии, ср |
Покупки, ср |
CR, ср |
Изменение |
|---|---|---|---|---|---|---|---|
Web |
50 000 |
1 650 |
3,30% |
61 500 |
1 899 |
3,09% |
−6,4% |
Android |
30 000 |
1 050 |
3,50% |
37 200 |
1 201 |
3,23% |
−7,8% |
iOS |
30 000 |
1 370 |
4,57% |
31 300 |
1 035 |
3,31% |
−27,6% |
Итого |
110 000 |
4 070 |
3,70% |
130 000 |
4 135 |
3,18% |
−14,0% |
Таблица 2. Тот же дашборд в разрезе новых и вернувшихся пользователей
Сегмент |
Сессии, вт |
Покупки, вт |
CR, вт |
Сессии, ср |
Покупки, ср |
CR, ср |
|---|---|---|---|---|---|---|
Web, новые |
20 000 |
300 |
1,50% |
31 000 |
496 |
1,60% |
Web, вернувшиеся |
30 000 |
1 350 |
4,50% |
30 500 |
1 403 |
4,60% |
Android, новые |
10 000 |
150 |
1,50% |
17 000 |
272 |
1,60% |
Android, вернувшиеся |
20 000 |
900 |
4,50% |
20 200 |
929 |
4,60% |
iOS, новые |
8 000 |
160 |
2,00% |
9 000 |
136 |
1,51% |
iOS, вернувшиеся |
22 000 |
1 210 |
5,50% |
22 300 |
899 |
4,03% |
Таблица 3. Оплаченные заказы из биллинга, которые аналитик до этого момента не проверял. Тип пользователя записывается в заказ при оформлении.
Сегмент |
Заказы, вт |
Заказы, ср |
|---|---|---|
Web, новые |
300 |
496 |
Web, вернувшиеся |
1 350 |
1 403 |
Android, новые |
150 |
272 |
Android, вернувшиеся |
900 |
929 |
iOS, новые |
160 |
189 |
iOS, вернувшиеся |
1 210 |
1 249 |
Итого |
4 070 |
4 538 |
Попробуйте решить
Сначала короткий вопрос: что бы вы проверили первым, имея эти три таблицы и двадцать минут?
А теперь главный: что вы скажете директору в 11:00? Выберите вариант или сформулируйте свой:
А. Релиз iOS 5.2 сломал оплату, нужно откатывать.
Б. Упало на всех трёх платформах, значит, причина внешняя: день недели, погода, конкурент. Ждём.
В. Посевы привели нецелевой трафик, кампанию надо останавливать.
Г. Покупок больше, чем вчера, тревога ложная.
Остановитесь здесь. Дальше разбор.
Неправильные ответы и почему они выглядят логично
А. «Релиз сломал оплату»
Самый соблазнительный ответ. Падение на iOS почти 28%, релиз вышел в 06:00, новый экран оплаты — всё сходится. Мысль понятная: что выкатили, то и сломалось.
Ломается он на Таблице 3. Оплаченных заказов с iOS в среду 1 438, во вторник было 1 370. Люди платят, и даже чуть чаще. Сломалась не оплата, а событие о ней. Откат релиза вернул бы старый экран, но оплаты и так проходили. А главное, этот ответ не объясняет, почему просели Web и Android, где релизов не было.
Б. «Упало везде, значит, причина внешняя»
Логика такая: если просели все платформы, значит, дело не в коде, а в мире вокруг. Звучит разумно, и на Таблице 1 действительно минус на каждой строке.
Таблица 2 этот вывод опровергает. У Web и Android конверсия выросла во всех четырёх сегментах: у новых с 1,50% до 1,60%, у вернувшихся с 4,50% до 4,60%. Общего ухудшения внутри сегментов нет, а значит, внешний фактор если и был, то менял состав трафика, а не поведение покупателей.
В. «Посевы привели плохой трафик»
Этот ответ ближе всех к правде, поэтому он опаснее остальных. Новых сессий в среду заметно больше, у новых конверсия ниже, чем у вернувшихся, общая цифра падает — механика верная.
Но слово «плохой» здесь лишнее. По событиям конверсия новых почти не изменилась, 1,61% и 1,59%, а по биллингу выросла с 1,61% до 1,68%: события искажены пропусками на iOS. А ещё этот ответ объясняет только часть падения. Если остановиться на нём, сломанный счётчик на iOS останется незамеченным, и через неделю дашборд будет врать уже про другое.
Г. «Покупок больше, тревога ложная»
В среду зафиксировано 4 135 событий purchase против 4 070 во вторник. Кажется, всё в порядке. Но сессий стало на 18% больше, а покупок по аналитике — всего на 1,6%. Абсолютное число при росте трафика ничего не доказывает, и на собеседовании эту ловушку проверяют одной из первых.
Правильный ход мысли
Я бы предложил на такие случаи один и тот же порядок, Рис. 2 показывает его целиком: сначала проверить, что метрика вообще считается правильно, потом разложить её на части, и только потом искать виноватых.

Главная мысль схемы: вопрос «кто виноват» стоит в самом конце. Пока не доказано, что счётчик исправен, любая гипотеза о поведении пользователей строится на песке.
Шаг 1. Проверить измерение, пять минут
Первым делом сверяем события аналитики с источником фактических оплат, то есть с биллингом. Не в целом за день, а по платформам и по часам, в явных границах суток:
-- сверка событий аналитики с оплаченными заказами за среду по платформам и часам SELECT b.platform, date_trunc('hour', b.paid_at, 'Europe/Moscow') AS hour, count(DISTINCT b.order_id) AS orders_billing, count(DISTINCT e.order_id) AS orders_with_event, count(DISTINCT b.order_id) - count(DISTINCT e.order_id) AS orders_without_event, count(e.order_id) - count(DISTINCT e.order_id) AS duplicate_events, round(1 - count(DISTINCT e.order_id)::numeric / count(DISTINCT b.order_id), 3) AS gap FROM billing.orders b -- только оплаченные заказы LEFT JOIN analytics.purchase_events e ON e.order_id = b.order_id WHERE b.paid_at >= '2026-10-07 00:00 Europe/Moscow'::timestamptz AND b.paid_at < '2026-10-08 00:00 Europe/Moscow'::timestamptz GROUP BY 1, 2 ORDER BY 1, 2;
count(DISTINCT ...) здесь не для красоты. Если одно событие пришло дважды, обычный count(*) после соединения размножит строки и спрячет пропуски за дублями. Поэтому сверка показывает отдельно заказы без события и повторные события. Границы суток заданы с часовым поясом бизнеса, иначе их сдвинет настройка сессии базы. И честное ограничение: запрос проверяет, есть ли у оплаченного заказа хоть одно сопоставленное событие. Опоздавшее событие он засчитает как вовремя пришедшее, а событие без order_id с заказом не свяжет. Для поиска пропусков этого достаточно, задержки доставки контролируют отдельно.
Результат: у Web и Android разрыв нулевой, дублей нет. У iOS до 06:00 среды разрыва нет, а начиная с часа релиза 30% оплаченных заказов остаются без события, за сутки выходит 28%. Если в заказе пишется версия приложения, добавьте её в группировку: весь разрыв окажется у версии 5.2. Самая вероятная гипотеза — новый экран оплаты не отправляет purchase, подтверждают её логами отправки событий с клиента. Это задача мобильной команде, а не откат.
Этот шаг пропускают чаще всего, а поломки учёта случаются по расписанию. Свежий пример — Shopify. По документации платформы, 26 августа 2026 года на странице статуса заказа перестали выполняться script tags у всех магазинов не на тарифе Plus, у Plus это случилось ещё 28 августа 2025-го. Для отслеживания конверсий Shopify заранее предлагал замену, web pixels. Но если покупку в аналитику и рекламные кабинеты отправлял старый скрипт, а его не перенесли, заказы продолжали идти, а событие покупки просто перестало приходить. Витрина и оформление заказа при этом работают как обычно, так что такую поломку замечают по просевшим конверсиям, а не по алертам. Сверка с биллингом из шага 1 ловит такую поломку в первый же час.
Шаг 2. Пересчитать конверсию по оплаченным заказам
Берём заказы из биллинга: 4 538 на 130 000 сессий, это 3,49%. Падение уже не 14%, а 5,7%. Больше половины тревоги дал сломанный счётчик. Но 5,7% — тоже не ноль, и тут большинство останавливается, решив, что «остальное — шум». Не останавливаемся.
Следующий вопрос простой: что сдвинулось, числитель или знаменатель? Конверсия — дробь, и падение дроби ещё не значит, что стало меньше покупок. Заказов по биллингу стало больше на 11,5%, сессий — на 18,2%. Покупатели никуда не ушли, просто в магазин пришло заметно больше людей, чем обычно, и знаменатель обогнал числитель. Значит, искать надо не того, кто «отпугнул» покупателей, а того, кто привёл новых посетителей.
Проверяем разрезом по источнику трафика. По UTM‑меткам на сайте и атрибуции установок через Universal Links и App Links в приложениях из 20 000 дополнительных сессий 19 000 пришли из Telegram: 11 000 на сайт, 7 000 в Android и 1 000 в iOS. Во вторник таких не было, а прямые заходы, поиск и прочие источники вместе дали ещё 1 000. Это связь по времени и по меткам, а не эксперимент, но для ответа директору её достаточно.
Шаг 3. Разложить оставшееся падение
Общая конверсия — это средневзвешенное конверсий сегментов, где вес — доля сессий сегмента. Значит, она может упасть по двум причинам: либо сегменты стали хуже конвертировать, либо вырос вес сегментов, которые конвертируют хуже. Это разные диагнозы и разные действия, поэтому их надо разделить в цифрах:
# Разложение изменения конверсии: эффект состава, эффект сегментов и перекрёстный член. # Сегмент = платформа x тип пользователя, заказы из биллинга (Таблица 3). tue = {("web", "new"): (20000, 300), ("web", "ret"): (30000, 1350), ("android", "new"): (10000, 150), ("android", "ret"): (20000, 900), ("ios", "new"): (8000, 160), ("ios", "ret"): (22000, 1210)} wed = {("web", "new"): (31000, 496), ("web", "ret"): (30500, 1403), ("android", "new"): (17000, 272), ("android", "ret"): (20200, 929), ("ios", "new"): (9000, 189), ("ios", "ret"): (22300, 1249)} def decompose(a, b): assert a.keys() == b.keys(), "наборы сегментов должны совпадать" sa = sum(s for s, _ in a.values()) sb = sum(s for s, _ in b.values()) w = lambda d, s, k: d[k][0] / s # доля сессий сегмента r = lambda d, k: d[k][1] / d[k][0] # конверсия сегмента mix = sum((w(b, sb, k) - w(a, sa, k)) * r(a, k) for k in a) seg = sum(w(a, sa, k) * (r(b, k) - r(a, k)) for k in a) cross = sum((w(b, sb, k) - w(a, sa, k)) * (r(b, k) - r(a, k)) for k in a) return mix * 100, seg * 100, cross * 100 # в процентных пунктах print("состав %+.2f, сегменты %+.2f, перекрёстный %+.2f п.п." % decompose(tue, wed)) # состав -0.31, сегменты +0.10, перекрёстный +0.00 п.п.
Все три таблицы, сверку и это разложение можно пересчитать самому: данные и скрипт лежат в репозитории conversion‑drop‑diagnosis‑demo, запуск одной командой ./run.sh.
Состав трафика отнял 0,31 процентного пункта, а конверсия внутри сегментов добавила 0,10. Перекрёстный член после округления равен нулю: в учебных данных конверсия всех сегментов выросла примерно на одну и ту же величину, около 0,10 пункта. На живых данных он бывает заметным, поэтому считать его нужно всегда. Доля новых пользователей выросла с 34,5% до 43,8%, и по времени и меткам это совпадает с посевами. При этом все шесть сегментов по биллингу стали конвертировать лучше, включая оба сегмента iOS. Каждая часть растёт, а целое падает — это парадокс Симпсона в чистом виде, и на дашборде без разреза его не увидеть.
На Рис. 3 всё падение собрано в одну картину.

Вывод из Рис. 3: внутри сегментов ухудшения нет, их конверсия выросла. Общая цифра снизилась из‑за сдвига состава, а ещё 0,31 пункта дал недоучёт событий на iOS, который чинится тикетом.
Шаг 4. Ответ директору
По времени это укладывается в двадцать минут: пять на сверку с биллингом, пять на пересчёт и разрез по источникам, пять на разложение, пять на формулировку. Оговорка честная: так быстро получается, только если таблицы событий и заказов уже связаны по order_id, а определения метрик согласованы заранее. Строить витрину сверки во время инцидента — это уже часы.
Три фразы, которые я бы предложил принести на созвон:
1. Оплаченных заказов в среду 4 538 против 4 070, на 11,5% больше. Выручку и средний чек сверим отдельно, но падения продаж по числу заказов нет.
2. На iOS 5.2 до аналитики не доходит событие покупки, дашборд недосчитывает 28% iOS‑заказов. Задача заведена на мобильную команду, метрики заказов за среду пересчитаем по биллингу.
3. Конверсия по оплаченным заказам снизилась на 5,7% из‑за сдвига состава: новых пользователей стало больше, и прирост совпадает с посевами по меткам источника. Внутри каждого сегмента она выросла. Кампанию оцениваем отдельно, по её собственной конверсии в заказы и с окном атрибуции: первые сутки её занижают.
Как это устроено у команд, которые не тушат такие пожары
Оба провала можно закрыть заранее, а не в 09:40.
Сверка аналитики с биллингом по расписанию. Ежечасный запрос из шага 1 с порогом разрыва и оповещением. Он ловит и пропавшие события, и дубли, то есть именно тот класс поломок, что в примере с Shopify.
Трекинг‑план как контракт. Список событий с обязательными полями живёт рядом с кодом, а новый экран не уходит в релиз без проверки, что
purchaseс него отправляется.Отметки релизов и кампаний на графиках. Когда на оси времени стоят «iOS 5.2» и «посевы», половина гипотез проверяется взглядом.
Конверсия в разрезе по умолчанию. Общая цифра на главном дашборде соседствует с конверсией новых и вернувшихся. Тогда парадокс Симпсона виден сразу, а не после разложения в ноутбуке.
Ограничения разбора
Цифры в задаче учебные и подобраны так, чтобы две причины дали примерно равный вклад. В жизни причин бывает больше, а разрез выбирается не так очевидно. Если состав сдвинулся по признаку, которого нет в разрезе, например по региону, разложение спишет это на «эффект сегментов».
Тип пользователя здесь записан прямо в заказ. В реальной системе его получают через связь заказа с пользователем или сессией, и без надёжной связи пересчитывать конверсию по биллингу внутри сегментов нельзя. И последнее: биллинг честен про заказы, но не про сессии. Если сломан учёт сессий, знаменатель сверяют с логами веб‑сервера или магазина приложений.
Усложнение
Допустим, в биллинге нет ни платформы, ни версии приложения, есть только order_id, время и сумма. Как за те же двадцать минут доказать, что потери событий идут именно с iOS 5.2? Подсказка: смотрите, где ещё в данных живёт order_id до момента оплаты.
Какой навык проверяла задача
Не знание SQL и не умение строить дашборды. Задача проверяет дисциплину диагностики: сначала усомниться в измерении, потом разделить метрику на числитель и знаменатель, состав и сегменты, и только после этого искать причину. Варианты А‑Г неверны не потому, что их авторы плохо знают продукт, а потому, что каждый из них начинает с третьего шага.
Если вы выбрали В, вы заметили важный сигнал, сдвиг состава трафика, но остановка кампании требует проверки её собственной конверсии. Если заметили разрыв с биллингом раньше, чем дочитали до разбора, — это тот уровень, который отличает middle от senior.
Хотите проверить и другие навыки работы с данными? Попробуйте пройти вступительный тест курса «Аналитик данных». Он поможет оценить текущие знания и понять, какие темы стоит подтянуть.
А какой вариант выбрали вы и как решили усложнение? Напишите в комментариях, на каком шаге ваша команда обычно спотыкается: на проверке счётчика или на разложении. Буду рад обсудить.
Ссылка на мой репозиторий с задачей: https://github.com/sproshchaev/conversion‑drop‑diagnosis‑demo
Начать стоит с expected.txt: там все три таблицы из условия, сверка событий с биллингом по часам и версиям приложения, разрез по источникам трафика и разложение минус 0,52 пункта. Как это считается, видно в diagnose.py, исходные заказы и события лежат в data/billing_orders.csv и data/purchase_events.csv, а ./run.sh пересобирает данные и сверяет вывод с эталоном.
Источники:
Репозиторий с задачей: https://github.com/sproshchaev/conversion‑drop‑diagnosis‑demo
Shopify, Order status script tags (документация): https://shopify.dev/docs/apps/build/online‑store/script‑tag‑deprecation/order‑status

Когда метрика неожиданно меняется, полезно сначала проверить качество данных, на которых она строится. Какие показатели помогут вовремя обнаружить пропущенные события, некорректные значения и другие аномалии? Как организовать такой контроль системно?
20 октября в 20:00 на бесплатном вебинаре курса «Качество данных» разберём, как выбирать метрики качества данных с учётом бизнес‑задач и выстраивать мониторинг — от первых проверок до внедрения в рабочие процессы. Можно будет задать вопросы и разобраться в подходах к контролю качества данных. Записаться.