Пропуски в данных — missing
Когда пригодится — когда нужно проверить, как код переживает дыры.
Настоящие данные почти никогда не бывают полными: где-то не заполнили телефон,
где-то потерялся доход, где-то поле просто пустое. Если тестировать только на
«идеальных» строках, первый же null в проде всё сломает. Атрибут missing
подмешивает пробелы специально, чтобы вы увидели поломку раньше своих
пользователей.
missing — сквозной атрибут: он работает на любом
<gen>, что бы тот ни генерировал.
Примеры вывода ниже — иллюстративные: конкретные строки, которые даст тот или
иной seed, могут отличаться между версиями ядра, но поведение, которое
гарантирует атрибут, — нет.
- 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>
полное=Москва | с пропусками=Москва полное=Питер | с пропусками=Питер полное=Казань | с пропусками= полное=Пермь | с пропусками= полное=Сочи | с пропусками=Сочи полное=Омск | с пропусками=Омск полное=Тула | с пропусками=Тула полное=Уфа | с пропусками=Уфа
Слева всё на месте; справа — те же строки, но Казань и Пермь исчезли.
Значение не заменилось на другое и не сдвинулось вверх — оно просто стало
пустым. (На 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>
полное=Москва | с пропусками=Москва полное=Питер | с пропусками=Питер полное=Казань | с пропусками=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>
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>
Дмитрий | тел: +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>
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>
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"— совсем без пропусков, ровно как если бы атрибута не было.
Смотрите также
- Последовательности — те самые колонки,
в которых вы пробиваете дыры, и как
${{Name}}их читает. - Текст и Числа — генераторы из примеров выше.
- Маски и регистр —
order="sequential"и остальные сквозные атрибуты генераторов.