Агрегации: sum, mean, std с axis и keepdims
Урок объясняет агрегации — свёртку массива в одно число или вдоль выбранной оси — и важнейший для них параметр axis.
Агрегация — операция, сворачивающая множество значений в одно (сумма, среднее, минимум); параметр
axisзадаёт, вдоль какой оси выполнять свёртку.
Агрегация всего массива
Без указания оси агрегирующая функция сворачивает весь массив в скаляр. a.sum() — сумма всех элементов, a.mean() — среднее, a.max() — максимум, и так далее. Эти методы есть и как методы массива (a.sum()), и как функции (np.sum(a)) — они эквивалентны.
import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6]])
print(a.sum()) # 21 — всё
print(a.mean()) # 3.5
print(a.min()) # 1
print(a.max()) # 6
print(a.std()) # стандартное отклонение всех элементов
Вывод:
21 3.5 1 6 1.707825127659933
Параметр axis: сворачиваем вдоль оси
Самое важное и поначалу контринтуитивное. axis указывает, какую ось убрать (свернуть). Для матрицы (строки × столбцы):
axis=0сворачивает строки — операция идёт вниз по столбцам, результат имеет длину числа столбцов. «Сумма по столбцам».axis=1сворачивает столбцы — операция идёт вдоль строк, результат имеет длину числа строк. «Сумма по строкам».
Запомнить помогает мысль: «axis=0 убирает ось 0 (строки), значит результат — по столбцам». Размер свёрнутой оси исчезает из формы результата.
import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6]]) # форма (2, 3)
print(a.sum(axis=0)) # вниз по столбцам: [1+4, 2+5, 3+6] = [5, 7, 9]
print(a.sum(axis=1)) # вдоль строк: [1+2+3, 4+5+6] = [6, 15]
print(a.sum(axis=0).shape) # (3,) — ушла ось 0
print(a.sum(axis=1).shape) # (2,) — ушла ось 1
Вывод:
[5 7 9] [ 6 15] (3,) (2,)
Прочувствуем axis на чистом Python — это просто выбор направления суммирования:
a = [[1, 2, 3],
[4, 5, 6]]
# axis=0: суммируем по столбцам (вниз)
by_cols = [sum(a[r][c] for r in range(2)) for c in range(3)]
# axis=1: суммируем по строкам (вдоль)
by_rows = [sum(row) for row in a]
print("axis=0 (по столбцам):", by_cols)
print("axis=1 (по строкам): ", by_rows)
Вывод:
axis=0 (по столбцам): [5, 7, 9] axis=1 (по строкам): [6, 15]
Методы массива против функций модуля
Почти у каждой агрегации есть две формы: метод массива (a.sum()) и функция модуля (np.sum(a)). Для базового применения они эквивалентны и взаимозаменяемы — выбирайте по вкусу и стилю кода. Но между ними есть и различия, которые иногда важны. Функции модуля чаще «терпимее» к входу: np.sum примет и список Python, и кортеж, неявно превратив их в массив, тогда как метод существует только у настоящих ndarray. Кроме того, некоторые операции (например, np.median, np.unique) есть только как функции модуля, без метода. Общий совет: для повседневной агрегации массива удобнее цепочка методов (data.mean(), data.std()) — она читается слева направо как «у данных взять среднее». Для функций, у которых нет метода, и для обработки разнородных входов берите форму np.функция(...). Зная, что это по сути одно и то же, вы не запутаетесь, встретив в чужом коде оба стиля.
keepdims: сохранить размерность для broadcasting
После агрегации по оси эта ось исчезает: (2, 3) с axis=1 даёт (2,). Но часто результат нужно использовать обратно с исходным массивом — например, вычесть среднее каждой строки. Тогда формы (2, 3) и (2,) не сложатся по правилам broadcasting (выравнивание справа: 3 и 2). Параметр keepdims=True оставляет свёрнутую ось длиной 1: (2, 3) → (2, 1), и тогда broadcasting срабатывает.
import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6]])
row_mean = a.mean(axis=1) # форма (2,)
row_mean_kd = a.mean(axis=1, keepdims=True) # форма (2, 1)
print(row_mean.shape, row_mean_kd.shape)
# Центрирование строк: вычесть среднее каждой строки
print(a - row_mean_kd) # работает за счёт keepdims
Вывод:
(2,) (2, 1) [[-1. 0. 1.] [-1. 0. 1.]]
Без keepdims=True выражение a - a.mean(axis=1) упало бы с ошибкой broadcasting. Это один из самых частых практических поводов использовать keepdims: нормировка и центрирование по строкам или столбцам.
axis в трёх и более измерениях
В 2D правило «axis=0 убирает строки, axis=1 убирает столбцы» запоминается быстро. В N измерениях держитесь общего принципа: axis=k сворачивает k-ю ось, и именно она исчезает из формы результата. Для трёхмерного массива формы (партии, строки, столбцы) сумма с axis=0 даст (строки, столбцы) — сложатся соответствующие элементы всех партий; axis=2 даст (партии, строки) — свернутся столбцы внутри каждой строки. Можно сворачивать и несколько осей сразу, передав кортеж: a.sum(axis=(0, 1)) уберёт обе первые оси. Универсальная проверка себя: возьмите форму, мысленно вычеркните из неё оси, перечисленные в axis — то, что осталось, и есть форма результата (с keepdims=True вычеркнутые оси превращаются в единицы, а не исчезают). Этот приём избавляет от угадывания и работает в любом числе измерений.
argmin и argmax: позиция, а не значение
Иногда нужно не само минимальное значение, а его позиция. Это дают argmin и argmax. По умолчанию они работают с «расплющенным» массивом и возвращают плоский индекс; с axis — индексы вдоль оси.
import numpy as np
a = np.array([[3, 7, 1],
[9, 2, 5]])
print(a.argmax()) # 3 — плоский индекс максимума (значение 9)
print(a.argmax(axis=0)) # индекс макс. в каждом столбце: [1, 0, 1]
print(a.argmin(axis=1)) # индекс мин. в каждой строке: [2, 1]
Вывод:
3 [1 0 1] [2 1]
argmax/argmin незаменимы, когда по массиву значений нужно найти «где», а не «сколько» — например, индекс лучшего варианта, метку класса с максимальной вероятностью и т. п.
cumsum и накопительные агрегации
Помимо «схлопывающих» агрегаций (sum, mean), которые дают одно число на ось, есть накопительные: cumsum (нарастающая сумма) и cumprod (нарастающее произведение). Они не уменьшают размер массива, а возвращают массив той же длины, где каждый элемент — это агрегат всех предыдущих. Нарастающая сумma [1, 2, 3, 4] даёт [1, 3, 6, 10]. Это незаменимо для временных рядов: накопленная выручка по дням, текущий баланс после каждой транзакции, пройденный путь по скоростям. На первый взгляд такая задача выглядит «последовательной» (каждый шаг зависит от предыдущего), и хочется написать цикл, — но NumPy предоставляет векторную форму, которая считается в скомпилированном коде. Это важный пример того, что даже кажущиеся последовательными вычисления часто имеют готовое векторное решение. К теме переписывания циклов мы вернёмся в финальном разделе, но cumsum/cumprod стоит запомнить уже сейчас как частый инструмент.
std и var: смещённая или несмещённая оценка
Стандартное отклонение (std) и дисперсия (var) по умолчанию считаются с делением на N (параметр ddof=0). Если вам нужна несмещённая выборочная оценка с делением на N−1 (как в статистике для выборки), задайте ddof=1. Это тонкость, на которой часто спотыкаются: NumPy по умолчанию даёт «популяционную» дисперсию, а многие ожидают «выборочную».
import numpy as np
a = np.array([2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0])
print(a.var()) # ddof=0: деление на N
print(a.var(ddof=1)) # ddof=1: деление на N-1 (выборочная)
print(a.std()) # корень из var(ddof=0)
Вывод:
4.0 4.571428571428571 2.0
Зачем агрегации так важны в анализе данных
Агрегации — это мост от «сырых данных» к «выводам». Любой отчёт, любая сводка, любая метрика модели — это в основе своей агрегация: средний чек, максимальная нагрузка, доля ошибок, суммарная выручка по регионам. Умение свернуть массив вдоль нужной оси — это умение задать данным правильный вопрос. Причём axis здесь несёт смысл: для таблицы «дни × товары» сумма по axis=0 — это «итог по каждому товару за все дни», а по axis=1 — «итог по каждому дню по всем товарам». Один и тот же массив, два разных бизнес-вопроса, и различает их только выбор оси. Поэтому ошибка в axis — это не просто неверное число, а ответ не на тот вопрос. Привыкайте проговаривать смысл оси, по которой сворачиваете: «я суммирую через дни, получая итог по товарам». Эта дисциплина почти исключает путаницу и делает код самодокументируемым.
Таблица агрегаций
| Функция | Что считает |
sum, prod | сумма, произведение |
mean, median | среднее, медиана |
std, var | стд. отклонение, дисперсия |
min, max | минимум, максимум значения |
argmin, argmax | позиция минимума/максимума |
cumsum, cumprod | накопленные сумма/произведение |
Подводные камни
- Путать смысл axis.
axis=0сворачивает строки (результат по столбцам). Многие думают наоборот. - Забыть keepdims при нормировке.
a - a.mean(axis=1)упадёт; нужноkeepdims=True. - Ожидать выборочную дисперсию по умолчанию. NumPy делит на N (ddof=0), для N−1 ставьте
ddof=1. - nan в данных. Обычный
sum/meanсnanдастnan. Для пропусков естьnp.nansum,np.nanmean.
Пропуски, переполнение и стабильность агрегаций
Несколько практических предостережений, без которых агрегации иногда дают неверные числа. Первое — пропуски: если в данных есть nan, обычные sum/mean/max вернут nan, потому что любая операция с nan даёт nan. Для данных с пропусками существуют устойчивые версии — np.nansum, np.nanmean, np.nanmax, — которые игнорируют nan. Второе — переполнение: сумма большого массива узких целых (int8, int16) может переполнить тип. NumPy для некоторых агрегаций повышает внутренний тип накопления, но не всегда; при риске указывайте dtype аккумулятора явно, например a.sum(dtype=np.int64). Третье — точность float: суммирование миллионов чисел в float32 накапливает ошибку округления, поэтому средние и суммы по большим массивам надёжнее считать в float64 (многие функции NumPy так и делают по умолчанию). Эти три момента — пропуски, переполнение, точность — стоит держать в голове всякий раз, когда агрегация даёт подозрительный результат.
Лучшие практики
- Всегда явно указывайте
axis, когда сворачиваете часть многомерного массива. - Для последующей нормировки/центрирования используйте
keepdims=True. - Решите осознанно, нужна ли выборочная дисперсия (
ddof=1) или популяционная (по умолчанию). - При наличии пропусков берите
nan-версии агрегаций.
Итог
- Без axis агрегация сворачивает весь массив в скаляр.
axis=0сворачивает строки (результат по столбцам),axis=1— столбцы (по строкам).keepdims=Trueсохраняет свёрнутую ось длиной 1 для дальнейшего broadcasting.argmin/argmaxдают позицию, аddof=1переключает std/var на выборочную оценку.