Тест Колмогорова-Смирнова (K-S test) — это непараметрический статистический тест, используемый для проверки гипотез о распределении данных. Он позволяет определить, отличается ли распределение выборки от некоторого теоретического распределения или отличаются ли распределения двух выборок между собой.
Основная идея
Тест сравнивает эмпирическую функцию распределения (ЭФР) выборки с теоретической функцией распределения (или две ЭФР между собой) и вычисляет максимальное расхождение между ними.
Проще говоря: “Насколько сильно график распределения моих данных отклоняется от идеального графика предполагаемого распределения?”
Математическая основа
Эмпирическая функция распределения (ЭФР)
Для выборки :
Статистика Колмогорова-Смирнова
Для одной выборки: где ( F(x) ) — теоретическая функция распределения.
Для двух выборок:
где:
- ( \sup_x ) — супремум (максимальное отклонение) по всем x
- ( F_n(x) ) — ЭФР первой выборки
- ( F(x) ) — теоретическая ФР (или ЭФР второй выборки)
Типы теста Колмогорова-Смирнова
1. Одновыборочный тест
- Сравнивает: Выборку с теоретическим распределением
- Вопрос: “Подчиняются ли мои данные нормальному распределению?”
- Пример: Проверка, нормально ли распределены ошибки модели
2. Двухвыборочный тест
- Сравнивает: Две выборки между собой
- Вопрос: “Имеют ли две группы одинаковое распределение?”
- Пример: Сравнение распределения доходов в двух городах
Как интерпретировать результаты?
Ключевые показатели:
-
D-статистика (D-statistic)
- Максимальное расстояние между функциями распределения
- Чем больше D, тем сильнее различия
- Диапазон: от 0 до 1
-
p-value
- Вероятность получить такие или более крайние результаты при условии, что H₀ верна
- p-value < 0.05: Отвергаем H₀ — распределения различны
- p-value ≥ 0.05: Нет оснований отвергать H₀ — распределения одинаковы
Гипотезы:
- H₀ (нулевая гипотеза): Распределения одинаковы
- H₁ (альтернативная гипотеза): Распределения различны
Визуализация теста
D-статистика — это максимальное вертикальное расстояние между кривыми.
Преимущества теста Колмогорова-Смирнова
- Непараметрический: Не делает предположений о параметрах распределения
- Универсальный: Работает с любыми непрерывными распределениями
- Инвариантный к монотонным преобразованиям: Результаты не меняются при преобразованиях типа логарифмирования
- Чувствительный к форме распределения: Обнаруживает различия в форме, а не только в среднем или дисперсии
- Визуально интерпретируемый: Легко представить графически
Недостатки и ограничения
- Менее мощный, чем параметрические тесты (если известно распределение)
- Чувствителен к объему выборки: На малых выборках может не обнаруживать различия, на больших — находить незначительные
- Только для непрерывных распределений
- Не обнаруживает различия в хвостах распределения, если основная часть совпадает
- Для двухвыборочного теста требует, чтобы выборки были независимы
Практическое применение в Data Science
1. Проверка нормальности распределения
from scipy import stats
import numpy as np
import matplotlib.pyplot as plt
# Генерируем данные
np.random.seed(42)
normal_data = np.random.normal(0, 1, 1000) # Нормальное распределение
non_normal_data = np.random.exponential(2, 1000) # Экспоненциальное распределение
# Одновыборочный тест на нормальность
d_stat, p_value = stats.kstest(normal_data, 'norm')
print(f"Нормальные данные: D = {d_stat:.3f}, p-value = {p_value:.3f}")
d_stat2, p_value2 = stats.kstest(non_normal_data, 'norm')
print(f"Не нормальные данные: D = {d_stat2:.3f}, p-value = {p_value2:.3f}")
# Визуализация
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.hist(normal_data, bins=30, density=True, alpha=0.7, label='Данные')
x = np.linspace(-4, 4, 100)
plt.plot(x, stats.norm.pdf(x), 'r-', label='Теоретическое N(0,1)')
plt.title(f'Нормальные данные (p-value = {p_value:.3f})')
plt.legend()
plt.subplot(1, 2, 2)
plt.hist(non_normal_data, bins=30, density=True, alpha=0.7, label='Данные')
plt.plot(x, stats.norm.pdf(x), 'r-', label='Теоретическое N(0,1)')
plt.title(f'Не нормальные данные (p-value = {p_value2:.3f})')
plt.legend()
plt.tight_layout()
plt.show()2. Сравнение двух выборок
# Двухвыборочный тест Колмогорова-Смирнова
sample1 = np.random.normal(0, 1, 500)
sample2 = np.random.normal(0.5, 1, 500) # Немного другое распределение
# Тест
d_stat_2samp, p_value_2samp = stats.ks_2samp(sample1, sample2)
print(f"Двухвыборочный тест: D = {d_stat_2samp:.3f}, p-value = {p_value_2samp:.3f}")
# Визуализация ECDF (Empirical CDF)
def ecdf(data):
"""Вычисляет эмпирическую функцию распределения"""
x = np.sort(data)
y = np.arange(1, len(data)+1) / len(data)
return x, y
x1, y1 = ecdf(sample1)
x2, y2 = ecdf(sample2)
plt.figure(figsize=(10, 6))
plt.plot(x1, y1, label='Выборка 1 (N(0,1))', linewidth=2)
plt.plot(x2, y2, label='Выборка 2 (N(0.5,1))', linewidth=2)
plt.xlabel('Значение')
plt.ylabel('ECDF')
plt.title(f'Двухвыборочный тест К-С (D = {d_stat_2samp:.3f}, p-value = {p_value_2samp:.3f})')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()3. Обнаружение дрейфа данных (Data Drift)
# Мониторинг дрейфа данных во времени
np.random.seed(42)
# Исходное распределение (базовое)
base_data = np.random.normal(0, 1, 1000)
# Новые данные (с дрейфом)
new_data_1 = np.random.normal(0, 1, 1000) # Без дрейфа
new_data_2 = np.random.normal(0.3, 1.2, 1000) # С дрейфом
# Проверка дрейфа
d1, p1 = stats.ks_2samp(base_data, new_data_1)
d2, p2 = stats.ks_2samp(base_data, new_data_2)
print(f"Без дрейфа: D = {d1:.3f}, p-value = {p1:.3f}")
print(f"С дрейфом: D = {d2:.3f}, p-value = {p2:.3f}")
# Интерпретация
alpha = 0.05
for i, (d, p, label) in enumerate([(d1, p1, "без дрейфа"), (d2, p2, "с дрейфом")], 1):
if p < alpha:
print(f"Выборка {i} ({label}): ОБНАРУЖЕН ДРЕЙФ (p-value = {p:.3f})")
else:
print(f"Выборка {i} ({label}): дрейф не обнаружен (p-value = {p:.3f})")Сравнение с другими тестами
| Тест | Что проверяет | Преимущества | Недостатки |
|---|---|---|---|
| K-S test | Распределение в целом | Универсальный, чувствителен к форме | Менее мощный для известных распределений |
| t-тест | Равенство средних | Мощный для нормальных данных | Требует нормальности, гомоскедастичности |
| Тест Манна-Уитни | Сдвиг распределений | Непараметрический, для любых распределений | Менее мощный, чем t-тест для нормальных данных |
| Хи-квадрат | Категориальные данные | Для дискретных распределений | Требует группировки непрерывных данных |
Особенности использования в Python
from scipy.stats import kstest, ks_2samp
# Одновыборочный тест с параметрами
data = np.random.normal(5, 2, 1000) # N(5, 2)
# Тест против нормального распределения с оценкой параметров из данных
d1, p1 = kstest(data, 'norm', args=(np.mean(data), np.std(data)))
print(f"С оценкой параметров: D = {d1:.3f}, p-value = {p1:.3f}")
# Тест против конкретного нормального распределения
d2, p2 = kstest(data, 'norm', args=(5, 2))
print(f"Против N(5,2): D = {d2:.3f}, p-value = {p2:.3f}")
# Тест против других распределений
d_exp, p_exp = kstest(data, 'expon', args=(np.mean(data),))
print(f"Против экспоненциального: D = {d_exp:.3f}, p-value = {p_exp:.3f}")Когда использовать тест Колмогорова-Смирнова?
✅ Хорошие сценарии:
- Проверка соответствия распределения (нормальность, равномерность и т.д.)
- Сравнение двух эмпирических распределений
- Обнаружение дрейфа данных в мониторинге моделей ML
- Когда неизвестны параметры распределения
❌ Плохие сценарии:
- Маленькие выборки (< 30 наблюдений)
- Дискретные распределения
- Когда известно, что распределение нормальное (лучше использовать параметрические тесты)
- Нужно обнаружить различия именно в хвостах распределения
Практический пример в ML: Мониторинг дрейфа
import pandas as pd
from sklearn.datasets import load_iris
from scipy.stats import ks_2samp
# Загрузка данных
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
# Базовое распределение (первые 50 наблюдений)
base_feature = df['sepal length (cm)'][:50]
# Мониторинг дрейфа для новых данных
drift_detected = []
for i in range(50, 150, 10):
new_feature = df['sepal length (cm)'][i:i+10]
d_stat, p_value = ks_2samp(base_feature, new_feature)
if p_value < 0.05:
drift_detected.append(True)
print(f"Наблюдения {i}-{i+10}: ОБНАРУЖЕН ДРЕЙФ (p-value = {p_value:.3f})")
else:
drift_detected.append(False)
print(f"Наблюдения {i}-{i+10}: дрейф не обнаружен (p-value = {p_value:.3f})")
print(f"\nВсего обнаружено дрейфов: {sum(drift_detected)} из {len(drift_detected)}")Краткий итог
- Тест Колмогорова-Смирнова — непараметрический тест для сравнения распределений
- Сравнивает эмпирические и теоретические функции распределения
- D-статистика — максимальное расстояние между распределениями
- p-value < 0.05 — свидетельствует о значимом различии распределений
- Применения: проверка нормальности, сравнение выборок, обнаружение дрейфа данных
- Преимущества: универсальность, не требует предположений о параметрах
- Недостатки: меньшая мощность, чувствительность к объему выборки
Тест Колмогорова-Смирнова — это важный инструмент в арсенале data scientist’а для проверки статистических гипотез о распределении данных, особенно когда параметры распределения неизвестны.