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
Метрика теста делится на три уровня:
- Primary (целевая) — главная, по которой считаем результат. Например, конверсия
/start→ заявка. Формулируется одна, заранее, и больше не меняется. - Guardrail (контролирующая) — что не должно ухудшиться. Например, retention D7, CSAT, среднее время до ответа оператора, доля отказов на этапе оплаты.
- 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 сходится к победителю, но не отвечает на вопрос «насколько».
Платформы
| Платформа | Тип | Цена | Когда брать |
|---|---|---|---|
| Statsig | SaaS | freemium до 1M events | Хочется готовое: feature flags + A/B + bayesian + sequential |
| GrowthBook | Open-source / SaaS | self-host бесплатно | Контроль данных, ставим рядом с PostgreSQL/ClickHouse |
| Eppo | SaaS | enterprise | Большая аналитическая команда, нужны warehouse-native эксперименты |
| Amplitude Experiment | SaaS | от $0, paid от 10K MTU | Уже используете Amplitude для аналитики |
| Optimizely | SaaS | enterprise | Веб-маркетинг + крупный B2C |
| Кастом на PostgreSQL | self | бесплатно | Маленький бот, 1–3 теста, понятная команда |
Готовых платформ A/B специально для Telegram-ботов нет — все универсальные. На практике в небольшом боте достаточно самописного разбиения + хранения exposure/events в PostgreSQL и анализа в SQL.
| Что тестируем | Тип теста | Метрика | Срок |
|---|---|---|---|
| Текст CTA | A/B/C | Конверсия в нажатие | 1–2 недели |
| Длина квиза | A/B | Конверсия в заявку + качество лида | 2–4 недели |
| Время рассылки | A/B/C/D | Open rate + CTR | 1 неделя |
| Цена / тариф | A/B | Конверсия в оплату + ARPU | 4+ недель |
| AI-промпт | bandit | CSAT / 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_idTelegram, телефон, 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 держите у себя.