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

Теперь переходим к практике: от минимизации хаоса в тестовых данных до автоматизации за 7 шагов. Порядок действий здесь важен: пропустите первый – развалится третий и т.д. Это проверенный на практике план, поэтому лучше придерживаться указанной методологии.
Шаг 1. Инвентаризация и профилирование: этап, который вечно хотят пропустить
Кажется, понять, что лежит в базе, проще простого. Но если посмотреть на реальные данные, а не на названия колонок, картина оказывается другой. На каждом проекте я повторяю одно и то же: классифицировать поля нужно по содержимому, а не по именам. В колонке field_17 в старой таблице легко может оказаться что-то вроде паспортных данных, тогда как колонка с красивым именем passport пуста с 2019 года. Автоматический классификатор, который смотрит на заголовки, найдет passport и пропустит field_17, а отвечать за утечку придется именно за field_17.
Для каждого поля нужно выяснить: какой там тип данных, сколько пустых значений – и отдельно пустых строк. Это не одно и то же, и разница между ними ломает код чаще, чем кажется. Ещё считаем количество уникальных значений и полезно собрать топ-20 из них, чтобы посмотреть их суммарную долю – так сразу видны перекосы в распределении. Вместо средней длины строки нагляднее строить гистограмму распределения длин.
Что почти всегда всплывает и портит планы?

Поля-помойки, где лежит и телефон, и адрес, и переписка с юристом. Справочники, которые внезапно оказываются ПД: к примеру, контрагенты, где половина записей – ИП, а ИП это физлицо с ФИО в названии. И косвенные идентификаторы: номер офиса, дата открытия счёта и сумма первого взноса (в небольшом городе этого достаточно, чтобы найти конкретного человека).
Профилирование делаем внутри защищённого контура. Наружу выносим только агрегаты. Никаких «выгрузим немного, посмотрим глазами» – с этой фразы часто начинается утечка тестовых данных.
Инструменты: 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% – те, ради которых и нужна нейросеть.
Шаг 4. Чем кормить генератор, чтобы синтетика была похожа на реальные данные?
Правило одно: модель учится там же, где живут исходные данные. Тот же контур, тот же уровень защиты, те же журналы доступа. Наружу выходит только готовая модель – и только после проверки на утечку. Дальше 4 вещи, о которых редко пишут, хотя именно они всё решают.
- Размер обучающей выборки. Для табличной модели хватает 1-5 млн строк. Больше – модель дольше учится и легче запоминает конкретные записи. Казалось бы, чем больше данных, тем лучше, но здесь всё наоборот.
- Дедупликация обязательна. Дубликаты и почти-дубликаты – главная причина, по которой модель запоминает данные вместо того, чтобы их обобщать. Используем MinHash или SimHash, порог схожести 0,8, всё лишнее убираем. На одном проекте только это снизило долю протекающих записей в 10 раз.
- Канарейки – дешёвый и очень полезный приём. Перед обучением вшиваем в выборку 300–1000 искусственных записей с редкими сочетаниями: необычная фамилия, редкий диагноз, точная дата. После генерации ищем их в результате. Нашли хоть одну – модель течёт. Не нашли – гарантии нет, но зато есть дешёвый сигнал, который можно запускать в CI каждый день.
- Дифференциальная приватность. Честно: для внутреннего контура обычно избыточна. При ε ≤ 1 хвосты распределения вымываются, а именно ради хвостов всё и затевалось. Ещё один неприятный эффект: на синтетике с дифференциальной приватностью статистические тесты массово выдают ложноположительные результаты – аналитик принимает добавленный шум за значимый эффект. Если данные уходят за периметр, дифференциальная приватность нужна. Если остаются внутри – почти наверняка обойдётесь без неё.
Шаг 5. Терабайты синтетики: двухконтурная схема, без которой ничего не получится
Смотрите, в чём суть. Нейросеть даёт структуру, а не объём. Если попытаться прогнать миллиард строк напрямую через CTGAN, выйдет как возить щебень на велосипеде: вроде технически можно, но смысла нет. Вот цифры для примера.
Глубокая табличная модель на нормальной видеокарте делает десятки тысяч строк в секунду. Значит, миллиард – это несколько дней непрерывной работы, и всё это время видеокарта занята. А копула или обычный сэмплер на Spark и ClickHouse выдают миллионы строк в секунду с одной машины, и масштабируются они линейно. Разница – три-четыре порядка. Поэтому нужно два контура: первый, медленный, учит модель; второй, быстрый, размножает результат по требованию.
после дедупликации
skew, NULL, длины
TVAE / LLM
полная валидация
условные таблицы, словари,
хеш + версия
N воркеров по диапазонам
на 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)
И три тонкости, которые я узнал через сломанный нагрузочный тест
- Физический порядок вставки. Сгенерируете данные по возрастанию ключа – и получите идеальную локальность: соседние строки похожи, страницы сжимаются гораздо лучше, индексы плотные. Четыре терабайта прода на стенде превратятся в полтора, всё уляжется в кэш, и нагрузочный тест покажет чудесные цифры. Но в проде данные приходят вперемешку. Поэтому перед загрузкой перемешивайте их, хотя бы блоками по несколько миллионов строк. Одна строчка кода – а на диске разница вдвое.
- Длина строк и доля пустых значений. Две скучные метрики, но обе работают. В проде merchant_name в среднем 34 символа, а генератор выдаёт 12 – таблица становится меньше в два с половиной раза. В проде 3,1% пустых значений в MCC, а у вас ноль – не словите ни одного падения на пустом поле и заодно получите другой размер строки в колоночном хранилище.
- Сколько вообще нужно терабайт. Спорно, но я настаиваю: большинству команд терабайты не нужны. Они нужны только нагрузочникам и тем, кто отлаживает планы запросов на больших таблицах. Остальным хватает правильного среза: 2–3% от прода с сохранением ссылочной целостности покрывают примерно 95% функциональных сценариев, генерируются за минуты и влезают на ноутбук. Когда каждая команда просит полную копию, это не потребность в данных, а недоверие к срезу. Лечится контрактом из шага 2, а не терабайтами.
Шаг 6. Что такое каталог патологий и зачем его вести?
Теперь самое важное во всей статье. Если дальше не осилите – прочитайте хотя бы этот подраздел.
Генеративная модель по своей природе сглаживает редкие случаи. Это не дефект реализации, а её работа: она учитывает плотность распределения, а нестандартные события по определению имеют низкую плотность. GAN вдобавок склонен схлопывать моды, VAE – размывать. Аналитику, которому нужны среднее и корреляции, это не мешает, а вот для тестировщика – это катастрофа, потому что баги живут именно в хвостах.
Ни один известный мне генератор сам не придумает клиента с 412 картами, платёж на одну копейку, ФИО длиной 78 символов и транзакцию ровно в момент перевода часов. А в проде всё это есть, и на каждом из этих случаев кто-то однажды упал. Отсюда гибрид: тело распределения даёт нейросеть, хвосты – отдельный каталог.

Каталог патологий – это структурированный список редких случаев: идентификатор, описание, ссылка на инцидент, целевая доля в датасете.
Источников четыре:
- Багтрекер за два-три года: выгрузите все дефекты, где в описании есть слова про данные, формат, пустое значение, длину или кодировку – это золотая жила.
- Постмортемы инцидентов прода: каждый инцидент, вызванный данными, обязан превратиться в запись каталога; это дисциплина, а не техника.
- Ограничения схемы: для каждого CHECK, NOT NULL и лимита длины нужны значения на границе и за ней.
- И наконец, сядьте и подумайте, как бы вы ломали свою систему, если бы вам за это платили.
Про доли – это важнее, чем кажется
Мой ориентир такой: на каталог уходит 0,3–1% строк датасета, но каждый класс патологий должен встретиться минимум 200 раз. Если сделать патологий 5%, они перестанут быть хвостом и испортят нагрузочный профиль. А если каждого класса по три штуки на миллиард строк – тест превращается в лотерею: сегодня запрос их зацепил, завтра нет, и полдня уходит на то, чтобы разобраться, почему сборка мигает.
Шаг 7. Что мешает синтетике сломать прод и зачем нужны три независимых проверки?
Датасет, не прошедший валидацию, не публикуется. Точка. Ворота стоят в CI, у каждых – жёсткий порог из контракта.
Приём, который экономит месяцы
Лучший тест пригодности данных – не статистика, а сравнение планов запросов. Возьмите топ-50 запросов из pg_stat_statements или query_log, прогоните EXPLAIN (ANALYZE, BUFFERS) на проде и на синтетике, сравните типы узлов и отношение оценочной кардинальности к фактической. Планы совпали – данные годные, дальше можно не мерить. Не совпали – значит, сломали ровно одно распределение, и по расхождению оценок видно, какое. Я нахожу этим способом за час то, что статистическими метриками искал бы неделю.
Ловушка DCR, в которую попадают почти все
Расстояние до ближайшей реальной записи часто показывают как доказательство приватности: мол, ни одна синтетическая строка не совпадает с настоящей. Но само по себе это ни о чём не говорит. Сравнивать надо так: отложите часть реальных данных, которую модель вообще не видела, и посчитайте DCR для неё и для синтетики. Если синтетика оказывается ближе к обучающей выборке, чем этот холдаут – модель просто запоминает. Именно за эту ошибку критикуют половину опубликованных работ по синтетическим данным.
Вывод после второй части
Эта часть была самой плотной с точки зрения практики. Давайте выдохнем и зафиксируем главное. Контракт на данные – это фундамент и страховка. Без него размножитель превращается в рулетку: сегодня данные одни, завтра другие, воспроизвести не получается, воркеры мешают друг другу. Поэтому сначала контракт, потом всё остальное. Не наоборот.

Описанные шаги пропускать нельзя. Решили сэкономить и сразу направить нейросеть на прод – получите гладкое распределение без хвостов, а именно в хвостах живут баги. Решили пропустить валидацию – вот вам и датасет, который внешне похож на реальный, а на деле сломает половину планов запросов. Каждый шаг – это не рекомендация, а контрольная точка.
В третьей части уже расскажу как всё это применять на реальных проектах. Поговорим о российской специфике, покажу 3 кейса из практики – где хорошая синтетика спасала прод, а плохая роняла его через 40 минут после релиза. Также вас будет ждать разбор инструментов и ошибок с прицелом на РФ. А ещё расчёт экономики для защиты бюджета и план на 90 дней по неделям.
Если на то, чтобы воплотить всё описанное в статье не хватает своей экспертизы – мы можем зайти и выстроить весь конвейер под ключ: от профилирования и контракта на данные до размножителя, каталога патологий, учёта регуляторных требований, валидации в CI, доставки по кнопке и масштабирования до терабайт. Приходите – разберём ваш контур и прикинем, что и за какой срок реально сделать. Первая консультация бесплатно.
Зачем вообще нужна синтетика, если есть маскирование?
Маскирование сохраняет структуру реальных данных, но искажает значения. Проблема в том, что искажение почти всегда ломает распределения. Плюс маскирование юридически не выводит данные из-под 152-ФЗ, пока есть ключ обратимости.
Что такое детерминированный размножитель?
Это функция от трёх аргументов: слепка распределений, seed и диапазона строк. Одинаковые аргументы – одинаковый результат побайтово. Это значит, что два воркера на разных машинах генерируют непересекающиеся куски без координации, а датасет воспроизводится хоть через год.
Сколько строк нужно для каталога патологий?
Около 0,3–1% от общего объёма, но не в абсолютных числах, а по классам. Каждый класс патологий должен встретиться минимум 200 раз, иначе тест стабилен на одной машине и падает на другой просто из-за того, что нужная комбинация не попала в батч.
Почему нельзя просто скопировать 2% прода, размножить и успокоиться?
Можно, если повезёт с выборкой. Но обычно не везёт: случайные 2% не сохраняют скошенность – 0,4% мерчантов, которые дают 61% трафика, могут вообще не попасть в выборку. Без профилирования вы не знаете, какие хвосты важны, а какие можно обрезать. Плюс копия прода – это копия ПДн, и тут вступает 152-ФЗ.
Тренды и фишки из мира IT,
экспертные статьи и всё о тестировании.










