Конверсия выросла в каждом канале, но упала в целом: разбор в pandas

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

Общая конверсия меняет веса

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

В первом периоде search привёл 900 пользователей и 90 покупателей, social — 100 и 2. Во втором search дал 100 пользователей и 12 покупателей, social — 900 и 27. В поиске конверсия выросла с 10% до 12%, в социальной сети — с 2% до 3%. Но основным источником стал менее конверсионный social. Это и меняет итог, хотя улучшение внутри каждого канала действительно произошло.

Такой рисунок называют парадоксом Симпсона: направление сравнения внутри сегментов отличается от направления после объединения. Это не сбой pandas. Арифметика правильно отражает разные веса, а аналитическая задача состоит в том, чтобы показать их пользователю отчёта.

Стандартизируем состав трафика

Для сопоставимости зафиксируем веса каналов первого периода: 90% search и 10% social. К этим весам применим конверсии обоих периодов. Первый стандартизованный результат совпадёт с фактическим первым периодом, второй покажет, какой была бы конверсия при старой структуре трафика и новых внутриканальных долях.

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

import pandas as pd
d = pd.DataFrame({"period":[0,0,1,1],"channel":["search","social"]*2,"users":[900,100,100,900],"buyers":[90,2,12,27]})
assert not d.duplicated(["period","channel"]).any()
d["conversion"] = d.buyers/d.users
totals = d.groupby("period")[["buyers","users"]].sum()
actual = totals.buyers/totals.users
base = d[d.period.eq(0)].set_index("channel").users
weights = base/base.sum()
rates = d.pivot(index="channel",columns="period",values="conversion")
standard = rates.mul(weights,axis=0).sum()
print("actual",actual.to_dict())
print("fixed_mix",standard.to_dict())
assert actual.loc[1] < actual.loc[0]
assert standard.loc[1] > standard.loc[0]
assert abs(standard.loc[1]-0.111) < 1e-10

Как читать два результата

Фактическая конверсия падает с 9,2% до 3,9%. При фиксированных весах второй период дал бы 11,1%. Поэтому в записке можно разделить два наблюдения: внутриканальные показатели улучшились, а изменение состава трафика снизило общую конверсию. Это понятнее, чем выбирать один процент и объявлять другой неправильным.

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

Когда разница возникла в A/B-тесте

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

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

При заметном различии состава экспериментальных групп начните с проверки назначения: SRM в A/B-тесте: как проверить распределение пользователей.

Проверяем расчёт без библиотеки

Первый итог получается как 92 покупателя на 1000 пользователей. Второй — 39 на те же 1000. Для фиксированного состава второго периода берём 90% от конверсии поиска 12% и 10% от конверсии social 3%: получается 10,8% плюс 0,3%, то есть 11,1%. Такая ручная сверка связывает результат кода с исходной таблицей.

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

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

Продолжить практику

Курс «A/B тесты и анализ данных на Python» связывает выбор продуктовой метрики с дизайном эксперимента и интерпретацией результата. Вы рассчитываете размер выборки, применяете статистические методы и объясняете uplift и интервалы команде. Практика заканчивается проектом полного цикла эксперимента. Откройте программу курса.

Продолжить на практике

  • A/B тесты и анализ данных на Python ↗

    Практический курс по A/B-тестированию и продуктовым метрикам: формулируем гипотезы, считаем размер выборки, запускаем эксперименты и анализируем результаты. От первой идеи до внедрения изменений в продукт — без «воды» и лишней теории.