Как работать с аномалиями в данных?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как работать с аномалиями в данных?
Систематический подход к обнаружению и анализу аномалий
Аномалии - это значения, которые существенно отличаются от нормального паттерна. Они могут быть либо ценным сигналом (что-то сломалось, что-то изменилось), либо просто noise в данных. Как аналитик, я должен быстро отличить одно от другого.
1. Что такое аномалия
Аномалия - это значение, которое:
- Существенно отклоняется от среднего
- Нарушает ожидаемый паттерн
- Может быть вызвано technical issue или business event
Примеры:
Дневная выручка:
45000, 48000, 50000, 52000, 500000 (аномалия!), 49000, 51000
Daily Active Users:
10000, 10500, 9800, 11000, 100 (аномалия!), 9900, 10200
Page Load Time (ms):
200, 210, 190, 220, 5000 (аномалия!), 195, 205
2. Метод 1: Статистическое обнаружение (Statistical Anomaly Detection)
3-Sigma Rule
Самый простой метод: аномалия - это значение за 3 стандартные отклонения от среднего.
import pandas as pd
import numpy as np
data = [45000, 48000, 50000, 52000, 500000, 49000, 51000]
mean = np.mean(data)
std = np.std(data)
# Границы нормы
lower_bound = mean - 3 * std
upper_bound = mean + 3 * std
print(f"Mean: {mean}")
print(f"Std: {std}")
print(f"Normal range: [{lower_bound}, {upper_bound}]")
# Найти аномалии
anomalies = [x for x in data if x < lower_bound or x > upper_bound]
print(f"Anomalies: {anomalies}")
# Результат:
# Mean: 107571.43
# Std: 186542.31
# Normal range: [-452056.54, 667199.40]
# Anomalies: [] (не сработало - слишком влияние outliers)
Проблема: Если есть большой outlier, он сильно влияет на mean и std. Нужна robust версия.
Median Absolute Deviation (MAD)
Более robust метод:
from scipy import stats
data = [45000, 48000, 50000, 52000, 500000, 49000, 51000]
# Медиана
median = np.median(data)
# Абсолютное отклонение от медианы
mad = np.median(np.abs(data - median))
# Границы (обычно используют 2.5 для MAD вместо 3 для sigma)
modified_z_scores = 0.6745 * (np.array(data) - median) / mad
anomalies_idx = np.where(np.abs(modified_z_scores) > 3.5)[0]
anomalies = [data[i] for i in anomalies_idx]
print(f"Median: {median}")
print(f"MAD: {mad}")
print(f"Anomalies indices: {anomalies_idx}")
print(f"Anomalies: {anomalies}")
# Результат:
# Median: 50000
# MAD: 2500
# Anomalies indices: [4]
# Anomalies: [500000]
Это сработало! Обнаружена аномалия 500000.
3. Метод 2: Temporal Anomaly Detection (для временных рядов)
Когда нам важен тренд и сезонность:
Метод: Control limits
import pandas as pd
# Данные по неделям
weekly_data = pd.DataFrame({
'week': range(1, 14),
'revenue': [45000, 48000, 50000, 52000, 500000, 49000, 51000,
46000, 49000, 51000, 50000, 48000, 52000]
})
# Исключи неделю 5 (выброс) и пересчитай статистику
training_data = weekly_data[weekly_data['week'] != 5]
mean = training_data['revenue'].mean()
std = training_data['revenue'].std()
# Контрольные пределы (UCL/LCL)
UCL = mean + 2 * std # Upper Control Limit
LCL = mean - 2 * std # Lower Control Limit
weekly_data['is_anomaly'] = (
(weekly_data['revenue'] > UCL) |
(weekly_data['revenue'] < LCL)
)
print(weekly_data[weekly_data['is_anomaly']])
# Result:
# week revenue is_anomaly
# 4 5 500000 True
Метод: Moving Average + Thresholds
# Скользящее среднее (более гладко следит за трендом)
weekly_data['ma_7'] = weekly_data['revenue'].rolling(window=7, min_periods=1).mean()
weekly_data['deviation'] = abs(weekly_data['revenue'] - weekly_data['ma_7'])
weekly_data['threshold'] = weekly_data['revenue'].rolling(window=7, min_periods=1).std() * 2
weekly_data['is_anomaly_ma'] = weekly_data['deviation'] > weekly_data['threshold']
print(weekly_data[['week', 'revenue', 'ma_7', 'is_anomaly_ma']])
4. Метод 3: Isolation Forest (для сложных паттернов)
from sklearn.ensemble import IsolationForest
import numpy as np
data = np.array([45000, 48000, 50000, 52000, 500000, 49000, 51000,
46000, 49000, 51000, 50000, 48000, 52000]).reshape(-1, 1)
# Isolation Forest
if_model = IsolationForest(contamination=0.1) # ожидаем 10% аномалий
predictions = if_model.fit_predict(data)
anomalies = data[predictions == -1] # -1 = аномалия
print(f"Detected anomalies: {anomalies}")
# Результат: [[500000]]
Этот метод работает с многомерными данными и сложными паттернами.
5. Как я работаю с аномалиями
Фаза 1: Обнаружение
Мониторинг в реальном времени:
-- Alerta SQL для обнаружения аномалий
SELECT
DATE(created_at) as date,
COUNT(*) as daily_orders,
AVG(COUNT(*)) OVER (ORDER BY DATE(created_at) ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) as avg_orders_7d,
STDDEV(COUNT(*)) OVER (ORDER BY DATE(created_at) ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) as stddev_7d,
CASE
WHEN COUNT(*) > AVG(COUNT(*)) OVER (ORDER BY DATE(created_at) ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) +
2 * STDDEV(COUNT(*)) OVER (ORDER BY DATE(created_at) ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING)
THEN 'SPIKE (UP)'
WHEN COUNT(*) < AVG(COUNT(*)) OVER (ORDER BY DATE(created_at) ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) -
2 * STDDEV(COUNT(*)) OVER (ORDER BY DATE(created_at) ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING)
THEN 'SPIKE (DOWN)'
ELSE 'NORMAL'
END as status
FROM orders
WHERE DATE(created_at) >= NOW() - INTERVAL '30 days'
GROUP BY DATE(created_at)
ORDER BY date DESC;
Результат:
date | daily_orders | avg_orders_7d | status
2025-03-26 | 500 | 450 | NORMAL
2025-03-25 | 100 | 450 | SPIKE (DOWN) ⚠️
2025-03-24 | 460 | 450 | NORMAL
2025-03-23 | 480 | 450 | NORMAL
Фаза 2: Диагностика
Когда я вижу аномалию, моя первая реакция - НЕ паниковать. Это может быть:
- Technical issue (bugs, infrastructure)
- Business event (holiday, sale, marketing campaign)
- External event (weather, news, competitor action)
- Data error (wrong logging, sampling issue)
Мой checklist:
🔴 Обнаружена аномалия
↓
❓ Это во всех метриках или только в одной?
├─ Во всех → Business event или technical issue
└─ В одной → Может быть calculation error
↓
❓ Это в определенном сегменте или везде?
├─ В одной стране → Regional issue
├─ В мобиле но не в web → Platform issue
└─ Везде → General issue
↓
❓ Когда это началось?
├─ Ночью (3 AM) → Scheduled job issue
├─ После deployment → Code issue
├─ В выходной → Staffing issue
└─ Random time → Business/external event
↓
❓ Как долго это продолжается?
├─ < 1 часа → Кратковременный спайк (часто это noise)
├─ 1-4 часа → Есть время разобраться
└─ > 4 часа → URGENT
Фаза 3: Расследование
Пример: Daily Revenue упал с 50k на 10k
-- Шаг 1: Разбей по источникам
SELECT
utm_source,
SUM(amount) as revenue,
COUNT(*) as orders
FROM orders
WHERE DATE(created_at) = '2025-03-25'
GROUP BY utm_source
ORDER BY revenue DESC;
-- Результат:
-- utm_source | revenue | orders
-- organic | 9000 | 100
-- google | 500 | 5
-- email | 200 | 2
-- direct | 300 | 3
-- Шаг 2: Сравни с вчерашним днем
SELECT
utm_source,
SUM(CASE WHEN DATE(created_at) = '2025-03-25' THEN amount ELSE 0 END) as revenue_today,
SUM(CASE WHEN DATE(created_at) = '2025-03-24' THEN amount ELSE 0 END) as revenue_yesterday,
ROUND(100.0 * (SUM(CASE WHEN DATE(created_at) = '2025-03-25' THEN amount ELSE 0 END) /
SUM(CASE WHEN DATE(created_at) = '2025-03-24' THEN amount ELSE 0 END) - 1), 2) as pct_change
FROM orders
WHERE DATE(created_at) IN ('2025-03-25', '2025-03-24')
GROUP BY utm_source;
-- Результат:
-- utm_source | revenue_today | revenue_yesterday | pct_change
-- organic | 9000 | 45000 | -80%
-- google | 500 | 3000 | -83%
-- email | 200 | 1500 | -87%
-- Шаг 3: Разбей по платформе
SELECT
device_type,
SUM(amount) as revenue,
COUNT(*) as orders
FROM orders
WHERE DATE(created_at) = '2025-03-25'
GROUP BY device_type;
-- Результат:
-- device_type | revenue | orders
-- mobile | 2000 | 30
-- web | 8000 | 80
-- Вывод: Mobile revenue ОЧЕНЬ упала (-95%)
-- Шаг 4: Проверь технические метрики
SELECT
DATE(created_at) as date,
device_type,
AVG(page_load_time_ms) as avg_load_time,
COUNT(*) as sessions,
COUNT(CASE WHEN error_code IS NOT NULL THEN 1 END) as errors
FROM page_views
WHERE DATE(created_at) IN ('2025-03-25', '2025-03-24')
GROUP BY DATE(created_at), device_type;
-- Результат:
-- date | device_type | avg_load_time | sessions | errors
-- 2025-03-25 | mobile | 5000 ms | 500 | 200
-- 2025-03-24 | mobile | 200 ms | 5000 | 10
-- ВЫВОД: Page load time на мобиле вырос в 25 раз!
-- Это вызвало drop in conversion and revenue
Фаза 4: Решение
Теперь я знаю что произошло. Мобильная версия сайта медленная.
Действия:
1. IMMEDIATE:
- Notify Dev team URGENT
- Check recent deployments
- Rollback if new code
2. INVESTIGATION:
- Check server logs
- Check database query performance
- Check for traffic spike from bots
- Check CDN status
3. RECOVERY:
- Once fixed, monitor recovery
- Check if revenue returning to normal
- Send incident report
4. ROOT CAUSE ANALYSIS:
- What caused the slowdown?
- How to prevent next time?
- Update monitoring thresholds?
6. Типичные аномалии и причины
| Метрика | Аномалия | Вероятная причина | Проверить |
|---|---|---|---|
| Daily Revenue | Спайк вверх | Sale, marketing campaign, holiday shopping | Маркетинг календарь, news |
| Daily Revenue | Спайк вниз | Technical issue, payment processor down, bugs | Logs, error monitoring |
| DAU (Daily Active Users) | Спайк вниз | App crash, iOS/Android update issues | Crash reports, store reviews |
| Conversion Rate | Спайк вверх | A/B test winner, seasonal interest | Git commits, A/B test calendar |
| Page Load Time | Спайк вверх | High server load, bad deploy, CDN issue | Server metrics, deploy timeline |
| Support Tickets | Спайк вверх | Bug introduced, payment issues, service down | Error logs, status page |
| Database Query Time | Спайк вверх | Missing index, N+1 query, bad deploy | Database logs, recent changes |
7. Лучшие практики
DO:
- Настрой автоматические алерты для критических метрик
- Всегда разбирай аномалии (даже если кажется понятно)
- Документируй найденные причины
- Используй Historical data для контекста
- Уведоми команду сразу (не жди до утра)
DON'T:
- Не удаляй данные с аномалиями (это может быть важный сигнал)
- Не усредняй их (искажает метрики)
- Не игнорируй маленькие аномалии (они могут быть ранним сигналом)
- Не полагайся на один метод обнаружения (используй несколько)
- Не забывай про domain knowledge (статистика ≠ бизнес разум)
8. Пример dashboard для мониторинга
SELECT
CURRENT_DATE as date,
'DAILY METRICS HEALTH CHECK' as title,
-- Revenue anomaly
CASE
WHEN (SELECT SUM(amount) FROM orders WHERE DATE(created_at) = CURRENT_DATE) <
(SELECT AVG(daily_revenue) - 2 * STDDEV(daily_revenue) FROM (
SELECT DATE(created_at), SUM(amount) as daily_revenue
FROM orders
WHERE DATE(created_at) >= CURRENT_DATE - 30
GROUP BY DATE(created_at)
) t)
THEN '🔴 REVENUE DOWN'
ELSE '✓ Revenue OK'
END as revenue_status,
-- Conversion rate anomaly
CASE
WHEN (SELECT COUNT(CASE WHEN converted = true THEN 1 END)::float / COUNT(*) * 100
FROM sessions WHERE DATE(created_at) = CURRENT_DATE) <
(SELECT AVG(daily_conversion) - 2 * STDDEV(daily_conversion) FROM (
SELECT DATE(created_at),
COUNT(CASE WHEN converted = true THEN 1 END)::float / COUNT(*) * 100 as daily_conversion
FROM sessions
WHERE DATE(created_at) >= CURRENT_DATE - 30
GROUP BY DATE(created_at)
) t)
THEN '🔴 CONVERSION DOWN'
ELSE '✓ Conversion OK'
END as conversion_status,
-- Error rate anomaly
CASE
WHEN (SELECT COUNT(CASE WHEN error_code IS NOT NULL THEN 1 END)::float / COUNT(*) * 100
FROM requests WHERE DATE(created_at) = CURRENT_DATE) >
(SELECT AVG(daily_errors) + 2 * STDDEV(daily_errors) FROM (
SELECT DATE(created_at),
COUNT(CASE WHEN error_code IS NOT NULL THEN 1 END)::float / COUNT(*) * 100 as daily_errors
FROM requests
WHERE DATE(created_at) >= CURRENT_DATE - 30
GROUP BY DATE(created_at)
) t)
THEN '🔴 ERROR RATE UP'
ELSE '✓ Errors OK'
END as error_status;
Заключение
Аномалии - это либо opportunity либо danger, зависит от контекста. Как аналитик:
- Обнаруживаешь аномалии (статистические методы)
- Диагностируешь причину (SQL разложение по сегментам)
- Действуешь быстро (алерты, коммуникация)
- Документируешь и учишься на ошибках
Без систематического подхода к аномалиям, можно пропустить критические issues и сделать неправильные выводы из данных.