Головна › Архітектура корпоративної аналітики
Інтелект рішень і виявлення аномалій
Архітектура систем підтримки рішень, базові лінії та ізоляційний ліс, економіка хибних спрацювань і замикання циклу «сигнал → рішення → результат».
У цьому напрямі
Четвертий шар корпоративної інтелектуальної архітектури не виробляє даних — він виробляє рішення. Цей модуль показує, чому дашборд аномалій не є системою підтримки рішень; як насправді оцінює точку ізоляційний ліс і чому його параметр contamination — це план завантаження вашої команди, а не гіперпараметр моделі; та як обирати робочий поріг у грошах, а не у F1. Наскрізна теза одна: детектор, результати рішень якого ніхто не записує, не має ані міток для навчання, ані доказів власної цінності.
Від сигналу до дії: анатомія системи підтримки рішень
Дашборд не є системою підтримки рішень. Дашборд показує стан; СППР веде конкретну людину від сигналу до дії й записує, що з цього вийшло. Різниця не косметична: без записаного результату система ніколи не отримає міток, а без міток жодна модель усередині неї не здатна навчатися й жоден аналітик не доведе її окупність.
Чотири підсистеми класичної СППР — і що кожна означає в сучасній архітектурі:
- Дані — не рядки таблиць, а розв'язані сутності онтології: «цей платіж», «цей контрагент», «цей вузол мережі». Аномалія, прив'язана до
row_id, а не до сутності, не має адресата і не переживає злиття дублікатів. - Моделі — детектори, правила, симулятори. Модель дає оцінку, а не вирок.
- Знання — пороги, регламенти, права доступу, обмеження мети. Тут живе бізнес-політика, зокрема головне обмеження шару: скільки сповіщень команда фізично здатна розібрати за зміну.
- Діалог — інтерфейс рішення: чому спрацювало, які докази, які варіанти дій, хто власник і до якого терміну.
Рішення — це об'єкт першого класу. Мінімальний контракт запису: ідентифікатор сутності, тригер (яка модель і яка її версія), оцінка й чинний на той момент поріг, знімок доказів, обраний варіант дії, автор, час, клас оборотності та — обов'язково — поле 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% менше хибних спрацювань при незмінній повноті.Антипатерн
«Дашборд аномалій» без власника: сповіщення не має ані відповідального, ані терміну, ані поля результату, а успіх системи вимірюють кількістю виявлених аномалій. Такий контур не може виміряти жодного хибнонегативного випадку і через рік не має чим обґрунтувати власний бюджет.Базові лінії та ізоляційний ліс: щодо чого ви вимірюєте відхилення
Аномалія — це порушення очікування, а не велике число. Тому перше архітектурне рішення стосується не алгоритму, а бази порівняння: глобальний розподіл, конкретна сутність чи група однолітків. Платіж на 40 000 € аномальний для роздрібного клієнта і абсолютно нормальний для оптовика; глобальний поріг «сума > 10 000» — це не детектор, а фільтр, який щодня генерує ту саму сотню відомих імен.
Три класи аномалій вимагають трьох різних механізмів:
- Точкові — окреме спостереження далеке від решти. Робочий базовий рівень — робастний z-показник на медіані та MAD:
0.6745·(x−med)/MAD, а не середнє та σ. Причина проста: викиди самі роздувають σ і тим маскують себе, а медіана й MAD мають точку зламу 50%. - Контекстуальні — значення нормальне взагалі, але не в цьому контексті: 300 запитів о третій ночі в неділю. Потрібне зняття сезонності (день тижня × година) до оцінювання, інакше детектор щопонеділка о 9:00 повідомляє вам про початок робочого тижня.
- Колективні — жодна точка не є викидом, аномальна сама послідовність або підграф: 200 переказів по 900 € (структурування). Точкові методи тут безсилі за побудовою; потрібні віконні агрегати за сутністю або графові моделі над резолюцією сутностей.
Ізоляційний ліс: чому він працює. Замість описувати «норму», алгоритм ізолює точку випадковими розрізами за випадковими ознаками. Аномалії відтинаються за кілька розрізів, тож середня довжина шляху 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
Точність 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 не інваріантна до базової частоти. Той самий клас помилки — перенавчання лише на розібраних сповіщеннях: модель оптимізує згоду сама з собою, а зона під порогом ніколи не перевіряється.Джерела, з яких виведено напрям
- A Look Toward the Future Decision Suppor
- A Framework to Assess Intelligent Decisi
- A conceptualization and general architec
- A Mobile Emergency Triage Decision Suppo
- A Comparative Study of Anomaly Detection
- A Revealing Large Scale Evaluation of Un
- Graph Anomaly Detection with Graph Neural Networks
- Isolation Forest
- A Systematic Review on Anomaly Detection
- A Review of Anomaly based Intrusion Dete
- A Survey on Anomaly Based Network Intrus
- A Review of Machine Learning based Anoma
Першоджерела поза корпусом
- scikit-learn 1.9.1 Documentation: sklearn.ensemble.IsolationForest
- PyOD source code: pyod/models/iforest.py
Пройти інтерактивно
У кожного напряму є питання, картки з інтервальним повторенням і облік прогресу. Для них потрібен акаунт — безкоштовний і на одну хвилину.
Відкрити інтерактивний трек Створити безкоштовний акаунт