Тест Колмогорова-Смирнова (K-S test) — это непараметрический статистический тест, используемый для проверки гипотез о распределении данных. Он позволяет определить, отличается ли распределение выборки от некоторого теоретического распределения или отличаются ли распределения двух выборок между собой.


Основная идея

Тест сравнивает эмпирическую функцию распределения (ЭФР) выборки с теоретической функцией распределения (или две ЭФР между собой) и вычисляет максимальное расхождение между ними.

Проще говоря: “Насколько сильно график распределения моих данных отклоняется от идеального графика предполагаемого распределения?”


Математическая основа

Эмпирическая функция распределения (ЭФР)

Для выборки :

Статистика Колмогорова-Смирнова

Для одной выборки: где ( F(x) ) — теоретическая функция распределения.

Для двух выборок:

где:

  • ( \sup_x ) — супремум (максимальное отклонение) по всем x
  • ( F_n(x) ) — ЭФР первой выборки
  • ( F(x) ) — теоретическая ФР (или ЭФР второй выборки)

Типы теста Колмогорова-Смирнова

1. Одновыборочный тест

  • Сравнивает: Выборку с теоретическим распределением
  • Вопрос: “Подчиняются ли мои данные нормальному распределению?”
  • Пример: Проверка, нормально ли распределены ошибки модели

2. Двухвыборочный тест

  • Сравнивает: Две выборки между собой
  • Вопрос: “Имеют ли две группы одинаковое распределение?”
  • Пример: Сравнение распределения доходов в двух городах

Как интерпретировать результаты?

Ключевые показатели:

  1. D-статистика (D-statistic)

    • Максимальное расстояние между функциями распределения
    • Чем больше D, тем сильнее различия
    • Диапазон: от 0 до 1
  2. p-value

    • Вероятность получить такие или более крайние результаты при условии, что H₀ верна
    • p-value < 0.05: Отвергаем H₀ — распределения различны
    • p-value ≥ 0.05: Нет оснований отвергать H₀ — распределения одинаковы

Гипотезы:

  • H₀ (нулевая гипотеза): Распределения одинаковы
  • H₁ (альтернативная гипотеза): Распределения различны

Визуализация теста

D-статистика — это максимальное вертикальное расстояние между кривыми.


Преимущества теста Колмогорова-Смирнова

  1. Непараметрический: Не делает предположений о параметрах распределения
  2. Универсальный: Работает с любыми непрерывными распределениями
  3. Инвариантный к монотонным преобразованиям: Результаты не меняются при преобразованиях типа логарифмирования
  4. Чувствительный к форме распределения: Обнаруживает различия в форме, а не только в среднем или дисперсии
  5. Визуально интерпретируемый: Легко представить графически

Недостатки и ограничения

  1. Менее мощный, чем параметрические тесты (если известно распределение)
  2. Чувствителен к объему выборки: На малых выборках может не обнаруживать различия, на больших — находить незначительные
  3. Только для непрерывных распределений
  4. Не обнаруживает различия в хвостах распределения, если основная часть совпадает
  5. Для двухвыборочного теста требует, чтобы выборки были независимы

Практическое применение в 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’а для проверки статистических гипотез о распределении данных, особенно когда параметры распределения неизвестны.