Воспроизводимые пайплайны данных: от seed до CI
Кратко
Вывод аналитика стоит ровно столько, сколько стоит его перепроверка. Если цифру нельзя получить повторно — запуском одного скрипта с тем же входом — она не цифра, а впечатление. Поэтому первый заказ аналитика — не график, а пайплайн, который перезапускается.
Три опоры воспроизводимости:
- Seeded-генератор — синтетические данные детерминированы (фиксированный seed), прогон за прогоном дают одно и то же.
- Типизированный каталог событий — единый источник правды о том, какие события и с какими свойствами существуют в продукте.
- Regression-тесты в CI — код и данные не могут «протухнуть» незаметно: изменение генератора, сломавшее метрику, останавливает сборку.
Пример
Типизированный каталог событий — схема, которая описывает событие, его свойства и обязательные поля:
# event_catalog.py — единый источник правды о событии
class Event(TypedDict):
name: str
user_id: str
ts: int # unix ms
properties: dict
EVENTS = {
"app_open": {"required": ["user_id"]},
"signup": {"required": ["user_id", "channel"]},
"trial_start": {"required": ["user_id", "plan"]},
"subscribe": {"required": ["user_id", "plan", "amount_usd"]},
}
Генератор с seed — одни и те же данные при каждом запуске:
import numpy as np
import pandas as pd
rng = np.random.default_rng(42) # детерминизм
n = 10_000
df = pd.DataFrame({
"user_id": np.arange(n),
"signup_month": rng.integers(0, 18, n),
"sessions_per_week": rng.poisson(3, n),
})
Как пишется
- Каталог событий первым. Пока нет схемы, генератор будет плодить импровизации. Опишите события и обязательные свойства — и генератор, и импорт-код, и тесты читают одну правду.
- Seed и только seed. Никаких «случайных» запусков:
np.random.default_rng(seed)и фиксированные параметры. Изменение seed — осознанное изменение данных, а не шум. - Разделяй генерацию и расчёт. Генератор пишет сырой датасет, пайплайн аналитики читает его. Один генератор → много отчётов с идентичными входами.
- Regression-тесты на метрики. Для каждого расчёта — инвариант: «сумма по сегментам = итог», «количество уникальных ≥ 0», «воронка монотонно не возрастает». Тест падает — значит, изменение данных или кода что-то сломало.
- CI как страж.
pytest+ линтер запускаются на каждый пуш. Пайплайн не может «потихоньку испортиться».
Как понять
Почему синтетика легитимна
Учебные и портфолио-данные почти всегда синтетические — и это ок, если всё честно подписано. Синтетика не замена реальным данным, но она отлично проверяет методологию: известный ground truth, контролируемый шум, повторяемые эксперименты. «Синтетика — методология боевая»: симуляция не делает метод правильным, но делает проверку метода возможной.
Каталог событий = контракт между командами
Когда в событии появляется поле, которого нет в каталоге, это либо новое свойство (обновляем каталог), либо баг импорта (ловим тестом). Каталог — это не документация «на потом», а тип, по которому пишут код.
Тесты на данные ≠ тесты на код
Код может быть синтаксически верным и давать неверные метрики. Поэтому тесты проверяют не функции, а инварианты данных: уникальность ключей, диапазоны, согласованность агрегатов. Это защита от регрессий, которые компилятор не видит.
Подсказки
- Один seed на весь датасет, не по одной строке — иначе корреляции между таблицами развалятся.
- Свойства событий не меняйте молча: ломка каталога = новая минорная версия.
- PII-поля скрабить на этапе генерации, а не в отчёте — меньше шансов утечки.
- Метрики считайте в SQL/пайплайне один раз, дашборд пусть «перекладывает», а не агрегирует заново.
- Regression-тест на каждую метрику, которую показываете вовне: если не тестируется — значит, может сломаться молча.
На практике
В проекте TaskFlow — PostHog Product Analytics Pipeline каталог событий — единый источник правды: типизированный словарь событий, который читают и генератор трафика, и capture/identify/group с PII-скрабингом, и анализ. CI (pytest + ruff) + Docker + render.yaml для деплоя. Аналитика читает те же события, что отправляет симулятор, — никакого расхождения между «данными» и «отчётом».
В Product Analytics + A/B on Supabase принцип доведён до конца: SQL-вьюхи считают воронку, когорты, MRR, DAU — дашборд на Streamlit «перекладывает» уже посчитанное, а не агрегирует на лету. Row Level Security изолирует данные по организациям на уровне БД.
На собеседовании
❓ Как вы обеспечиваете воспроизводимость аналитических расчётов?
Тремя вещами: seeded-генератор, который даёт одни и те же данные при каждом запуске; типизированный каталог событий как единый источник правды; regression-тесты на инварианты метрик, которые гоняются в CI. Если метрику нельзя пересчитать запуском скрипта с тем же входом — это не метрика, а впечатление.
— Nikita Boyarkin
❓ Зачем тестировать данные, а не только код?
Код может быть синтаксически корректным и давать неверные метрики: изменился генератор, поле стало nullable, воронка перестала быть монотонной. Тесты на инварианты данных — уникальность ключей, диапазоны, согласованность агрегатов — ловят регрессии, которые компилятор не видит.
— Nikita Boyarkin
Ссылки
- TaskFlow — PostHog Product Analytics Pipeline — каталог событий + симулятор + CI
- Product Analytics + A/B on Supabase — «дашборд перекладывает, не агрегирует» + RLS
- SQL Analytics Case Study — regression-тесты на SQL-инварианты