Legan Studio
Все статьи
~ 15 мин чтения

A/B тесты в Telegram-боте: гипотезы и метрики

Как корректно проводить A/B-тесты в Telegram-боте: формулировка гипотез, расчёт выборки, разбиение пользователей и интерпретация результатов.

  • Telegram
  • аналитика
  • конверсия

A/B-тесты в Telegram-боте — недооценённый инструмент роста. Тексты сообщений, порядок шагов, формулировка кнопок, время рассылки, эмодзи в превью, длина квиза — всё это можно проверять данными, а не интуицией. Но есть нюансы: малый трафик, эффект новизны, длинные циклы конверсии, недельная сезонность — и без аккуратной методики тесты дают ложные результаты, на которых легко принять плохое продуктовое решение.

В этом материале — практическое руководство: что и как тестировать, как считать выборку, как корректно делить пользователей, какие платформы существуют, как анализировать результаты в SQL и где не наступить на грабли байесовских и частотных методов.

Зачем A/B-тесты вообще

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

A/B-тест даёт три вещи:

  • Data-driven решения. Видим в цифрах, какой вариант лучше, и не тратим энергию на холивары на ретро.
  • Риск-менеджмент. Раскатывая изменение на 50%, мы ограничиваем потенциальный ущерб. Если вариант B провален — откатили, потеряв конверсию только половины пользователей.
  • Отказ от карго-культа. «У конкурента так — давайте и нам». Возможно, у них работает, а у вас в нише — нет.

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

Что можно тестировать в боте

Реальные кандидаты на тест в Telegram-боте:

  • Текст приветствия на /start. «Привет! Я бот X» vs «Здравствуйте, помогу подобрать тариф за 30 секунд».
  • Формулировка кнопок CTA. «Купить» vs «Оставить заявку» vs «Узнать цену» vs «Получить расчёт».
  • Цена и тариф. Не сам факт — это уже стратегия — а упаковка: «990 ₽ в месяц» vs «33 ₽ в день» vs «11 880 ₽ в год со скидкой 10%».
  • Длина квиза. 3 вопроса до контакта vs 5 vs 7. Часто короче побеждает в конверсии, но проигрывает в качестве лида.
  • Время отправки рассылки. 10:00 vs 12:00 vs 19:00 vs 21:00. У разных аудиторий открываемость отличается в 2–3 раза.
  • Эмодзи в первом сообщении. С эмодзи / без / с одним конкретным. Не в каждой нише это работает; в B2B часто хуже.
  • Последовательность шагов воронки. Сначала телефон, потом услуга — или наоборот.
  • Цвет и положение кнопок Mini App. Главный CTA сверху vs снизу, акцентный цвет vs нейтральный.
  • AI-промпты. Для AI-бота можно тестировать формулировки system prompt — какой вариант даёт больше довольных диалогов.
  • Каналы платежа. СБП первым vs картой vs YooKassa виджет.
  • Промо-механика. Скидка vs бонусы vs «бесплатная неделя».

Не стоит тестировать:

  • Микрокопии типа точки в конце сообщения — эффект ниже шума.
  • Цвет кнопки внутри обычного бота (Telegram стандартизирует UI, у вас нет контроля).
  • Технические оптимизации (тут нужны не A/B-тесты, а бенчмарки).

Формулировка гипотезы

Хорошая гипотеза имеет структуру:

Если мы [изменение], то [метрика] вырастет на [величину], потому что [механизм].

Пример:

Если в /start показывать сразу кнопку «Записаться» вместо «Узнать цену», конверсия в заявку вырастет с 8% до 12%, потому что мы убираем лишний шаг и снижаем барьер входа.

Без числа изменения и механизма гипотеза превращается в «давайте попробуем» — и потом непонятно, удался тест или нет. С механизмом проще делать выводы: если конверсия упала, понятно, что «убирание шага» сработало не так, как ожидалось — и можно копать дальше.

Метрики: primary, guardrail, secondary

Метрика теста делится на три уровня:

  1. Primary (целевая) — главная, по которой считаем результат. Например, конверсия /start → заявка. Формулируется одна, заранее, и больше не меняется.
  2. Guardrail (контролирующая) — что не должно ухудшиться. Например, retention D7, CSAT, среднее время до ответа оператора, доля отказов на этапе оплаты.
  3. Secondary (вспомогательные) — для понимания механизма. Глубина прохождения квиза, время на шаг, доля open-нажатий по сообщениям.

Если primary вырос, но guardrail упал — тест провален. Например, выросла конверсия в заявку, но упал ретеншн D7 — значит, давили слабых клиентов и собрали лиды, которые не платят. Принимать решение только по primary — путь к деградации продукта.

Минимальная детектируемая разница (MDE) и расчёт выборки

Главная ошибка — запускать тест без расчёта. На малой выборке любой шум превращается в «победу», и решение принимается зря.

Базовая формула для z-теста двух пропорций при alpha=0.05, power=0.8:

n = ((z_alpha + z_beta)^2 * (p1*(1-p1) + p2*(1-p2))) / (p1 - p2)^2

где z_alpha ≈ 1.96 (двусторонний 95%), z_beta ≈ 0.84 (мощность 80%), p1 — базовая конверсия, p2 — ожидаемая.

Реализация на Python:

from math import ceil
from scipy.stats import norm

def sample_size(p1: float, mde_relative: float,
                alpha: float = 0.05, power: float = 0.8) -> int:
    p2 = p1 * (1 + mde_relative)
    z_alpha = norm.ppf(1 - alpha / 2)
    z_beta = norm.ppf(power)
    pooled_var = p1 * (1 - p1) + p2 * (1 - p2)
    n = ((z_alpha + z_beta) ** 2 * pooled_var) / (p1 - p2) ** 2
    return ceil(n)

# Прирост с 10% до 12% (относительный +20%):
print(sample_size(0.10, 0.20))   # ≈ 3 836 на группу
print(sample_size(0.10, 0.10))   # ≈ 14 745 на группу — для +10%
print(sample_size(0.10, 0.05))   # ≈ 58 233 на группу — для +5%

Проще пользоваться калькуляторами: evanmiller.org/ab-testing/sample-size.html, abtestguide.com/calc/, Statsig sample size calculator. Если бот собирает 200 уникальных стартов в день, тест на конверсию заявок займёт минимум 2–4 недели для MDE +20%, и месяцы для меньших эффектов. Меньше — нечестно.

Распределение пользователей и sticky assignments

Стандартный подход — хеш от связки user_id + test_name для стабильного разбиения. У одного пользователя группа не должна меняться между визитами, иначе тест ломается.

import hashlib

def assign_group(user_id: int, test_name: str,
                 weights: list[int] = [50, 50]) -> str:
    key = f"{user_id}_{test_name}".encode()
    bucket = int(hashlib.md5(key).hexdigest(), 16) % 100
    cum = 0
    for i, w in enumerate(weights):
        cum += w
        if bucket < cum:
            return chr(ord("A") + i)   # "A", "B", "C", ...
    return "A"

Свойства:

  • Один user_id всегда попадает в одну и ту же группу для одного и того же test_name — это и есть sticky assignment.
  • Для двух тестов одновременно — два разных test_name (соль), чтобы не было корреляции. Параллельные тесты допустимы, если изменения не пересекаются.
  • Для multi-variant (A/B/C/D) меняйте weights на [25, 25, 25, 25].
  • hash() Python нестабилен между процессами (PYTHONHASHSEED), поэтому используем hashlib.md5 или xxhash.

Фиксация exposure

Записывать «попал в группу» нужно строго в момент, когда пользователь увидел изменение, а не при первом контакте с ботом. Иначе считаете тех, кто и не дошёл до тестируемого экрана.

async def show_pricing_step(user_id: int, db):
    group = assign_group(user_id, test_name="pricing_v3")
    await db.execute(
        """
        INSERT INTO ab_exposures (user_id, test_name, variant, exposed_at)
        VALUES ($1, $2, $3, NOW())
        ON CONFLICT (user_id, test_name) DO NOTHING
        """,
        user_id, "pricing_v3", group,
    )
    if group == "A":
        await send_pricing_v1(user_id)
    else:
        await send_pricing_v2(user_id)

ON CONFLICT DO NOTHING гарантирует, что для каждого пользователя exposure пишется один раз — это важно для последующего SQL-анализа, чтобы не задвоить.

Новые vs старые пользователи

Если запускаете тест на всех — обязательно сегментируйте отчёт. Старые пользователи привыкли к интерфейсу, у них ниже конверсия в новый CTA из-за «слепых пятен», но выше retention. Новые — наоборот: конверсия чувствительнее, retention ещё не сформировался.

Безопаснее тестировать только на новых (когорта auth_date >= test_start) — так результат чище, его можно применять к будущим новым пользователям без поправок.

Длительность теста

Закладывайте минимум:

  • Одну полную неделю — чтобы покрыть недельные циклы (понедельник vs воскресенье отличаются в любом B2C на 30–50%).
  • Достижение расчётной выборки в обеих группах.
  • Отсутствие аномальных событий внутри окна (распродажа, рекламная кампания, бан конкурента).
  • Lookback-period для отложенных конверсий: если платёж приходит в среднем через 5 дней, ждите 5 дней после exposure последнего пользователя.

Не закрывайте тест раньше плана, даже если результаты «радуют». Это эффект peeking: чем больше промежуточных проверок при фиксированном alpha=0.05, тем выше реальный false positive rate. После 5 «подглядываний» — уже около 14% вместо 5%.

Multi-variant и A/A-тесты

A/B/C/D. Полезно, когда есть 3–4 равноправных варианта формулировки. Минусы: выборка нужна больше (делится на N групп), нужны поправки на множественные сравнения (Bonferroni: alpha делим на число пар сравнений).

A/A-тест — две идентичные группы. Запускают для проверки трекинга. Если в A/A появляется «значимая» разница в 10% — у вас сломано назначение, exposure или метрика, тесты доверять нельзя.

SRM: Sample Ratio Mismatch

Если назначаете 50/50, а в данных видите 5 200 vs 4 800 — это может быть SRM, и его нельзя игнорировать. Проверяется хи-квадратом:

from scipy.stats import chisquare
observed = [5200, 4800]
expected = [5000, 5000]
stat, p = chisquare(observed, expected)
if p < 0.001:
    print("SRM detected, do not trust the test")

Причины SRM: бот падает на одном из вариантов и теряет exposure, кеш фронта отдаёт старый вариант, бот применяет sticky неправильно, рассылка дошла не всем. Любая значимая SRM = тест не валиден, надо чинить инфру.

Sequential testing и проблемы peeking

Альтернатива классическому A/B — sequential-методики (mSPRT, group-sequential, alpha-spending). Они позволяют корректно подглядывать в результаты по ходу теста, не теряя статистической чистоты — за счёт более строгих порогов значимости.

Полезно при низком трафике, когда хочется как можно быстрее остановить очевидно плохой вариант. Реализации: evanmiller.org/sequential-ab-testing.html, библиотеки confseq, auto-stop в Statsig.

Альтернатива — фиксировать минимум sample size и не смотреть результаты до его достижения вообще. Дисциплинарно, но работает.

Bayesian A/B

Bayesian подход даёт ответ в формате «вероятность того, что B лучше A — 92%», что часто понятнее бизнесу, чем «p-value 0.04». Подходит для малых выборок и для непрерывных решений (raise / shrink rollout).

import numpy as np

def bayesian_ab(success_a: int, n_a: int,
                success_b: int, n_b: int,
                samples: int = 200_000) -> float:
    # Prior Beta(1, 1) — равномерный
    posterior_a = np.random.beta(success_a + 1, n_a - success_a + 1, samples)
    posterior_b = np.random.beta(success_b + 1, n_b - success_b + 1, samples)
    return float((posterior_b > posterior_a).mean())

prob = bayesian_ab(420, 5000, 478, 5000)
print(f"P(B > A) = {prob:.3f}")   # например 0.951

Для боевых задач — PyMC для полноценных моделей с иерархией, или готовые сервисы (Statsig включает байесовский режим из коробки).

Multi-armed bandit

Если хочется не просто узнать победителя, а максимизировать награду по ходу, используют bandit-алгоритмы (Thompson sampling, UCB1). Каждый раз, когда нужно показать вариант, алгоритм выбирает с вероятностью, пропорциональной текущей оценке его эффективности.

Подходит для «вечных» оптимизаций — заголовков рассылок, темы дня в AI-ассистенте, сортировки карточек. Не подходит, если важно понять, почему один вариант лучше — bandit сходится к победителю, но не отвечает на вопрос «насколько».

Платформы

ПлатформаТипЦенаКогда брать
StatsigSaaSfreemium до 1M eventsХочется готовое: feature flags + A/B + bayesian + sequential
GrowthBookOpen-source / SaaSself-host бесплатноКонтроль данных, ставим рядом с PostgreSQL/ClickHouse
EppoSaaSenterpriseБольшая аналитическая команда, нужны warehouse-native эксперименты
Amplitude ExperimentSaaSот $0, paid от 10K MTUУже используете Amplitude для аналитики
OptimizelySaaSenterpriseВеб-маркетинг + крупный B2C
Кастом на PostgreSQLselfбесплатноМаленький бот, 1–3 теста, понятная команда

Готовых платформ A/B специально для Telegram-ботов нет — все универсальные. На практике в небольшом боте достаточно самописного разбиения + хранения exposure/events в PostgreSQL и анализа в SQL.

Что тестируемТип тестаМетрикаСрок
Текст CTAA/B/CКонверсия в нажатие1–2 недели
Длина квизаA/BКонверсия в заявку + качество лида2–4 недели
Время рассылкиA/B/C/DOpen rate + CTR1 неделя
Цена / тарифA/BКонверсия в оплату + ARPU4+ недель
AI-промптbanditCSAT / thumbs upнепрерывно

Логирование и SQL-анализ

Минимальный набор таблиц:

CREATE TABLE ab_exposures (
    user_id     BIGINT NOT NULL,
    test_name   TEXT   NOT NULL,
    variant     TEXT   NOT NULL,
    exposed_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (user_id, test_name)
);

CREATE TABLE ab_events (
    user_id     BIGINT NOT NULL,
    event_name  TEXT   NOT NULL,
    occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    properties  JSONB
);
CREATE INDEX ON ab_events (user_id, event_name);

Базовый отчёт по тесту — конверсия в целевое событие по группам:

WITH exposures AS (
    SELECT user_id, variant, exposed_at
    FROM ab_exposures
    WHERE test_name = 'pricing_v3'
),
conversions AS (
    SELECT DISTINCT e.user_id, ex.variant
    FROM ab_events e
    JOIN exposures ex ON ex.user_id = e.user_id
    WHERE e.event_name = 'payment_success'
      AND e.occurred_at >= ex.exposed_at
      AND e.occurred_at <= ex.exposed_at + INTERVAL '14 days'
)
SELECT
    ex.variant,
    COUNT(*)                                 AS users,
    COUNT(c.user_id)                         AS conversions,
    ROUND(COUNT(c.user_id)::numeric
          / COUNT(*)::numeric * 100, 2)      AS conversion_rate
FROM exposures ex
LEFT JOIN conversions c ON c.user_id = ex.user_id
GROUP BY ex.variant
ORDER BY ex.variant;

Над этим строится dbt-модель, которая каждое утро прогоняет все активные тесты и кладёт результат в Metabase / Superset. Если тестов больше 3 — оно того стоит.

Интерпретация результатов

Смотрите не только на p-value, но и на доверительный интервал:

  • Прирост +12% [-2%; +26%] — «возможно ничего», ширина больше эффекта.
  • Прирост +12% [+8%; +16%] — реальная разница, надо катить.

Если результат значим, но мал (рост менее 5%) — оцените стоимость внедрения. Иногда «победа» не стоит времени на полную раскатку и поддержки двух вариантов до полного выкатывания.

Антипаттерны

  • Меняет дизайн посреди теста. «А давайте я тут чуть-чуть поправлю текст в варианте B». Всё, тест сломан, откат к началу.
  • Останавливает рано. Увидели «значимость» на третий день и остановили — почти всегда false positive.
  • Не учитывает novelty effect. Резкое изменение интерфейса первую неделю даёт всплеск интереса, потом откат. Закладывайте в анализ.
  • Тестирует без гипотезы. «Запустили варианты, посмотрим, что выстрелит» — это не A/B, это рулетка. Без гипотезы вы не сможете объяснить результат и масштабировать его на следующий тест.
  • Меряет неправильное событие. «У нас выросли клики» — а заявок нет. Primary всегда должна быть бизнес-метрикой.

152-ФЗ и эксперименты

A/B-эксперименты юридически допустимы — это улучшение продукта в интересах пользователя (основание ст. 6 ч. 1 п. 5 152-ФЗ). Согласие на ПДн при этом получается через стандартную форму при первом контакте с ботом.

Что важно соблюдать:

  • Не передавать персональные данные (user_id Telegram, телефон, email) в третьи системы аналитики (Amplitude, Statsig SaaS) без отдельного согласия и без указания этих систем в политике.
  • Хранить exposure и events на серверах, удовлетворяющих требованиям локализации (РФ для ПДн граждан РФ).
  • Если используете SaaS-платформу A/B — отдавайте туда обезличенный bucket id (например, sha256 от user_id + соль), а связку «bucket → user_id» храните только у себя.

Итого

A/B-тест в боте — это гипотеза с числом, расчёт выборки заранее, стабильное sticky-разбиение по user_id, фиксация exposure в момент показа изменения, primary + guardrail + secondary метрики, минимум неделя длительности, проверка SRM и отказ от подглядываний. Из 10 гипотез реально «выстрелят» 1–3 — это нормальная статистика, и она же оправдывает регулярную работу с тестами.

Стартовать можно с самописного решения на PostgreSQL и SQL-отчётов, по мере роста — переходить на GrowthBook self-host или Statsig. Главное — не сломать методику ради скорости: один корректный тест приносит больше, чем десять «на глазок».

Частые вопросы

Что имеет смысл A/B-тестировать в Telegram-боте?

Реальные кандидаты: тексты приветствия на /start, формулировка CTA («Купить» vs «Оставить заявку» vs «Узнать цену»), порядок шагов воронки (телефон до или после выбора услуги), длина квиза (3 vs 5 vs 7 вопросов), время отправки рассылки (10:00 vs 19:00 vs 21:00), эмодзи в первом сообщении, упаковка цены (990 в месяц vs 33 в день), цвет и положение кнопок Mini App, AI-промпты для AI-бота, каналы платежа (СБП первым vs картой). Не стоит тестировать: микрокопии типа точки в конце, цвет кнопки в обычном боте (Telegram стандартизирует UI), технические оптимизации — для них нужны бенчмарки, а не A/B.

Как правильно сформулировать гипотезу A/B-теста?

Хорошая гипотеза имеет структуру: «Если мы [изменение], то [метрика] вырастет на [величину], потому что [механизм]». Пример: «Если в /start показывать сразу кнопку Записаться вместо Узнать цену, конверсия в заявку вырастет с 8% до 12%, потому что мы убираем лишний шаг и снижаем барьер входа». Без числа изменения и механизма гипотеза превращается в «давайте попробуем». С механизмом проще делать выводы: если конверсия упала, понятно, что «убирание шага» сработало не так, и можно копать дальше. Из 10 гипотез реально выстрелят 1–3 — это нормальная статистика тестирования.

Какая выборка нужна для статистически значимого A/B-теста?

Расчёт через z-тест двух пропорций при alpha=0.05 и power=0.8. Чтобы детектировать прирост конверсии с 10% до 12% (относительный +20%) нужно около 4 000 пользователей в каждой группе, для +10% — около 15 000, для +5% — около 58 000. Используйте калькуляторы evanmiller.org, abtestguide.com, Statsig. Если бот собирает 200 уникальных стартов в день, тест на конверсию заявок займёт минимум 2–4 недели для MDE +20%, и месяцы для меньших эффектов. На малой выборке любой шум превращается в «победу», и решение принимается зря.

Как стабильно разбивать пользователей на группы A/B-теста?

Стандартный подход — sticky assignment: хеш от связки user_id + test_name, бакет 0–99, по бакету выбираем вариант. Это гарантирует, что один user_id всегда попадает в одну и ту же группу. Для двух параллельных тестов — два разных test_name (соль), чтобы не было корреляции. Используйте hashlib.md5 или xxhash, не встроенный hash() Python — он нестабилен между процессами. Для multi-variant (A/B/C/D) меняйте веса на [25, 25, 25, 25]. Exposure пишите в момент показа изменения с ON CONFLICT DO NOTHING, а не при первом контакте с ботом — иначе считаете тех, кто не дошёл до экрана.

Какие метрики должны быть в A/B-тесте бота и что такое guardrail?

Метрики делятся на три уровня. Primary (целевая) — главная, по которой считаем результат, например конверсия в платёж. Guardrail (контролирующая) — что не должно ухудшиться: retention D7, CSAT, доля отказов на оплате. Secondary (вспомогательные) — глубина квиза, время на шаг, open rate. Если primary вырос, но guardrail упал — тест провален. Например, выросла конверсия в заявку, но упал retention — значит, давили слабых клиентов и собрали лиды, которые не платят. Принимать решение только по primary — путь к деградации продукта. Primary формулируется одна заранее и не меняется по ходу теста.

Что такое peeking, SRM и почему нельзя останавливать тест рано?

Peeking — подглядывание в результаты по ходу теста при фиксированном alpha=0.05. Чем больше проверок, тем выше реальный false positive rate: после 5 подглядываний — около 14% вместо 5%. Решение — фиксировать sample size заранее и не смотреть до его достижения, либо использовать sequential-методики (mSPRT, alpha-spending). SRM (Sample Ratio Mismatch) — расхождение фактического распределения с заявленным (например, 5200 vs 4800 при назначении 50/50). Проверяется хи-квадратом, p менее 0.001 = тест не валиден, чините инфру (бот падает на одном варианте, кеш отдаёт старое, sticky сломан).

Какие платформы для A/B-тестов подходят Telegram-боту?

Готовых платформ специально для Telegram нет — все универсальные. Statsig — SaaS с freemium до 1M events, есть feature flags, bayesian, sequential. GrowthBook — open-source, можно self-host рядом с PostgreSQL. Eppo — enterprise warehouse-native. Amplitude Experiment — если уже используете Amplitude. Optimizely — крупный B2C веб. Для маленького бота с 1–3 тестами достаточно самописного разбиения на Python + хранения exposure/events в PostgreSQL + SQL-отчётов в Metabase. По мере роста — переход на GrowthBook self-host или Statsig. Помните про 152-ФЗ: в SaaS-аналитику отдавайте обезличенные bucket id, а связку с user_id держите у себя.