numpyаналитика данныхагрегацииaxispercentilenumpy с нуля

NumPy с нуля. Часть 5: агрегации по осям — sum, mean, std, axis

2026-06-28 15 мин

Главное сразу: агрегация — это когда из массива чисел вы получаете одно число или одну строку чисел. Сумма выручки, средний чек, перцентиль латентности, накопительный итог — всё это агрегации. В numpy они делаются одним вызовом: revenue.sum(), data.mean(), latency.std(). А параметр axis решает, считать ли по всей таблице сразу, по столбцам или по строкам — это самая важная и самая путающая вещь в этом уроке, поэтому ей мы уделим максимум внимания.

Это пятая часть курса «numpy с нуля для аналитика данных». В прошлых частях вы научились создавать массивы, индексировать их и применять векторные операции. Теперь пора их сворачивать в метрики — то, ради чего аналитик вообще берёт в руки numpy. Любой дашборд, любой отчёт, любая проверка A/B-теста под капотом — это агрегации над массивами.

Разберём по порядку: базовые сводки (sum, mean, std, min, max, median), затем главную тему — axis=0 против axis=1 на матрице, потом argmin/argmax (где находится минимум/максимум), и в конце два инструмента, без которых не живёт ни один аналитик — percentile (перцентили для латентности и зарплат) и cumsum (накопительный итог). Простым языком, на примерах с реальными метриками, с тремя интерактивными песочницами в конце.

Что такое агрегация и зачем она аналитику

Представьте, что у вас есть массив выручки по дням за месяц — 30 чисел. Сами по себе они мало что говорят. Аналитику нужны выводы: сколько всего заработали, сколько в среднем в день, насколько стабильна выручка, какой был лучший день. Каждый такой вывод — это свёртка массива в одно число (или в несколько). Это и называется агрегацией.

В numpy агрегирующие функции есть в двух формах, и работают они одинаково:

import numpy as np

revenue = np.array([100, 200, 150, 300, 250])

# Форма 1: метод массива
print(revenue.sum())     # 1000

# Форма 2: функция numpy
print(np.sum(revenue))   # 1000 — то же самое

Обе формы дают одинаковый результат. Метод revenue.sum() читается короче, поэтому в коде аналитика встречается чаще. Функцию np.sum(...) удобно использовать, когда данные приходят не массивом, а обычным списком — numpy сам его превратит в массив.

Главное преимущество numpy здесь — скорость и краткость. Чтобы посчитать сумму списка циклом, нужно несколько строк. В numpy это один вызов, и он работает в десятки раз быстрее на больших данных, потому что весь цикл выполняется внутри C-кода, а не в Python. Для аналитика, который гоняет агрегации по миллионам строк, это решающе.

Базовые сводки: sum, mean, std, min, max, median

Это шесть функций, которые покрывают 90% повседневной работы. Разберём каждую на массиве чеков.

import numpy as np

checks = np.array([500, 800, 1200, 600, 900])

print(checks.sum())      # 4000 — суммарная выручка
print(checks.mean())     # 800.0 — средний чек
print(checks.min())      # 500 — минимальный чек
print(checks.max())      # 1200 — максимальный чек
print(np.median(checks)) # 800.0 — медианный чек
print(checks.std())      # стандартное отклонение

Что значит каждая:

Среднее против медианы — важная для аналитика разница. Среднее чувствительно к выбросам, медиана — нет. Посмотрите:

import numpy as np

# у одного клиента гигантский чек
checks = np.array([500, 600, 700, 800, 50000])

print(checks.mean())     # 10520.0 — среднее задрано выбросом
print(np.median(checks)) # 700.0 — медиана честно показывает «типичного» клиента

Один богатый клиент перекосил среднее в 15 раз. Поэтому для зарплат, чеков, времени ответа аналитики почти всегда смотрят медиану, а не среднее — она устойчивее.

Маленький нюанс про std. По умолчанию numpy считает *популяционное* стандартное отклонение (делит на N). В статистике для выборки часто нужно делить на N−1 — это задаётся параметром ddof=1:

import numpy as np

x = np.array([1, 2, 3, 4, 5])
print(x.std())        # 1.414... — делит на N (популяция)
print(x.std(ddof=1))  # 1.581... — делит на N-1 (выборка)

Для новичка достаточно запомнить: если делаете статвыводы по выборке (например, оцениваете разброс метрики в A/B-тесте) — добавляйте ddof=1.

Попробуй сам

Задача · Python
Условие
Перед вами матрица выручки: 4 канала трафика (строки) за 3 месяца (столбцы), в тыс. руб. Посчитайте: (1) суммарную выручку по каждому месяцу, (2) суммарную выручку по каждому каналу, (3) общую выручку, (4) среднюю выручку по каждому каналу. Главное — правильно выбрать axis: по столбцам это axis=0, по строкам — axis=1.
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. Запомни главное правило: axis=0 сворачивает строки и даёт итог по каждому СТОЛБЦУ (по месяцам), axis=1 сворачивает столбцы и даёт итог по каждой СТРОКЕ (по каналам). Без axis — сумма всей матрицы в одно число. mean работает с axis так же, как sum. Решение: print("По месяцам:", revenue.sum(axis=0)) print("По каналам:", revenue.sum(axis=1)) print("Всего:", revenue.sum()) print("Среднее по каналам:", revenue.mean(axis=1))

Главное: axis=0 по столбцам, axis=1 по строкам

До сих пор мы агрегировали одномерный массив — вектор. Но данные аналитика чаще двумерные: строки и столбцы, как таблица. И тут возникает вопрос: а в какую сторону сворачивать? По строкам? По столбцам? Или всё сразу в одно число? За это отвечает параметр axis.

Возьмём матрицу: строки — это каналы трафика, столбцы — месяцы.

import numpy as np

# 2 канала (строки) x 3 месяца (столбцы)
revenue = np.array([
    [1, 2, 3],   # канал A
    [4, 5, 6],   # канал B
])
print(revenue.shape)   # (2, 3) — 2 строки, 3 столбца

Теперь три варианта суммы:

# Без axis — сворачивает ВСЁ в одно число
print(revenue.sum())          # 21

# axis=0 — сворачивает СТРОКИ, остаются столбцы (сумма по каждому месяцу)
print(revenue.sum(axis=0))    # [5 7 9]

# axis=1 — сворачивает СТОЛБЦЫ, остаются строки (сумма по каждому каналу)
print(revenue.sum(axis=1))    # [6 15]

Разберём, что произошло, потому что это сбивает с толку всех новичков.

axis=0 идёт сверху вниз — по столбцам. Он схлопывает строки. Было 2 строки и 3 столбца, осталось 3 числа — по одному на столбец. [5 7 9] это 1+4, 2+5, 3+6 — сумма каждого месяца по всем каналам. Запомните: axis=0 = «итог по каждому столбцу».

axis=1 идёт слева направо — по строкам. Он схлопывает столбцы. Осталось 2 числа — по одному на строку. [6 15] это 1+2+3, 4+5+6 — сумма каждого канала по всем месяцам. Запомните: axis=1 = «итог по каждой строке».

Правило-подсказка, которое спасает: axis — это номер оси, которая ИСЧЕЗАЕТ. В форме (2, 3) ось 0 — это строки (их 2), ось 1 — столбцы (их 3). Указали axis=0 — пропадает размерность строк, остаётся (3,). Указали axis=1 — пропадает размерность столбцов, остаётся (2,).

import numpy as np
revenue = np.array([[1, 2, 3], [4, 5, 6]])
print(revenue.sum(axis=0).shape)  # (3,) — исчезла ось строк
print(revenue.sum(axis=1).shape)  # (2,) — исчезла ось столбцов

Это работает не только для sum, но и для mean, std, min, max, median — у всех есть параметр axis с той же логикой:

import numpy as np
revenue = np.array([[1, 2, 3], [4, 5, 6]])

print(revenue.mean(axis=0))   # [2.5 3.5 4.5] — средний месяц по каналам
print(revenue.max(axis=1))    # [3 6] — лучший месяц каждого канала

Если запомнить только одну вещь из всего урока, пусть это будет: axis=0 — вниз по столбцам, axis=1 — вбок по строкам.

axis=0 (вниз по столбцам) vs axis=1 (вбок по строкам)

Матрица revenue, shape (2, 3):  строки = каналы, столбцы = месяцы

                 м1    м2    м3
              +-----+-----+-----+
   канал A    |  1  |  2  |  3  |  --. axis=1 (свернуть столбцы
              +-----+-----+-----+    |        -> итог по СТРОКЕ)
   канал B    |  4  |  5  |  6  |  --.        sum(axis=1) = [ 6, 15]
              +-----+-----+-----+    |        1+2+3=6   4+5+6=15
                 |     |     |       v
                 v     v     v   шкала строк ИСЧЕЗАЕТ -> shape (2,)
         axis=0 (свернуть строки -> итог по СТОЛБЦУ)
         sum(axis=0) = [ 5,  7,  9 ]
                        1+4  2+5  3+6
         шкала столбцов ИСЧЕЗАЕТ -> shape (3,)

   без axis: всё в одно число -> sum() = 21

   ПРАВИЛО: axis = номер оси, которая ИСЧЕЗАЕТ.
            axis=0 -> вниз по столбцам   axis=1 -> вбок по строкам

axis=0 схлопывает строки и оставляет по одному числу на столбец ([5 7 9], сумма каждого месяца), axis=1 схлопывает столбцы и оставляет по числу на строку ([6 15], сумма каждого канала). Стрелки показывают направление свёртки; подпись «ИСЧЕЗАЕТ» — мнемоника «axis — номер оси, которая пропадает».

Где находится экстремум: argmin и argmax

max отвечает на вопрос «какое самое большое значение?». Но аналитику часто нужен другой вопрос — «где находится самое большое значение?». Какой день был лучшим, какой канал худшим, на каком шаге воронки больше всего отвалилось. На это отвечают argmax и argmin — они возвращают не само значение, а его индекс (позицию в массиве).
import numpy as np

# конверсия (%) по дням недели: пн, вт, ср, чт, пт
conv = np.array([4.0, 6.0, 3.0, 7.0, 5.0])

print(conv.max())      # 7.0 — само значение лучшей конверсии
print(conv.argmax())   # 3 — ИНДЕКС лучшего дня (четверг, считая с 0)

print(conv.min())      # 3.0 — худшая конверсия
print(conv.argmin())   # 2 — индекс худшего дня (среда)
argmax() вернул 3 — это позиция максимума, индекс четверга (отсчёт с нуля: пн=0, вт=1, ср=2, чт=3, пт=4). Дальше по этому индексу можно достать что угодно из связанных данных:
import numpy as np

days = np.array(["пн", "вт", "ср", "чт", "пт"])
conv = np.array([4.0, 6.0, 3.0, 7.0, 5.0])

best = conv.argmax()
print("Лучший день:", days[best])   # Лучший день: чт

Это частый паттерн: нашли индекс экстремума в одном массиве, применили его к другому, чтобы достать связанную информацию (название дня, id пользователя, имя канала).

Важный нюанс про ничьи. Если максимум встречается несколько раз, argmax вернёт индекс первого из них:

import numpy as np
a = np.array([10, 50, 30, 50])
print(a.argmax())   # 1 — первый из двух максимумов, не последний

И так же как у остальных функций, у argmax/argmin есть axis — можно искать позицию максимума в каждой строке или каждом столбце матрицы.

Попробуй сам

Задача · Python
Условие
Массив dau — это DAU (тыс.) по 5 рабочим дням недели (пн..пт). Найдите: максимум и минимум, ИНДЕКС лучшего и худшего дня (argmax/argmin), медиану, среднее и стандартное отклонение. Обратите внимание на разницу между max (само значение) и argmax (позиция).
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. max() и min() возвращают САМО значение, а argmax() и argmin() — его ИНДЕКС (позицию, отсчёт с нуля). Здесь максимум 9 стоит на позиции 4 (пятница), минимум 3 — на позиции 0 (понедельник). Медиана считается через np.median(dau), а среднее и std — методами dau.mean() и dau.std(). Решение: print("Максимум:", dau.max()) print("Минимум:", dau.min()) print("Индекс максимума:", dau.argmax()) print("Индекс минимума:", dau.argmin()) print("Медиана:", np.median(dau)) print("Среднее:", dau.mean()) print("Std:", dau.std())

Перцентили: percentile для латентности и зарплат

Среднее и медиана — это «центр» данных. Но аналитику часто важнее хвосты: какое время ответа у 95% самых медленных запросов, какая зарплата у топ-10% сотрудников. Для этого есть перцентили. Перцентиль p — это значение, ниже которого лежит p процентов данных. Медиана — это просто 50-й перцентиль.

import numpy as np

# время ответа API (мс)
latency = np.array([90, 110, 120, 150, 200, 300, 800])

print(np.percentile(latency, 50))   # 150.0 — медиана (p50)
print(np.percentile(latency, 90))   # p90 — 90% запросов быстрее этого
print(np.percentile(latency, 95))   # p95 — классическая SLA-метрика

Почему перцентили, а не среднее? Потому что среднее время ответа врёт. Если 99 запросов отработали за 100 мс, а один завис на 10 секунд — среднее будет около 200 мс и выглядит прилично. Но p99 покажет 10 секунд — и это реальная боль ваших пользователей. Поэтому в мониторинге всегда смотрят p90/p95/p99, а не среднее.

То же самое с зарплатами и чеками — перцентили показывают распределение честнее среднего:

import numpy as np

salaries = np.array([80, 90, 100, 110, 120, 150, 400])  # тыс. руб.

print(np.percentile(salaries, 25))   # нижний квартиль
print(np.percentile(salaries, 50))   # медиана
print(np.percentile(salaries, 75))   # верхний квартиль

25-й, 50-й и 75-й перцентили называют квартилями — они делят данные на четыре равные части. Можно запросить сразу несколько перцентилей списком:

import numpy as np
latency = np.array([90, 110, 120, 150, 200, 300, 800])
print(np.percentile(latency, [50, 90, 95, 99]))
# вернёт массив из 4 значений — удобно для дашборда

Одно предупреждение про точность: между точками данных numpy интерполирует, поэтому перцентиль может оказаться дробным числом с длинным хвостом (например 499.99999999). Это нормально — для отчёта округлите через round. В песочнице ниже данные подобраны так, чтобы перцентили выходили ровными.

Накопительный итог: cumsum

Все функции выше схлопывали массив до меньшего размера. cumsum — особенная: она возвращает массив той же длины, где каждый элемент это сумма всех предыдущих плюс текущий. Это нарастающий итог — то, что строят на графиках «выручка с начала месяца», «накопленные регистрации», «прогресс к цели».

import numpy as np

# выручка по дням
daily = np.array([100, 200, 150, 300, 250])

print(np.cumsum(daily))   # [ 100  300  450  750 1000]

Разберём результат [100 300 450 750 1000]:

Последний элемент 1000 равен обычной sum() — это итог за весь период. А промежуточные показывают, как сумма росла день за днём. Именно это нужно для линии «накопленная выручка»:

import numpy as np
daily = np.array([100, 200, 150, 300, 250])

cum = np.cumsum(daily)
print("Итог за период:", cum[-1])          # 1000
print("Доля от цели 2000:", cum[-1] / 2000) # 0.5 — выполнили половину плана

У cumsum тоже есть axis для матриц — можно считать нарастающий итог вдоль каждой строки или столбца. И есть родственники: np.cumprod (накопительное произведение, для сложных процентов и роста) и np.cummax (бегущий максимум, для «рекорд на текущий момент»). Логика у всех одна — массив той же длины с накопленным результатом.

На этом базовый набор агрегаций аналитика собран. Этих функций хватит, чтобы посчитать практически любую табличную метрику. Закрепим на практике.

Попробуй сам

Задача · Python
Условие
Массив latency — время ответа API (мс) на 11 запросов (отсортировано). Посчитайте перцентили p50 (медиана), p90 и p100 (максимум). Затем для массива дневной выручки daily посчитайте нарастающий итог через cumsum и достаньте итоговую сумму за период как последний элемент.
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. np.percentile(latency, p) даёт значение, ниже которого лежит p% данных: p50 — это медиана, p100 — максимум. Здесь массив из 11 элементов, поэтому перцентили попадают точно на значения. np.cumsum(daily) даёт массив той же длины с нарастающей суммой; его последний элемент (индекс -1) равен общей сумме за период. Решение: print("Медиана (p50):", np.percentile(latency, 50)) print("p90:", np.percentile(latency, 90)) print("Максимум (p100):", np.percentile(latency, 100)) print("Нарастающий итог:", np.cumsum(daily)) print("Итого за период:", np.cumsum(daily)[-1])

Главное

Частые вопросы

Что означают axis=0 и axis=1 в numpy?

axis — это ось, вдоль которой numpy «сворачивает» массив. Для 2D-массива axis=0 схлопывает строки и считает по каждому столбцу (результат — вектор длиной в число столбцов), а axis=1 схлопывает столбцы и считает по каждой строке. Проще запомнить так: указанная ось исчезает из формы результата. Потренироваться на живых данных можно в Python-тренажёре.

Почему np.mean без axis возвращает одно число?

Если не передать axis, агрегация идёт по всему массиву сразу и схлопывает его в скаляр. arr.mean() даст среднее по всем элементам, arr.mean(axis=0) — среднее по столбцам, arr.mean(axis=1) — по строкам. Это самая частая ошибка новичков: забыли ось и получили одно число вместо вектора.

Как найти не само максимальное значение, а его индекс?

Для значения используют max/min, а для позиции — argmax/argmin. Например, np.argmax(sales) вернёт индекс дня с пиком продаж, а np.argmin — с минимумом. С axis они работают тоже: argmax(axis=1) даст индекс максимума в каждой строке.

Чем percentile отличается от median в numpy?

Медиана — это частный случай перцентиля: np.median(x) равно np.percentile(x, 50). percentile гибче — можно взять любой уровень: np.percentile(x, 95) для 95-го перцентиля времени ответа, np.percentile(x, [25, 50, 75]) сразу для квартилей. Перцентили устойчивее к выбросам, чем среднее, поэтому их часто берут для latency и чеков.

Что делает cumsum и чем отличается от обычного sum?

sum возвращает итоговую сумму, а cumsum — накопительную: массив той же длины, где каждый элемент это сумма всех предыдущих. np.cumsum([10, 20, 30]) даст [10, 30, 60]. Удобно для нарастающего итога выручки, кумулятивного охвата когорты или построения ступенчатых графиков.
Закрепи NumPy на практике
Реши Python-задачи аналитика в тренажёре — pandas, numpy, scipy прямо в браузере. Первые 5 — без регистрации.
Открыть Python-тренажёр →