Statistics Basics

Викиди та аномалії

Методи виявлення викидів (IQR, Z-score), візуалізація через box plots, стратегії обробки та вплив на ML-моделі

Викиди та аномалії

🎯 Мета підтеми

  • Зрозуміти, що таке викиди (outliers) та чому вони важливі
  • Опанувати два методи виявлення: IQR та Z-score
  • Навчитися візуалізувати викиди через box plots
  • Вивчити стратегії обробки викидів
  • Усвідомити вплив викидів на ML-моделі

🔑 Ключові терміни

  • Викид (outlier) — значення, що значно відрізняється від інших спостережень
  • Метод IQR — виявлення через міжквартильний розмах
  • Метод Z-score — виявлення через стандартні відхилення
  • Box plot — діаграма «ящик з вусами» для візуалізації викидів
  • Робастні методи — алгоритми, стійкі до викидів

Історія про зарплати в стартапі

Уявіть: ви аналізуєте зарплати співробітників невеликого стартапу з 20 людей. Отримуєте такі дані (у тисячах $):

[45, 48, 50, 52, 47, 51, 49, 53, 46, 50, 48, 52, 51, 49, 47, 50, 51, 48, 49, 250]

Обчислюєте середню зарплату: $58.5k

Але чекайте! Більшість людей отримують ~$48–52k, а середнє — $58.5k. Що сталося?

Останнє значення — $250k — це зарплата CEO (співзасновника). Це викид (outlier), що драматично спотворює середнє значення.

startup_salaries.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np
import matplotlib.pyplot as plt

salaries = np.array([45, 48, 50, 52, 47, 51, 49, 53, 46, 50, 48, 52, 51, 49, 47, 50, 51, 48, 49, 250])

# Статистика з викидом
mean_with = salaries.mean()
median_with = np.median(salaries)
std_with = salaries.std()

# Статистика без викиду
salaries_clean = salaries[salaries < 100]
mean_without = salaries_clean.mean()
median_without = np.median(salaries_clean)
std_without = salaries_clean.std()

# Візуалізація
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# Графік 1: Гістограма з викидом
ax1.hist(salaries, bins=20, edgecolor='black', alpha=0.7, color='coral')
ax1.axvline(mean_with, color='red', linestyle='--', linewidth=2, label=f'Середнє = ${mean_with:.1f}k')
ax1.axvline(median_with, color='green', linestyle='--', linewidth=2, label=f'Медіана = ${median_with:.1f}k')
ax1.set_xlabel('Зарплата ($k)', fontsize=12)
ax1.set_ylabel('Частота', fontsize=12)
ax1.set_title('З викидом (CEO)', fontweight='bold')
ax1.legend()
ax1.grid(alpha=0.3)

# Графік 2: Гістограма без викиду
ax2.hist(salaries_clean, bins=10, edgecolor='black', alpha=0.7, color='steelblue')
ax2.axvline(mean_without, color='red', linestyle='--', linewidth=2, label=f'Середнє = ${mean_without:.1f}k')
ax2.axvline(median_without, color='green', linestyle='--', linewidth=2, label=f'Медіана = ${median_without:.1f}k')
ax2.set_xlabel('Зарплата ($k)', fontsize=12)
ax2.set_ylabel('Частота', fontsize=12)
ax2.set_title('Без викиду', fontweight='bold')
ax2.legend()
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.show()

print("📊 Порівняння статистики:\n")
print(f"{'':20} {'З викидом':>12} {'Без викиду':>12} {'Різниця':>12}")
print("-" * 60)
print(f"{'Середнє':<20} ${mean_with:>10.1f}k ${mean_without:>10.1f}k {(mean_with - mean_without):>+10.1f}k")
print(f"{'Медіана':<20} ${median_with:>10.1f}k ${median_without:>10.1f}k {(median_with - median_without):>+10.1f}k")
print(f"{'Стд. відхилення':<20} ${std_with:>10.1f}k ${std_without:>10.1f}k {(std_with - std_without):>+10.1f}k")

print("\n💡 Висновок:")
print("  Один викид ($250k) збільшив середнє на $9.4k (+19%)")
print("  Медіана майже не змінилася (стійка до викидів)")
print("  Стандартне відхилення зросло в 20 разів!")
Out[1]:

📊 Порівняння статистики:

                      З викидом  Без викиду     Різниця
------------------------------------------------------------
Середнє                  $58.5k      $49.1k      +9.4k
Медіана                  $49.5k      $49.5k      +0.0k
Стд. відхилення          $45.4k       $2.1k     +43.3k

💡 Висновок:
  Один викид ($250k) збільшив середнє на $9.4k (+19%)
  Медіана майже не змінилася (стійка до викидів)
  Стандартне відхилення зросло в 20 разів!

Аналогія: Викид — це як камінь у взутті. Якщо його не помітити, весь шлях буде некомфортним. Один екстремальний елемент може зіпсувати всю картину даних та спотворити висновки.


Що таке викид (Outlier)?

Викид (outlier) — це спостереження, яке значно відрізняється від решти даних. Воно лежить далеко від основної маси значень.

Види викидів

  1. Природні викиди (natural outliers) — реальні, але рідкісні значення
    • Білл Гейтс у датасеті доходів американців
    • Землетрус магнітудою 9.0 (99.9% землетрусів < 5.0)
    • Геніальний студент з IQ 160 у класі
  2. Помилки вимірювання (measurement errors) — технічні збої, людські помилки
    • Сенсор записав температуру −999°C (фізично неможливо)
    • Оператор ввів 1,000,000 замість 1,000 (зайва цифра)
    • Вік людини = 200 років (помилка введення)
  3. Помилки обробки даних (data processing errors)
    • Об'єднання датасетів з різними одиницями вимірювання (метри + сантиметри)
    • Дублювання запису

Чому викиди важливі?

❌ Шкода від викидів

  • Спотворюють середнє та стандартне відхилення
  • Порушують припущення ML-алгоритмів (нормальність, лінійність)
  • Погіршують якість моделей (особливо лінійна регресія, KNN)
  • Вводять в оману при інтерпретації даних

✅ Цінність викидів

  • Сигнал аномалій (шахрайство, несправність обладнання)
  • Рідкісні, але важливі події (епідемія, фінансова криза)
  • Нові знання про систему (екстремальні умови)
Критична помилка: Автоматичне видалення всіх викидів без аналізу!Перед видаленням викиду запитайте:
  1. Це помилка вимірювання (можна видалити) чи реальне значення (потрібно зберегти)?
  2. Чи це цільова аномалія (наприклад, шахрайство у фінансових даних)?
  3. Чи модель має бути робастною до екстремальних значень?
Золоте правило: Розумійте природу викиду перед його обробкою!
У програмній інженерії викиди — це не просто «сміття в датасеті», яке треба видалити. У багатьох доменах викид є найдорожчим та найважливішим сигналом системи:
  1. FinTech та захист від шахрайства (Antifraud):
    • Уявіть користувача, який щодня купує каву за $4 та продукти в супермаркеті за $45. Раптом о 03:45 ночі з його акаунта надходить платіж на $9,800 на криптобіржу з нової IP-адреси. Зі статистичного погляду це колосальний викид ($z > 15$).
    • Якщо ML-інженер автоматично відфільтрує цей запис під час очищення датасету (вважаючи його «шумом»), він знищить саме ту аномалію, заради пошуку якої створюється модель. У системах Antifraud викид — це головний цільовий клас (target label).
  2. DevOps та профілювання пам'яті (Memory Leak Profiling):
    • Якщо 999 контейнерів мікросервісу в кластері Kubernetes стабільно тримають 250 MB оперативної пам'яті, а один контейнер раптово споживає 3.9 GB і падає через OOMKilled — цей статистичний викид є єдиним свідченням циклічного витоку пам'яті (memory leak) або нескінченної рекурсії у фоновому worker-і. Видалити таку аномалію з логів означає проігнорувати системний збій.

Метод 1: IQR (Міжквартильний розмах)

Метод IQR (Interquartile Range) — найпопулярніший робастний метод виявлення викидів, заснований на квартилях.

Крок за кроком

  1. Обчислити Q1 (25-й перцентиль) та Q3 (75-й перцентиль)
  2. Обчислити IQR = Q3 − Q1 (міжквартильний розмах)
  3. Визначити межі:
    • Нижня межа: Q1 − 1.5 × IQR
    • Верхня межа: Q3 + 1.5 × IQR
  4. Значення поза межами = викиди
Завантаження...
\begin{aligned} \text{IQR} &= Q_3 - Q_1 \ \text{Нижня межа} &= Q_1 - 1.5 \times \text{IQR} \ \text{Верхня межа} &= Q_3 + 1.5 \times \text{IQR} \end{aligned}

Чому 1.5? Це емпіричне правило, запропоноване Джоном Тьюкі. Воно виявляє ~0.7% найекстремальніших значень у нормальному розподілі. Іноді використовують 3.0 для більш консервативного виявлення.

iqr_method.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(seed=42)

# Генеруємо дані з викидами
data = np.concatenate([
    rng.normal(50, 10, 95),  # Основна маса
    np.array([10, 15, 90, 95, 100])  # Викиди
])

# Крок 1: Обчислюємо квартилі
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1

# Крок 2: Обчислюємо межі
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# Крок 3: Виявляємо викиди
outliers = data[(data < lower_bound) | (data > upper_bound)]
inliers = data[(data >= lower_bound) & (data <= upper_bound)]

# Візуалізація
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# Графік 1: Гістограма з межами
ax1.hist(data, bins=30, edgecolor='black', alpha=0.7, color='lightblue')
ax1.axvline(Q1, color='orange', linestyle='--', linewidth=2, label=f'Q1 = {Q1:.1f}')
ax1.axvline(Q3, color='orange', linestyle='--', linewidth=2, label=f'Q3 = {Q3:.1f}')
ax1.axvline(lower_bound, color='red', linestyle='-', linewidth=2, label=f'Нижня межа = {lower_bound:.1f}')
ax1.axvline(upper_bound, color='red', linestyle='-', linewidth=2, label=f'Верхня межа = {upper_bound:.1f}')
ax1.set_xlabel('Значення', fontsize=12)
ax1.set_ylabel('Частота', fontsize=12)
ax1.set_title('Метод IQR: межі викидів', fontweight='bold')
ax1.legend(loc='upper left')
ax1.grid(alpha=0.3)

# Графік 2: Точки (inliers vs outliers)
ax2.scatter(range(len(inliers)), inliers, c='green', alpha=0.6, s=50, edgecolors='black', linewidths=0.5, label='Нормальні')
ax2.scatter(range(len(inliers), len(data)), outliers, c='red', s=100, edgecolors='black', linewidths=1, label='Викиди', marker='^')
ax2.axhline(lower_bound, color='red', linestyle='--', linewidth=1.5, alpha=0.7)
ax2.axhline(upper_bound, color='red', linestyle='--', linewidth=1.5, alpha=0.7)
ax2.set_xlabel('Індекс', fontsize=12)
ax2.set_ylabel('Значення', fontsize=12)
ax2.set_title('Виявлені викиди', fontweight='bold')
ax2.legend()
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.show()

print("📊 Результати методу IQR:\n")
print(f"  Q1 (25-й перцентиль): {Q1:.2f}")
print(f"  Q3 (75-й перцентиль): {Q3:.2f}")
print(f"  IQR: {IQR:.2f}")
print(f"  Нижня межа: {lower_bound:.2f}")
print(f"  Верхня межа: {upper_bound:.2f}")
print(f"\n🔍 Виявлено викидів: {len(outliers)} / {len(data)} ({len(outliers)/len(data)*100:.1f}%)")
print(f"  Викиди: {sorted(outliers)}")
Out[1]:

📊 Результати методу IQR:

  Q1 (25-й перцентиль): 42.45
  Q3 (75-й перцентиль): 57.78
  IQR: 15.33
  Нижня межа: 19.45
  Верхня межа: 80.78

🔍 Виявлено викидів: 5 / 100 (5.0%)
  Викиди: [10.0, 15.0, 90.0, 95.0, 100.0]

Box Plot (діаграма «ящик з вусами»)

Box plot — найкращий спосіб візуалізувати викиди методом IQR. Він показує:

  • Ящик (box) — від Q1 до Q3 (50% середніх даних)
  • Лінія всередині ящика — медіана (Q2)
  • Вуса (whiskers) — від Q1 − 1.5×IQR до Q3 + 1.5×IQR
  • Точки поза вусами — викиди
box_plot_visualization.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(seed=42)

# Три датасети з різною кількістю викидів
data_clean = rng.normal(50, 10, 100)
data_few_outliers = np.concatenate([rng.normal(50, 10, 95), np.array([10, 15, 90, 95, 100])])
data_many_outliers = np.concatenate([rng.normal(50, 10, 80), rng.uniform(0, 20, 10), rng.uniform(80, 100, 10)])

datasets = [data_clean, data_few_outliers, data_many_outliers]
labels = ['Без викидів', 'Кілька викидів (5%)', 'Багато викидів (20%)']

# Візуалізація
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

for ax, data, label in zip(axes, datasets, labels):
    bp = ax.boxplot(data, vert=True, patch_artist=True, 
                     boxprops=dict(facecolor='lightblue', edgecolor='black'),
                     whiskerprops=dict(color='black', linewidth=1.5),
                     capprops=dict(color='black', linewidth=1.5),
                     medianprops=dict(color='red', linewidth=2),
                     flierprops=dict(marker='o', markerfacecolor='red', markersize=8, linestyle='none', markeredgecolor='black'))
    
    ax.set_ylabel('Значення', fontsize=12)
    ax.set_title(label, fontweight='bold', fontsize=12)
    ax.grid(axis='y', alpha=0.3)
    
    # Анотації
    Q1 = np.percentile(data, 25)
    Q3 = np.percentile(data, 75)
    median = np.median(data)
    
    ax.text(1.15, Q1, f'Q1={Q1:.1f}', fontsize=9, va='center')
    ax.text(1.15, Q3, f'Q3={Q3:.1f}', fontsize=9, va='center')
    ax.text(1.15, median, f'Q2={median:.1f}', fontsize=9, va='center', color='red', fontweight='bold')

plt.tight_layout()
plt.show()

print("📦 Інтерпретація Box Plot:\n")
print("  🟦 Ящик (box): 50% середніх даних (від Q1 до Q3)")
print("  🔴 Червона лінія: медіана (Q2)")
print("  📏 Вуса (whiskers): від Q1−1.5×IQR до Q3+1.5×IQR")
print("  🔴 Червоні точки: викиди (поза вусами)")
print("\n💡 Чим довші вуса → тим більший розкид даних")
print("💡 Чим більше червоних точок → тим більше викидів")
Out[1]:

📦 Інтерпретація Box Plot:

  🟦 Ящик (box): 50% середніх даних (від Q1 до Q3)
  🔴 Червона лінія: медіана (Q2)
  📏 Вуса (whiskers): від Q1−1.5×IQR до Q3+1.5×IQR
  🔴 Червоні точки: викиди (поза вусами)

💡 Чим довші вуса → тим більший розкид даних
💡 Чим більше червоних точок → тим більше викидів
Коли використовувати метод IQR:
  • ✅ Дані не обов'язково нормальні (робастний метод)
  • ✅ Потрібна візуалізація (box plot)
  • ✅ Швидке виявлення екстремальних значень
  • ✅ Універсальний для більшості типів даних
Обмеження:
  • ❌ Не враховує форму розподілу
  • ❌ Може пропустити викиди в дуже скошених розподілах

Метод 2: Z-Score (Стандартизація)

Метод Z-score виявляє викиди через стандартні відхилення від середнього. Підходить для нормально розподілених даних.

Алгоритм

  1. Обчислити Z-оцінку для кожного значення:
Завантаження...
  1. Визначити поріг (зазвичай |z| > 3)
  2. Значення з |z| > поріг = викиди

Інтуїція: Згадайте правило 68-95-99.7:

  • 99.7% даних лежать у межах μ ± 3σ
  • Отже, |z| > 3 → значення екстремально рідкісне (< 0.3% ймовірність)

Анатомія математичної формули Z-score

Формула стандартизації перетворює значення будь-якої фізичної природи (долари, мілісекунди, гігабайти) на безрозмірне число, яке показує відхилення в масштабі самого процесу:

  1. Центрування ($(x - \mu)$):
    • Віднімання математичного сподівання знаходить абсолютну векторну відстань спостереження від центру розподілу. Якщо $x > \mu$, чисельник додатний; якщо $x < \mu$ — від'ємний.
    • Фізична розмірність на цьому кроці зберігається (наприклад, $1500\text{ ms} - 200\text{ ms} = +1300\text{ ms}$).
  2. Масштабування ($\frac{1}{\sigma}$):
    • Ділення на стандартне відхилення слугує «універсальною калібрувальною лінійкою». Воно відповідає на питання: «Скільки типових інтервалів розкиду міститься в цьому конкретному відхиленні?».
    • Одиниці вимірювання чисельника та знаменника взаємно скорочуються ($\frac{\text{ms}}{\text{ms}}$), утворюючи абсолютно безрозмірний індекс.
  3. Практична інтерпретація результату $z$:
    • $z = 0$: значення ідеально збігається з центром вибірки.
    • $z = +3.5$: точка віддалена від середнього на три з половиною типових розмахи вгору.
    • Завдяки Z-score інженер може на одному графіку відстежувати аномальність параметрів несумісних розмірностей: наприклад, порівнювати, що є більшою аномалією в конкретну секунду — навантаження CPU ($z = +3.1$) чи затримка звернення до бази даних ($z = +5.4$).
z_score_method.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(seed=42)

# Генеруємо нормально розподілені дані з викидами
data = np.concatenate([
    rng.normal(100, 15, 95),  # Основна маса N(100, 15²)
    np.array([40, 45, 160, 165, 170])  # Викиди
])

# Обчислюємо Z-оцінки
mean = data.mean()
std = data.std()
z_scores = (data - mean) / std

# Визначаємо поріг (зазвичай 3)
threshold = 3
outliers_mask = np.abs(z_scores) > threshold

outliers = data[outliers_mask]
inliers = data[~outliers_mask]
z_outliers = z_scores[outliers_mask]
z_inliers = z_scores[~outliers_mask]

# Візуалізація
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# Графік 1: Оригінальні дані
ax1.scatter(range(len(inliers)), inliers, c='green', alpha=0.6, s=50, edgecolors='black', linewidths=0.5, label='Нормальні')
ax1.scatter(np.where(outliers_mask)[0], outliers, c='red', s=100, edgecolors='black', linewidths=1, label='Викиди', marker='^')
ax1.axhline(mean, color='blue', linestyle='--', linewidth=2, label=f'μ = {mean:.1f}')
ax1.axhline(mean + 3*std, color='red', linestyle='--', linewidth=1.5, alpha=0.7, label=f'μ+3σ = {mean + 3*std:.1f}')
ax1.axhline(mean - 3*std, color='red', linestyle='--', linewidth=1.5, alpha=0.7, label=f'μ-3σ = {mean - 3*std:.1f}')
ax1.set_xlabel('Індекс', fontsize=12)
ax1.set_ylabel('Значення', fontsize=12)
ax1.set_title('Оригінальні дані з межами ±3σ', fontweight='bold')
ax1.legend()
ax1.grid(alpha=0.3)

# Графік 2: Z-оцінки
ax2.scatter(range(len(z_inliers)), z_inliers, c='green', alpha=0.6, s=50, edgecolors='black', linewidths=0.5, label='Нормальні')
ax2.scatter(np.where(outliers_mask)[0], z_outliers, c='red', s=100, edgecolors='black', linewidths=1, label='Викиди', marker='^')
ax2.axhline(0, color='blue', linestyle='--', linewidth=2, label='z = 0 (середнє)')
ax2.axhline(3, color='red', linestyle='--', linewidth=1.5, alpha=0.7, label='z = +3')
ax2.axhline(-3, color='red', linestyle='--', linewidth=1.5, alpha=0.7, label='z = -3')
ax2.set_xlabel('Індекс', fontsize=12)
ax2.set_ylabel('Z-оцінка', fontsize=12)
ax2.set_title('Z-оцінки (стандартизовані)', fontweight='bold')
ax2.legend()
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.show()

print("📊 Результати методу Z-score:\n")
print(f"  Середнє (μ): {mean:.2f}")
print(f"  Стандартне відхилення (σ): {std:.2f}")
print(f"  Поріг: |z| > {threshold}")
print(f"\n🔍 Виявлено викидів: {outliers_mask.sum()} / {len(data)} ({outliers_mask.sum()/len(data)*100:.1f}%)")
print(f"\n  Викиди (значення → Z-оцінка):")
for val, z in zip(outliers, z_outliers):
    print(f"    {val:.1f} → z = {z:+.2f}")
Out[1]:

📊 Результати методу Z-score:

  Середнє (μ): 101.25
  Стандартне відхилення (σ): 17.82
  Поріг: |z| > 3

🔍 Виявлено викидів: 5 / 100 (5.0%)

  Викиди (значення → Z-оцінка):
    40.0 → z = -3.44
    45.0 → z = -3.16
    160.0 → z = +3.30
    165.0 → z = +3.58
    170.0 → z = +3.86
Важлива пастка методу Z-score:Метод Z-score сам залежить від викидів! Викиди впливають на середнє та стандартне відхилення, що може приховати екстремальні значення.Приклад: Якщо є один дуже великий викид, він збільшує μ та σ. Тоді його власна Z-оцінка може виявитися < 3, і він не буде виявлений!Рішення:
  • Використовуйте робастні оцінки (медіану замість середнього, MAD замість σ)
  • Або спочатку застосуйте метод IQR, потім Z-score на очищених даних

Порівняння порогів Z-score

z_score_thresholds.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np
from scipy import stats

rng = np.random.default_rng(seed=42)
data = rng.normal(100, 15, 1000)

thresholds = [2, 2.5, 3, 3.5]

print("📊 Вплив порогу Z-score на виявлення викидів:\n")
print(f"{'Поріг |z|':<12} {'Викидів':<10} {'%':<8} Теоретично (для N(0,1))")
print("-" * 60)

for thresh in thresholds:
    z_scores = np.abs((data - data.mean()) / data.std())
    outliers_count = (z_scores > thresh).sum()
    outliers_pct = outliers_count / len(data) * 100
    
    # Теоретична ймовірність для нормального розподілу
    theoretical_pct = (1 - stats.norm.cdf(thresh)) * 2 * 100
    
    print(f"  >{thresh:<10} {outliers_count:<10} {outliers_pct:>5.1f}%   ~{theoretical_pct:.2f}%")

print("\n💡 Висновок:")
print("  Більший поріг → менше викидів (консервативний підхід)")
print("  Менший поріг → більше викидів (агресивний підхід)")
print("  Стандарт: |z| > 3 (виявляє ~0.3% найекстремальніших значень)")
Out[1]:
📊 Вплив порогу Z-score на виявлення викидів:

Поріг |z|    Викидів    %       Теоретично (для N(0,1))
------------------------------------------------------------
  >2           46          4.6%   ~4.55%
  >2.5         13          1.3%   ~1.24%
  >3            3          0.3%   ~0.27%
  >3.5          0          0.0%   ~0.05%

💡 Висновок:
  Більший поріг → менше викидів (консервативний підхід)
  Менший поріг → більше викидів (агресивний підхід)
  Стандарт: |z| > 3 (виявляє ~0.3% найекстремальніших значень)
Коли використовувати метод Z-score:
  • ✅ Дані приблизно нормально розподілені
  • ✅ Потрібна числова міра відхилення (скільки σ)
  • ✅ Швидке обчислення для великих датасетів
Обмеження:
  • ❌ Не робастний — сам залежить від викидів
  • ❌ Не підходить для скошених розподілів
  • ❌ Припускає симетричний розподіл

Стратегії обробки викидів

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

1. Видалення (Removal)

Ідея: Просто видалити рядки з викидами з датасету.

Коли використовувати:

  • ✅ Викид — явна помилка (сенсор показав −999°C, вік = 200 років)
  • ✅ Викидів мало (< 5% даних)
  • ✅ Видалення не зменшує датасет критично

Обережно:

  • ❌ Втрата інформації
  • ❌ Може спотворити розподіл (якщо викидів багато)
  • ❌ Не підходить, якщо викид — цільова аномалія (шахрайство, несправність)
outlier_removal.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np

rng = np.random.default_rng(seed=42)

# Генеруємо дані
data = np.concatenate([
    rng.normal(50, 10, 95),
    np.array([10, 15, 90, 95, 100])  # 5 викидів
])

# Видалення через IQR
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

data_clean = data[(data >= lower_bound) & (data <= upper_bound)]

print("📊 Результати видалення викидів:\n")
print(f"  Розмір до:  {len(data)} зразків")
print(f"  Розмір після: {len(data_clean)} зразків")
print(f"  Видалено: {len(data) - len(data_clean)} зразків ({(len(data) - len(data_clean))/len(data)*100:.1f}%)")
print(f"\n  Середнє до:  {data.mean():.2f}")
print(f"  Середнє після: {data_clean.mean():.2f}")
print(f"  Різниця: {abs(data.mean() - data_clean.mean()):.2f}")
Out[1]:
📊 Результати видалення викидів:

  Розмір до:  100 зразків
  Розмір після: 95 зразків
  Видалено: 5 зразків (5.0%)

  Середнє до:  51.75
  Середнє після: 50.12
  Різниця: 1.63

2. Обрізання (Capping / Winsorization)

Ідея: Замінити викиди на граничні допустимі значення (нижню/верхню межу).

Приклад: Якщо верхня межа = 80, всі значення > 80 стають 80.

Коли використовувати:

  • ✅ Важливо зберегти всі рядки датасету
  • ✅ Викид — реальне значення, але екстремальне
  • ✅ Модель чутлива до викидів (лінійна регресія)
outlier_capping.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np

rng = np.random.default_rng(seed=42)

data = np.concatenate([
    rng.normal(50, 10, 95),
    np.array([10, 15, 90, 95, 100])
])

# Обрізання через IQR
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

data_capped = np.copy(data)
data_capped[data_capped < lower_bound] = lower_bound
data_capped[data_capped > upper_bound] = upper_bound

print("📊 Результати обрізання викидів:\n")
print(f"  Розмір: {len(data)} → {len(data_capped)} (не змінився)")
print(f"\n  Змінені значення:")
print(f"    10 → {lower_bound:.2f} (нижня межа)")
print(f"    15 → {lower_bound:.2f} (нижня межа)")
print(f"    90 → {upper_bound:.2f} (верхня межа)")
print(f"    95 → {upper_bound:.2f} (верхня межа)")
print(f"    100 → {upper_bound:.2f} (верхня межа)")
print(f"\n  Середнє до:  {data.mean():.2f}")
print(f"  Середнє після: {data_capped.mean():.2f}")
Out[1]:
📊 Результати обрізання викидів:

  Розмір: 100 → 100 (не змінився)

  Змінені значення:
    10 → 19.45 (нижня межа)
    15 → 19.45 (нижня межа)
    90 → 80.78 (верхня межа)
    95 → 80.78 (верхня межа)
    100 → 80.78 (верхня межа)

  Середнє до:  51.75
  Середнє після: 50.69

3. Трансформація (Transformation)

Ідея: Застосувати математичну трансформацію, яка зменшує вплив викидів.

Популярні трансформації:

  • Логарифм: log(x) — стискає великі значення
  • Квадратний корінь: √x — м'якша, ніж логарифм
  • Box-Cox — автоматичний вибір оптимальної трансформації

Коли використовувати:

  • ✅ Дані скошені (довгий хвіст)
  • ✅ Викиди — природні, але екстремальні (доходи, ціни на акції)
  • ✅ Потрібно наблизити до нормального розподілу
outlier_transformation.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(seed=42)

# Генеруємо скошені дані (логнормальний розподіл)
data = rng.lognormal(mean=3, sigma=0.8, size=200)

# Трансформація логарифмом
data_log = np.log(data)

# Візуалізація
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

ax1.hist(data, bins=30, edgecolor='black', alpha=0.7, color='coral')
ax1.set_xlabel('Значення', fontsize=12)
ax1.set_ylabel('Частота', fontsize=12)
ax1.set_title('До трансформації (скошений розподіл)', fontweight='bold')
ax1.grid(alpha=0.3)

ax2.hist(data_log, bins=30, edgecolor='black', alpha=0.7, color='lightgreen')
ax2.set_xlabel('log(Значення)', fontsize=12)
ax2.set_ylabel('Частота', fontsize=12)
ax2.set_title('Після log-трансформації (ближче до нормального)', fontweight='bold')
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.show()

print("📊 Результати трансформації:\n")
print(f"  До трансформації:")
print(f"    Середнє: {data.mean():.2f}")
print(f"    Стд. відхилення: {data.std():.2f}")
print(f"    Min: {data.min():.2f}, Max: {data.max():.2f}")
print(f"\n  Після log-трансформації:")
print(f"    Середнє: {data_log.mean():.2f}")
print(f"    Стд. відхилення: {data_log.std():.2f}")
print(f"    Min: {data_log.min():.2f}, Max: {data_log.max():.2f}")
print(f"\n💡 Розкид зменшився, розподіл став більш симетричним")
Out[1]:

📊 Результати трансформації:

  До трансформації:
    Середнє: 24.18
    Стд. відхилення: 20.12
    Min: 3.52, Max: 140.29

  Після log-трансформації:
    Середнє: 3.02
    Стд. відхилення: 0.79
    Min: 1.26, Max: 4.94

💡 Розкид зменшився, розподіл став більш симетричним

4. Заміна на імпутацію (Imputation)

Ідея: Замінити викиди на середнє, медіану або передбачене значення.

Коли використовувати:

  • ✅ Викид — помилка, але видалити рядок не можна (мало даних)
  • ✅ Важливіша повнота датасету, ніж точність окремих значень

Методи:

  • Середнє / Медіана
  • Інтерполяція (для часових рядів)
  • Модельна імпутація (KNN, регресія)

5. Залишити як є

Ідея: Нічого не робити з викидами.

Коли використовувати:

  • ✅ Викид — цільова аномалія (виявлення шахрайства, несправностей)
  • ✅ Використовуєте робастні алгоритми (дерева рішень, Random Forest, Gradient Boosting)
  • ✅ Викид несе важливу інформацію
Рекомендації за типом ML-моделі:Чутливі до викидів:
  • Лінійна регресія → видаліть або обріжте
  • KNN → стандартизуйте + видаліть екстремальні
  • SVM → стандартизуйте
Робастні (стійкі) до викидів:
  • Дерева рішень → можна залишити
  • Random Forest → можна залишити
  • Gradient Boosting → можна залишити
  • Медіанна регресія → спеціально для викидів

Порівняння методів IQR vs Z-Score

iqr_vs_zscore_comparison.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(seed=42)

# Генеруємо три типи даних
data_normal = rng.normal(50, 10, 100)
data_skewed = rng.exponential(20, 100)
data_with_outliers = np.concatenate([rng.normal(50, 10, 95), np.array([10, 15, 90, 95, 100])])

datasets = [data_normal, data_skewed, data_with_outliers]
titles = ['Нормальний розподіл', 'Скошений розподіл', 'З викидами']

fig, axes = plt.subplots(3, 2, figsize=(14, 10))

for row, (data, title) in enumerate(zip(datasets, titles)):
    # Метод IQR
    Q1 = np.percentile(data, 25)
    Q3 = np.percentile(data, 75)
    IQR = Q3 - Q1
    iqr_lower = Q1 - 1.5 * IQR
    iqr_upper = Q3 + 1.5 * IQR
    iqr_outliers = (data < iqr_lower) | (data > iqr_upper)
    
    # Метод Z-score
    mean = data.mean()
    std = data.std()
    z_scores = np.abs((data - mean) / std)
    z_outliers = z_scores > 3
    
    # Графік IQR
    axes[row, 0].scatter(range(len(data)), data, c=['red' if o else 'green' for o in iqr_outliers], 
                         s=50, alpha=0.6, edgecolors='black', linewidths=0.5)
    axes[row, 0].axhline(iqr_lower, color='red', linestyle='--', linewidth=1.5, label='IQR межі')
    axes[row, 0].axhline(iqr_upper, color='red', linestyle='--', linewidth=1.5)
    axes[row, 0].set_title(f'{title} — Метод IQR', fontweight='bold')
    axes[row, 0].set_ylabel('Значення')
    axes[row, 0].legend()
    axes[row, 0].grid(alpha=0.3)
    axes[row, 0].text(0.02, 0.98, f'Викидів: {iqr_outliers.sum()} ({iqr_outliers.sum()/len(data)*100:.0f}%)', 
                      transform=axes[row, 0].transAxes, va='top', fontsize=10,
                      bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7))
    
    # Графік Z-score
    axes[row, 1].scatter(range(len(data)), data, c=['red' if o else 'green' for o in z_outliers], 
                         s=50, alpha=0.6, edgecolors='black', linewidths=0.5)
    axes[row, 1].axhline(mean + 3*std, color='red', linestyle='--', linewidth=1.5, label='±3σ межі')
    axes[row, 1].axhline(mean - 3*std, color='red', linestyle='--', linewidth=1.5)
    axes[row, 1].set_title(f'{title} — Метод Z-score', fontweight='bold')
    axes[row, 1].set_ylabel('Значення')
    axes[row, 1].legend()
    axes[row, 1].grid(alpha=0.3)
    axes[row, 1].text(0.02, 0.98, f'Викидів: {z_outliers.sum()} ({z_outliers.sum()/len(data)*100:.0f}%)', 
                      transform=axes[row, 1].transAxes, va='top', fontsize=10,
                      bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7))

plt.tight_layout()
plt.show()

print("📊 Висновки порівняння:\n")
print("  IQR (Міжквартильний розмах):")
print("    ✅ Робастний — не залежить від викидів")
print("    ✅ Працює для будь-яких розподілів")
print("    ✅ Візуалізація через box plot")
print("    ❌ Може виявити більше 'хибних' викидів у довгих хвостах\n")
print("  Z-Score (Стандартизація):")
print("    ✅ Інтуїтивний (скільки σ від середнього)")
print("    ✅ Підходить для нормальних розподілів")
print("    ❌ Не робастний — залежить від викидів")
print("    ❌ Не підходить для скошених розподілів")
Out[1]:

📊 Висновки порівняння:

  IQR (Міжквартильний розмах):
    ✅ Робастний — не залежить від викидів
    ✅ Працює для будь-яких розподілів
    ✅ Візуалізація через box plot
    ❌ Може виявити більше 'хибних' викидів у довгих хвостах

  Z-Score (Стандартизація):
    ✅ Інтуїтивний (скільки σ від середнього)
    ✅ Підходить для нормальних розподілів
    ❌ Не робастний — залежить від викидів
    ❌ Не підходить для скошених розподілів

Вплив викидів на ML-моделі

outliers_impact_on_ml.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor

rng = np.random.default_rng(seed=42)

# Генеруємо дані для регресії
X_clean = np.linspace(0, 10, 50).reshape(-1, 1)
y_clean = 2 * X_clean.ravel() + 3 + rng.normal(0, 1, 50)

# Додаємо 3 викиди
X_outliers = np.array([[2], [5], [8]])
y_outliers = np.array([20, 22, 24])

X_with_outliers = np.vstack([X_clean, X_outliers])
y_with_outliers = np.hstack([y_clean, y_outliers])

# Тренуємо моделі
lr_clean = LinearRegression().fit(X_clean, y_clean)
lr_outliers = LinearRegression().fit(X_with_outliers, y_with_outliers)

tree_clean = DecisionTreeRegressor(max_depth=3, random_state=42).fit(X_clean, y_clean)
tree_outliers = DecisionTreeRegressor(max_depth=3, random_state=42).fit(X_with_outliers, y_with_outliers)

# Передбачення
X_test = np.linspace(0, 10, 100).reshape(-1, 1)
y_lr_clean = lr_clean.predict(X_test)
y_lr_outliers = lr_outliers.predict(X_test)
y_tree_clean = tree_clean.predict(X_test)
y_tree_outliers = tree_outliers.predict(X_test)

# Візуалізація
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# Лінійна регресія
ax1.scatter(X_clean, y_clean, c='green', alpha=0.6, s=50, edgecolors='black', linewidths=0.5, label='Чисті дані')
ax1.scatter(X_outliers, y_outliers, c='red', s=150, marker='^', edgecolors='black', linewidths=1, label='Викиди')
ax1.plot(X_test, y_lr_clean, 'b-', linewidth=2, label='Модель (без викидів)')
ax1.plot(X_test, y_lr_outliers, 'r--', linewidth=2, label='Модель (з викидами)')
ax1.set_xlabel('X', fontsize=12)
ax1.set_ylabel('Y', fontsize=12)
ax1.set_title('Лінійна регресія: ЧУТЛИВА до викидів', fontweight='bold')
ax1.legend()
ax1.grid(alpha=0.3)

# Дерево рішень
ax2.scatter(X_clean, y_clean, c='green', alpha=0.6, s=50, edgecolors='black', linewidths=0.5, label='Чисті дані')
ax2.scatter(X_outliers, y_outliers, c='red', s=150, marker='^', edgecolors='black', linewidths=1, label='Викиди')
ax2.plot(X_test, y_tree_clean, 'b-', linewidth=2, label='Модель (без викидів)')
ax2.plot(X_test, y_tree_outliers, 'r--', linewidth=2, label='Модель (з викидами)')
ax2.set_xlabel('X', fontsize=12)
ax2.set_ylabel('Y', fontsize=12)
ax2.set_title('Дерево рішень: РОБАСТНЕ до викидів', fontweight='bold')
ax2.legend()
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.show()

print("📊 Вплив викидів на моделі:\n")
print("  Лінійна регресія:")
print("    ❌ Викиди драматично змінили нахил лінії")
print("    ❌ Модель намагається 'підлаштуватися' під викиди")
print("    ❌ Погіршилася якість передбачень для основної маси даних\n")
print("  Дерево рішень:")
print("    ✅ Модель майже не змінилася")
print("    ✅ Викиди потрапили в окремі листи дерева")
print("    ✅ Не впливають на передбачення для основної маси даних")
Out[1]:

📊 Вплив викидів на моделі:

  Лінійна регресія:
    ❌ Викиди драматично змінили нахил лінії
    ❌ Модель намагається 'підлаштуватися' під викиди
    ❌ Погіршилася якість передбачень для основної маси даних

  Дерево рішень:
    ✅ Модель майже не змінилася
    ✅ Викиди потрапили в окремі листи дерева
    ✅ Не впливають на передбачення для основної маси даних

Практичні завдання

Рівень 1: Базові вправи

Рівень 2: Алгоритмічні задачі

Рівень 3: Проєктні завдання

Часті запитання студентів (FAQ)


Резюме: ключові takeaways

Викид = екстремальне значення

Викид — спостереження, що значно відрізняється від решти даних. Може бути помилкою або цінною аномалією.

Метод IQR — робастний

Виявлення через квартилі: Q1 − 1.5×IQR та Q3 + 1.5×IQR. Не залежить від викидів. Box plot — ідеальна візуалізація.

Метод Z-score — для нормальних даних

Виявлення через стандартні відхилення: |z| > 3. Інтуїтивний, але НЕ робастний (сам залежить від викидів).

5 стратегій обробки

Видалення, обрізання, трансформація, імпутація, залишити як є. Вибір залежить від контексту та цілей.

Вплив на ML

Лінійна регресія, KNN, SVM — чутливі. Дерева, Random Forest, Gradient Boosting — робастні. Завжди аналізуйте викиди перед тренуванням!

Золоте правило

Розумійте природу викиду перед обробкою. Помилка вимірювання (видалити) ≠ реальна аномалія (зберегти).

Що далі?

Ви завершили модуль «Основи статистики»! Тепер ви опанували:

✅ Випадкові величини (детерміновані vs випадкові, дискретні vs неперервні)
✅ Міри центральної тенденції (середнє, медіана, мода)
✅ Міри варіативності (розмах, дисперсія, стандартне відхилення, IQR)
✅ Розподіли (рівномірний, нормальний, Пуассона, правило 68-95-99.7, Z-оцінка)
✅ Кореляція (коваріація, Пірсон, матриці, кореляція ≠ причинність)
✅ Викиди (IQR, Z-score, стратегії обробки, вплив на ML)

Ці знання — фундамент для всієї Data Science. Ви готові:

  • Досліджувати датасети (EDA)
  • Готувати дані для ML (feature engineering)
  • Інтерпретувати результати моделей
  • Критично мислити про дані
💡 Порада для закріплення: Візьміть будь-який реальний датасет з Kaggle. Виконайте повний статистичний аналіз:
  1. Описові статистики для кожного стовпця (mean, median, std, quartiles)
  2. Візуалізація розподілів (гістограми, box plots)
  3. Матриця кореляції (heatmap)
  4. Виявлення та обробка викидів
  5. Висновки: які змінні важливі, які зв'язки є, які дані потребують очищення
Це стане вашим портфоліо та закріпить всі навички цього модуля!

Наступний крок: ML-алгоритми

Тепер, коли ви розумієте статистику, настав час перейти до Machine Learning:

  • Супервізоване навчання: лінійна/логістична регресія, дерева рішень, KNN
  • Метрики якості: точність, повнота, F1-score, ROC-AUC, MSE, R²
  • Валідація: train/test split, cross-validation, overfitting/underfitting
  • Feature engineering: стандартизація, one-hot encoding, feature selection

Статистика — ваш компас у світі Machine Learning. Удачі! 🚀

Copyright © 2026