Перейти к содержанию

NumPy: Шпаргалка

~3 минуты чтения

NumPy -- фундамент всего ML-стека в Python: PyTorch tensors, Pandas DataFrames, sklearn внутри используют ndarray. На собеседовании NumPy-вопросы встречаются в 80%+ ML-интервью (vectorization, broadcasting, view vs copy). Знание подводных камней NumPy -- разница между "пишу код" и "понимаю что происходит под капотом".

Быстрый старт

import numpy as np

# Создание
arr = np.array([1, 2, 3, 4, 5])
arr = np.array([[1, 2, 3], [4, 5, 6]])

# Атрибуты
arr.shape      # (2, 3)
arr.dtype      # int64
arr.ndim       # 2
arr.size       # 6

Создание массивов

# Из Python
np.array([1, 2, 3])
np.array([[1, 2], [3, 4]])

# Специальные массивы
np.zeros((3, 4))           # Нули
np.ones((3, 4))            # Единицы
np.full((3, 4), 7)         # Заполнить значением
np.empty((3, 4))           # Неинициализированный (быстрее)
np.eye(3)                  # Единичная матрица
np.diag([1, 2, 3])         # Диагональная матрица

# Последовательности
np.arange(0, 10, 2)        # [0, 2, 4, 6, 8]
np.linspace(0, 1, 5)       # [0, 0.25, 0.5, 0.75, 1]
np.logspace(0, 3, 4)       # [1, 10, 100, 1000]

# Случайные
np.random.rand(3, 4)       # Uniform [0, 1)
np.random.randn(3, 4)      # Normal(0, 1)
np.random.randint(0, 10, (3, 4))  # Integers
np.random.choice([1, 2, 3], size=5)  # Случайный выбор
np.random.permutation(10)  # Перемешанные индексы
# ВАЖНО: np.random.seed() -- глобальный state, лучше default_rng (см. ниже)

# Повторения
np.repeat([1, 2, 3], 3)    # [1,1,1,2,2,2,3,3,3]
np.tile([1, 2, 3], 3)      # [1,2,3,1,2,3,1,2,3]

Индексация и Slicing

arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

# Базовая индексация
arr[0]           # Первая строка: [1, 2, 3]
arr[0, 1]        # Элемент: 2
arr[:, 0]        # Первый столбец: [1, 4, 7]
arr[0:2]         # Первые 2 строки
arr[0:2, 1:3]    # Подматрица

# Fancy indexing
arr[[0, 2]]      # Строки 0 и 2
arr[:, [0, 2]]   # Столбцы 0 и 2
arr[[0, 1], [1, 2]]  # Элементы [0,1] и [1,2]

# Boolean indexing
arr[arr > 5]     # Элементы > 5: [6, 7, 8, 9]
arr[arr % 2 == 0]  # Чётные

# Присваивание
arr[arr > 5] = 0
arr[:, 0] = [10, 20, 30]

Присваивание массива -- это view, не копия

b = a НЕ копирует данные. b и a указывают на одну память -- изменение b[0] = 999 изменит и a[0]. Slicing тоже возвращает view: c = a[0:3] -- та же ловушка. Для независимой копии используйте b = a.copy(). Это источник #1 неожиданных багов в data pipeline, когда одна функция "портит" данные другой.

Изменение формы

arr = np.arange(12)

arr.reshape(3, 4)          # Новая форма (возвращает view)
arr.reshape(3, -1)         # -1 = вычислить автоматически
arr.flatten()              # 1D копия
arr.ravel()                # 1D view (если возможно)

arr.T                      # Транспонирование
arr.transpose(1, 0, 2)     # Перестановка осей
arr.swapaxes(0, 1)         # Поменять 2 оси

np.expand_dims(arr, axis=0)  # Добавить ось
np.squeeze(arr)              # Убрать оси размера 1

# Stacking
np.vstack([a, b])          # Вертикально
np.hstack([a, b])          # Горизонтально
np.stack([a, b], axis=0)   # По новой оси
np.concatenate([a, b], axis=0)

# Splitting
np.split(arr, 3)           # На 3 части
np.hsplit(arr, 3)          # Горизонтально
np.vsplit(arr, 3)          # Вертикально

Математические операции

Поэлементные

a + b, a - b, a * b, a / b
a ** 2                     # Степень
np.sqrt(a)                 # Корень
np.exp(a)                  # e^x
np.log(a)                  # ln(x)
np.log10(a)                # log10(x)
np.abs(a)                  # Модуль
np.sin(a), np.cos(a), np.tan(a)
np.ceil(a), np.floor(a), np.round(a)
np.clip(a, min, max)       # Ограничить диапазон

Агрегации

arr.sum()                  # Сумма всех
arr.sum(axis=0)            # Сумма по столбцам
arr.sum(axis=1)            # Сумма по строкам
arr.sum(axis=0, keepdims=True)  # Сохранить размерность

arr.mean()                 # Среднее
arr.std()                  # Стандартное отклонение
arr.var()                  # Дисперсия
arr.min(), arr.max()
arr.argmin(), arr.argmax() # Индексы мин/макс
arr.cumsum()               # Кумулятивная сумма
arr.cumprod()              # Кумулятивное произведение

np.percentile(arr, 50)     # Медиана
np.percentile(arr, [25, 50, 75])  # Квартили

Линейная алгебра

# Матричное умножение
np.dot(A, B)               # Или A @ B
np.matmul(A, B)            # Или A @ B

# Произведения
np.inner(a, b)             # Скалярное произведение
np.outer(a, b)             # Внешнее произведение

# Линейная алгебра
np.linalg.inv(A)           # Обратная матрица
np.linalg.det(A)           # Определитель
np.linalg.eig(A)           # Собственные значения/векторы
np.linalg.svd(A)           # SVD разложение
np.linalg.norm(a)          # Норма вектора
np.linalg.solve(A, b)      # Решение Ax = b
np.linalg.lstsq(A, b)      # Наименьшие квадраты

Логические операции

a > b                      # Поэлементное сравнение
np.greater(a, b)           # То же самое
np.equal(a, b)
np.logical_and(a > 0, a < 5)
np.logical_or(a < 0, a > 5)
np.logical_not(a > 0)

np.all(arr > 0)            # Все True?
np.any(arr > 0)            # Хоть один True?
np.where(arr > 0, arr, 0)  # Условная замена
np.argwhere(arr > 0)       # Индексы где True

np.isnan(arr)              # Проверка на NaN
np.isinf(arr)              # Проверка на inf
np.isfinite(arr)

Сортировка

np.sort(arr)               # Возвращает отсортированную копию
arr.sort()                 # Сортирует in-place
np.sort(arr, axis=0)       # По столбцам
np.sort(arr, axis=1)       # По строкам

np.argsort(arr)            # Индексы сортировки
np.lexsort((b, a))         # Сортировка по нескольким ключам

np.partition(arr, 3)       # Частичная сортировка (k-th element)
np.argpartition(arr, 3)

Broadcasting

# Правила:
# 1. Выравниваем размерности справа
# 2. Размерность 1 растягивается

a = np.array([[1, 2, 3]])   # Shape (1, 3)
b = np.array([[1], [2]])    # Shape (2, 1)
a + b                        # Shape (2, 3)

# Примеры
arr = np.array([[1, 2, 3],
                [4, 5, 6]])
arr - arr.mean(axis=0)      # Центрирование по столбцам
arr / arr.sum(axis=1, keepdims=True)  # Нормализация по строкам

Broadcasting: ошибки тихие, не падают

Если формы несовместимы для broadcasting, NumPy кинет ValueError -- это хороший исход. Опасность в другом: когда формы совместимы, но вы имели в виду другую операцию. Пример: a.shape = (3, 1), b.shape = (1, 4) -- a * b даст матрицу (3, 4). Если b должен был быть (3,) для поэлементного умножения -- ошибки не будет, но результат неправильный. Всегда проверяйте .shape после операций.


np.float64 vs np.float32: молчаливая потеря скорости

NumPy по умолчанию создает float64 массивы: np.array([1.0, 2.0]).dtype = float64. PyTorch и большинство GPU-фреймворков работают с float32. Если передать float64 в PyTorch, он молча конвертирует, удваивая memory. Хуже: matmul двух float64 матриц в 2x медленнее float32 на одном CPU. Привычка: np.array([...], dtype=np.float32) или arr.astype(np.float32) перед передачей в ML-пайплайн.

Работа с NaN

np.nan                     # Not a Number
np.isnan(arr)              # Проверка
np.nansum(arr)             # Сумма игнорируя NaN
np.nanmean(arr)            # Среднее игнорируя NaN
np.nanstd(arr)
np.nanmax(arr), np.nanmin(arr)

# Замена NaN
arr[np.isnan(arr)] = 0
np.nan_to_num(arr, nan=0)  # Заменить NaN на 0

Полезные функции

# Уникальные значения
np.unique(arr)
np.unique(arr, return_counts=True)

# Сравнение
np.allclose(a, b, rtol=1e-5)  # Примерное равенство

# Копирование
b = a.copy()               # Глубокая копия
b = a                      # Это view!

# Типы данных
arr.astype(np.float32)
arr.astype('int32')

# Сохранение/загрузка
np.save('array.npy', arr)
arr = np.load('array.npy')
np.savez('arrays.npz', a=arr1, b=arr2)
data = np.load('arrays.npz')
arr1 = data['a']

Случайные числа (новый API)

rng = np.random.default_rng(seed=42)

rng.random((3, 4))         # Uniform [0, 1)
rng.standard_normal((3, 4)) # Normal(0, 1)
rng.integers(0, 10, (3, 4)) # Integers
rng.choice([1, 2, 3], size=5)
rng.shuffle(arr)           # In-place shuffle
rng.permutation(arr)       # Возвращает перемешанную копию

Полезные паттерны

Нормализация

# Min-Max [0, 1]
arr_norm = (arr - arr.min()) / (arr.max() - arr.min())

# Z-score
arr_std = (arr - arr.mean()) / arr.std()

# L2 нормализация
arr_l2 = arr / np.linalg.norm(arr)

One-hot encoding

labels = np.array([0, 1, 2, 1, 0])
n_classes = 3
one_hot = np.eye(n_classes)[labels]

Cosine similarity

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

Batch matrix multiply

# A: (batch, n, m), B: (batch, m, k)
C = np.einsum('bnm,bmk->bnk', A, B)
# или
C = A @ B  # Broadcasting работает

Вопросы на интервью

Q: В чём разница между view и copy в NumPy?

❌ "reshape всегда создает копию" -- неверно, reshape возвращает view когда возможно

✅ "Slicing (a[0:3]), reshape(), .T возвращают view -- разделяют память с оригиналом. Изменение view меняет оригинал. copy(), flatten() создают независимые копии. Проверить: np.shares_memory(a, b). Это критично в pipeline -- функция может случайно модифицировать входные данные."

Q: Как работает broadcasting?

❌ "NumPy просто копирует массив до нужного размера" -- копирования не происходит

✅ "Broadcasting -- виртуальное растягивание. Правила: выравнивание shapes справа, размерность 1 растягивается, несовместимые размерности (не 1 и не равны) дают ValueError. Пример: (3,1) + (1,4) = (3,4). NumPy не копирует данные -- использует strides с нулевым шагом. Это O(1) по памяти, но результат -- новый массив (3,4)."

Q: Почему vectorized операции быстрее циклов?

❌ "Потому что NumPy написан на C" -- часть ответа, но не главная

✅ "Три причины: (1) C-реализация без Python interpreter overhead, (2) SIMD-инструкции (AVX2/AVX512) обрабатывают 8-16 float за такт, (3) cache locality -- contiguous memory access. np.dot(a, b) вызывает BLAS (MKL/OpenBLAS) с 100x ускорением vs Python loop. На массиве 1M элементов: loop = ~500ms, vectorized = ~1ms."


See Also