Из A/B-отчёта пропали исходы: как проверить смещение из-за потери данных

Вариант B выглядит лучше: среди пользователей с известным исходом покупку совершили 22%, в A — 20%. Но в B чаще ломался сбор событий. Часть назначенных пользователей исчезла из итоговой таблицы. Если просто удалить их перед расчётом, сравнение будет относиться к разным отобранным подмножествам. Начнём с полного журнала назначения и проверим, может ли неизвестный результат изменить решение.

Неизвестный исход отличается от отсутствия покупки

Если покупок нет, а система гарантированно наблюдала пользователя всё окно, исход равен нулю. Если журнал платежей недоступен или пользователь потерялся при соединении, исход неизвестен. Оба случая могут выглядеть как пустая ячейка после LEFT JOIN. Поэтому нужен отдельный признак полноты наблюдения: он строится из статуса загрузки, покрытия источника и правил окна, а не только из наличия покупки.

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

Посчитать границы без догадки о потерянных пользователях

Пусть в каждой группе назначено по тысяче человек. В A известны исходы 950, среди них 190 покупок; в B известны 900, среди них 198. Удаление пропусков даёт 20% против 22%. Для всей назначенной группы нижняя граница конверсии предполагает, что среди неизвестных никто не купил. Верхняя — что купили все. Это крайние сценарии, которые показывают, что вообще допускают имеющиеся сведения.

groups = {'A':{'assigned':1000,'observed':950,'success':190},
          'B':{'assigned':1000,'observed':900,'success':198}}
bounds = {}
for name,g in groups.items():
    n,obs,s = g['assigned'],g['observed'],g['success']
    assert 0 <= s <= obs <= n and n > 0
    missing = n-obs
    bounds[name] = (s/n,(s+missing)/n)
    print(name, {'observed_rate':s/obs,'missing_rate':missing/n,
                 'full_group_bounds':bounds[name]})
difference = (bounds['B'][0]-bounds['A'][1],
              bounds['B'][1]-bounds['A'][0])
print('difference_bounds',tuple(round(x,3) for x in difference))
assert tuple(round(x,3) for x in difference) == (-0.042,0.108)

Что означает интервал возможных значений

Разница B минус A может лежать от минус 4,2 до плюс 10,8 процентного пункта только из-за неизвестных исходов. Это границы чувствительности к пропускам, а не доверительный интервал случайной ошибки. Они не говорят, что все значения одинаково вероятны. Но направление эффекта пока не определено даже без статистической неопределённости, поэтому объявить победу по 22% против 20% нельзя.

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

Отделить причину потери данных от действия продукта

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

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

Продолжить работу с данными

В «A/B тесты и анализ данных на Python» вы научитесь формулировать метрики, анализировать результат в Python и SQL и распознавать типичные искажения. Эти материалы помогут заранее задать полный знаменатель и объяснить команде, когда данные ещё не позволяют принять решение о варианте. Откройте программу платного курса «A/B тесты и анализ данных на Python».

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

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

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