NumPy массивы: эффективные инструменты для научных расчётов
Основы работы с массивами NumPy
Массивы NumPy (объекты ndarray) обеспечивают быструю векторизованную обработку однородных данных. Основные преимущества: компактное хранение, почленные операции без циклов Python, универсальные функции (ufunc) и широковещательная передача (broadcasting).
Самый эффективный способ начать работу - создать массив с помощью np.array() и сразу применять векторизованные операции:
import numpy as np
a = np.array([1, 2, 3, 4, 5])
b = a * 2 + 1Python numpy массивы (массивы numpy в python)
array([ 3, 5, 7, 9, 11])
Здесь каждый элемент умножается на 2 и прибавляется 1 без явного цикла - это и есть векторизация. Такой подход минимизирует накладные расходы Python.
Как создать массив из другого итерируемого объекта?
Используйте np.array() со списком, кортежем или генератором:
data_tuple = (10, 20, 30)
arr = np.array(data_tuple)
array([10, 20, 30])
Распространённая ошибка - передача объектов разного типа. NumPy попытается привести все к общему типу (например, float), если это возможно. Если невозможно - возникнет ValueError или массив станет object-типа, теряя производительность.
Как создать массив заранее известной формы, заполненный нулями или единицами?
Функции np.zeros() и np.ones():
zeros_2x3 = np.zeros((2, 3))
ones_2x3 = np.ones((2, 3), dtype=int)
array([[0., 0., 0.],
[0., 0., 0.]])
array([[1, 1, 1],
[1, 1, 1]])
Обратите внимание: по умолчанию zeros и ones создают массив типа float64. Чтобы изменить тип, укажите параметр dtype. Забыв об этом, можно неожиданно получить дробные значения.
Как создать последовательность чисел с равномерным шагом?
np.arange() работает как встроенный range, но возвращает массив:
seq = np.arange(0, 10, 2) # start, stop, step
array([0, 2, 4, 6, 8])
Для нецелых шагов используйте np.linspace():
seq_lin = np.linspace(0, 1, 5) # 5 точек от 0 до 1
array([0. , 0.25, 0.5 , 0.75, 1. ])
arange с дробным шагом может дать неожиданное количество элементов из-за ошибок округления. Рекомендуется linspace для точного количества точек.
Как изменить форму массива без изменения данных?
Метод reshape():
arr = np.array([1,2,3,4,5,6])
arr_2x3 = arr.reshape(2, 3)
array([[1, 2, 3],
[4, 5, 6]])
Общее количество элементов должно совпадать. Ошибка ValueError: cannot reshape возникает, если произведение новых размерностей не равно числу элементов. Использование -1 в reshape автоматически вычисляет размер по остальным осям.
Как выполнять условную выборку и маскирование?
Булева индексация - одна из сильных сторон NumPy:
a = np.array([1, 2, 3, 4, 5])
mask = a > 2
result = a[mask]
array([3, 4, 5])
Маска должна быть массивом булевых значений той же длины (для одномерного массива). Если размеры не совпадают, возникает IndexError. Также стоит помнить, что маска создаёт копию данных, а не представление.
Как выполнять поэлементные математические операции?
Все арифметические операторы (+ - * / **) работают поэлементно:
x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
z = x * y + 2
array([ 6, 12, 20])
Для матричного умножения используется @ или np.dot().
Путаница между поэлементным (*) и матричным (@) умножением - частая ошибка. Для одномерных массивов @ даёт скалярное произведение, для двумерных - матричное.
Расширенные примеры и нестандартные приёмы
Дополнительные возможности NumPy раскрываются через сложную индексацию, широковещание и работу с разными размерностями.
Fancy indexing (индексация массивами индексов)
Позволяет извлечь произвольные элементы по их индексам, заданным в другом массиве:
arr = np.arange(10, 20) # array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
indices = np.array([0, 2, 7])
result = arr[indices]
array([10, 12, 17])
Индексы могут быть как положительными, так и отрицательными (отсчёт с конца). Fancy indexing всегда возвращает копию, а не представление.
Broadcasting (широковещательная передача) в деталях
NumPy автоматически расширяет размерности массивов при выполнении операций, если это возможно. Правила: оси сравниваются с конца; размерность должна совпадать или одна из них равна 1.
a = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
b = np.array([10, 20, 30]) # shape (3,) -> broadcast до (1,3) -> (2,3)
c = a + b
array([[11, 22, 33],
[14, 25, 36]])
# Пример с разными осями
x = np.ones((3, 1)) # shape (3,1)
y = np.ones((1, 4)) # shape (1,4) -> broadcast до (3,4)
z = x * y
array([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
Ошибка broadcasting возникает, когда размерности не совпадают и ни одна не равна 1. Например, (3,2) + (3,) - несоответствие по оси 1 (2 vs 1). Получаем ValueError: operands could not be broadcast together.
Использование универсальных функций (ufunc) с параметрами
Ufunc (например, np.add, np.sin) можно вызывать с дополнительными аргументами, такими как out для записи результата в существующий массив:
a = np.array([1, 2, 3])
b = np.empty(3)
np.multiply(a, 2, out=b) # результат записывается в b
array([2., 4., 6.])
Это экономит память и время, особенно в больших циклах.
Агрегации по осям (сокращение размерности)
Параметр axis определяет, вдоль какой оси сворачивать:
mat = np.array([[1, 2, 3],
[4, 5, 6]])
row_sums = np.sum(mat, axis=1) # сумма по строкам -> shape (2,)
col_sums = np.sum(mat, axis=0) # сумма по столбцам -> shape (3,)
array([ 6, 15]) # row_sums: 1+2+3=6, 4+5+6=15 array([5, 7, 9]) # col_sums: 1+4=5, 2+5=7, 3+6=9
При указании keepdims=True размерность сохраняется (равна 1), что удобно для дальнейшего broadcasting.
Матричное умножение и тензорные произведения
Для многомерных массивов np.dot и np.matmul (@) выполняют умножение по последним двум осям:
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = A @ B
array([[19, 22],
[43, 50]])
Для пакетной обработки (batch) - массив формы (N, M, K) умножается на (N, K, L) с результатом (N, M, L).
Работа с масками и условная замена с помощью np.where
a = np.array([1, -2, 3, -4, 5])
# Заменить отрицательные на 0
result = np.where(a < 0, 0, a)
array([1, 0, 3, 0, 5])
np.where может также возвращать индексы ненулевых элементов (если передан только один аргумент).
Чтение и запись массива в бинарный файл (формат .npy)
arr = np.arange(20).reshape(4, 5)
np.save('array_data.npy', arr)
loaded = np.load('array_data.npy')
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
Формат .npy сохраняет данные и тип без потерь, подходит для передачи между приложениями.
Изменение порядка байтов (endianness) и типов данных
arr = np.array([1, 256, 65536], dtype='') # big-endian
array([ 1, 256, 65536], dtype='>u2') # значения сохранены, порядок байтов изменён
Это может быть критично при межплатформенном обмене данными.
Логические операции над массивами (any, all)
a = np.array([True, False, True])
print(a.any()) # хотя бы один True
print(a.all()) # все True
True False
Часто используется для проверки условий: np.all(a > 0) - все ли элементы положительны.