Воспроизводимые пайплайны данных: от 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),
})

Как пишется

  1. Каталог событий первым. Пока нет схемы, генератор будет плодить импровизации. Опишите события и обязательные свойства — и генератор, и импорт-код, и тесты читают одну правду.
  2. Seed и только seed. Никаких «случайных» запусков: np.random.default_rng(seed) и фиксированные параметры. Изменение seed — осознанное изменение данных, а не шум.
  3. Разделяй генерацию и расчёт. Генератор пишет сырой датасет, пайплайн аналитики читает его. Один генератор → много отчётов с идентичными входами.
  4. Regression-тесты на метрики. Для каждого расчёта — инвариант: «сумма по сегментам = итог», «количество уникальных ≥ 0», «воронка монотонно не возрастает». Тест падает — значит, изменение данных или кода что-то сломало.
  5. 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

Ссылки

Смотрите также

← К статьям