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

Блок <mix>

Когда использовать — когда доли в столбце не равны и каждая доля больше одного слова. Настоящие данные перекошены: большинство заказов — «оплачено», а «отменено» — единицы; большинство аккаунтов бесплатные, премиум — редкость. Нужен столбец, где варианты идут в фиксированной пропорции — не поровну и не случайным шумом.

<mix> — это распределение: именованный источник, который раскладывает свои варианты — ветки <case> — по строкам в точных процентах. По сути это <sequence>, значения которой распределены по percent, только каждая ветка может быть целым набором литералов и генераторов, а не одним простым значением. Раскладка детерминирована для заданного seed.

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

Заявленное против полученного на 1000 строках. Не «примерно» — количества попадают в заявленные доли точно.
  • проценты, написанные в конфиге
  • доля, которая получилась на самом деле

Именованное распределение

<mix> стоит прямо в <env>, рядом с <sequence> — обёртка не нужна, достаточно задать name. К значению обращаются через ${{Имя}}, как к любому другому именованному источнику:

<env count="100" seed="demo" inject="${{%}}">
<mix name="Code" percent="25,70">
<case><gen type="text" value="A"/></case>
<case><gen type="text" value="B"/></case>
<case><gen type="text" value="C"/></case>
</mix>
</env>
<block>
<line><data>${{Code}}</data></line>
</block>

Первые строки ничего не говорят — пропорции видны только на всей выборке:

./run code.tdc (первые 6 строк)
A
B
A
B
B
A

Посчитаем все 100 строк — раскрой точный:

./run code.tdc (count=100, с подсчётом)
A   25
B   70
C    5

Ровно 25 / 70 / 5. Маска задаёт первые две доли; третья ветка получает остаток, 100 − 25 − 70 = 5. Это не «примерно 25 %», а точная раскладка методом Хэмилтона (наибольших остатков) — тем же, что управляет percent у генератора text.

Поменяли проценты — поменялись доли

Тот же набор веток A/B/C, но percent="60,30":

<mix name="Code" percent="60,30">
<case><gen type="text" value="A"/></case>
<case><gen type="text" value="B"/></case>
<case><gen type="text" value="C"/></case>
</mix>
./run code.tdc (count=100, с подсчётом)
A   60
B   30
C   10

60 / 30, а остаток 10 уходит третьей ветке. Одно число в маске перекраивает весь столбец.

Когда нужен <mix>, а когда достаточно text

Для простого распределения готовых строк <mix> избыточен: генератор text уже раскладывает точные доли через percent.

<sequence name="Gender">
<gen type="text" value="Мужчина,Женщина" percent="50,50"/>
</sequence>
./run gender.tdc (count=20, с подсчётом)
Мужчина  10
Женщина  10

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

Атрибуты

АтрибутОбязательныйЧто делает
nameдаИмя для интерполяции через ${{Имя}}
percentнетДоля каждого <case>; опустите — получите равномерное деление
parentнетРодительская последовательность — доли считаются внутри её подмножества
flagнетДобавляет колонку-ответ, отмечающую ветку-выброс (см. ниже)
commentнетСвободная заметка для автора конфига; в вывод не попадает

В <mix> должна быть хотя бы одна <case> — одна ветка. Всё остальное необязательно.

percent — необязательный, и короткие маски

Опустите percent полностью — и ветки поделятся равномерно. Три кейса на 99 строк дают по 33 в каждом:

<mix name="Bucket">
<case><gen type="text" value="low"/></case>
<case><gen type="text" value="mid"/></case>
<case><gen type="text" value="high"/></case>
</mix>
./run bucket.tdc (count=99, с подсчётом)
low    33
mid    33
high   33

Если маску всё-таки задаёте, она подчиняется той же грамматике, что и percent у text: число фиксирует долю своей ветки, а каждая пустая позиция — голая запятая внутри маски ("25,,70") или хвостовая запятая ("25,70,") — делит остаток от 100 поровну. Оба примера в начале страницы уже опираются на это правило: percent="25,70" оставляет третьей ветке остаток.

count решает, какой строке достанется какая ветка

percent — это квота на весь прогон, а не монетка на каждую строку. Обещание полезное: просите 15% — получаете ровно 15%, а не «примерно, как повезёт». Но у него есть следствие, которое в первый раз удивляет, и лучше встретить его здесь, чем в своих данных.

<mix name="Temperature" percent="85">
<case><gen type="number" value="-5..5" decimals="2"/></case>
<case><gen type="number" value="[450..501],[650..701],[1000..1100]" decimals="2"/></case>
</mix>

Запустите с count="10", потом с count="100" и сравните первые десять строк:

./run temp.tdc — первые десять строк, один и тот же сид
count=10    -1.76  3.70    -0.44  0.96  499.88  0.21  2.18  2.20  -3.16  0.69
count=100   -1.76  468.42  -0.44  0.96  499.88  0.21  2.18  2.20  -3.16  0.69

Изменилась вторая строка. Больше ничего: -1.76, -0.44, 0.96, 0.21, 2.18 и 0.69 те же самые, и даже выброс в пятой строке остался на месте. Значения не поехали — поехала принадлежность строки к ветке. Число каждой строки привязано к самой строке и держится; меняется только то, какую ветку этой строке выдали.

Причина арифметическая. На десяти строках 15% — это полторы строки, а строка неделимая, поэтому квота выходит в один выброс. На ста строках это ровно пятнадцать. Разное число строк — разное число выбросов, а места для них берутся из перестановки по всему прогону, и у длинного прогона перестановка другая:

countстрок-выбросов15% от count
1011.5
2033.0
1001515.0
1000150150.0

Если нужно, чтобы строка не менялась при смене count, нужно решение по каждой строке отдельно — anomaly на генераторе решает построчно, поэтому первые десять строк одинаковы при любой длине прогона. Размен зеркальный: доля становится приблизительной, и на десяти строках выбросов может оказаться один или три.

Итого: <mix percent=> — когда нужна точная доля и вы готовите фиксированный набор данных. anomaly= — когда нужны устойчивые строки и вы поднимаете count, чтобы посмотреть, что дальше.

parent — распределение внутри подмножества

Задайте <mix> атрибут parent — и проценты считаются против отфильтрованного подмножества строк, а не всего count. Это то же правило, что управляет зависимой <sequence>. Здесь платные аккаунты получают разбивку по уровням, а бесплатные оставляют столбец пустым:

<sequence name="Segment">
<gen type="text" value="Free,Paid" percent="70,30"/>
</sequence>

<mix name="Tier" parent="Segment.Paid" percent="60,30">
<case><gen type="text" value="Silver"/></case>
<case><gen type="text" value="Gold"/></case>
<case><gen type="text" value="Platinum"/></case>
</mix>

При count="100" строк Paid получается 30. Маска 60 / 30 применяется к этим 30 строкам, поэтому Tier делится 18 / 9 / 3:

./run tier.tdc (только строки Paid, с подсчётом)
Silver     18
Gold        9
Platinum    3

18 + 9 + 3 = 30 — всё платное подмножество, а не процент от общей сотни. Это сердце иерархической модели; полный разбор, с вложенными уровнями, — в статье Иерархические зависимости.

<mix> вкладываются друг в друга

Внутри <case> может стоять вложенный <mix>, и вложенное деление считается против строк, выбравших внешнюю ветку — то же правило подмножества, на уровень глубже. Здесь треть всех строк — error, и внутри них внутренний <mix> градуирует тяжесть:

<mix name="Status" percent="34,33">
<case>
<gen type="text" value="ok"/>
</case>
<case>
<gen type="text" value="warn"/>
</case>
<case>
<mix percent="70,20">
<case><data>error/minor</data></case>
<case><data>error/major</data></case>
<case><data>error/fatal</data></case>
</mix>
</case>
</mix>

При count="100" внешнее деление — 34 ok / 33 warn / 33 error. Внутренняя маска 70 / 20 затем делит эти 33 строки error на 23 / 7 / 3:

./run status.tdc (count=100, с подсчётом)
ok            34
warn          33
error/minor   23
error/major    7
error/fatal    3

23 + 7 + 3 = 33 — ровно подмножество ошибок.

То же верно уровнем выше: <mix>, записанный внутри ветки <switch>, берёт квоту по строкам, которые эта ветка совпала, а не по всему прогону.

Доля меньше одной записи

У правила подмножества есть край, о котором лучше узнать заранее. Процент — это доля строк, которые доходят до ветки, а не вероятность, разыгрываемая для каждой строки. Когда эта доля оказывается меньше одной целой строки, ветка может дать значение, а может не дать ничего.

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

<env count="10" seed="demo" local="en">
<sequence name="Gender">
<gen type="text" value="Male,Female" percent="50,50"/>
</sequence>
<switch name="Diagnosis" on="Gender">
<case is="Male">
<mix percent="10,90">
<case><gen type="template" value="medical.diagnosisMale"/></case>
<case><gen type="template" value="medical.diagnosis"/></case>
</mix>
</case>
<case is="Female">
<mix percent="10,90">
<case><gen type="template" value="medical.diagnosisFemale"/></case>
<case><gen type="template" value="medical.diagnosis"/></case>
</mix>
</case>
</switch>
</env>
./run diagnosis.tdc (count=10, seed=demo)
Female,Coronary Artery Disease
Female,Tuberculosis
Male,Attention Deficit Hyperactivity Disorder
Male,Osteoarthritis
Male,Hypothyroidism
Male,Celiac Disease
Female,Anemia
Female,Postpartum Hemorrhage
Female,Tinnitus
Male,Obstructive Sleep Apnea

Женская половина сработала: Postpartum Hemorrhage в строке 8 пришёл из medical.diagnosisFemale. А из medical.diagnosisMale не пришло ничего — все пять мужских строк несут общие заболевания.

Данные верны, конфиг верен, и разбиение тоже верно. Посчитайте мужские строки: их пять. Десять процентов от пяти строк — это полстроки.

Доля меньше одной строки — это не малый шанс, а невидимый бросок монеты

percent="10" на подмножестве из пяти строк просит 0.5 записи. Половину записи движок выдать не может, поэтому выдаёт одну или ни одной, и решает это исключительно seed. Прогону выше не «не повезло». Оставьте конфиг и смените только seed — специфичный мужской диагноз появится примерно в половине прогонов.

Как только доля мала, умножьте её на число строк, которые до неё дойдут. Меньше 1 — и колонка становится броском монеты.

Монету бросают один раз, когда вы выбираете seed, а не заново на каждом прогоне. Конфиг выше будет возвращать те же десять записей всегда: пустая колонка останется пустой, и перезапуск ничего не докажет. Это работает детерминизм, ровно как обещано, — и раньше именно поэтому ловушку было трудно заметить. Вывод устойчив, воспроизводим и при этом не соответствует запрошенной доле.

Теперь check говорит об этом до прогона и сам считает арифметику — по разу на каждую ветку:

tdcv2 check diagnosis.tdc
warning[TDC251]: percent="10" over 5 rows asks for 0.5 records — the result is 0 or 1, and the seed decides which

Плавной середины здесь нет

Тот же конфиг, изменено одно число; замерено по 30 seed-ов на строку:

percentЗапрошено строк из 5Специфичных мужских диагнозов
50.250 при каждом seed
90.450 при каждом seed
100.50 или 1 — решает seed
110.551 при каждом seed
160.81 при каждом seed
201.01 при каждом seed

Ничто не нарастает постепенно. Ниже 10 % ветка не срабатывает никогда, выше 10 % срабатывает ровно один раз, и 10 % — единственное значение в этом промежутке, которое вообще колеблется.

Причина — в методе раздачи. Сначала каждая ветка забирает целые строки, которые покрывает её доля, затем оставшиеся строки уходят веткам с наибольшей дробной частью. При percent="11" доли равны 0.55 и 4.45, поэтому единственная остаточная строка достаётся первой ветке при любом seed. При percent="9" доли равны 0.45 и 4.55, и остаток уходит второй ветке — тоже при любом seed. И только при percent="10" обе дробные части равны 0.5, разделить их нечем, и ничья решается через seed.

Два способа получить определённость

Поднять долю — чтобы дробная часть выиграла без ничьей. Меняется один символ:

<mix percent="20,80">
./run diagnosis.tdc (count=10, seed=demo, percent=20,80)
Female,Coronary Artery Disease
Female,Tuberculosis
Male,Attention Deficit Hyperactivity Disorder
Male,Spermatocele
Male,Hypothyroidism
Male,Celiac Disease
Female,Anemia
Female,Postpartum Hemorrhage
Female,Tinnitus
Male,Obstructive Sleep Apnea

В строке 4 появился Spermatocele. Больше в колонке не сдвинулось ничего.

Либо поднять count, сохранив те же 10 %. При count="20" мужское подмножество держит десять строк, а 10 % от десяти — ровно одна:

./run diagnosis.tdc (count=20, seed=demo, только мужские строки)
Male,Osteoarthritis
Male,Hypothyroidism
Male,Obesity
Male,Vitamin D Deficiency
Male,Cryptorchidism
Male,Obstructive Sleep Apnea
Male,Varicose Veins
Male,Vitamin D Deficiency
Male,Attention Deficit Hyperactivity Disorder
Male,Cholecystitis

Один Cryptorchidism на десять мужских строк — при любом seed. Как только запрошенное число строк становится целым, счёт точен, а seed решает лишь то, каким именно строкам оно достанется.

Составные ветки

Вот что даёт <mix> и чего не даст голый список строк: <case> может собрать своё значение из нескольких кусков — литерального фрагмента <data>, одного или нескольких генераторов и вложенных <mix>. В этом контексте <data> — просто литеральный кусок значения («клей»), а не форматирование вывода; о форматировании см. Маски и регистр.

<mix name="Charge" percent="10,12,34,">
<case><data>возврат: </data><gen type="number" value="1..10"/></case>
<case><data>чарджбэк: </data><gen type="number" value="11..20"/></case>
<case><gen type="number" value="21..40"/></case>
<case><gen type="number" value="41..100"/><data> (помечено)</data></case>
</mix>
./run charge.tdc (первые 8 строк)
36
возврат: 4
чарджбэк: 18
66 (помечено)
возврат: 8
возврат: 1
73 (помечено)
82 (помечено)

Каждая ветка собрала свою форму: возврат: 4 — это литерал возврат: плюс number в диапазоне 1..10; 66 (помечено) — число из 41..100, за которым идёт литерал (помечено); третья ветка — голое число без всякой обёртки. Свернём 100 строк по веткам — доли по-прежнему точны:

./run charge.tdc (count=100, с подсчётом по веткам)
возврат     10
чарджбэк    12
чистое      34
помечено    44

10 / 12 / 34 / 44. У последней ветки нет собственного процента — хвостовая запятая в percent="10,12,34," оставляет её открытой, поэтому она берёт остаток, 44.

Распределение генерирует — оно не форматирует

<mix> порождает данные, поэтому живёт только в <env>. Поставить его в блок вывода нельзя: <mix> прямо в <line> отклоняется ещё до запуска с ошибкой TDC132, потому что блок вывода отвечает только за раскладку. Объявите <mix name="…"> в <env> и подставьте ${{Имя}} там, где нужно значение.

Если нужен выбор не по проценту, его покрывают два соседа:

  • <switch> выбирает по ключу — таблица соответствий, вроде страна → валюта.
  • <sequence> с условными ветками <gen if="…"> выбирает по произвольному условию — побеждает первая истинная ветка.

Пометка выбросов через flag

Ветку можно объявить аномальной — <case anomaly="true"> — и попросить у mix колонку-ответ через flag. В результате получается набор данных, на котором можно проверить детектор аномалий: и выбросы есть, и соседний столбец записывает, на какие именно строки они пришлись.

<mix name="Temp" percent="75,25" flag="Bad">
<case><gen type="number" value="20..24"/></case>
<case anomaly="true"><gen type="number" value="90..99"/></case>
</mix>

${{Bad}} равно true ровно на тех строках, что пришли из помеченной ветки — на 25 % строк, — и false на всех остальных. Метка выводится из того же решения, которым выбиралась ветка, поэтому разойтись со значением она не может. anomaly="true" — всего лишь ярлык: сам выброс — это то, что выдаёт генератор ветки, поэтому вы полностью управляете тем, как он выглядит. Это лишь уголок более широкой темы — инъекция выбросов и колонка flag получают полный разбор в руководстве по аномалиям.

Дальше

  • Генератор text — точные доли percent для простого списка вариантов, когда ветки — отдельные слова.
  • Последовательности — именованный источник, рядом с которым стоит <mix>, и модель parent, которую они делят.
  • Иерархические зависимости — полная история про parent, со вложенными процентами на нескольких уровнях.