Почему эта цифра не совпадает?
Вы в два клика посчитали дисперсию для одного столбца. Сначала через NumPy. Потом через Pandas. И получили две разные цифры.
Первая мысль – баг. Проверяете данные, пересчитываете, ищете опечатку. Ее нет. Результаты упорно различаются. Знакомо? Это не ошибка. Это принципиально разные формулы, и если не понять разницу – можно наломать дров в любом анализе, от A/B-теста до проверки гипотез.
Вот наглядный пример, который сводит с ума новичков. Запустите этот код прямо сейчас.
import numpy as np
import pandas as pd
data = [2, 4, 6, 8, 10]
arr = np.array(data)
series = pd.Series(data)
print(f"NumPy variance: {np.var(arr):.2f}") # 8.00
print(f"Pandas variance: {series.var():.2f}") # 10.00
Вот оно. Дисперсия отличается. На целых 25%. Кого слушать? За ответом нужно залезть в статистику лет на 50 назад.
Две формулы дисперсии: простая правда и неудобная правда
В школе нам давали самую простую формулу. Сумма квадратов отклонений от среднего, деленная на количество элементов. Это смещенная (biased) дисперсия.
| Тип дисперсии | Формула | Когда использовать |
|---|---|---|
| Смещенная (Population) | σ² = Σ(xᵢ - μ)² / N | Когда ваши данные – это ВСЯ генеральная совокупность. (Например, зарплаты всех сотрудников компании). |
| Несмещенная (Sample) | s² = Σ(xᵢ - x̄)² / (N - 1) | Когда ваши данные – ВЫБОРКА из большой генеральной совокупности. (Например, 1000 пользователей из миллионов). |
Ключевая разница в знаменателе. Деление на N или на N - 1. Этот "-1" – не прихоть, а поправка Бесселя. Она компенсирует тот факт, что при расчете по выборке мы используем выборочное среднее (x̄), а не истинное среднее генеральной совокупности (μ). Использование x̄ делает оценку дисперсии чуть заниженной, и деление на N-1 эту "смещенность" убирает.
ddof расшифровывается как Delta Degrees of Freedom – дельта степеней свободы. Это число, которое вычитается из знаменателя. При ddof=0 знаменатель = N. При ddof=1 знаменатель = N - 1.NumPy и Pandas: война мировоззрений
Разработчики библиотек сделали разный выбор по умолчанию. И в этом есть своя логика, хоть она и раздражает.
- NumPy (
np.var): по умолчаниюddof=0. Считает, что вы работаете с чистыми массивами чисел и сами решите, какая формула нужна. Это позиция «мы даем базовую математику». - Pandas (
.var()): по умолчаниюddof=1. Исходит из того, что DataFrame или Series – это почти всегда выборка из реальных данных. Это позиция «мы работаем со статистикой в data science».
1 Прямое сравнение: где собака зарыта
# Создаем одинаковые данные
data = np.random.randn(100) # 100 значений из стандартного нормального распределения
df = pd.DataFrame({'col': data})
# Считаем дисперсию тремя способами с явным указанием ddof
var_numpy_default = np.var(data) # ddof=0
var_numpy_unbiased = np.var(data, ddof=1) # ddof=1
var_pandas_default = df['col'].var() # ddof=1
var_pandas_biased = df['col'].var(ddof=0) # ddof=0
print(f"NumPy (ddof=0): {var_numpy_default:.4f}")
print(f"NumPy (ddof=1): {var_numpy_unbiased:.4f}")
print(f"Pandas (ddof=1): {var_pandas_default:.4f}")
print(f"Pandas (ddof=0): {var_pandas_biased:.4f}")
Теперь видно: np.var(data) и df['col'].var(ddof=0) дают одинаковый результат. Так же, как np.var(data, ddof=1) и df['col'].var(). Вся магия – в одной строке.
Самая частая ошибка – сравнивать результаты np.var(arr) и series.var() без понимания, что это разные формулы. Это как сравнивать километры и мили, не зная о коэффициенте.
Практическое правило: какую дисперсию выбрать?
Забудьте про то, что использует библиотека по умолчанию. Спросите себя:
- Это вся генеральная совокупность? У вас есть данные по КАЖДОМУ элементу изучаемой группы. Пример: результаты всех выпускников школы за год, температура в городе за каждый день месяца. Используйте ddof=0 (смещенную оценку).
- Это выборка из большой совокупности? Вы опросили 1000 человек из миллионов пользователей, взяли 100 транзакций из миллионов. Используйте ddof=1 (несмещенную оценку). Именно ее ждут большинство статистических тестов.
2 Пример из реальной жизни: A/B-тест
Представьте, вы проводите A/B-тест для нового функционала. У вас есть выборка пользователей в группах А и Б. Вы оцениваете разницу средних. Для расчета стандартной ошибки и t-статистики вам нужна дисперсия выборки.
# Группа A (контрольная)
group_a = df[df['group'] == 'A']['metric'].values
# Группа B (тестовая)
group_b = df[df['group'] == 'B']['metric'].values
# ПРАВИЛЬНО для выборки: использовать несмещенную оценку (ddof=1)
var_a = np.var(group_a, ddof=1)
var_b = np.var(group_b, ddof=1)
# или через pandas, что по умолчанию даст тот же результат
# var_a = df.loc[df['group'] == 'A', 'metric'].var()
# Используем var_a и var_b для расчета t-статистики...
Использование ddof=0 здесь занизило бы оценку дисперсии и могло бы привести к ложному обнаружению статистически значимого эффекта. Опасно.
Эпидемия тихой ошибки: где еще прячется ddof?
Проблема не заканчивается на .var(). Параметр ddof всплывает в других местах, про которые часто забывают.
# Стандартное отклонение (std) – корень из дисперсии. Те же правила!
print(np.std(data)) # ddof=0
print(np.std(data, ddof=1)) # ddof=1
print(df['col'].std()) # ddof=1
print(df['col'].std(ddof=0)) # ddof=0
# Функция np.cov() для ковариационной матрицы
# Её параметр ddof тоже по умолчанию равен 1, если rowvar=False!
# Будьте внимательны, читайте доки.
Чек-лист: как никогда не ошибиться с дисперсией
- Всегда явно указывайте
ddof. Не полагайтесь на значения по умолчанию, особенно если пишете библиотечный код или ноутбук для коллег.np.var(data, ddof=1)читается лучше, чем простоnp.var(data). - Пишите комментарии. «Здесь используем дисперсию выборки (ddof=1), так как данные – случайная подвыборка из лога».
- Проверяйте свои цепочки вычислений. Если вы считаете дисперсию через Pandas, а потом используете ее в формуле, написанной под NumPy, убедитесь, что
ddofсогласован. Эта проблема часто ломает расчеты объяснимости моделей. - Тестируйте на простых данных. Как в самом начале статьи. Посчитайте вручную, убедитесь, что библиотека возвращает ожидаемое.
Частые вопросы (FAQ)
Что делать, если я не знаю, выборка у меня или генеральная совокупность?
В 95% случаев в data science и анализе данных вы работаете с выборками. Логи, записи о транзакциях, активность пользователей – это почти всегда срез. Смело используйте ddof=1 (несмещенную оценку). Это более консервативный и общепринятый в статистике выбор. Для работы с реальными данными это правило спасет от излишнего оптимизма.
Почему просто не сделать один стандарт для всех?
Исторически так сложилось. NumPy старше и ближе к научным вычислениям (физика, инженерия), где часто работают с полными наборами измерений. Pandas младше и создан для анализа неполных, «грязных» бизнес-данных. Конфликт неизбежен. Похожие разногласия есть и в других инструментах, например, когда сравнивают Pandas и PySpark.
Влияет ли это на машинное обучение?
Косвенно. Например, при стандартизации признаков (StandardScaler из sklearn) используется несмещенная оценка дисперсии (ddof=1). Если вы будете пересчитывать scaling вручную через NumPy с ddof=0, получите другие коэффициенты. Всегда сверяйтесь с документацией библиотек.
Итог простой, но важный: ddof – не техническая деталь, а статистическое решение. Следующий раз, когда будете считать дисперсию, остановитесь на секунду и спросите: «А что эти данные на самом деле представляют?». Эта привычка сэкономит вам часы отладки и сделает ваши выводы на порядок надежнее.