AI Architect Trainer Відкрити інтерактивний трек

Головна › Архітектура корпоративної аналітики

Інтелект рішень і виявлення аномалій

Архітектура систем підтримки рішень, базові лінії та ізоляційний ліс, економіка хибних спрацювань і замикання циклу «сигнал → рішення → результат».

Востаннє переглянуто: 2026-09-27 · In English

У цьому напрямі

Четвертий шар корпоративної інтелектуальної архітектури не виробляє даних — він виробляє рішення. Цей модуль показує, чому дашборд аномалій не є системою підтримки рішень; як насправді оцінює точку ізоляційний ліс і чому його параметр contamination — це план завантаження вашої команди, а не гіперпараметр моделі; та як обирати робочий поріг у грошах, а не у F1. Наскрізна теза одна: детектор, результати рішень якого ніхто не записує, не має ані міток для навчання, ані доказів власної цінності.

Від сигналу до дії: анатомія системи підтримки рішень

E7.1

Дашборд не є системою підтримки рішень. Дашборд показує стан; СППР веде конкретну людину від сигналу до дії й записує, що з цього вийшло. Різниця не косметична: без записаного результату система ніколи не отримає міток, а без міток жодна модель усередині неї не здатна навчатися й жоден аналітик не доведе її окупність.

Чотири підсистеми класичної СППР — і що кожна означає в сучасній архітектурі:

Рішення — це об'єкт першого класу. Мінімальний контракт запису: ідентифікатор сутності, тригер (яка модель і яка її версія), оцінка й чинний на той момент поріг, знімок доказів, обраний варіант дії, автор, час, клас оборотності та — обов'язково — поле outcome, що дописується пізніше. Саме воно замикає цикл виявлення → рішення → дія → спостереження і перетворює операційну систему на джерело навчальних даних. Відхилене сповіщення — теж рішення: саме відмови становлять більшість негативних міток, і саме їх найчастіше не пишуть.

Бюджет затримки рішення. Архітектуру визначає не «потужність моделі», а час, що лишається між сигналом і моментом, коли дія ще щось змінює. Авторизація картки — сотні мілісекунд; розслідування мережевого інциденту — хвилини; перегляд кредитного ліміту — доба. Один і той самий детектор у цих трьох режимах живе у трьох різних місцях: у синхронному шляху транзакції, у потоковому конвеєрі, у нічному пакеті. Спроба обслужити всі три одним синхронним викликом — найдорожча помилка цього шару; правильний хід — винести важкі агрегати у feature store, лишивши на синхронному шляху лише оцінювання.

Затримка мітки. Істина майже ніколи не відома одразу: підтверджене шахрайство приходить чарджбеком через 30–60 днів, наслідок клінічного рішення — через тижні. Тому навчальна вибірка завжди відстає від продакшену. Зберігайте дві мітки часу: коли подія сталася і коли ви про неї дізналися. Різати навчальну вибірку треба за другою — інакше модель непомітно навчається на інформації, якої в момент оцінювання не існувало.

-- The decision as a first-class object: the record that closes the loop.
-- EVERY alert that reaches a human lands here, including the dismissed ones.
CREATE TABLE decision (
  decision_id   BIGSERIAL PRIMARY KEY,
  entity_urn    TEXT             NOT NULL,  -- ontology object, NOT a source row id
  detector      TEXT             NOT NULL,  -- 'iforest_payments'
  detector_ver  TEXT             NOT NULL,  -- exact model version that fired
  score         DOUBLE PRECISION NOT NULL,
  threshold     DOUBLE PRECISION NOT NULL,  -- threshold IN FORCE at fire time
  evidence      JSONB            NOT NULL,  -- top features + baseline snapshot
  action        TEXT             NOT NULL
                CHECK (action IN ('block','hold','ignore','escalate')),
  reversible    BOOLEAN          NOT NULL,  -- gates what may auto-execute
  decided_by    TEXT             NOT NULL,  -- analyst id, or 'auto'
  decided_at    TIMESTAMPTZ      NOT NULL DEFAULT now()
);

-- Outcome arrives LATER. Two timestamps, never one:
-- when the truth happened, and when we found out.
CREATE TABLE decision_outcome (
  decision_id   BIGINT      PRIMARY KEY REFERENCES decision(decision_id),
  label         TEXT        NOT NULL CHECK (label IN ('tp','fp','fn','unknown')),
  loss_amount   NUMERIC(14,2),
  occurred_at   TIMESTAMPTZ NOT NULL,
  known_at      TIMESTAMPTZ NOT NULL       -- label lag = known_at - decided_at
);

-- Training sets are cut on known_at, never on occurred_at: cutting on
-- occurred_at leaks labels that did not exist when the score was produced.
CREATE VIEW trainable_decisions AS
SELECT d.*, o.label, o.loss_amount,
       o.known_at - d.decided_at AS label_lag
FROM decision d
JOIN decision_outcome o USING (decision_id)
WHERE o.known_at <= now() - INTERVAL '1 day'
  AND o.label <> 'unknown';

На практиці

Європейський платіжний провайдер писав у сховище лише ескальовані сповіщення — 18% від усіх. Решта 82% (відхилення аналітиків) не залишали сліду, тож навчальна вибірка складалася майже виключно з позитивів. Після додавання рядка рішення з action='ignore' кількість розмічених прикладів зросла з 6 000 до 41 000 за квартал, а перенавчений детектор дав на 34% менше хибних спрацювань при незмінній повноті.

Антипатерн

«Дашборд аномалій» без власника: сповіщення не має ані відповідального, ані терміну, ані поля результату, а успіх системи вимірюють кількістю виявлених аномалій. Такий контур не може виміряти жодного хибнонегативного випадку і через рік не має чим обґрунтувати власний бюджет.

Базові лінії та ізоляційний ліс: щодо чого ви вимірюєте відхилення

E7.2

Аномалія — це порушення очікування, а не велике число. Тому перше архітектурне рішення стосується не алгоритму, а бази порівняння: глобальний розподіл, конкретна сутність чи група однолітків. Платіж на 40 000 € аномальний для роздрібного клієнта і абсолютно нормальний для оптовика; глобальний поріг «сума > 10 000» — це не детектор, а фільтр, який щодня генерує ту саму сотню відомих імен.

Три класи аномалій вимагають трьох різних механізмів:

Ізоляційний ліс: чому він працює. Замість описувати «норму», алгоритм ізолює точку випадковими розрізами за випадковими ознаками. Аномалії відтинаються за кілька розрізів, тож середня довжина шляху E(h(x)) коротка; оцінка s = 2^(−E(h(x))/c(n)) нормує її в (0,1). Практичні наслідки: лінійна за кількістю точок складність (оцінювання n точок t деревами на підвибірках розміру ψ — це O(n·t·log ψ)), відсутність потреби в мітках, стійкість у високій вимірності — і контрінтуїтивна деталь: підвибірка 256 точок на дерево працює краще за повний набір, бо великі підвибірки посилюють ефект затоплення.

Contamination — не гіперпараметр, а рішення про штат. Цей параметр під час навчання задає поріг оцінок так, щоб позначалася саме ця частка навчальних даних; на потоці, схожому на навчальні дані, позначається приблизно така сама частка подій, а це й є довжина черги на розбір. Лишити значення 0.1 (типове в PyOD, а в scikit-learn — до версії 0.22; тепер типове значення scikit-learn — 'auto') на потоці 480 000 подій на добу означає замовити близько 48 000 сповіщень команді, здатній розібрати 120. Правильний шлях зворотний: візьміть денну спроможність, поділіть на обсяг потоку і візьміть відповідний квантиль розподілу оцінок.

Маскування та затоплення. Кілька схожих аномалій поруч ховають одна одну (masking); нормальні точки біля щільного кластера аномалій отримують високі оцінки (swamping). Обидва ефекти послаблюються підвибіркою та базовими лініями за сутністю — і жодного з них не видно на агрегованій метриці, лише на розборі конкретних кейсів.

# Per-entity baseline + isolation forest, with the alert threshold taken from
# the team's review capacity rather than from a default contamination value.
import numpy as np
from sklearn.ensemble import IsolationForest

REVIEWS_PER_DAY = 120        # analyst capacity -- the real binding constraint
EVENTS_PER_DAY  = 480_000

def robust_z(g):
    med = g.median()
    mad = (g - med).abs().median()
    return 0.6745 * (g - med) / (mad if mad > 0 else 1e-9)   # MAD, not sigma

# Point deviation, measured against the counterparty's OWN history:
df['amt_z'] = df.groupby('counterparty_urn')['amount'].transform(robust_z)

# Contextual deviation: same hour, same weekday, same counterparty.
df['ctx_z'] = (df.groupby(['counterparty_urn', 'dow', 'hour'])['req_count']
                 .transform(robust_z))

feats = df[['amt_z', 'ctx_z', 'velocity_1h', 'new_device', 'geo_jump']].values

# max_samples=256 is the published sweet spot: larger subsamples increase
# swamping and cost, they do not improve recall.
iso = IsolationForest(n_estimators=200, max_samples=256,
                      contamination='auto', random_state=7).fit(feats)

score = -iso.score_samples(feats)        # higher = more anomalous (a RANK,
                                         # not a calibrated probability)

# Threshold = capacity quantile, NOT a guessed contamination rate.
alert_rate = REVIEWS_PER_DAY / EVENTS_PER_DAY        # 0.00025
threshold  = float(np.quantile(score, 1.0 - alert_rate))
df['alert'] = score >= threshold

assert df['alert'].sum() <= REVIEWS_PER_DAY * 1.2, 'review queue will overflow'

На практиці

Оператор CDN виявляв сплески трафіку глобальним правилом «середнє + 3σ» і отримував близько 4 100 сповіщень на добу. Перехід на базову лінію за тенантом (медіана/MAD) зі зняттям сезонності «день тижня × година» і поріг за квантилем спроможності знизив потік до 118 сповіщень на добу, зберігши виявлення 7 із 8 відомих інцидентів. Медіанний час розбору одного сповіщення впав із 6,2 до 1,1 хвилини, бо доказ спрацювання став локальним і зрозумілим.

Антипатерн

Порогування важкохвостої метрики через середнє і 3σ, а потім здивування, що детектор «нічого не бачить»: кілька великих викидів роздули σ настільки, що поріг проліз вище за них самих. Другий бік тієї ж помилки — лишити contamination=0.1 у продакшені й отримати чергу, що фізично не може бути розібрана.

Економіка хибних спрацювань: поріг обирає гаманець, а не F1

E7.3

Точність 99,9% на розрідженій задачі — порожня цифра. При поширеності шахрайства 0,1% детектор, який завжди відповідає «норма», має accuracy 99,9%. Єдина величина, що має значення для команди розбору, — прогностична цінність позитивного результату (PPV, вона ж precision), і вона визначається базовою частотою події не менше, ніж якістю моделі.

Арифметика, яку варто вміти рахувати усно. Мільйон транзакцій, поширеність 0,1% → 1 000 шахрайських. Детектор із чутливістю 90% і часткою хибнопозитивних 1% дає 900 істинних спрацювань і 9 990 хибних. Precision = 900 / 10 890 ≈ 8%: аналітик відкриває дванадцять справ, аби знайти одну. Щоб дійти до 50% precision за тієї ж чутливості, FPR має впасти приблизно до 0,09% — удесятеро. Саме тому на цьому шарі оптимізують специфічність, а не чутливість, і саме тому метрика, виміряна на збалансованій тестовій вибірці 1:1, не переноситься у продакшен узагалі.

Поріг обирає економіка. Порогом ви призначаєте ціну: E[cost](τ) = FN(τ)·L + FP(τ)·C, де L — середні втрати від пропуску, C — вартість розбору одного сповіщення (хвилини аналітика плюс тертя для клієнта). F1 неявно припускає L = C, що майже ніколи не так: у AML пропуск коштує тисячі, а розбір — одиниці; у медичному тріажі співвідношення протилежне. Оптимальний поріг — мінімум цієї кривої, і його треба перераховувати щоразу, коли змінюються L, C або штат.

Друга межа — спроможність. Навіть економічно оптимальний поріг марний, якщо черга росте швидше, ніж її розбирають: alerts/day ≤ analysts × shifts × 60 / minutes_per_alert. Перевищення не означає «знайдемо більше» — воно означає зростання часу до реакції, тобто фактичне падіння повноти, якого офлайн-метрика не показує.

Зворотний зв'язок отруює вибірку. Мітки надходять лише з розібраних сповіщень, тобто з-над порога. Модель, перенавчена винятково на них, дедалі впевненіше повторює власні минулі рішення, а зона під порогом стає сліпою плямою. Протиотрута коштує грошей і є обов'язковою: постійна випадкова аудит-вибірка 1–2% подій нижче порога, які розбирають попри низьку оцінку. Це єдиний спосіб виміряти хибнонегативні й отримати незміщену повноту.

Нова версія виходить у тінь. Претендент рахує оцінки на живому потоці, не породжуючи сповіщень; порівняння — за precision@k на однаковому бюджеті сповіщень, а не за AUC. Перемикати можна лише після того, як минув період затримки міток і результати за той самий проміжок стали відомі.

# Choosing the operating point by expected cost under a capacity constraint,
# not by argmax F1. Scores come from the shadow-scored live stream.
import numpy as np

LOSS_PER_FN  = 850.0     # EUR, measured average uncaught loss
COST_PER_FP  = 7.5       # EUR, 6 analyst-minutes + customer friction
CAPACITY     = 120       # alerts/day the team can actually clear
DAILY_EVENTS = 480_000

def operating_point(scores, labels):
    best = None
    for tau in np.quantile(scores, np.linspace(0.90, 0.99999, 400)):
        flag = scores >= tau
        tp = int((flag & (labels == 1)).sum())
        fp = int((flag & (labels == 0)).sum())
        fn = int((~flag & (labels == 1)).sum())
        alerts_day = float(flag.mean()) * DAILY_EVENTS
        if alerts_day > CAPACITY:        # hard constraint, tested FIRST
            continue                     # an unreviewable alert is not detection
        cost = fn * LOSS_PER_FN + fp * COST_PER_FP
        if best is None or cost < best['cost']:
            best = {'tau': float(tau), 'cost': cost,
                    'precision': tp / max(tp + fp, 1),
                    'recall':    tp / max(tp + fn, 1),
                    'alerts_day': alerts_day}
    return best

# Unbiased recall needs labels from BELOW the line. Keep a permanent random
# audit sample of non-alerted events in the review queue -- forever, not once.
audit_mask = np.random.rand(len(scores)) < 0.01

# NEVER report precision measured on a rebalanced test set: precision is not
# prevalence-invariant, and 1:1 down-sampling inflates it by ~100x here.

На практиці

Банк середнього розміру налаштував AML-детектор за максимумом F1 і отримав 2 400 сповіщень на добу проти спроможності 300. Медіанний час до розбору сягнув 9 днів, і фактична повнота впала до 31% при офлайн-повноті 88%. Перекалібрування за L = 850 € та C = 7,5 € з обмеженням у 300 сповіщень дало 290 сповіщень на добу, precision зросла з 4% до 19%, а кількість підтверджених повідомлень про підозрілі операції — на 22%.

Антипатерн

Звітувати precision, виміряну на тестовій вибірці зі штучно збалансованими класами: 92% у звіті перетворюються на 8% у продакшені, бо precision не інваріантна до базової частоти. Той самий клас помилки — перенавчання лише на розібраних сповіщеннях: модель оптимізує згоду сама з собою, а зона під порогом ніколи не перевіряється.

Джерела, з яких виведено напрям

  1. A Look Toward the Future Decision Suppor
  2. A Framework to Assess Intelligent Decisi
  3. A conceptualization and general architec
  4. A Mobile Emergency Triage Decision Suppo
  5. A Comparative Study of Anomaly Detection
  6. A Revealing Large Scale Evaluation of Un
  7. Graph Anomaly Detection with Graph Neural Networks
  8. Isolation Forest
  9. A Systematic Review on Anomaly Detection
  10. A Review of Anomaly based Intrusion Dete
  11. A Survey on Anomaly Based Network Intrus
  12. A Review of Machine Learning based Anoma

Першоджерела поза корпусом

  1. scikit-learn 1.9.1 Documentation: sklearn.ensemble.IsolationForest
  2. PyOD source code: pyod/models/iforest.py

Пройти інтерактивно

У кожного напряму є питання, картки з інтервальним повторенням і облік прогресу. Для них потрібен акаунт — безкоштовний і на одну хвилину.

Відкрити інтерактивний трек Створити безкоштовний акаунт

Далі в цьому треку