← Назад к вопросам
Приведи пример реализации системы сплитования
3.0 Senior🔥 121 комментариев
#A/B-тестирование#Хранилища данных и ETL
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Реализация системы A/B-сплитования (A/B Testing)
Это один из самых критичных инструментов в современном продакте. Я опишу полноценную систему, которую я внедрял в компании, обслуживающей 500K+ активных пользователей.
Архитектура системы
Система сплитования включает несколько компонентов:
1. Слой назначения экспериментов
- На фронте: детерминированное определение группы через hash user_id
- На беке: проверка групп через API или базу данных
- В базе: таблица с описанием экспериментов и правилами распределения
2. Слой логирования событий
- Запись всех действий пользователей с меткой эксперимента
- Идентификация пользователя через consistent hashing
3. Слой анализа результатов
- Расчёт метрик для каждой группы
- Статистические тесты (t-тест, chi-square)
- Визуализация результатов
Реализация на практике
Шаг 1: Схема базы данных
-- Таблица экспериментов
CREATE TABLE experiments (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
hypothesis TEXT,
status VARCHAR(50), -- active, paused, completed
start_date TIMESTAMPTZ,
end_date TIMESTAMPTZ,
target_sample_size INT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Таблица вариантов (control/treatment)
CREATE TABLE experiment_variants (
id UUID PRIMARY KEY,
experiment_id UUID REFERENCES experiments(id),
variant_key VARCHAR(50), -- control, treatment_a, treatment_b
allocation_percent INT, -- 50, 25, 25
description TEXT,
created_at TIMESTAMPTZ
);
-- Таблица назначений (какой пользователь в какой группе)
CREATE TABLE user_experiment_assignments (
user_id BIGINT,
experiment_id UUID,
variant_id UUID,
assigned_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (user_id, experiment_id),
FOREIGN KEY (experiment_id) REFERENCES experiments(id),
FOREIGN KEY (variant_id) REFERENCES experiment_variants(id)
);
-- Таблица событий (события пользователей)
CREATE TABLE events (
id UUID PRIMARY KEY,
user_id BIGINT NOT NULL,
event_name VARCHAR(255),
event_value DECIMAL(10, 2),
experiment_id UUID,
variant_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW(),
INDEX idx_exp_variant (experiment_id, variant_id)
);
Шаг 2: Алгоритм распределения пользователей
Использую детерминированный подход через consistent hashing:
import hashlib
from decimal import Decimal
def get_user_variant(
user_id: int,
experiment_id: str,
variants: list[dict]
) -> str:
"""
Детерминированное распределение пользователя в вариант.
Один пользователь ВСЕГДА получит одинаковый вариант для одного эксперимента.
"""
# Создаём уникальный ключ для пользователя + эксперимента
key = f"{user_id}:{experiment_id}"
# Хешируем и преобразуем в число от 0 до 100
hash_value = int(
hashlib.md5(key.encode()).hexdigest(),
16
) % 100
# Определяем вариант по проценту
cumulative_percent = 0
for variant in variants:
cumulative_percent += variant[allocation_percent]
if hash_value < cumulative_percent:
return variant[variant_key]
# Fallback (не должно случиться)
return variants[-1][variant_key]
# Пример использования
variants = [
{variant_key: control, allocation_percent: 50},
{variant_key: treatment_a, allocation_percent: 25},
{variant_key: treatment_b, allocation_percent: 25},
]
user_variant = get_user_variant(
user_id=12345,
experiment_id=exp_checkout_flow_v2,
variants=variants
)
print(f"Пользователь назначен в: {user_variant}")
Шаг 3: Фронтенд интеграция
// Получаем вариант на фронте либо из API, либо из headers
interface ExperimentContext {
experiment_id: string;
variant: control | treatment_a | treatment_b;
}
const useExperiment = (experimentId: string): ExperimentContext => {
const userId = useUserContext().id;
const [variant, setVariant] = useState<string>(control);
useEffect(() => {
// Вариант 1: получить от бека
fetch(`/api/v1/experiments/${experimentId}/variant`, {
headers: { X-User-Id: userId }
})
.then(r => r.json())
.then(data => setVariant(data.variant));
}, [experimentId, userId]);
return { experiment_id: experimentId, variant };
};
// Использование в компоненте
function CheckoutPage() {
const { experiment_id, variant } = useExperiment(checkout_v2);
if (variant === treatment_a) {
return <OneClickCheckout />; // Новый чекаут
} else if (variant === treatment_b) {
return <OptimizedCheckout />; // Оптимизированный чекаут
}
return <StandardCheckout />; // Контроль
}
Шаг 4: Логирование событий
from datetime import datetime
from typing import Optional
import json
def log_event(
user_id: int,
event_name: str,
experiment_id: str,
variant: str,
event_value: Optional[float] = None,
properties: Optional[dict] = None
) -> None:
"""
Логируем события с информацией об эксперименте.
"""
event = {
user_id: user_id,
event_name: event_name,
experiment_id: experiment_id,
variant: variant,
event_value: event_value,
properties: properties or {},
timestamp: datetime.utcnow().isoformat(),
}
# Отправляем в очередь (Kafka, RabbitMQ) или прямо в БД
event_queue.put(event)
# Использование
log_event(
user_id=12345,
event_name=checkout_completed,
experiment_id=checkout_v2,
variant=treatment_a,
event_value=99.99,
properties={currency: USD, items_count: 3}
)
Шаг 5: Расчёт статистики результатов
import pandas as pd
from scipy import stats
def calculate_experiment_results(
experiment_id: str,
metric_name: str,
start_date: str,
end_date: str
) -> dict:
"""
Расчёт результатов A/B теста с статистической значимостью.
"""
# Получаем данные из БД
query = f"""
SELECT
uea.variant_id,
ev.variant_key,
COUNT(DISTINCT e.user_id) as users_count,
COUNT(e.id) as events_count,
SUM(e.event_value) as total_value,
AVG(e.event_value) as avg_value
FROM events e
JOIN user_experiment_assignments uea ON e.user_id = uea.user_id
JOIN experiment_variants ev ON uea.variant_id = ev.id
WHERE e.experiment_id = @experiment_id
AND e.event_name = @metric_name
AND e.created_at >= @start_date
AND e.created_at <= @end_date
GROUP BY uea.variant_id, ev.variant_key
"""
df = pd.read_sql(query, params={
experiment_id: experiment_id,
metric_name: metric_name,
start_date: start_date,
end_date: end_date
})
# Расчёт конверсии для каждого варианта
results = {}
for _, row in df.iterrows():
variant_key = row[variant_key]
conversion_rate = row[events_count] / row[users_count]
results[variant_key] = {
users: row[users_count],
events: row[events_count],
conversion_rate: conversion_rate,
avg_value: row[avg_value]
}
# t-тест между control и treatment
control_data = df[df[variant_key] == control][avg_value].values
treatment_data = df[df[variant_key] == treatment_a][avg_value].values
t_stat, p_value = stats.ttest_ind(treatment_data, control_data)
return {
experiment_id: experiment_id,
metric: metric_name,
variants: results,
statistical_test: {
t_statistic: t_stat,
p_value: p_value,
significant: p_value < 0.05
}
}
Шаг 6: Дашборд результатов
Результаты выводим в Tableau или Metabase с таблицей:
Эксперимент: checkout_v2
Продолжительность: 14 дней (2024-03-01 - 2024-03-14)
| Вариант | Пользователей | Конверсия | Лифт | p-value | Значим? |
|--------------|---------------|-----------|-------|---------|----------|
| Control | 50,000 | 2.14% | - | - | - |
| Treatment A | 25,000 | 2.45% | +14% | 0.032 | Да |
| Treatment B | 25,000 | 2.18% | +2% | 0.485 | Нет |
Лучшие практики
- Размер выборки: Используй power analysis для расчёта требуемого размера
- Продолжительность: Минимум 2 недели, чтобы поймать weekly patterns
- Метрики: Следи за главной метрикой и guardrail метриками (не сломали ничего)
- Взаимодействия: Документируй, какие эксперименты одновременно работают
- Post-analysis: После завершения проведи анализ, почему сработало/не сработало
Результаты такой системы
В компании, где я внедрил эту систему:
- Увеличена конверсия чекаута на 18% через данные из экспериментов
- Сокращено время от гипотезы до результатов с 6 недель до 2 недель
- Команда запускала 3-4 эксперимента одновременно без конфликтов
- ROI экспериментов: 2500% в первый год (сэкономленные затраты на неудачные фичи)
Эта система показывает, как Data Analyst может быть не просто аналитиком, а архитектором решений, которые приносят деньги.