7 шагов к конвейеру синтетических данных для тестирования

Что такое конвейер синтетических данных?

Конвейер синтетических данных – это процесс генерации искусственных тестовых данных. Они повторяют статистические свойства боевых: распределения, пропорции, корреляции и редкие случаи, – но не содержат персональных данных пользователей. Результат конвейера – синтетические данные, которые ведут себя как реальные, но при этом без юридических рисков.


Наш ведущий эксперт Олег написал цикл из трёх статей, где пошагово показывает, как построить конвейер для обезличенных данных в промышленных объёмах и убедиться, что всё работает корректно. В первой части разобрались, где заканчивается маскирование и начинается синтез, и почему граница между ними не техническая, а правовая.

sinteticheskie-dannye-vmesto-boevyh
Теперь переходим к практике: от минимизации хаоса в тестовых данных до автоматизации за 7 шагов. Порядок действий здесь важен: пропустите первый – развалится третий и т.д. Это проверенный на практике план, поэтому лучше придерживаться указанной методологии.

Шаг 1. Инвентаризация и профилирование: этап, который вечно хотят пропустить

Кажется, понять, что лежит в базе, проще простого. Но если посмотреть на реальные данные, а не на названия колонок, картина оказывается другой. На каждом проекте я повторяю одно и то же: классифицировать поля нужно по содержимому, а не по именам. В колонке field_17 в старой таблице легко может оказаться что-то вроде паспортных данных, тогда как колонка с красивым именем passport пуста с 2019 года. Автоматический классификатор, который смотрит на заголовки, найдет passport и пропустит field_17, а отвечать за утечку придется именно за field_17.

Для каждого поля нужно выяснить: какой там тип данных, сколько пустых значений – и отдельно пустых строк. Это не одно и то же, и разница между ними ломает код чаще, чем кажется. Ещё считаем количество уникальных значений и полезно собрать топ-20 из них, чтобы посмотреть их суммарную долю – так сразу видны перекосы в распределении. Вместо средней длины строки нагляднее строить гистограмму распределения длин.

Что почти всегда всплывает и портит планы?

testovye-dannye-dlya-testirovaniya
Поля-помойки, где лежит и телефон, и адрес, и переписка с юристом. Справочники, которые внезапно оказываются ПД: к примеру, контрагенты, где половина записей – ИП, а ИП это физлицо с ФИО в названии. И косвенные идентификаторы: номер офиса, дата открытия счёта и сумма первого взноса (в небольшом городе этого достаточно, чтобы найти конкретного человека).

Профилирование делаем внутри защищённого контура. Наружу выносим только агрегаты. Никаких «выгрузим немного, посмотрим глазами» – с этой фразы часто начинается утечка тестовых данных.

Инструменты: ydata-profiling для быстрого старта, Great Expectations или Soda для повторяемых проверок, обычный SQL на ClickHouse для больших таблиц – он быстрее любой библиотеки. Для поиска персональных данных из коробки – «Гарда Data Masking» с машинным обучением: на старых схемах с полями f_01…f_88 это экономит недели.

Метрика готовности к следующему шагу – доля полей с подтверждённой классификацией. Если меньше 95%, дальше идти бессмысленно: вы просто перенесёте неизвестность на следующий этап и найдёте её в проде. Да, скучно. Да, две-три недели. Обойти нельзя – я пробовал трижды.

Шаг 2. Зачем нужен контракт на данные и что в нём должно быть?

Дальше то, что вас защитит от того, чтобы проект не превратился в бесконечную переписку. Нужен документ – контракт на данные, который отвечает на вопрос: какой датасет мы считаем правильным. Пишет его потребитель, а не тот, кто генерирует. Это принципиально: если контракт пишет генератор, он напишет то, что умеет. Формат – YAML в репозитории рядом с кодом, чтобы версионировался и проходил ревью. 

# contracts/acquiring_load.yaml
dataset: acquiring_load_4tb
purpose: performance          # одна из четырёх задач, не «универсальный»
owner: perf-team
source_profile: profiles/acquiring@2026-07-14.json

volume:
  rows: 1_300_000_000
  size_tb: 4.1
  tolerance: 0.03

must_preserve:
  cardinality:
    merchant_id: { value: 41_000, tol: 0.05 }
    card_token:  { value: 78_000_000, tol: 0.05 }
  skew:
    merchant_id: { gini: 0.81, tol: 0.03 }
  partitions:
    last_90d_share: { value: 0.70, tol: 0.02 }
  nulls:
    mcc: { value: 0.031, tol: 0.005 }
  string_length:
    merchant_name: { mean: 34.2, p95: 61, tol: 0.10 }

must_not_contain: [ real_pan, real_msisdn, real_fio, real_contract_no, real_inn ]

edge_cases:                  # ссылки на каталог патологий, см. шаг 6
  include: [ EC-014, EC-021, EC-088, EC-102, EC-137 ]
  min_occurrences: 200

privacy_gates:
  exact_row_match: 0
  dcr_p5: ">= holdout"
  mia_auc: "<= 0.55"

reproducibility:
  seed: 20260901
  generator_version: "2.3.1"

lifecycle:
  ttl_days: 14
  storage_class: restricted-test
  

Выглядит как бюрократия, а работает как страховка: через полгода на вопрос, почему у вас сорок одна тысяча мерчантов, а не двенадцать, ответит конкретный файл, а не уволившийся коллега по памяти. Три поля, которые все норовят выкинуть и потом жалеют: seed, ttl_days и min_occurrences.

Шаг 3. Как подобрать генератор для каждого типа данных?

Главное не то, какой генератор выбрать. Правильный вопрос: какой из генераторов подходит каждой группе полей. В типичной корпоративной базе примерно 80% данных спокойно генерируются правилами, ещё 15% требуют статистики, и только оставшиеся 5% – те, ради которых и нужна нейросеть.

Разбивка по группам полей
Что генерируем Чем Почему именно так Инструменты
Первичные и внешние ключи, токены Детерминированные функции: HMAC-SHA256, формат-сохраняющее шифрование FF1 Нужна воспроизводимость и согласованность между системами. Нейросеть здесь не нужна и опасна: она может «запомнить» реальный идентификатор pgcrypto, библиотеки FF1, собственный сервис идентичности
Форматные атрибуты РФ: ИНН, СНИЛС, БИК, номер карты Правила с корректными контрольными разрядами Валидаторы на входе отбракуют всё остальное, и вы будете тестировать не систему, а свой генератор Собственные функции, Faker(ru_RU), Mimesis — с обязательной доработкой
Числовые и категориальные поля с зависимостями Гауссовы копулы, CTGAN, TVAE Ловят совместное распределение и корреляции. Копула часто не хуже GAN и в десятки раз быстрее — начинайте с неё SDV (GaussianCopula, CTGAN, TVAE), synthpop в R
Цепочки событий: транзакции, сессии, статусы заявок PAR из SDV, DoppelGANger, либо марковская цепь + распределение интервалов Порядок и интервалы важнее значений. В восьми случаях из десяти марковской модели достаточно, а стоит она в пятьдесят раз дешевле SDV PARSynthesizer, свои реализации на NumPy
Свободный текст: обращения, комментарии, протоколы Локальная LLM с жёсткими ограничителями и пост-фильтрами Единственное место, где без нейросети правда никак. И единственное, где реален риск дословного запоминания Self-hosted модели с русским языком; для деидентификации — NER: Natasha, DeepPavlov, Presidio
Ссылочная структура между таблицами Обход графа FK сверху вниз, генерация родителей раньше детей Многотабличные синтезаторы формально не гарантируют целостность — это известная проблема, проверяйте руками SDV HMASynthesizer + собственные проверки, K2view и подобные — в коммерческом сегменте
Справочники: ОКВЭД, регионы, валюты, МСС Копируем как есть Это не персональные данные. Синтезировать их — вредительство: сломаете join и потратите неделю на отладку

Разбивка по группам полей. Самая частая ошибка на этом шаге — пытаться прогнать через нейросеть всю таблицу целиком, включая ключи и справочники. Получается дорого, медленно и с битой целостностью.

Шаг 4. Чем кормить генератор, чтобы синтетика была похожа на реальные данные?

Правило одно: модель учится там же, где живут исходные данные. Тот же контур, тот же уровень защиты, те же журналы доступа. Наружу выходит только готовая модель – и только после проверки на утечку. Дальше 4 вещи, о которых редко пишут, хотя именно они всё решают.

  1. Размер обучающей выборки. Для табличной модели хватает 1-5 млн строк. Больше – модель дольше учится и легче запоминает конкретные записи. Казалось бы, чем больше данных, тем лучше, но здесь всё наоборот.
  2. Дедупликация обязательна. Дубликаты и почти-дубликаты – главная причина, по которой модель запоминает данные вместо того, чтобы их обобщать. Используем MinHash или SimHash, порог схожести 0,8, всё лишнее убираем. На одном проекте только это снизило долю протекающих записей в 10 раз.
  3. Канарейки – дешёвый и очень полезный приём. Перед обучением вшиваем в выборку 300–1000 искусственных записей с редкими сочетаниями: необычная фамилия, редкий диагноз, точная дата. После генерации ищем их в результате. Нашли хоть одну – модель течёт. Не нашли – гарантии нет, но зато есть дешёвый сигнал, который можно запускать в CI каждый день.
  4. Дифференциальная приватность. Честно: для внутреннего контура обычно избыточна. При ε ≤ 1 хвосты распределения вымываются, а именно ради хвостов всё и затевалось. Ещё один неприятный эффект: на синтетике с дифференциальной приватностью статистические тесты массово выдают ложноположительные результаты – аналитик принимает добавленный шум за значимый эффект. Если данные уходят за периметр, дифференциальная приватность нужна. Если остаются внутри – почти наверняка обойдётесь без неё.

Шаг 5. Терабайты синтетики:  двухконтурная схема, без которой ничего не получится

Смотрите, в чём суть. Нейросеть даёт структуру, а не объём. Если попытаться прогнать миллиард строк напрямую через CTGAN, выйдет как возить щебень на велосипеде: вроде технически можно, но смысла нет. Вот цифры для примера.

Глубокая табличная модель на нормальной видеокарте делает десятки тысяч строк в секунду. Значит, миллиард – это несколько дней непрерывной работы, и всё это время видеокарта занята. А копула или обычный сэмплер на Spark и ClickHouse выдают миллионы строк в секунду с одной машины, и масштабируются они линейно. Разница – три-четыре порядка. Поэтому нужно два контура: первый, медленный, учит модель; второй, быстрый, размножает результат по требованию.

КОНТУР А — медленный. Раз в квартал или при дрейфе данных. Внутри защищённого периметра
Срез прода
1–5 млн строк
после дедупликации

Профилирование
кардинальность,
skew, NULL, длины

Обучение
копула / CTGAN /
TVAE / LLM

Эталонный корпус
5–20 млн строк
полная валидация

СЛЕПОК
параметры распределений,
условные таблицы, словари,
хеш + версия

Наружу выходит только слепок — и только после прохождения ворот приватности

КОНТУР Б — быстрый. По кнопке, минуты. Обычный тестовый контур, никаких ПДн
Слепок + seed
+ контракт на данные

Правила и ключи
Каталог патологий
Справочники as is

РАЗМНОЖИТЕЛЬ
Spark / ClickHouse,
N воркеров по диапазонам

Перемешивание
Загрузка в СУБД
Ворота качества

4,1 ТБ
за 4 часа
на 8 воркерах

Теперь о самом размножителе. Тут главное – предсказуемость. Если заданы три параметра: слепок модели, seed и диапазон номеров строк, результат будет один и тот же всегда. За счёт этого каждый воркер генерирует свой кусок сам, без общего состояния и координации, в результате сходится побайтово.

-- ClickHouse: скелет размножителя. Zipf по мерчантам через словарь весов,
-- детерминированный PRNG от (seed, номер строки)
INSERT INTO tx_synth
SELECT
    cityHash64(:seed, n)                                  AS rid,
    dictGet('merchant_zipf', 'merchant_id',
            toUInt64(zipf_bucket(rid)))                   AS merchant_id,
    dictGet('card_pool', 'token',
            toUInt64(rid % 78000000))                     AS card_token,
    -- 70% объёма — в последние 90 дней, как в проде
    now() - toIntervalSecond(
        if(rid % 100 < 70, rid % 7776000, 7776000 + rid % 86400000)) AS ts,
    -- логнормаль по суммам + магнит на круглые значения
    round_magnet(exp(4.1 + 0.92 * randNormal(rid)))       AS amount,
    if(rid % 1000 < 31, NULL, dictGet('mcc', 'code', rid % 400)) AS mcc
FROM numbers_mt(:row_from, :row_count)

И три тонкости, которые я узнал через сломанный нагрузочный тест

  1. Физический порядок вставки. Сгенерируете данные по возрастанию ключа – и получите идеальную локальность: соседние строки похожи, страницы сжимаются гораздо лучше, индексы плотные. Четыре терабайта прода на стенде превратятся в полтора, всё уляжется в кэш, и нагрузочный тест покажет чудесные цифры. Но в проде данные приходят вперемешку. Поэтому перед загрузкой перемешивайте их, хотя бы блоками по несколько миллионов строк. Одна строчка кода – а на диске разница вдвое.
  2. Длина строк и доля пустых значений. Две скучные метрики, но обе работают. В проде merchant_name в среднем 34 символа, а генератор выдаёт 12 – таблица становится меньше в два с половиной раза. В проде 3,1% пустых значений в MCC, а у вас ноль – не словите ни одного падения на пустом поле и заодно получите другой размер строки в колоночном хранилище.
  3. Сколько вообще нужно терабайт. Спорно, но я настаиваю: большинству команд терабайты не нужны. Они нужны только нагрузочникам и тем, кто отлаживает планы запросов на больших таблицах. Остальным хватает правильного среза: 2–3% от прода с сохранением ссылочной целостности покрывают примерно 95% функциональных сценариев, генерируются за минуты и влезают на ноутбук. Когда каждая команда просит полную копию, это не потребность в данных, а недоверие к срезу. Лечится контрактом из шага 2, а не терабайтами.

Шаг 6. Что такое каталог патологий и зачем его вести?

Теперь самое важное во всей статье. Если дальше не осилите – прочитайте хотя бы этот подраздел.

Генеративная модель по своей природе сглаживает редкие случаи. Это не дефект реализации, а её работа: она учитывает плотность распределения, а нестандартные события по определению имеют низкую плотность. GAN вдобавок склонен схлопывать моды, VAE – размывать. Аналитику, которому нужны среднее и корреляции, это не мешает, а вот для тестировщика – это катастрофа, потому что баги живут именно в хвостах.

Ни один известный мне генератор сам не придумает клиента с 412 картами, платёж на одну копейку, ФИО длиной 78 символов и транзакцию ровно в момент перевода часов. А в проде всё это есть, и на каждом из этих случаев кто-то однажды упал. Отсюда гибрид: тело распределения даёт нейросеть, хвосты – отдельный каталог.
sinteticheskie-dannye-1

Каталог патологий – это структурированный список редких случаев: идентификатор, описание, ссылка на инцидент, целевая доля в датасете.

Источников четыре:

  1. Багтрекер за два-три года: выгрузите все дефекты, где в описании есть слова про данные, формат, пустое значение, длину или кодировку – это золотая жила.
  2. Постмортемы инцидентов прода: каждый инцидент, вызванный данными, обязан превратиться в запись каталога; это дисциплина, а не техника.
  3. Ограничения схемы: для каждого CHECK, NOT NULL и лимита длины нужны значения на границе и за ней.
  4. И наконец, сядьте и подумайте, как бы вы ломали свою систему, если бы вам за это платили.
Фрагмент каталога патологий
ID Класс Патология Что ловит
EC-014 ФИО Фамилия из одной буквы; фамилия 78 символов; двойная через дефис; отчество «оглы»/«кызы»; «ё» и «е» в одной записи; висячий пробел в конце Обрезка полей, сортировки, поиск, ширина в отчётах и печатных формах
EC-021 Даты и время 29 февраля; 31-е в 30-дневном месяце; дата рождения 1900 и завтрашняя; переход на летнее время в исторических данных до 2014; Камчатка (+12) и Калининград (+2) в одной таблице Расчёт возраста, дедлайнов, SLA, ночные джобы, оконные функции
EC-034 Суммы 0,01; максимально допустимая; отрицательная; четыре знака после запятой; ровно на пороге лимита; банковское округление половин Переполнения, округления, лимиты, сверки на копейку
EC-088 Кардинальность Клиент с нулём продуктов; клиент с 412 картами; мерчант с одной транзакцией за три года; мерчант с 30% всего трафика Пагинация, N+1, таймауты, планы запросов, деление на ноль в агрегатах
EC-102 Текст и кодировки Пустая строка против NULL; строка из одних пробелов; эмодзи; кавычки-ёлочки; неразрывный пробел; CRLF внутри поля; апостроф в «О’Коннор» Парсинг, экспорт в CSV, интеграции, экранирование, ширина в UI
EC-137 Дубли и грязь Полные дубли строк; два клиента с одинаковым ИНН; запись со всеми NULL кроме ключа; «висячая» ссылка на удалённый справочник Дедупликация, идемпотентность, отчёты, целостность витрин

Фрагмент каталога патологий. У зрелой команды таких записей 200–400. Это, а вовсе не обученная модель, — главный актив всего проекта: модель вы переучите за неделю, а каталог из реальных инцидентов за три года не купите ни за какие деньги.

Про доли – это важнее, чем кажется

Мой ориентир такой: на каталог уходит 0,3–1% строк датасета, но каждый класс патологий должен встретиться минимум 200 раз. Если сделать патологий 5%, они перестанут быть хвостом и испортят нагрузочный профиль. А если каждого класса по три штуки на миллиард строк – тест превращается в лотерею: сегодня запрос их зацепил, завтра нет, и полдня уходит на то, чтобы разобраться, почему сборка мигает.

Шаг 7. Что мешает синтетике сломать прод и зачем нужны три независимых проверки?

Датасет, не прошедший валидацию, не публикуется. Точка. Ворота стоят в CI, у каждых – жёсткий порог из контракта.

Приём, который экономит месяцы

Лучший тест пригодности данных – не статистика, а сравнение планов запросов. Возьмите топ-50 запросов из pg_stat_statements или query_log, прогоните EXPLAIN (ANALYZE, BUFFERS) на проде и на синтетике, сравните типы узлов и отношение оценочной кардинальности к фактической. Планы совпали – данные годные, дальше можно не мерить. Не совпали – значит, сломали ровно одно распределение, и по расхождению оценок видно, какое. Я нахожу этим способом за час то, что статистическими метриками искал бы неделю.

Ловушка DCR, в которую попадают почти все

Расстояние до ближайшей реальной записи часто показывают как доказательство приватности: мол, ни одна синтетическая строка не совпадает с настоящей. Но само по себе это ни о чём не говорит. Сравнивать надо так: отложите часть реальных данных, которую модель вообще не видела, и посчитайте DCR для неё и для синтетики. Если синтетика оказывается ближе к обучающей выборке, чем этот холдаут – модель просто запоминает. Именно за эту ошибку критикуют половину опубликованных работ по синтетическим данным. 

Вывод после второй части

Эта часть была самой плотной с точки зрения практики. Давайте выдохнем и зафиксируем главное. Контракт на данные – это фундамент и страховка. Без него размножитель превращается в рулетку: сегодня данные одни, завтра другие, воспроизвести не получается, воркеры мешают друг другу. Поэтому сначала контракт, потом всё остальное. Не наоборот.

ci-cd-testovye-dannye
Описанные шаги пропускать нельзя. Решили сэкономить и сразу направить нейросеть на прод – получите гладкое распределение без хвостов, а именно в хвостах живут баги. Решили пропустить валидацию – вот вам и датасет, который внешне похож на реальный, а на деле сломает половину планов запросов. Каждый шаг – это не рекомендация, а контрольная точка. 

В третьей части уже расскажу как всё это применять на реальных проектах. Поговорим о российской специфике, покажу 3 кейса из практики – где хорошая синтетика спасала прод, а плохая роняла его через 40 минут после релиза. Также вас будет ждать разбор инструментов и ошибок с прицелом на РФ. А ещё расчёт экономики для защиты бюджета и план на 90 дней по неделям. 

Если на то, чтобы воплотить всё описанное в статье не хватает своей экспертизы – мы можем зайти и выстроить весь конвейер под ключ: от профилирования и контракта на данные до размножителя, каталога патологий, учёта регуляторных требований, валидации в CI, доставки по кнопке и масштабирования до терабайт. Приходите – разберём ваш контур и прикинем, что и за какой срок реально сделать. Первая консультация бесплатно. 

Зачем вообще нужна синтетика, если есть маскирование?

Маскирование сохраняет структуру реальных данных, но искажает значения. Проблема в том, что искажение почти всегда ломает распределения. Плюс маскирование юридически не выводит данные из-под 152-ФЗ, пока есть ключ обратимости.

Что такое детерминированный размножитель?

Это функция от трёх аргументов: слепка распределений, seed и диапазона строк. Одинаковые аргументы – одинаковый результат побайтово. Это значит, что два воркера на разных машинах генерируют непересекающиеся куски без координации, а датасет воспроизводится хоть через год.

Сколько строк нужно для каталога патологий?

Около 0,3–1% от общего объёма, но не в абсолютных числах, а по классам. Каждый класс патологий должен встретиться минимум 200 раз, иначе тест стабилен на одной машине и падает на другой просто из-за того, что нужная комбинация не попала в батч.

Почему нельзя просто скопировать 2% прода, размножить и успокоиться?

Можно, если повезёт с выборкой. Но обычно не везёт: случайные 2% не сохраняют скошенность – 0,4% мерчантов, которые дают 61% трафика, могут вообще не попасть в выборку. Без профилирования вы не знаете, какие хвосты важны, а какие можно обрезать. Плюс копия прода – это копия ПДн, и тут вступает 152-ФЗ.

Подпишитесь на рассылку

Тренды и фишки из мира IT,
экспертные статьи и всё о тестировании.

Другие статьи
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
Email
guest
0 комментариев
Популярные
Новые Старые
Межтекстовые Отзывы
Посмотреть все комментарии
Об авторе
author

Редакция сайта

Поиск
Получите совет
Лаборатория Качества
Здравствуйте! Мы онлайн и готовы вам помочь!
79202240126
Quality_Lab_bot?start=officialsitelk