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

Аномалии и выбросы — anomaly

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

Есть два способа, и эта страница описывает оба:

СпособКак выглядит выбросКолонка-ответ
anomaly="p" на <gen>, выдающем числабазовое значение × коэффициентanomaly_flag="Имя"
anomaly="true" на <case> внутри <mix>то, что генерирует эта веткаflag="Имя"

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

Примеры вывода иллюстративны

Числа ниже — то, что даёт типичный прогон. Конкретные значения могут отличаться от версии ядра и seed. Неизменной остаётся структура: какие строки всплеснут, решает номер строки, поэтому колонка-ответ всегда согласована со значением.

Один и тот же генератор, 80 строк, с поочерёдно включёнными модификаторами.
  • 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. Выброс — это ровно базовое значение, умноженное на коэффициент. Работает только с числами; нечисловые значения проходят мимо нетронутыми (см. Детали).

До и после — тот же ряд с выбросами и без

Проблема. Если показать только «грязную» колонку, всплеск не отличишь от по-настоящему большого значения: 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>
./run readings.tdc (12 строк)
норма=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 строк в каждом:

./run factor.tdc (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="text" value="50" order="sequential" anomaly="0.1" anomaly_factor="10"/></sequence>
<sequence name="High"><gen type="text" value="50" order="sequential" anomaly="0.5" anomaly_factor="10"/></sequence>
</env>
...
<data>доля 0.1: ${{Low}} доля 0.5: ${{High}}</data>
./run rate.tdc (20 строк)
доля 0.1:  50   доля 0.5:  50
доля 0.1:  50   доля 0.5:  50
доля 0.1:  50   доля 0.5: 500
доля 0.1:  50   доля 0.5: 500
доля 0.1:  50   доля 0.5: 500
доля 0.1:  50   доля 0.5:  50
доля 0.1:  50   доля 0.5: 500
доля 0.1:  50   доля 0.5: 500
доля 0.1:  50   доля 0.5: 500
доля 0.1:  50   доля 0.5: 500
доля 0.1:  50   доля 0.5: 500
доля 0.1:  50   доля 0.5:  50
доля 0.1:  50   доля 0.5: 500
доля 0.1: 500   доля 0.5:  50
доля 0.1:  50   доля 0.5: 500
доля 0.1:  50   доля 0.5:  50
доля 0.1:  50   доля 0.5:  50
доля 0.1:  50   доля 0.5:  50
доля 0.1:  50   доля 0.5:  50
доля 0.1:  50   доля 0.5: 500

Левая колонка всплеснула 1 раз из 20, правая — 11: чем выше доля, тем гуще выбросы. 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>
./run labeled.tdc
360,true
48,false
49,false
51,false
52,false
52,false
47,false
49,false

360 (≈ 45 × 8) помечен true; обычные показания — false. Метка считается тем же решением, что и сам выброс, поэтому эти двое не могут разойтись — и так во всех движках, при любом объёме.

Метка — обычная колонка, поэтому её можно фильтровать через if. Оставим только выбросы через if="IsOutlier" (читается «истинность» строки, как у встроенных _first/_last):

<block><line if="IsOutlier"><data>выброс: ${{Reading}}</data></line></block>
./run outliers-only.tdc (20 показаний)
выброс: 360
выброс: 392
выброс: 400

Из 20 показаний три оказались аномалиями — готовый размеченный датасет для оценки точности и полноты детектора.

Зачем/когда. Всякий раз, когда детектор нужно измерить, а не просто накормить. Учтите: anomaly_flag без anomaly — ошибка, помечать нечего.

Свой выброс — 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:

./run sensor.tdc (12 строк)
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=:

./run sensor.tdc --format parquet (схема)
id     INT64    REQUIRED
temp   INT64    REQUIRED
bad    BOOLEAN  REQUIRED

Тип значений выводится, только если все ветки согласны между собой (здесь обе — числа). Если одна ветка возвращает число, а другая слово, колонка останется текстом — TDC не угадывает тип, в котором не уверен. См. Форматы вывода.

Ещё варианты применения

  • Проверка чистильщика данных. Пометьте ветку, выдающую мусор, — кривой адрес, пустой телефон — и посмотрите, сколько помеченных строк чистильщик на самом деле убрал.
  • Несколько видов брака. Пометить можно не одну ветку; flag встаёт в true на всех помеченных случаях сразу.
  • Разметка «редкого события». Ярлык не обязан означать поломку — он может помечать редкий, но вполне законный случай, который модель всё равно должна уметь ловить.

Детали

  • Детерминированно. Тот же seed даёт те же выбросы, на тех же строках. См. Детерминизм и пропорции.
  • Любой движок, любой объём. Выбросы решаются по номеру строки, поэтому прогоны в памяти и на диске совпадают точь-в-точь.
  • Только числа, и решает значение, а не type=. В примерах выше список записан через type="text", но значения — числа, поэтому anomaly их умножает. Поставьте его на колонку имён — значения пройдут без изменений, причём молча: tdcv2 check не скажет ничего, потому что текстовый список вполне может содержать числа, и TDC не знает, что вы имели в виду. Если колонка, в которой должны быть всплески, вышла чистой, проверять надо в первую очередь это.
  • anomaly="0" означает отсутствие выбросов.

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