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

Пропуски в данных — missing

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

missing — сквозной атрибут: он работает на любом <gen>, что бы тот ни генерировал.

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

Один и тот же генератор, 80 строк, с поочерёдно включёнными модификаторами.
  • Aчистый ряд
  • Bс anomaly= — отмеченные точки это подмешанные выбросы
  • Cс missing= — засечки внизу это строки, где значения нет

Как включить

Поставьте missing="p" на <gen>, где p — доля от 0 до 1, то есть доля значений, которые станут пустыми:

<gen type="number" value="30000..90000" missing="0.3"/>

Примерно 30% доходов выйдут пустыми, остальные — обычными числами.

Каждое значение «теряется» независимо с этой вероятностью. В статистике это называется MCAR — Missing Completely At Random, «пропуски совершенно случайны»: дыры выпадают безотносительно к другим полям и к самому значению.

По умолчанию пропуск — это пустая строка. Хотите свой маркер (NULL, NA, )? Задайте missing_as — см. раздел ниже.

До и после — что именно меняет пропуск

Проблема. Пока не увидите одно и то же поле «целым» и «дырявым» рядом, трудно понять, что вообще происходит: где значение потерялось, а где его и не было.

Инструмент. Возьмём один список городов и сделаем из него две колонки: Full — как есть, и Holey — тот же список, но с missing. order="sequential" берёт значения строго по порядку, поэтому обе колонки построчно совпадают — видно ровно то, что «выпало»:

<env count="8" seed="demo">
<sequence name="Full"> <gen type="text" value="Москва,Питер,Казань,Пермь,Сочи,Омск,Тула,Уфа" order="sequential"/></sequence>
<sequence name="Holey"><gen type="text" value="Москва,Питер,Казань,Пермь,Сочи,Омск,Тула,Уфа" order="sequential" missing="0.4"/></sequence>
</env>
...
<data>полное=${{Full}} | с пропусками=${{Holey}}</data>
./run cities.tdc (count=8, seed=demo)
полное=Москва  | с пропусками=Москва
полное=Питер   | с пропусками=Питер
полное=Казань  | с пропусками=
полное=Пермь   | с пропусками=
полное=Сочи    | с пропусками=Сочи
полное=Омск    | с пропусками=Омск
полное=Тула    | с пропусками=Тула
полное=Уфа     | с пропусками=Уфа

Слева всё на месте; справа — те же строки, но Казань и Пермь исчезли. Значение не заменилось на другое и не сдвинулось вверх — оно просто стало пустым. (На 8 строках при missing="0.4" выпало 2 — доля случайная, и на маленькой выборке разброс большой.)

Свой маркер вместо пустоты — missing_as

Проблема. Пустая строка в выгрузке невидима: в CSV это «ничего между двумя запятыми», глазами не отличить пропуск от короткого значения. В настоящих данных дыру обычно помечают явно — NULL, NA, .

Инструмент. missing_as="маркер" печатает ваш текст вместо пустоты. Строки, где случился пропуск, — те же самые (их выбирает seed и имя колонки, а не маркер); меняется только то, что стоит на месте дыры:

Маркер форматируется как обычное значение

mask= и case= работают после того, как дыра заполнена, поэтому они меняют и маркер: missing_as="n/a" case="upper" напишет N/A. Пишите маркер сразу таким, каким он должен выйти, а если маска его портит — вынесите форматирование в отдельную последовательность.

<sequence name="Holey">
<gen type="text" value="Москва,Питер,Казань,Пермь,Сочи,Омск,Тула,Уфа"
order="sequential" missing="0.4" missing_as="NULL"/>
</sequence>
./run cities.tdc (missing_as=NULL)
полное=Москва  | с пропусками=Москва
полное=Питер   | с пропусками=Питер
полное=Казань  | с пропусками=NULL
полное=Пермь   | с пропусками=NULL
полное=Сочи    | с пропусками=Сочи
полное=Омск    | с пропусками=Омск
полное=Тула    | с пропусками=Тула
полное=Уфа     | с пропусками=Уфа

Дыры на тех же строках 3 и 4, что и раньше, — но теперь их видно. Поставьте missing_as="—" или missing_as="NA" и получите свой маркер на тех же местах.

Сколько дырок — вариации доли

Проблема. Хочется понять, как поведёт себя система при «редких» и при «частых» пропусках. Одна доля этого не покажет.

Инструмент. Меняем только missing, всё остальное то же. X — значение на месте, [] — дыра. Три колонки, 20 строк:

<env count="20" seed="demo">
<sequence name="A"><gen type="text" value="X" order="sequential" missing="0.1"/></sequence>
<sequence name="B"><gen type="text" value="X" order="sequential" missing="0.3"/></sequence>
<sequence name="C"><gen type="text" value="X" order="sequential" missing="0.6"/></sequence>
</env>
...
<data>0.1:[${{A}}] 0.3:[${{B}}] 0.6:[${{C}}]</data>
./run rates.tdc (count=20, seed=demo)
0.1:[X]  0.3:[X]  0.6:[]
0.1:[X]  0.3:[]  0.6:[X]
0.1:[X]  0.3:[X]  0.6:[X]
0.1:[X]  0.3:[]  0.6:[X]
0.1:[X]  0.3:[]  0.6:[]
0.1:[X]  0.3:[]  0.6:[X]
0.1:[X]  0.3:[X]  0.6:[]
0.1:[]  0.3:[X]  0.6:[]
0.1:[X]  0.3:[X]  0.6:[]
0.1:[X]  0.3:[X]  0.6:[X]
0.1:[]  0.3:[X]  0.6:[X]
0.1:[X]  0.3:[X]  0.6:[X]
0.1:[X]  0.3:[X]  0.6:[]
0.1:[X]  0.3:[X]  0.6:[]
0.1:[X]  0.3:[]  0.6:[]
0.1:[X]  0.3:[X]  0.6:[]
0.1:[X]  0.3:[X]  0.6:[X]
0.1:[X]  0.3:[X]  0.6:[X]
0.1:[X]  0.3:[X]  0.6:[]
0.1:[X]  0.3:[X]  0.6:[X]

Считаем пустые [] по колонкам: 0.1 → 2 дыры из 20, 0.3 → 5, 0.6 → 10. Доля растёт — дыр становится больше. Доля × 20 дала бы 2 / 6 / 12, и попала в это только первая колонка: каждое значение теряется независимо, поэтому на 20 строках счёт «гуляет» вокруг ожидаемого, а не совпадает с ним. На 20 000 строках сойдётся куда ближе.

Пропуски в нескольких полях

missing сочетается с чем угодно — с обычным диапазоном, с template, с шаблоном regex, со статистическим распределением. Соберём запись из трёх полей: имя (без пропусков), телефон (missing="0.3", маркер ) и доход (missing="0.25", маркер по умолчанию — пусто):

<env count="8" seed="rec" local="ru">
<sequence name="Name"> <gen type="template" value="person.male.firstName"/></sequence>
<sequence name="Phone"> <gen type="regex" value="\+7 \(9[0-9]{2}\) [0-9]{3}-[0-9]{2}-[0-9]{2}" missing="0.3" missing_as=""/></sequence>
<sequence name="Income"> <gen type="number" value="30000..90000" missing="0.25"/></sequence>
</env>
...
<data>${{Name}} | тел: ${{Phone}} | доход: ${{Income}}</data>
./run record.tdc (count=8, seed=rec)
Дмитрий   | тел: +7 (922) 889-53-97 | доход: 38370
Тимофей   | тел: +7 (906) 344-33-99 | доход: 70315
Станислав | тел: —                  | доход: 41008
Вадим     | тел: +7 (918) 997-61-79 | доход: 85063
Григорий  | тел: +7 (952) 879-35-78 | доход:
Евгений   | тел: +7 (914) 240-78-00 | доход: 48334
Демьян    | тел: —                  | доход: 46153
Ярослав   | тел: +7 (929) 079-00-13 | доход: 33986

У части строк телефон — , у пятой строки пустой доход (маркер по умолчанию — ничего). Пропуски в двух полях независимы: дыра в одной колонке ничего не говорит о другой.

Какие строки вообще могут опустеть — missing_when

Один только missing="p" роняет значения, ни на что не глядя. Это один из трёх способов, какими данные теряются в жизни, и самый бедный: такие дыры не несут никакой информации — из них ничему не научишься и ничего по ним не предскажешь.

missing_when="…" добавляет два остальных. Это условие на том же языке выражений, что и if=, и оно решает, какие строки вообще попадают под раздачу. А missing="p" по-прежнему решает, как часто попавшая под раздачу строка действительно опустеет.

Что пишетеКак это называетсяОткуда берётся дыра
missing="0.2"MCARниоткуда — все строки равновероятны
missing="0.8" missing_when="Age < 30"MARдругая колонка, которую видно
missing="0.8" missing_when="_value > 60000"MNARсамо значение, которого уже нет

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

MAR — дыра зависит от другой колонки

<sequence name="Age"><gen type="number" value="18..70"/></sequence>
<sequence name="Income">
<gen type="number" value="30000..90000" missing="0.8" missing_as="NULL" missing_when="Age < 30"/>
</sequence>
./run mar.tdc (count=8, seed=ages)
age: 66 | income: 33520
age: 56 | income: 76606
age: 28 | income: NULL
age: 26 | income: NULL
age: 67 | income: 40610
age: 33 | income: 86092
age: 68 | income: 86789
age: 18 | income: NULL

Каждый пропуск стоит в строке моложе 30; никто старше дохода не потерял. Age читается ровно так же, как её прочитал бы if="Age < 30", — значит, и правила те же: колонка должна быть объявлена выше той, что её называет.

MNAR — дыра зависит от значения, которого уже нет

Внутри missing_when имя _value — это значение, которое генератор выдал для строки, то самое, что дыра и прячет. Такое имя даёт сам язык, как _count и _last; в конфигурации его никто не объявляет.

<sequence name="Income">
<gen type="number" value="30000..90000" missing="0.8" missing_as="NULL" missing_when="_value > 60000"/>
</sequence>
./run mnar.tdc (count=8, seed=mnar)
income: NULL
income: 53006
income: 52544
income: NULL
income: 52481
income: NULL
income: NULL
income: 35921

Все уцелевшие доходы — меньше 60 000. Не ответили как раз те, кто зарабатывает много: это и есть перекос, который называют MNAR, и это ровно то, чего модель, обученная на видимых строках, увидеть не может.

_value — это значение после anomaly=, потому что именно его строка и несла бы дальше. Поэтому missing_when="_value > 150000" рядом с anomaly= вычищает как раз выбросы, а колонка anomaly_flag с этим согласна: у опустевшей ячейки не осталось выброса, который можно было бы пометить.

Мелким шрифтом

  • Доля всё равно нужна. missing_when без missing ничего не решает — такое не игнорируется, а отклоняется.
  • Голое слово — это литерал, здесь как и везде в языке: missing_when="Tier == hi" сравнивает со словом hi. Опечатку в имени колонки покажут (TDC215), а не прочитают молча как слово.
  • Не вместе с repeat=. В повторяющейся ячейке на одной строке несколько значений, а условие спрашивает про одно; вместо того чтобы гадать, что имелось в виду, сочетание отклоняется.
  • Оба движка. Условие вычисляется построчно через тот же читатель колонок, которым пользуются оба движка, — потоковый прогон и прогон в памяти дают один и тот же файл.

Детали

  • Детерминированно. Тот же seed даёт те же пропуски — см. Детерминизм и пропорции. Один и тот же «дырявый» датасет воспроизводится байт в байт, запуск за запуском.
  • Оба движка, любой объём. Пропуски считаются по номеру строки, поэтому память не растёт с размером датасета — см. Большие выгрузки и стриминг.
  • missing="0" — совсем без пропусков, ровно как если бы атрибута не было.

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