Привет, Хабр! Меня зовут Андрей Каймаков, я работаю в продуктовой аналитике Mail.ru в VK. Сейчас практически каждая IT-компания (да и не только IT) знает про A/B-тесты и понимает важность проверки новых фичей с помощью этого метода. Когда фичей становится много, то A/B-тесты начинают занимать значительное время в работе команд. Чтобы автоматизировать эти процессы создаются платформы для проведения A/B-тестов. Мы разрабатываем свою систему с 2017 года, а недавно сильно ее обновили. Хочу вместе со своим коллегой разработчиком Андреем Чубаркиным поделиться опытом и инсайтами, которые мы обнаружили в ходе этого проекта. 

Инструмент для проведения A/B-тестов в Mail.ru называется Omicron. Платформа умеет раздавать конфиги клиентам, таргетироваться на определенные срезы пользователей, а также считать метрики и выводить результаты в своем интерфейсе. Сейчас платформу используют такие проекты как почта Mail.ru, VK почта, Облако, Календарь, RuStore и другие. В день количество экспериментов может доходить до 100, а число метрик до 5 000.

На старте внедрения системы АБ, метрик и экспериментов было гораздо меньше, а с конца прошлого года их количество начало быстро расти. Тогда мы столкнулись с несколькими проблемами: 

  • Когда количество экспериментов, рассчитываемых в день начинает переваливать за несколько десятков, а количество метрик за несколько сотен - это означает возросшую нагрузку. При неоптимальных расчетах метрики могут считаться сутками, а в моменты пиковой нагрузки команды начинают волноваться, что же там происходит с их экспериментами.

  • В каждом проекте и платформе есть свои особенности. У большинства фич может быть своя аналитика, свои специфичные типы метрик и требования. Когда ваша платформа недостаточно гибкая, то принять решение по результатам A/B-теста нельзя, нужен дополнительный ручной расчет и анализ метрик. Этим начинают заниматься аналитики, тратя время на рутинную задачу. Параллельно разработка пытается добавить необходимый функционал, а при неоптимальной архитектуре поддержка и любое изменение начинает занимать все больше и больше времени.

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

Одни и те же метрики могут считаться на разных платформах или проектах, а значит у них могут быть разные источники (таблицы), тогда приходится под каждый источник дублировать скрипты для расчета метрик. Со временем все начинает перемешиваться и при внедрении какой-то новой фичи, связанной с добавлением новых источников или изменением расчетов, появляются проблемы.

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

Давайте посмотрим, что у нас получилось и погрузимся в расчеты A/B-экспериментов чуть глубже.

Кумулятивная таблица

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

Действительно, такая таблица дает множество преимуществ, наша таблица в общем виде имеет следующую схему:

  • app_id — идентификатор приложения в Omicron;

  • dt — дата, за которую были рассчитаны события по пользователям;

  • user_id — строковый идентификатор пользователя;

  • user_type — тип идентификатора (email, id_device и т. д.);

  • event_hash — хэш события, по которому в дальнейшем можно будет получить любую инфу по метрике;

  • row_type — тип строки, возможны два варианта: event и filter (о типе filter расскажем позже);

  • hits — количество хитов по событию у пользователя;

  • params — агрегированная сумма значений числовых параметров, указанных в поле params селектора;

  • variant — группа эксперимента.

Чтобы её рассчитать Omicron собирает все события из всех метрик и определяет из каких источников (таблиц) нужно их доставать. Для каждого источника заранее заведен отдельный скрипт, который приводит данные к виду, который нужен в таблице. Затем с группировкой по проекту, дате, пользователю и группе эксперимента агрегируются хиты, так таблица становится по пользователям. Сами метрики уже считаются по этой таблице.

Какие преимущества получаем:

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

  • Таблица позволяет многократно использовать данные, нам не нужно каждый раз по новой доставать их из DWH.

  • Когда у нас есть стандартизированная таблица, по которой считаются метрики, то если мы добавляем какой-то новый источник данных, то сами скрипты для расчета метрики менять не нужно. Нужно только сделать скрипт, который бы приводил данные к нужному виду.

Также мы думали, что использовать для расчетов: Spark или Clickhouse. И в итоге остановили выбор на Spark. В определенных сценариях Clickhouse действительно очень хорош, но так как наше основное хранилище в HDFS, то с помощью Spark мы получаем доступ абсолютно ко всем нашим данным. Кроме того он более гибкий в использовании, так как позволяет совмещать статистические библиотеки и мощности распределенных вычислений.

Аналитический репозиторий

В Omicron мы разделяем типы метрик и сами метрики. Например, есть тип метрики ARPU — она считает средний рекламный доход на пользователя, у которого было указанное событие. А вот в самой метрике мы уже указываем это событие. У типа метрики есть определённый скрипт для расчета и заранее определенная математика (какие статические критерии использовать, какие формулы нужны для расчета размера выборки и так далее). Самих же метрик с типом ARPU может быть много, можно их посчитать для всех пользователей, которые заходили в почту, или только для тех, кто открывал письмо.

В первых реализациях A/B-системы добавление нового типа метрики было долгой процедурой. Аналитик создавал Jupyter-ноутбук, где содержался скрипт как нужно считать метрику. Разработчик платформы его анализировал, приводил к нужному виду, тестировал и выкатывал. Затем аналитик должен провалидировать результат и вернуть разработке, если были найдены ошибки. Такой процесс мог растягиваться на спринты, что было не очень хорошо.

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

Скрипты и правила их расчета задаются с помощью YAML-конфигов, один для расчета метрики в эксперименте, второй для расчета метрики для калькулятора длительности эксперимента. 

Пример YAML для расчета ARPU в эксперименте:

dependencies:
  - name: monetization
    path: monetization_per_user/dt={{ dt }}/_SUCCESS

result_sql: 
  SELECT
    metric_id,
    variant,
    COUNT(DISTINCT user_id) as attempts,
    SUM(revenue) as successes,
    MEAN(revenue) as mean,
    STDDEV(revenue) as std
  FROM
    …

stat_sql:
  SELECT
    metric_id,
    variant,
    bucket,
    AVG(value) AS value
  FROM
    …

stat_tests:
  - mannwhitney

stat_indicators: continuous

Разберём смысл полей, чтобы лучше понять как это работает:

dependencies

Это healthcheck, если в метрике используются какие-то данные, кроме аналитических событий из кумулятивной таблицы (например для ARPU нужны монетизационные данные), то метрика не будет считаться, пока не пройдёт этот healthcheck. Важно, что типы метрик считаются независимо друг от друга, и если где-то данных ещё нет, то это не будет блокировать расчет других метрик и всего эксперимента в целом.

result_sql

Это скрипт, который выводит итоговое значение в отчет с результатами, а также здесь считаются абсолюты и стандартное отклонение, которые нужны для расчета дополнительных показателей (MDE и доверительный интервал).

stat_sql

Скрипт, который собирает распределение, по которому будет считаться статистическая значимость. Разделять эти скрипты может быть полезно, если например вы считаете ratio-метрики, применяя линеаризацию, которая переводит метрику в другое признаковое пространство, и по такому распределению мы не можем вывести результат в понятном виде.

stat_tests

Здесь указываются статистические критерии, можно указать несколько. Для выбора какой критерий использовать, мы разделяем метрики на три вида:

  • Пропорция — уникальные пользователи в числителе и уникальные пользователи в знаменателе, то используем z‑test для пропорций, он вычисляется проще чем хи‑квадрат, но можно использовать и его.

  • Непрерывная метрика по пользователям — хиты или сумма чего‑то в числителе и уникальные пользователи в знаменателе. Используем бакетирование + критерий Манна‑Уитни. К критерию Манна‑Уитни часто предъявляют много претензий из‑за того, что он не сравнивает средние, но благодаря бакетированию, он начинает отражать изменения именно средних, при этом продолжая оставаться устойчивым к выбросам, если вы их сильно не вычищаете. В итоге, это получается самый мощный и универсальный вариант для работы с такими метриками.

  • Непрерывные Ratio‑метрики — хиты или сумма чего‑то в числителе и хиты или сумма чего‑то в знаменателе. Мы используем линеаризацию + t‑test, если вам не нужна разбивка по пользователям, то можно использовать дельта‑метод + t‑test, он даже будет работать быстрее.

stat_indicators

Для каждой метрики мы дополнительно считаем MDE и доверительный интервал. Здесь нужно указать какому типу относится метрика: proportions (пропорции) или continuous (непрерывные), тогда будут использоваться соответствующие формулы.

Таким образом, мы можем быстро вводить и изменять метрики, а также использовать любые данные, которые лежат в HDFS, вешая на них healthcheck.

Заведение метрик в интерфейсе

В нашем сервисе сами метрики можно заводить в прямо в интерфейсе, для этого делать коммиты куда-то в Git не нужно. Существуют три режима для заведения, которые отличаются по сложности:

Metida mode

Этот мод самый простой в использовании. Omicron связан с другим нашим сервисом Metida, который служит каталогом событий (подробнее тут). Если пользователь совсем не знает техническое название события, то он может просто найти его по понятному названию в каталоге событий.

Advanced mode

Этот мод сложнее, так как здесь нужно указывать настоящее название события. Чаще всего его используют аналитики и разработчики, которые знакомы с аналитикой.

Raw mode

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

Таким образом, события можно задавать в виде JSON, где с помощью SQL указывается какие события и параметры нужно брать. В этом JSON есть три основных поля:

  • sql — само событие для метрики. Можно указывать несколько eventname, параметров и т. д.

  • filter — указываем фильтры для событий из блока sql. Например, что у пользователя за время эксперимента срабатывало определённое событие больше/меньше/равно N раз. Таких событий может быть несколько, указываем их в sql_set через запятую, затем в formula пишем условия.

  • params — заполняем это поле, если нам нужно не просто посчитать хиты по событию, а достать из параметра какое‑то количественное значение и сложить его.

Особенность в том, что указанное в Raw Mode отправится в условие WHERE в запросе, который собирает метрики, а значит можно делать довольно кастомные условия, а также использовать методы из SQL-запросов:

{
    "sql": "(eventname = 'Push_Action' and lower(params['Action']) = 'subscribe') or 
              (eventname = 'Message_Action' and lower(params['Action']) = 'subscribe') or 
              eventname = 'SubscribeTapped'",
    "filter": {
        "sql_set": [],
        "formula": ""
    },
    "params": {}
}

Заключение

Выкатка фич через A/B-эксперименты с универсальными метриками требует определенного опыта. Мы создали архитектуру метрик для A/B-тестов, которая позволяет масштабировать сложность расчетов. Теперь у нас есть возможность использовать любые данные и рассчитывать сложные кастомные метрики. 

Так например, коллегам из Облака нужно считать экстраполированный LTV-подписок на основе исторических данных. Их метод периодически модифицируется, и они могут самостоятельно его обновлять, благодаря появлению аналитического репозитория. Когда ваши метрики начинают отходить от стандартных конверсий, важно иметь гибкий инструмент, который может быстро решать такие специфичные задачи.

Благодаря кумулятивной таблице, мы серьезно ускорили расчет экспериментов, теперь результаты готовы к 10:00 утра, а почти все эксперименты проводятся без каких-либо ручных дополнительных расчетов аналитиками.

В дальнейших планах у нас работа над интерфейсом сервиса. Мы хотим обновить его тему, добавить новые кнопки и иконки. В общем, добиться приятного UI. Если у вас есть вопросы - пишите в комментариях, постараемся на них ответить.

Комментарии (0)