Перейти к основному содержимому

Генератор file

Когда пригодится — значения уже лежат в файле: справочник городов, выгрузка, CSV — и не хочется переносить их в конфиг руками. Атрибут src говорит генератору, откуда их читать, а ещё один атрибут, column, решает, простой это список или таблица.

Примеры вывода ниже иллюстративные — конкретные значения случайны и могут отличаться от версии к версии ядра; важны только форма и количество строк.

Один CSV, прочитанный дважды, по шесть строк.
  • Aисходный файл: четыре строки, три колонки
  • Bбез row= каждое поле выбирает свою строку, и запись собирается из кусков, которые никогда не были вместе (серые ячейки)
  • Cс row= все три поля читают одну строку, поэтому каждая запись — настоящая строка файла

Коротко

АтрибутОбязательныйЧто делает
srcдаГде файл — относительный путь, @data или абсолютный путь
columnнетЧитать одну колонку CSV, по имени или номеру (с 1) — включает CSV-режим
delimiterнетРазделитель ячеек в CSV-режиме — по умолчанию запятая
headerнетПропустить первую строку, когда колонка выбрана по номеру
rowнетСвязать несколько полей с одной CSV-строкой (запись остаётся цельной)

src — где файл

src обязателен. Это либо обычный путь, либо резолвер-источник:

srcКуда разрешается
src="names.txt"Рядом с файлом конфига .tdc
src="@data/names.txt"Ищется в папках, переданных через --data-path
src="/absolute/path/names.txt"Абсолютный путь
src="file:///absolute/path/names.txt"Тот же файл, записанный как URL

Файл читается как UTF-8. Если путь не удаётся разрешить, рендер завершается с ошибкой, а не молча выдаёт пустоту.

Два режима — список или CSV

Один и тот же src читает файл в одном из двух режимов, и режим выбирается не самим src, а тем, присутствует ли column:

  • без column — файл читается как простой список: каждая непустая строка это одно значение;
  • с column — файл читается как CSV, и значения берутся из указанной колонки.

Режим списка — одна строка, одно значение

Проблема. Нужен пул городов, но зашивать длинный список прямо в value="…" неудобно: его тяжело править и невозможно переиспользовать.

Инструмент. Кладём по одному значению на строку в файл — data/cities.txt:

Москва
Казань
Пермь
Омск
Тверь
<sequence name="City">
<gen type="file" src="@data/cities.txt"/>
</sequence>
...
<data>${{City}}</data>

Папку с данными передаём при запуске через --data-path:

./run example.tdc --data-path ./data

Результат. Строки выбираются равномерно случайно (с повторами — Пермь выпала дважды):

./run example.tdc --data-path ./data
Омск
Москва
Пермь
Пермь
Казань

Пустые строки в режиме списка пропускаются. Пустая ЯЧЕЙКА в колоночном режиме — это другое, и она отвергается: пропустить её значит убрать всю строку из пула, и пропорции самого файла перестанут быть пропорциями прогона. Заполните её, удалите строку или наведите column= на полную колонку. Чтобы получить строгий порядок файла вместо случайного выбора, добавьте order="sequential" — он выдаёт строки ровно в том порядке, в каком они идут в файле (см. order= / cycle=).

Дойдя до конца файла, начинает сначала

order="sequential" проходит файл и возвращается к первой строке, поэтому count больше длины файла задваивает строки — молча, без предупреждения. Для списка дней недели это и нужно; для файла с настоящими записями это дубликаты в выводе, на которые никто не укажет.

Поставьте cycle="false", если исчерпание строк должно останавливать прогон, а не повторять его. Он назовёт строку, на которой файл кончился, и ничего не запишет.

Режим CSV — значение из колонки

Проблема. В файле не один столбец, а таблица, и нужен всего один столбец — например только адреса. Пусть data/users.csv:

first_name,last_name,email,city
Анна,Орлова,anna.orlova@example.com,Москва
Борис,Петров,boris.petrov@example.com,Казань
Вера,Сидорова,vera.sidorova@example.com,Пермь
Григорий,Иванов,grigori.ivanov@example.com,Омск

Инструмент. Тот же src плюс column — само его наличие переключает генератор в CSV-режим:

<sequence name="Email">
<gen type="file" src="@data/users.csv" column="email"/>
</sequence>
...
<data>${{Email}}</data>

Результат. Первая строка считается заголовком и в вывод не попадает; значения берутся только из колонки email:

./run example.tdc --data-path ./data
anna.orlova@example.com
grigori.ivanov@example.com
anna.orlova@example.com
boris.petrov@example.com
anna.orlova@example.com

column — по имени или по номеру

Само наличие column превращает файл из списка «строка = значение» в CSV. Без него генератор взял бы строку целиком (Анна,Орлова,anna.orlova@example.com,Москва) как одно значение. Адресовать колонку можно двумя способами.

По имени

Имя из строки-заголовка. Строка-заголовок отбрасывается автоматически, значения начинаются со второй строки:

<gen type="file" src="@data/users.csv" column="email"/>
./run example.tdc --data-path ./data
anna.orlova@example.com
grigori.ivanov@example.com
anna.orlova@example.com
boris.petrov@example.com
anna.orlova@example.com

По номеру (с 1)

Вместо имени задайте номер, начиная с единицы. column="2" — это второй столбец (last_name): нумерация с единицы, поэтому первый столбец это column="1", а не column="0". При адресации по номеру у TDC нет имён колонок, чтобы их распознать, поэтому добавьте header="true" (принимаются также "1" и "0"), чтобы пропустить строку-заголовок:

<gen type="file" src="@data/users.csv" column="2" header="true"/>
./run example.tdc --data-path ./data
Сидорова
Петров
Петров
Орлова
Орлова

Тот же файл с column="3" читает третий столбец, email — данные те же, что и при column="email", просто адресуемся по позиции. Нужен header="true" по той же причине, что и для column="2": при обращении по номеру заголовок распознать не по чему, и без него само слово email попадёт в выборку как значение.

<gen type="file" src="@data/users.csv" column="3" header="true"/>
./run example.tdc (column="3" header="true")
vera.sidorova@example.com
boris.petrov@example.com
boris.petrov@example.com
anna.orlova@example.com
anna.orlova@example.com

Крайние случаи

  • column="0" не является номером (нумерация с 1) — он трактуется как буквальное имя 0, которого нет в заголовке, поэтому получаете error[TDC062]: file generator: CSV column "0" was not found in the header row.
  • Номер за пределами последней колонки (column="9" на файле из четырёх колонок) падает с error[TDC062]: file generator: CSV column "9" is past the last column — the file has 4.
  • Если разделитель файла не запятая, задайте delimiter — иначе вся строка попадёт в одну ячейку и колонка не найдётся.

delimiter — чем разделены ячейки

Проблема. Не каждая таблица разделена запятой. Выгрузки из Excel часто идут через точку с запятой или табуляцию. Не скажете TDC ничего — он разобьёт строку по запятым, не найдёт ячеек и посчитает всю строку одним полем, так что колонка не найдётся.

delimiter принимает либо один символ (delimiter=";"), либо один из алиасов-имён:

ЗначениеРазделитель
commaзапятая , (умолчание)
semicolonточка с запятой ;
pipeвертикальная черта
tabтабуляция (TSV-файлы)
\tтабуляция (то же, что tab)

Для TSV-файла (колонки разделены табуляцией) delimiter="tab" и delimiter="\t" равнозначны — оба читают табуляцию как разделитель.

Точка с запятой — частый случай

Возьмём тех же пользователей, но с разделителем ;data/users_semicolon.csv:

first_name;last_name;email;city
Анна;Орлова;anna.orlova@example.com;Москва
Борис;Петров;boris.petrov@example.com;Казань
Вера;Сидорова;vera.sidorova@example.com;Пермь
Григорий;Иванов;grigori.ivanov@example.com;Омск

Без delimiter (подразумевается запятая) вся строка становится одной ячейкой, и колонка email не находится:

<gen type="file" src="@data/users_semicolon.csv" column="email"/>
./run example.tdc --data-path ./data
error[TDC062]: file generator: CSV column "email" was not found in the header row
note: For CSV files, use a header name like column="email" or a 1-based index like column="2".

С delimiter="semicolon" (или delimiter=";") ячейки разбираются верно:

<gen type="file" src="@data/users_semicolon.csv" column="email" delimiter="semicolon"/>
./run example.tdc --data-path ./data
anna.orlova@example.com
grigori.ivanov@example.com
anna.orlova@example.com
boris.petrov@example.com

Вертикальная черта

Файл, где колонки разделены |, читается через delimiter="pipe":

<gen type="file" src="@data/users_pipe.csv" column="email" delimiter="pipe"/>
./run example.tdc --data-path ./data
boris.petrov@example.com
boris.petrov@example.com
anna.orlova@example.com

Табуляция (TSV)

Файл с разделителем-табуляцией читается через delimiter="tab" (или delimiter="\t"):

<gen type="file" src="@data/users.tsv" column="email" delimiter="tab"/>
./run example.tdc --data-path ./data
anna.orlova@example.com
boris.petrov@example.com
anna.orlova@example.com

header — пропустить заголовок для числовой колонки

Проблема. В CSV первая строка обычно это заголовок (first_name,last_name,…). Когда вы выбираете колонку по имени, TDC знает, что первая строка — заголовок, и отбрасывает её. Но при выборе по номеру имён колонок неоткуда взять — TDC не может отличить заголовок от данных, поэтому по умолчанию берёт всё, включая строку заголовка, и мусор вроде first_name протекает в вывод.

header принимает true или false (по умолчанию false). Он влияет только на числовую column.

Без header — ячейка заголовка first_name считается обычным значением и показывается в выводе:

<gen type="file" src="@data/users.csv" column="1"/>
./run example.tdc --data-path ./data
Борис
Борис
Борис
first_name
first_name

С header="true" — первая строка отбрасывается, остаются только настоящие значения:

<gen type="file" src="@data/users.csv" column="1" header="true"/>
./run example.tdc --data-path ./data
Вера
Борис
Борис
Анна
Анна

Когда header не нужен

Для колонки, выбранной по имени (column="email"), header="true" не нужен никогда: именованная колонка всегда ищется в первой строке, а данные читаются начиная со второй. header важен только для числовой column.

row — держать запись вместе

Проблема. Несколько полей должны браться из одной и той же строки CSV. Без row каждый генератор file выбирает независимо, и запись рассыпается: имя из одной строки, фамилия из другой, город из третьей.

row принимает любой непустой ключ, например row="user". Все генераторы type="file" с одинаковым row — тем же src, тем же delimiter и тем же режимом header — читают одну и ту же строку для каждой записи. На запись выбирается одна строка, а разные значения column читают из неё разные ячейки. Ключ над двумя разными файлами check отклоняет: связка — это строка ОДНОГО файла, поэтому строки, принадлежащей обоим, не существует.

Без row — три независимых генератора, поэтому записи не сходятся (Вера с фамилией Петров из Москвы, Борис — с фамилией Сидорова):

<sequence name="User">
<gen name="First" type="file" src="@data/users.csv" column="first_name"/>
<gen name="Last" type="file" src="@data/users.csv" column="last_name"/>
<gen name="City" type="file" src="@data/users.csv" column="city"/>
</sequence>
...
<data>${{User.First}} ${{User.Last}} — ${{User.City}}</data>
./run example.tdc --data-path ./data
Вера Петров — Москва
Анна Орлова — Казань
Борис Сидорова — Омск
Борис Иванов — Казань
Борис Петров — Омск

С row="user" — все три поля берутся из одной строки, поэтому каждая запись согласована:

<sequence name="User">
<gen name="First" type="file" src="@data/users.csv" column="first_name" row="user"/>
<gen name="Last" type="file" src="@data/users.csv" column="last_name" row="user"/>
<gen name="City" type="file" src="@data/users.csv" column="city" row="user"/>
</sequence>
./run example.tdc --data-path ./data
Вера Сидорова — Пермь
Григорий Иванов — Омск
Борис Петров — Казань
Борис Петров — Казань
Анна Орлова — Москва

Теперь first_name, last_name и city всегда берутся из одной CSV-строки — поля не могут разъехаться. Это работает на любом движке (по умолчанию — потоковый, так что память не растёт с числом строк).

Взвешенные строки — row + weight

По умолчанию связанная строка выбирается равномерно. Добавьте weight="колонка" на одно поле группы — и строку выберет взвешенный жребий по этой колонке (точно, как percent), а остальные поля по-прежнему возьмут значения из той же выбранной строки. Тогда товар выпадает по своей настоящей частоте продаж, а его цена и категория берутся с его же строки — data/catalog.csv:

name,category,price,sales
Ручка,Канцелярия,1.10,500
Кофе,Напитки,4.50,1200
Рюкзак,Сумки,45.00,80
<sequence name="Item">
<gen name="Name" type="file" src="@data/catalog.csv" column="name" row="i" weight="sales"/>
<gen name="Price" type="file" src="@data/catalog.csv" column="price" row="i"/>
<gen name="Cat" type="file" src="@data/catalog.csv" column="category" row="i"/>
</sequence>
...
<data>${{Item.Name}} | ${{Item.Cat}} | ${{Item.Price}}</data>
./run example.tdc --data-path ./data
Кофе | Напитки | 4.50
Ручка | Канцелярия | 1.10
Кофе | Напитки | 4.50

Про движок. Без weight связанная группа работает на любом движке. С weight такой конфиг всегда считает движок в памяти: потоковый не может взвесить выбор строки, не зная сначала итогов по файлу. Если принудительно задать --engine 2, TDC честно скажет об этом, а не выдаст молча несвязные колонки. Цена этому — память, которая теперь растёт вместе с count; см. Какой движок считает ваш конфиг. Про сами связанные группы — в Согласованные и связанные данные.

Ограничения (v1)

  • row работает только внутри <sequence>. В блоке вывода генераторов нет вовсе, поэтому вопрос там не возникает.
  • row требует column — это возможность для CSV, а не для простого текстового списка.
  • Один и тот же ключ row с разными источниками не связывает их между собой: TDC заводит отдельную группу строк на каждое сочетание источника, разделителя и режима заголовка. Это не ошибка, просто стоит иметь в виду.

read="quantile" — измеренная выборка как распределение

Всё, что выше, читает файл как мешок значений: возьми одно, положи в ячейку. Для считаемых вещей это ровно то, что нужно — город, статус, число заказов, — а weight= ещё и соблюдает их доли до строки.

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

read="quantile" читает тот же файл иначе: сортирует один раз и смотрит на него как на линейку. Строка попадает в любую точку линейки, а если между двумя наблюдениями — берёт значение между ними.

amounts.txt
23.10
25.40
25.40
31.00
40.75
<sequence name="Amount"><gen type="file" src="amounts.txt" read="quantile"/></sequence>

Померено на выборке из 951 суммы, 100 000 строк:

источникread="quantile"обычный выбор
10-й процентиль25.2525.1325.12
медиана53.3052.9853.64
99-й процентиль227.66227.39231.52
разных значений95115 083951

Из такого чтения следуют три вещи, и каждая — причина предпочесть его для измеряемой величины:

  • Разрешение садится туда, где масса, а не размазывается по диапазону. Хвост длиной в шесть порядков стоит столько же точек, сколько узкий горб: где детали, решает плотность самого файла.
  • Повторяющееся значение остаётся атомом. 25.40 дважды в выборке выше — это плоская полка на линейке, и она держит ровно свою долю прогона, пока всё вокруг остаётся непрерывным. Дискретное и непрерывное живут в одном файле.
  • За пределами выборки ничего не выдумывается. Диапазон вывода — ровно наблюдённый: выборка не может отвечать за значения, которых не видела.

Точность задаёт ваш файл, а не догадка. Между 31 и 40 интерполяция даёт 35.4 — это верно для денег и неверно для числа заказов. Поэтому ответ пишется с тем же числом знаков после запятой, что и источник: целая выборка даёт целые числа, выборка в копейках — копейки. decimals это переопределяет.

Чего квантильное чтение НЕ обещает

Оно воспроизводит форму, а не долю каждого отдельного значения. Интерполяция забирает массу у наблюдённых точек и отдаёт её значениям между ними — в этом и состоит лечение гребёнки, и без этой платы его не бывает. На выборке из двадцати целых значение 46 (одно из двадцати, то есть 5% выборки) выходит в 0.4% строк; остальное ушло к 47, 48 и 49 — их выборка не видела, но непрерывная величина, за которую она отвечает, их безусловно содержит.

Решает РАССТОЯНИЕ до соседнего наблюдения, а не то, сколько раз значение повторяется. Померено на восьми наблюдениях, каждое из которых встречается ровно один раз — четыре стоят через единицу, четыре далеко:

10 → 12.500% 11 → 12.500% 12 → 12.500% 13 → 6.481%
40 → 0% 80 → 0% 160 → 0% 320 → 0%

Каждому причитается 12.5%. Три, у которых сосед есть с обеих сторон, держат её целиком; 13 держит половину, потому что с одной стороны плотно, а с другой дыра; дальние четыре растворяются в значениях между ними. Большой атом выживает по той же причине: 40% ровно нулей возвращаются как 39.997%, потому что его полка много шире скатов по краям.

Отсюда проверка, которую можно сделать на своём файле, ничего не запуская:

  • Дыр нет на собственном шаге выборки — целые подряд, копейки подряд — и все доли выходят точными. Померено на целых 0…20 без единой дыры: худшее отклонение 0.0003 процентного пункта по всем 21 значению.
  • Дыры есть — воспроизводится форма, а доля отдельного значения растекается по дыре тем сильнее, чем дыра шире.

А если нужна точная доля каждого перечисленного значения — это weight= с его точной квотой. Квантильное чтение — для случая, когда выборка отвечает за нечто непрерывное.

sample="exact" — воспроизвести выборку без шума

По умолчанию каждая строка бросает свой жребий, поэтому доли гуляют настолько, насколько гуляет любая выборка. С sample="exact" жребий не бросается вообще: строка i берёт свою собственную точку на линейке, и за прогон точки покрывают её равномерно.

<sequence name="Amount">
<gen type="file" src="amounts.txt" read="quantile" sample="exact"/>
</sequence>

Тот же файл, те же 100 000 строк, худшее расхождение по 99 процентилям:

худшее расхождение
с жребием (по умолчанию)0.600%
sample="exact"0.024%

и оставшиеся 0.024% — это округление до копейки, а не шум выборки.

Колонка не выходит отсортированной — точки разбросаны той же перестановкой от сида, которой пользуется uniq, — и прогон остаётся воспроизводимым, потоковым и параллельным: все три движка дают одни и те же байты, а --jobs 7 равен --jobs 1.

Какое чтение выбрать

ваша колонкафайлчто писать
считаемое (город, статус, число заказов)value,countweight="count" — точная квота
измеряемое (деньги, вес, длительность)сырая выборка, по числу в строкеread="quantile", плюс sample="exact" чтобы убрать шум

read="quantile" не сочетается с weight=, row= и order="sequential" — каждое из них это свой способ читать тот же файл, и просить два сразу значит получить TDC297.

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