numpybroadcastingpython для аналитиканормализациявекторизацияnumpy с нуля

NumPy с нуля. Часть 3: Broadcasting

2026-06-28 12 мин

Broadcasting — это правило, по которому numpy автоматически растягивает массивы разной формы, чтобы посчитать их поэлементно без единого цикла. Именно благодаря ему revenue * 1.2 (НДС на весь столбец выручки) и (X - mean) / std (нормализация признаков перед ML) работают одной строкой и за миллисекунды. Если вы поймёте broadcasting один раз, половина «магии» pandas и numpy перестанет быть магией.

В прошлых частях курса numpy с нуля мы разобрали массивы и индексацию. Теперь — то, что отличает человека, который пишет циклы по строкам, от аналитика, который думает массивами. Broadcasting нужен буквально каждый день: пересчитать валюту, добавить НДС, отнять среднее по когорте, нормализовать данные перед кластеризацией или регрессией.

Урок построен для новичка, который знает базовый Python, но ещё путается в формах массивов. Мы пройдём правила broadcasting по шагам, разберём скаляр + массив, вектор + матрицу, соберём формулу z-score нормализации и научимся читать ошибку operands could not be broadcast together — самую частую ошибку numpy для аналитика. В конце — playground'ы в браузере с точной проверкой вывода.

Что такое broadcasting и зачем он аналитику

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

Самый простой пример вы уже видели: умножение массива на число.

import numpy as np

revenue = np.array([1200, 3400, 5600, 980])
with_vat = revenue * 1.2   # добавили НДС 20% ко всему столбцу
print(with_vat)
# [1440. 4080. 6720. 1176.]

Число 1.2 — это скаляр, форма (). Массив — форма (4,). numpy «растянул» 1.2 на все 4 элемента. Без broadcasting пришлось бы писать цикл:

with_vat = []
for x in revenue:
    with_vat.append(x * 1.2)

Цикл на чистом Python в сотни раз медленнее и читается хуже. Для аналитика это значит: пересчёт выручки на миллион строк через broadcasting займёт миллисекунды, а через for — секунды. Векторизация = broadcasting под капотом.

Два правила broadcasting (выравнивание справа)

Чтобы понять, можно ли «соединить» два массива, numpy сравнивает их формы по простому алгоритму. Запомните два правила — этого хватит на 95% задач.

Правило 1. Формы выравниваются справа. numpy ставит формы массивов друг под другом, прижимая по правому краю. Если у одного массива измерений меньше, слева ему мысленно дописываются единицы.

Массив A: (3, 4)
Массив B:    (4,)   -> мысленно (1, 4)
Сравниваем по столбцам справа налево:
  4 и 4 -> совпадают
  3 и 1 -> размер 1 растянется до 3
Итог: (3, 4) -- OK

Правило 2. По каждому измерению размеры должны либо совпадать, либо один из них равен 1. Размер 1 растягивается до размера соседа. Если размеры разные и ни один не равен 1 — ошибка.

(3, 4) и (4,)  -> OK  (4==4, 3 vs 1)
(3, 4) и (3,)  -> ОШИБКА (справа 4 vs 3, ни один не 1)
(3, 1) и (1, 4) -> OK  -> результат (3, 4)
(2, 3) и (2, 1) -> OK  -> результат (2, 3)

Обратите внимание на ловушку: (3, 4) и (3,) НЕ совместимы, хотя «троек» вроде бы поровну. Выравнивание идёт справа, поэтому numpy сравнивает 4 с 3. Это самая частая ошибка новичка — об этом будет отдельная секция.

Проверять форму всегда можно атрибутом .shape:

a = np.zeros((3, 4))
print(a.shape)   # (3, 4)
b = np.array([1, 2, 3, 4])
print(b.shape)   # (4,)

Broadcasting: как (3,) растягивается под (2, 3)

ВЫРАВНИВАНИЕ ФОРМ СПРАВА

      ось0  ось1
  A:    2  ,  3        <- матрица (2, 3)
  B:    .  ,  3        <- вектор (3,) = мысленно (1, 3)
        |     |
        |     +--> 3 == 3      совпадают -> OK
        +--------> 2 vs 1      размер 1 растянется -> 2

  Итог формы: (2, 3)


ЧТО ПРОИСХОДИТ С ДАННЫМИ (копий в памяти НЕТ)

  B = [10  20  30]            форма (3,)

             |  numpy «размножает» строку вниз
             v

        c0   c1   c2
      +----+----+----+
  r0  | 10 | 20 | 30 |   <- B как есть
      +----+----+----+
  r1  | 10 | 20 | 30 |   <- та же B, растянута на 2-ю строку
      +----+----+----+
            (2, 3)

  A + B  ->  к каждой строке A прибавляется один и тот же вектор B

Формы прижимаются по правому краю: 3==3 совпадают, недостающее измерение слева у вектора считается за 1 и растягивается до 2. Вектор (3,) логически копируется на все строки матрицы (2, 3) — без реального копирования в памяти.

Попробуй сам

Задача · Python
Условие
Дан массив дневной выручки в рублях `revenue_rub`. Через broadcasting (скаляр + массив): 1) переведи выручку в доллары по курсу 100 руб = 1 USD и напечатай результат; 2) посчитай выручку с НДС 20% (умножение на 1.2) и напечатай. Никаких циклов.
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. Деление и умножение массива на число — это broadcasting: numpy сам растянет скаляр на все элементы. Пиши просто revenue_rub / 100 и revenue_rub * 1.2, без for.

Скаляр + массив: самый частый случай

Операция «один массив и одно число» — это broadcasting в чистом виде, и аналитик делает её десятки раз в день. Скаляр имеет форму () и растягивается на любую форму массива.

import numpy as np

revenue_rub = np.array([1200, 3400, 5600, 980])

# Конвертация в доллары (курс 100 руб = 1 USD)
revenue_usd = revenue_rub / 100
print(revenue_usd)
# [12.  34.  56.   9.8]

# Добавить НДС 20%
with_vat = revenue_rub * 1.2
print(with_vat)
# [1440. 4080. 6720. 1176.]

Точно так же работают сравнения и логика — они тоже броадкастятся и дают булев массив (маску):

big = revenue_rub > 3000
print(big)
# [False  True  True False]

# Сколько строк выше порога
print(big.sum())   # 2

Это работает и для 2D. Скаляр растянется на всю матрицу:

sales = np.array([[100, 120], [200, 210]])
print(sales * 2)
# [[200 240]
#  [400 420]]

Запомните: любое число в операции с массивом — это broadcasting. Никаких циклов, чтобы «пройтись по каждой ячейке».

Вектор + матрица: вычитаем средние по столбцам и строкам

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

import numpy as np

sales = np.array([
    [100, 120, 130, 150],   # продукт A
    [200, 210, 190, 200],   # продукт B
    [50,   60,  55,  65],   # продукт C
])
print(sales.shape)   # (3, 4)

Среднее по столбцам (по месяцам). axis=0 схлопывает строки, остаётся вектор длины 4 — среднее каждого месяца по всем продуктам:

col_mean = sales.mean(axis=0)
print(col_mean.shape)   # (4,)

# Вычитаем среднее месяца из каждого значения
centered = sales - col_mean
print(centered.shape)   # (3, 4) -- broadcasting сработал

Здесь формы (3, 4) и (4,) совместимы по правилу выравнивания справа: 4==4, а 3 против неявной 1. Вектор col_mean растянулся вниз на все 3 строки.

Среднее по строкам (по продуктам). axis=1 даёт вектор длины 3. И вот тут ловушка: (3, 4) - (3,) НЕ сработает напрямую, потому что справа сравнивается 4 и 3. Нужно превратить вектор в столбец формы (3, 1):

row_mean = sales.mean(axis=1)
print(row_mean.shape)        # (3,)

centered_rows = sales - row_mean.reshape(3, 1)
print(centered_rows.shape)   # (3, 4)

Правило: чтобы броадкастить по строкам, делайте вектор столбцом через .reshape(-1, 1) (или [:, None]). Чтобы броадкастить по столбцам — вектор и так подходит как есть.

Попробуй сам

Задача · Python
Условие
Матрица `sales`: 3 продукта (строки) x 4 месяца (столбцы). Посчитай среднюю выручку каждого продукта (среднее по строкам, `axis=1`), напечатай этот вектор и его `.shape`. Затем через broadcasting вычти из каждой строки её собственное среднее (центрирование по строкам) и напечатай результат. Подсказка: чтобы броадкастить по строкам, вектор средних надо сделать столбцом.
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. row_mean имеет форму (3,). Прямое sales - row_mean даст ошибку (справа 4 vs 3). Преврати вектор в столбец: row_mean.reshape(3, 1) -> форма (3, 1), и broadcasting растянет его по 4 столбцам.

Нормализация (X - mean) / std — главный приём

Самое полезное применение broadcasting для аналитика — стандартизация (z-score). Перед кластеризацией, регрессией, KNN или просто чтобы сравнить признаки в разных единицах (возраст в годах vs чек в рублях), их приводят к одному масштабу: среднее 0, стандартное отклонение 1.

Формула: Z = (X - mean) / std. И числитель, и деление — это broadcasting.

import numpy as np

# 4 пользователя x 3 признака: возраст, число покупок, средний чек
X = np.array([
    [25, 10, 1500],
    [35,  4, 3200],
    [45,  8, 2100],
    [55,  2, 4000],
], dtype=float)

mean = X.mean(axis=0)   # среднее по каждому столбцу -> (3,)
std  = X.std(axis=0)    # отклонение по каждому столбцу -> (3,)

Z = (X - mean) / std    # (4,3) - (3,) -> (4,3), затем /(3,) -> (4,3)
print(np.round(Z, 2))

Каждый признак (столбец) центрируется на своё среднее и делится на своё std. После нормализации Z.mean(axis=0) будет нулём, а Z.std(axis=0) — единицей по каждому столбцу. Это и есть смысл стандартизации.

Почему axis=0? Потому что мы нормализуем признаки, а они лежат по столбцам. mean и std дают по одному числу на признак, а broadcasting раздаёт их на все 4 строки-пользователя.

В A/B-аналитике тот же приём — посчитать z-score метрики, чтобы найти выбросы: значения с |Z| > 3 обычно аномалии. Один и тот же broadcasting-паттерн закрывает и подготовку фич для ML, и чистку данных.

Ошибки shape mismatch: как читать и чинить

Когда формы несовместимы, numpy бросает понятную (если знать, куда смотреть) ошибку:

import numpy as np

sales = np.array([
    [100, 120, 130, 150],
    [200, 210, 190, 200],
    [50,   60,  55,  65],
])
row_mean = sales.mean(axis=1)   # shape (3,)

sales - row_mean
# ValueError: operands could not be broadcast together with shapes (3,4) (3,)

Читаем сообщение справа налево: формы (3,4) и (3,). Прижимаем справа: 4 против 3 — не равны и ни один не 1. Несовместимо.

Чек-лист, когда поймали could not be broadcast:

- нужно по строкам -> vec.reshape(-1, 1) (превратить в столбец (n, 1));

- нужно по столбцам -> оставить (n,) как есть.

# Фикс: делаем вектор столбцом
centered = sales - row_mean.reshape(-1, 1)
print(centered.shape)   # (3, 4) -- работает

Ещё одна частая причина — забыли axis у mean/sum. Без axis агрегат схлопывает массив до одного скаляра, и тогда «среднее по строке» считается неправильно. Всегда указывайте axis=0 (по столбцам) или axis=1 (по строкам) осознанно.

Полезный приём для отладки — np.newaxis (он же None), добавляет ось размера 1 без копирования:

print(row_mean[:, None].shape)   # (3, 1) -- то же, что reshape(-1, 1)

Если держать в голове правило «выравнивание справа + размер 1 растягивается», ошибки shape mismatch перестают пугать: сообщение само показывает обе формы, остаётся подогнать одну под другую через reshape.

Попробуй сам

Задача · Python
Условие
Дана матрица признаков `X`: 4 пользователя (строки) x 3 признака (столбцы) — возраст, число покупок, средний чек. Сделай z-score нормализацию по столбцам: `Z = (X - mean) / std`, где mean и std считаются по `axis=0`. Напечатай: среднее столбцов, округлённый std (np.round(std, 2)), нормализованную матрицу (np.round(Z, 2)), а также новое среднее и новый std по столбцам (оба np.round(..., 2)), чтобы убедиться, что получилось 0 и 1.
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. mean = X.mean(axis=0) и std = X.std(axis=0) дают по 3 числа (по одному на признак). (X - mean) броадкастит вектор (3,) на все 4 строки, потом деление на std тоже броадкастится. После нормализации среднее каждого столбца станет 0, а std — 1.

Главное

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

Что такое broadcasting в numpy простыми словами?

Broadcasting — это механизм, который позволяет складывать и умножать массивы разной формы без ручного копирования данных. Например, прибавить скаляр ко всему массиву или вектор к каждой строке матрицы. numpy как бы «растягивает» меньший массив до формы большего, но физически ничего не дублирует, поэтому это быстро и экономно по памяти.

По каким правилам numpy выравнивает формы при broadcasting?

Формы сравниваются справа налево. Два измерения совместимы, если они равны или одно из них равно 1 — тогда оно растягивается. Недостающие измерения слева достраиваются как 1. Например, (3, 4) и (4,) совместимы — вектор применится к каждой строке, а (3, 4) и (3,) — нет.

Почему возникает ошибка operands could not be broadcast together?

Это значит, что формы несовместимы по правилам broadcasting — какое-то измерение не равно и не единица. Классика: пытаешься вычесть вектор из 3 элементов из матрицы (3, 4) по столбцам. Решается через reshape: преврати вектор в (3, 1), тогда он растянется по столбцам. Всегда проверяй .shape обоих операндов перед операцией.

Как нормализовать матрицу по столбцам через broadcasting?

Посчитай статистику вдоль строк с keepdims=True: mean = arr.mean(axis=0, keepdims=True) даст форму (1, n), и тогда (arr - mean) / arr.std(axis=0, keepdims=True) растянет её на все строки. keepdims=True тут ключевой — он сохраняет ось, чтобы broadcasting сработал по нужному направлению. Потренироваться на массивах можно в Python-тренажёре.

Закрепи NumPy на практике
Реши Python-задачи аналитика в тренажёре — pandas, numpy, scipy прямо в браузере. Первые 5 — без регистрации.
Открыть Python-тренажёр →