Аномалии и выбросы — anomaly
Когда пригодится — когда вы готовите тестовые данные для детектора аномалий,
для алерта мониторинга или для модели, устойчивой к выбросам, — для всего, что
должно замечать значения далеко за пределами нормального диапазона. В реальных
данных такие всегда есть: залипший датчик, опечатка при вводе, подозрительная
транзакция. Атрибут anomaly подмешивает эти всплески специально, с той
частотой, которую вы зададите, — чтобы вашему конвейеру было на что реагировать.
Есть два способа, и эта страница описывает оба:
| Способ | Как выглядит выброс | Колонка-ответ |
|---|---|---|
anomaly="p" на <gen>, выдающем числа | базовое значение × коэффициент | anomaly_flag="Имя" |
anomaly="true" на <case> внутри <mix> | то, что генерирует эта ветка | flag="Имя" |
Первый способ — когда «слишком большое» число вас как выброс устраивает. Второй — когда выбросу нужна своя форма: другой диапазон, мусорная строка, редкое, но допустимое событие.
Числа ниже — то, что даёт типичный прогон. Конкретные значения могут отличаться от
версии ядра и seed. Неизменной остаётся структура: какие строки всплеснут,
решает номер строки, поэтому колонка-ответ всегда согласована со значением.
- Aчистый ряд
- Bс anomaly= — отмеченные точки это подмешанные выбросы
- Cс missing= — засечки внизу это строки, где значения нет
Как включить
Поставьте anomaly="p" на генератор, выдающий числа, где p — доля значений (от 0 до 1),
которые станут выбросами. Размер каждого всплеска задаёт anomaly_factor (по
умолчанию 10):
<gen type="number" distribution="normal" mean="50" sd="3" anomaly="0.15" anomaly_factor="8"/>
Около 15% значений умножаются на 8 — обычные показания держатся у 50, выбросы
попадают ближе к 400. Выброс — это ровно базовое значение, умноженное на
коэффициент. Работает только с числами; нечисловые значения в смешанном списке
проходят мимо нетронутыми, а список, где чисел нет вовсе, отклоняется сразу (TDC243) —
чтобы атрибут не висел там, где он никогда не сработает (см. Детали).
Выброс сохраняет форму значения, которое заменил: столько же знаков после точки и ту же
ширину, до которой значение дополнял нулями length=. Выброс
далёк от остальных по значению, а не по форме — колонка пятизначных идентификаторов остаётся
пятизначной, а колонка с decimals="2" сохраняет два знака и по-прежнему грузится тем типом,
которым объявлена. Дополнение только добавляет, поэтому выброс, реально переросший ширину,
сохраняет лишние цифры.
До и после — тот же ряд с выбросами и без
Проблема. Если показать только «грязную» колонку, всплеск не отличишь от
по-настоящему большого значения: 460 — это сбой или просто крупное показание?
Нужен чистый эталон прямо рядом.
Инструмент. Берём один список показаний с order="sequential"
(строго по порядку) и строим из него две колонки: Clean — как есть, и Dirty —
тот же ряд, но с anomaly. Построчно базовое значение совпадает, поэтому каждый
всплеск очевиден: это то же число, умноженное на коэффициент.
<env count="12" seed="demo">
<sequence name="Clean"><gen type="text" value="48,50,46,52,49,51,47,53,50,48,52,49" order="sequential"/></sequence>
<sequence name="Dirty"><gen type="text" value="48,50,46,52,49,51,47,53,50,48,52,49" order="sequential" anomaly="0.3" anomaly_factor="10"/></sequence>
</env>
...
<data>норма=${{Clean}} | с выбросами=${{Dirty}}</data>
норма=48 | с выбросами=48 норма=50 | с выбросами=50 норма=46 | с выбросами=460 норма=52 | с выбросами=52 норма=49 | с выбросами=49 норма=51 | с выбросами=510 норма=47 | с выбросами=47 норма=53 | с выбросами=53 норма=50 | с выбросами=50 норма=48 | с выбросами=480 норма=52 | с выбросами=52 норма=49 | с выбросами=49
Три строки выбились: 46 → 460, 51 → 510, 48 → 480 — ровно ×10. Никаких пометок
«← выброс» не нужно: эталон слева, всплеск справа, разница говорит сама за себя. На
12 строках при anomaly="0.3" всплесков вышло 3 (доля случайная, а на маленькой
выборке разброс большой).
Зачем/когда. Такое сравнение бок о бок — самый быстрый способ убедиться, что
anomaly делает именно то, что вы думаете, прежде чем разворачивать это на реальном
датасете.
Насколько большой всплеск — anomaly_factor
Проблема. «Мягкий» выброс (чуть выше нормы) и «жёсткий» (в разы больше) ловятся разными порогами. Нужно уметь регулировать высоту всплеска.
Инструмент. Меняем только anomaly_factor; всё остальное оставляем как есть — ту
же последовательность, тот же seed, ту же anomaly="0.25". При фиксированных сиде,
имени и доле всплеск случается на тех же строках каждый раз — меняется лишь их
высота.
<gen type="text" value="48,50,46,52,49,51,47,53,50,48,52,49"
order="sequential" anomaly="0.25" anomaly_factor="5"/> <!-- затем 10, затем 20 -->
anomaly_factor="5" → "10" → "20", те же 12 строк в каждом:
factor=5 factor=10 factor=20 240 480 960 50 50 50 230 460 920 52 52 52 49 49 49 51 51 51 235 470 940 265 530 1060 50 50 50 48 48 48 52 52 52 245 490 980
База на строке 8 — 53: 53×5=265, 53×10=530, 53×20=1060. Строки-выбросы
(1, 3, 7, 8, 12) одни и те же во всех трёх колонках — anomaly_factor управляет
только высотой, а не тем, какие строки всплеснут. (Какие именно строки
всплеснут — зависит от seed и имени последовательности: переименуйте
последовательность — и всплески сдвинутся.)
Зачем/когда. Прогоните коэффициент по диапазону, чтобы найти, где порог вашего детектора начинает и перестаёт срабатывать, ничего больше не трогая.
Сколько выбросов — доля anomaly
Проблема. Редкий сбой (раз в сотню) и постоянный шум требуют разных порогов.
Само значение anomaly задаёт, как часто происходит всплеск.
Инструмент. Фиксируем базовое значение на 50, чтобы всплеск (500) был виден
с одного взгляда. Левая колонка anomaly="0.1", правая — anomaly="0.5", 20 строк:
<env count="20" seed="demo">
<sequence name="Low"> <gen type="number" value="50" anomaly="0.1" anomaly_factor="10"/></sequence>
<sequence name="High"><gen type="number" value="50" anomaly="0.5" anomaly_factor="10"/></sequence>
</env>
...
<data>доля 0.1: ${{Low}} доля 0.5: ${{High}}</data>
value="50" у number — это константа: вся колонка равна 50, пока аномалия не поднимет
строку до 500. Берите number, а не список text из одного элемента, всюду, где в
колонке числа: текстовый список проверяется чтением, поэтому смешанный
value="hello,50" принимает anomaly= и потом даёт всплески только там, где попалось
число, — реже заявленного. В number каждое значение число по построению, поэтому
заявленная доля равна фактической.
rate 0.1: 500 rate 0.5: 50 rate 0.1: 50 rate 0.5: 500 rate 0.1: 50 rate 0.5: 500 rate 0.1: 50 rate 0.5: 500 rate 0.1: 50 rate 0.5: 500 rate 0.1: 50 rate 0.5: 50 rate 0.1: 50 rate 0.5: 50 rate 0.1: 50 rate 0.5: 50 rate 0.1: 50 rate 0.5: 50 rate 0.1: 50 rate 0.5: 50 rate 0.1: 500 rate 0.5: 500 rate 0.1: 50 rate 0.5: 500 rate 0.1: 50 rate 0.5: 50 rate 0.1: 500 rate 0.5: 500 rate 0.1: 500 rate 0.5: 50 rate 0.1: 50 rate 0.5: 50 rate 0.1: 50 rate 0.5: 50 rate 0.1: 500 rate 0.5: 500 rate 0.1: 50 rate 0.5: 50 rate 0.1: 50 rate 0.5: 500
Левая колонка всплеснула 5 раз из 20, правая — 9: чем выше доля, тем гуще выбросы.
Двадцать строк — маленькая выборка, и это видно: 5 и 9 против заявленных 0.1 и 0.5.
Доля — это вероятность на строку, а не квота, поэтому короткий прогон вокруг неё
колеблется, а длинный на неё садится: тот же конфиг на 4000 строках даёт 10.2% и
50.1%. Ваши собственные двадцать строк будут выглядеть иначе — это доля работает, а
не ломается.
anomaly сочетается с чем угодно — с диапазоном, с
распределением или с missing.
Зачем/когда. Подбирайте долю под моделируемое явление — редкий дефект на 0.01,
болтливый неисправный датчик на 0.4.
Колонка-ответ (ground-truth) — anomaly_flag
Проблема. Выбросы впрыснуты — но чтобы оценить детектор, нужен правильный ответ: какие именно строки аномальны. Глядя только на числа, сбойный всплеск не отличишь от законно большого показания.
Инструмент. Добавьте anomaly_flag="Имя" рядом с anomaly, и TDC заводит
новую колонку — последовательность Имя со значениями true/false, где true стоит
ровно на тех строках, что всплеснули:
<gen type="number" distribution="normal" mean="50" sd="3"
anomaly="0.2" anomaly_factor="8" anomaly_flag="IsOutlier"/>
...
<data>${{Reading}},${{IsOutlier}}</data>
360,true 48,false 49,false 51,false 52,false 52,false 47,false 49,false
360 (≈ 45 × 8) помечен true; обычные показания — false. Метка считается тем
же решением, что и сам выброс, поэтому эти двое не могут разойтись — и так во всех
движках, при любом объёме.
Именно это обещание решает единственный случай, где они могли бы разойтись: у ячейки,
которую обнулил missing, не осталось выброса, который
можно описать, — значит, её метка false. Надпись «выброс» рядом с пустой ячейкой учила
бы детектор неправде на каждой такой строке, а про обнуление розыгрыш аномалии ничего не
знает.
Метка — обычная колонка, поэтому её можно фильтровать через
if. Оставим только выбросы через
if="IsOutlier" (читается «истинность» строки, как у встроенных
_first/_last):
<block><line if="IsOutlier"><data>выброс: ${{Reading}}</data></line></block>
выброс: 360 выброс: 392 выброс: 400
Из 20 показаний три оказались аномалиями — готовый размеченный датасет для оценки точности и полноты детектора.
Зачем/когда. Всякий раз, когда детектор нужно измерить, а не просто накормить.
Учтите: anomaly_flag без anomaly — ошибка, помечать нечего.
Флагу нужна последовательность, значение которой и есть этот <gen>. Добавьте в тело вторую часть — ещё один <gen>, литерал <data> или name=, превращающий генератор в поле, — и колонке уровня строки становится негде жить, поэтому конфигурация отклоняется (TDC283), а не запускается без неё. Вынесите генератор в отдельную <sequence>: заодно получите и само значение отдельной колонкой.
Свой выброс — flag на <mix>
Проблема. anomaly="p" умеет только умножать. Иногда выброс должен выглядеть
иначе — со своим диапазоном, своим текстом, редким, но допустимым событием, — и вам
всё ещё нужна честная колонка-ответ, говорящая, какие строки подложные.
Инструмент. Опишите выброс отдельной веткой <mix>,
пометьте этот <case> через anomaly="true" и попросите у
mix колонку-ответ через flag="Имя". Здесь
температура обычно 20–24, но в четверти случаев залипший датчик показывает
90–99:
<env count="12" seed="sensor-2026">
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<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>
</env>
<block>
<line><data>${{Id}},${{Temp}},${{Bad}}</data></line>
</block>
Колонки — id, temp, bad:
1,23,false 2,21,false 3,22,false 4,21,false 5,24,false 6,23,false 7,21,false 8,92,true 9,20,false 10,97,true 11,21,false 12,98,true
Три строки из двенадцати — ровно 25%, как и просили в
percent, — и bad равно true на каждой из них. Метка
берётся из того же выбора, которым выбиралась ветка, поэтому разойтись со
значением она не может.
Зачем/когда. anomaly="true" на <case> — это только ярлык: он ничего не
подмешивает и ничего не меняет; выброс целиком делает собственный генератор ветки
(<gen type="number" value="90..99"/>). А значит, вы полностью управляете тем, как
выглядит выброс, — в отличие от anomaly="p", который умеет только масштабировать
число.
Обе половины работают только вместе
TDC проверяет, что и ярлык, и колонка на месте:
| Что написали | Что скажет TDC |
|---|---|
anomaly="true", но у <mix> нет flag= | ошибка TDC203 — ярлыку некуда попасть |
flag=, но ни один <case> не помечен | ошибка TDC202 — колонка была бы вся false |
flag= на вложенном <mix> | ошибка TDC203 — колонку заводит только именованный mix |
В Parquet — настоящий тип
При выгрузке в Parquet колонка-ответ становится честным BOOLEAN, а значения
сохраняют свой числовой тип, и нигде никакого type=. Типизированному файлу нужны
именованные колонки, поэтому <block> называет их — по одному <data name="…">
на колонку вместо одной строки через запятую:
<block>
<line>
<data name="id">${{Id}}</data>
<data name="temp">${{Temp}}</data>
<data name="bad">${{Bad}}</data>
</line>
</block>
Контейнер выбирается по расширению вывода — флага --format нет:
id INT64 REQUIRED temp INT64 REQUIRED bad BOOLEAN REQUIRED
Тип значений выводится, только если все ветки согласны между собой (здесь обе — числа). Если одна ветка возвращает число, а другая слово, колонка останется текстом — TDC не угадывает тип, в котором не уверен. См. Форматы вывода.
Ещё варианты применения
- Проверка чистильщика данных. Пометьте ветку, выдающую мусор, — кривой адрес, пустой телефон — и посмотрите, сколько помеченных строк чистильщик на самом деле убрал.
- Несколько видов брака. Пометить можно не одну ветку;
flagвстаёт вtrueна всех помеченных случаях сразу. - Разметка «редкого события». Ярлык не обязан означать поломку — он может помечать редкий, но вполне законный случай, который модель всё равно должна уметь ловить.
Детали
- Детерминированно. Тот же
seedдаёт те же выбросы, на тех же строках. См. Детерминизм и пропорции. - Любой движок, любой объём. Выбросы решаются по номеру строки, поэтому прогоны в памяти и на диске совпадают точь-в-точь.
- Только числа, и решает значение, а не
type=. В примерах выше список записан черезtype="text", но значения — числа, поэтомуanomalyих умножает. Поставьте его на колонку имён — значения пройдут без изменений.checkскажет об этом, если весь список записан в конфиге (TDC243): там ему видны все кандидаты. Если значения приходят из пакета, файла или регулярки, увидеть их нельзя — и тамanomaly=на колонке имён проходит молча. Если колонка, в которой должны быть всплески, вышла чистой, проверять надо в первую очередь это. anomaly="0"означает отсутствие выбросов.
Смотрите также
- Числовой генератор — диапазоны и распределения, к
которым цепляется
anomaly. - Обзор генераторов —
missing=иanomaly=бок о бок, оба применяются уже после того, как значение произведено. - Справочник тегов —
<mix>и<case>, ветвление, на котором строится способ со своим выбросом. - Вывод и форматирование — выражение
if, которым фильтруют по колонке-ответу.