NumPy массивы: эффективные инструменты для научных расчётов

Раздел: Машинное обучение -> NumPy: массивы

Основы работы с массивами NumPy

Массивы NumPy (объекты ndarray) обеспечивают быструю векторизованную обработку однородных данных. Основные преимущества: компактное хранение, почленные операции без циклов Python, универсальные функции (ufunc) и широковещательная передача (broadcasting).

Самый эффективный способ начать работу - создать массив с помощью np.array() и сразу применять векторизованные операции:

import numpy as np
a = np.array([1, 2, 3, 4, 5])
b = a * 2 + 1

Python 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) - все ли элементы положительны.

Массивы NumPy в Python - comments

En
Python numpy массивы (python)