Все материалы
Продуктовая аналитикаматериалстарт

NumPy для аналитика: массивы, маски и векторные расчёты

Что нужно знать аналитику о NumPy: массивы, типы, boolean-маски, np.where и векторизация, которая лежит под многими операциями pandas.

КейсПрактика5 августа 2026 г.обновлено 15 сентября 2026 г.10 мин

Pandas часто скрывает NumPy под привычными операциями над колонками. Но понимание массивов помогает объяснить типы, broadcasting, boolean-маски и причины, по которым векторный расчёт работает быстрее цикла по строкам. Аналитику не нужен весь NumPy: достаточно нескольких идей, которые встречаются каждый день.

Коротко

NumPy хранит однородные числовые массивы и выполняет операции над ними без явного цикла Python. ndarray, маски, np.where, np.select и базовые агрегаты помогают писать расчёты над колонками pandas проще и предсказуемее.

  • Массив обычно содержит значения одного dtype.
  • Операция над массивом применяется к элементам поэлементно.
  • Boolean-маска выбирает нужные значения без цикла.
  • Broadcasting позволяет применять одно число или вектор к массиву совместимой формы.

Массив и dtype

В списке Python числа и строки могут сосуществовать свободно. NumPy старается хранить массив компактно и однородно, поэтому тип влияет на память и результат операции. В аналитике это особенно заметно на больших числовых колонках и при смешении чисел с пропусками.

pythonСоздать массив и выполнить операцию над ним
import numpy as np

revenue = np.array([1200, 800, 450, 1500], dtype='int32')
net_revenue = revenue * 0.9
print(net_revenue)
print(revenue.dtype, revenue.shape)

Маски вместо циклов

Boolean-маска — массив True/False той же длины, что и исходные значения. Она превращает правило фильтрации в объект, который можно проверить и передать дальше. В pandas тот же подход работает для Series и DataFrame.

pythonВыбрать крупные заказы
revenue = np.array([1200, 800, 450, 1500])
large_mask = revenue >= 1000
large_orders = revenue[large_mask]

print(large_mask)       # [ True False False  True ]
print(large_orders)     # [1200 1500]

np.where и np.select для правил

np.where подходит для простого выбора из двух вариантов. Для нескольких условий используйте np.select, чтобы порядок правил был виден в коде. В обоих случаях обязательно задайте значение для случая, который не попал в условия.

pythonСоздать признак и сегмент заказа
orders['is_large'] = np.where(
    orders['revenue'].ge(1000), True, False
)

conditions = [orders['revenue'].ge(5000), orders['revenue'].ge(1500)]
orders['segment'] = np.select(
    conditions, ['high', 'medium'], default='low'
)

Срез массива меняет источник, а маска создаёт отдельные данные

Допустим, вы проверяете сегменты заказов и хотите временно заменить первую сумму на ноль. Присваивание sample = revenue[:2] выглядит как создание маленького набора, но обычный срез NumPy разделяет память с исходным массивом. После sample[0] = 0 изменится и revenue[0]. Если затем пересчитать долю крупных заказов, она поменяется без явной правки основного набора. В ноутбуке это легко принять за результат нового правила сегментации.

При выборе по булевой маске, например revenue[revenue >= 1000], получается копия. Её изменение не вернётся в revenue. Но запись revenue[mask] = 0 напрямую изменяет источник: здесь индексирование стоит слева от присваивания. Различайте получение выбранных значений и запись по выбранным позициям. Для самостоятельного эксперимента используйте revenue[:2].copy(), а наличие общей памяти можно проверить через np.shares_memory, не дожидаясь неожиданного результата.

Эта разница важна и при подготовке условий для np.select. Сохраните исходный revenue, создайте отдельный результат segment и проверьте граничные значения. Если вместо маски фильтрации сначала обнулить все подозрительные суммы в представлении, отрицательный заказ потеряет признак ошибки и попадёт в low. Вычисление станет аккуратнее только потому, что данные были незаметно переписаны. В контрольном примере сравните исходный массив до и после создания категорий.

Для числового массива с NaN сравнения с порогами возвращают false, поэтому пропуск проваливается в default. Поставьте проверку np.isnan первой и выдели unknown, а отрицательное значение направь в invalid. Затем идут high и medium, после них low. Порядок отражает приоритет: сумма 5 000 удовлетворяет и условию medium, но должна остановиться на high. Новая категория не требует изменения самих денежных значений.

У np.where есть ещё одно отличие от обычного if: выражения обоих вариантов вычисляются до выбора элементов. Запись np.where(denominator != 0, numerator / denominator, np.nan) не предотвращает само деление на ноль. Для него используйте np.divide с where и заранее заполненным out, чтобы позиции без расчёта остались определены. С NumPy-массивами это отдельная задача управления вычислением; переносить на неё ожидания от короткого замыкания Python нельзя.

pythonКопия для эксперимента и сегмент без изменения исходных сумм
import numpy as np
revenue = np.array([1200., 800., 450., 1500.])
view = revenue[:2]
assert np.shares_memory(view, revenue)
sample = view.copy()
sample[0] = 0
assert revenue[0] == 1200
values = np.array([np.nan, -1., 0., 1500., 5000.])
segment = np.select(
    [np.isnan(values), values < 0, values >= 5000, values >= 1500],
    ['unknown', 'invalid', 'high', 'medium'], default='low',
)
assert segment.tolist() == ['unknown', 'invalid', 'low', 'medium', 'high']
numerator = np.array([10., 0.])
denominator = np.array([2., 0.])
ratio = np.full(2, np.nan)
np.divide(numerator, denominator, out=ratio, where=denominator != 0)
assert ratio[0] == 5 and np.isnan(ratio[1])

Агрегаты и broadcasting

Агрегаты NumPy считают сумму, среднее и квантили по массиву или выбранной оси. Broadcasting позволяет вычесть среднее из каждого значения без ручного цикла. В реальной метрике всё равно проверьте пропуски и знаменатель: NumPy не знает, что означает отсутствие наблюдения.

pythonОтклонение заказа от среднего
revenue = np.array([1200, 800, 450, 1500], dtype='float64')
average = revenue.mean()
deviation = revenue - average

print('mean:', average)
print('p90:', np.quantile(revenue, .9))
print('deviation:', deviation)
NumPy не делает бизнес-правило очевидным автоматически

Векторная операция отвечает за вычисление. Определение периода, пользователя, активного действия и знаменателя остаётся аналитической задачей.

Когда использовать NumPy, а когда оставить pandas

NumPy удобен, когда работаете с плотным числовым массивом: считаете преобразование, маску или статистику и точно знаете форму данных. Pandas добавляет индексы, имена колонок, nullable-типы и операции по группам. Поэтому в продуктовой аналитике обычно читают и описывают данные через pandas, а NumPy используют внутри правила, где он делает код короче или быстрее.

Не превращайте DataFrame в values без причины. После этого теряются имена колонок и связь с индексом, а случайное изменение порядка может привести к неверному присваиванию. Если массив нужен для вычисления, возвращайте результат обратно в Series с тем же индексом или используйте to_numpy только в коротком, явно проверенном участке.

Выбор инструмента должен следовать форме вопроса. groupby, merge, даты и категориальные поля обычно читаются лучше в pandas. Матричные операции, маски над несколькими числовыми рядами и broadcasting естественнее выражаются в NumPy. Граница между ними — часть читаемости pipeline.

Практическая граница между pandas и NumPy
ЗадачаИнструмент первого выбораПочему
прочитать CSV и проверить схемуpandasесть имена колонок и типы таблицы
выбрать строки по условиюpandas или NumPy maskоба варианта сохраняют правило явно
groupby по каналуpandasагрегация привязана к измерению
применить формулу к числовому массивуNumPyоперация векторная и компактная
сложить таблицы по ключуpandasнужно сохранить семантику JOIN

dtype, пропуски и неожиданное приведение

Однородность массива — сила NumPy и источник некоторых сюрпризов. Если в числовой массив добавить строку, NumPy может привести все значения к строковому типу. Если добавить np.nan, целочисленный массив часто станет float. На небольшом примере это незаметно, но на больших данных меняет память и поведение сравнений.

Перед арифметикой проверьте dtype, наличие конечных значений и диапазон. np.mean вернёт nan, если в массиве есть пропуск; np.nanmean игнорирует его, но это уже методологическое решение. Игнорировать пропуски можно не всегда: для конверсии отсутствие события может быть нулём, а отсутствие revenue — неизвестным значением.

Для денежных и процентных расчётов отдельно решите, где округлять. Округление каждой строки до сложения и округление итоговой суммы дают разные ответы. NumPy честно выполнит обе операции, но не скажет, какая соответствует договорённости с бизнесом.

pythonПроверить пропуски до агрегирования
values = np.array([1200.0, np.nan, 800.0])
print(values.dtype)
print('mean with NA:', np.mean(values))
print('mean ignoring NA:', np.nanmean(values))

finite = np.isfinite(values)
print('invalid values:', (~finite).sum())

Составные маски нужно читать как условие

Одна маска проста: revenue >= 1000. С несколькими условиями легко ошибиться в приоритетах операторов. Для NumPy используйте &, | и ~, а каждое сравнение заключай в скобки. Операторы and и or предназначены для одиночных boolean-значений и не работают как поэлементный выбор массива.

Полезно сохранять маски с именами: is_paid, is_recent, has_valid_user. Тогда итоговое условие можно обсудить с коллегой и проверить долю каждой части. Если итоговая выборка неожиданно пустая, посмотрите пересечение условий вместо того, чтобы менять порог наугад.

В продуктовой аналитике маска часто является определением аудитории. Например, «активный пользователь» может означать событие в периоде, исключённое QA-устройство и подтверждённую учётную запись. Унесите это правило из временного массива в документацию метрики, иначе следующий расчёт создаст другую аудиторию.

pythonСобрать и диагностировать аудиторию
is_recent = orders['created_at'].ge('2026-08-01')
is_valid = orders['user_id'].notna()
is_paid = orders['revenue'].gt(0)
audience = is_recent & is_valid & is_paid

print({
    'recent': is_recent.mean(),
    'valid': is_valid.mean(),
    'paid': is_paid.mean(),
    'audience': audience.mean(),
})

Broadcasting: форма массива важнее магии

Broadcasting позволяет применить массив формы (3,) к матрице (2, 3) или одно число ко всем элементам. NumPy сравнивает размеры справа налево: измерения должны совпадать или одно из них должно быть равно единице. Ошибка формы лучше явного цикла, но сообщение может быть непривычным, если размерность не выписана в коде.

Классический сценарий аналитика — нормализовать значения по колонкам или вычесть среднее каждой группы. Для этого часто нужен keepdims=True, чтобы результат сохранил ось и корректно применился обратно. Перед расчётом выведите shape каждого объекта. Это занимает секунду и экономит час отладки.

Не используйте broadcasting для скрытой бизнес-логики. Если вектор коэффициентов соответствует каналам, сохраните порядок и названия отдельно. Без индекса NumPy не знает, что третий коэффициент относится к email, а не к referral.

pythonНормализовать строки матрицы
matrix = np.array([[10, 20, 30], [4, 8, 12]], dtype='float64')
row_mean = matrix.mean(axis=1, keepdims=True)
centered = matrix - row_mean

print(matrix.shape, row_mean.shape, centered.shape)
assert centered.shape == matrix.shape

Ось определяет, какую среднюю вы вычитаете из выручки

Возьмём суммы из первого примера и расположим их в матрице: первая строка содержит 1 200 и 800 ₽, вторая — 450 и 1 500 ₽. Для этого упражнения строки обозначают два канала, столбцы — два последовательных заказа каждого канала. Такая форма задана специально для разбора осей; в реальной выгрузке сначала нужно обеспечить одинаковое значение каждой позиции. Средняя по каждому каналу равна 1 000 и 975 ₽.

Вызов mean(axis=1, keepdims=True) сворачивает столбцы внутри строки и оставляет форму (2, 1). При вычитании этот столбец растягивается вправо: из первой строки вычитается 1 000, из второй — 975. Получаем отклонения [200, −200] и [−525, 525]. Если убрать keepdims, средние будут иметь форму (2,), и NumPy совместит их с последней осью. Из первого столбца каждой строки вычтется 1 000, из второго — 975.

Квадратная матрица делает ошибку опасной: обе операции разрешены, обе возвращают форму (2, 2), но отвечают на разные вопросы. Во втором варианте получится [200, −175] и [−550, 525]. Проверки одного shape недостаточно. Для центрирования строк проверьте, что их средние после преобразования близки к нулю. Ещё полезно добавить в контрольный набор третий столбец: случайно потерянная ось уже не сможет незаметно совпасть по длине.

Теперь замените второй заказ первого канала на NaN. nanmean(axis=1) даст 1 200 и 975, поскольку в первой строке известно только одно значение. Для каждой средней выведите число наблюдений: 1 и 2. Это средние по доступным заказам, а не оценка неизвестного заказа. Если данные пропадают именно у дорогих покупок, игнорирование NaN не восстановит правильное среднее, даже когда вычисление выполнено без предупреждений.

Для полностью пустой строки nanmean вернёт NaN и предупредит о пустой базе. nansum такой строки даст ноль, который нельзя без дополнительной проверки назвать наблюдаемой выручкой. Число известных значений помогает различить эти случаи. Отдельно проверяйте бесконечности: nan-функции пропускают NaN, но не обязаны исключать inf. Если бесконечность появилась после деления, исправьте правило деления до агрегата, иначе средняя станет сигналом вычислительной ошибки вместо свойства заказов.

pythonОдинаковая форма результата не гарантирует правильную ось
import numpy as np
revenue = np.array([[1200., 800.], [450., 1500.]])
row_mean = revenue.mean(axis=1, keepdims=True)
centered = revenue - row_mean
wrong_axis = revenue - revenue.mean(axis=1)
np.testing.assert_allclose(centered, [[200, -200], [-525, 525]])
np.testing.assert_allclose(centered.mean(axis=1), [0, 0])
assert not np.allclose(wrong_axis.mean(axis=1), 0)
with_missing = revenue.copy()
with_missing[0, 1] = np.nan
print(np.nanmean(with_missing, axis=1))  # [1200., 975.]
print((~np.isnan(with_missing)).sum(axis=1))  # [1, 2]

От массива к продуктовой метрике

NumPy ускоряет вычислительную часть, но сама метрика начинается раньше: с определения пользователя, события и периода. Перед тем как вызвать .mean() или np.quantile, ответьте, что является наблюдением. Среднее время ответа по событиям не равно среднему времени ответа по пользователям. Массив может быть идеально посчитан и всё равно отвечать не на тот вопрос.

Полезная практика — сначала собрать DataFrame с именованными колонками, сделать проверки и только затем передать числовой массив в вычислительную функцию. После расчёта верните результат в таблицу с ключами сегмента и периода. Так промежуточная оптимизация не разрывает связь между числом и его смыслом.

Для следующего шага возьмите одну свою метрику, запишите её зерно, создайте маску аудитории, проверьте пропуски и сравните векторную формулу с понятной реализацией на маленьком наборе. Если ответы совпали, NumPy стал частью воспроизводимого расчёта, а не отдельным фокусом с массивами.

  • Проверить форму и dtype до операции.
  • Явно решить, что делать с пропусками.
  • Скобками закрепить составные маски.
  • Проверить shape при broadcasting.
  • Вернуть вычисление в таблицу с ключами и понятными названиями.
Продолжить чтение
Вся библиотека