← Назад к вопросам

Как работать с аномалиями в данных?

2.0 Middle🔥 201 комментариев
#SQL и базы данных#Статистика и математика

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работать с аномалиями в данных?

Систематический подход к обнаружению и анализу аномалий

Аномалии - это значения, которые существенно отличаются от нормального паттерна. Они могут быть либо ценным сигналом (что-то сломалось, что-то изменилось), либо просто 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: Диагностика

Когда я вижу аномалию, моя первая реакция - НЕ паниковать. Это может быть:

  1. Technical issue (bugs, infrastructure)
  2. Business event (holiday, sale, marketing campaign)
  3. External event (weather, news, competitor action)
  4. 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, bugsLogs, error monitoring
DAU (Daily Active Users)Спайк внизApp crash, iOS/Android update issuesCrash reports, store reviews
Conversion RateСпайк вверхA/B test winner, seasonal interestGit commits, A/B test calendar
Page Load TimeСпайк вверхHigh server load, bad deploy, CDN issueServer metrics, deploy timeline
Support TicketsСпайк вверхBug introduced, payment issues, service downError logs, status page
Database Query TimeСпайк вверхMissing index, N+1 query, bad deployDatabase 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, зависит от контекста. Как аналитик:

  1. Обнаруживаешь аномалии (статистические методы)
  2. Диагностируешь причину (SQL разложение по сегментам)
  3. Действуешь быстро (алерты, коммуникация)
  4. Документируешь и учишься на ошибках

Без систематического подхода к аномалиям, можно пропустить критические issues и сделать неправильные выводы из данных.

Как работать с аномалиями в данных? | PrepBro