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) — без реального копирования в памяти.
Попробуй сам
Подсказка к решению — раскрыть только если застрял
Деление и умножение массива на число — это 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]). Чтобы броадкастить по столбцам — вектор и так подходит как есть.
Попробуй сам
Подсказка к решению — раскрыть только если застрял
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:
- Распечатайте обе формы:
print(a.shape, b.shape). Это первое, что нужно сделать всегда. - Выровняйте справа мысленно. Найдите измерение, где числа разные и ни одно не равно 1.
- Решите, по какой оси вы вообще хотите броадкастить. Чаще всего фикс — это
reshape:
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.
Попробуй сам
Подсказка к решению — раскрыть только если застрял
mean = X.mean(axis=0) и std = X.std(axis=0) дают по 3 числа (по одному на признак). (X - mean) броадкастит вектор (3,) на все 4 строки, потом деление на std тоже броадкастится. После нормализации среднее каждого столбца станет 0, а std — 1.Главное
- Broadcasting автоматически растягивает массивы разной формы для поэлементных операций — без циклов и без копирования памяти.
- Правило 1: формы выравниваются справа, недостающие измерения слева дополняются единицами.
- Правило 2: по каждому измерению размеры должны совпадать ИЛИ один из них равен 1 (тогда он растягивается).
- Скаляр + массив — самый частый случай: revenue * 1.2 добавляет НДС всему столбцу одной строкой.
- Чтобы броадкастить по строкам матрицы, вектор средних делают столбцом через reshape(-1, 1) или [:, None].
- Нормализация (X - mean) / std по axis=0 — главный broadcasting-приём аналитика: приводит признаки к среднему 0 и std 1.
- Ошибка operands could not be broadcast читается справа налево: распечатай обе .shape и подгони форму через reshape.
Частые вопросы
Что такое 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-тренажёре.