Семестр 3 · Модуль 1 · Неделя 2

Индексация, broadcasting, ufuncs

view и копия · fancy indexing · boolean masking · правила broadcasting · универсальные функции · агрегации с осью

Мотивация недели: нормализация признаков перед обучением ML-модели — это ровно то, что делает каждая ML-библиотека перед fit. Одна строка NumPy заменит 10 строк циклов.

Индексация и срезы

a = np.arange(12).reshape(3, 4) a[1, 2] # → 6 (строка 1, столбец 2) a[-1, -1] # → 11 (последний элемент) a[:, 1] # весь 2-й столбец → [1 5 9] a[::-1] # реверс строк a[0:2, 0:2] # блок 2×2

Ключевой момент: срез возвращает view — вид на те же данные, а не копию. Изменение view меняет исходный массив.

Срез строк из 1-го семестра работал для строк — здесь то же, но возвращается view. Как получить настоящую копию?

View vs копия — главная ловушка недели

# срез = view a = np.arange(12).reshape(3, 4) view = a[0:2, 0:2] view[0, 0] = 99 print(a[0, 0]) # 99 — исходный изменён!
# .copy() = копия a = np.arange(12).reshape(3, 4) b = a[0:2, 0:2].copy() b[0, 0] = 99 print(a[0, 0]) # 0 — исходный не тронут

Правило: срезы — view; fancy indexing (списки индексов) — копия; явная копия — .copy().

Почему изменение «под-массива» меняет исходный массив? Когда это удобно?

Fancy indexing — выборка по спискам индексов

a = np.arange(12).reshape(3, 4) a[[0, 2], [1, 3]] array([ 1, 11]) # a[0,1] и a[2,3]

В отличие от срезов, fancy indexing возвращает копию. Применения: перестановка строк, случайная выборка строк.

a[[2, 0, 1]] # строки в новом порядке array([[ 8, 9, 10, 11], [ 0, 1, 2, 3], [ 4, 5, 6, 7]])
Как выбрать элементы a[0,1] и a[2,3] одной операцией?

Boolean masking — выбор по условию

a = np.arange(12).reshape(3, 4) a[a > 5] array([ 6, 7, 8, 9, 10, 11])

Маска — булев массив той же формы; a[mask] возвращает элементы, где маска истинна.

# комбинирование условий: & | ~ со СКОБКАМИ (a > 3) & (a < 7) array([[False, False, False, False], [ True, True, True, False], [False, False, False, False]])

Ошибка: a > 3 and a < 7ValueError. Для массивов нет логического and — только поэлементные &.

Что нужно поставить в (a > 3) _ (a < 7), чтобы получить пересечение?

Присваивание по маске и np.where

# замена по маске a = np.arange(12).reshape(3, 4) a[a > 5] = -1 array([[ 0, 1, 2, 3], [ 4, 5, -1, -1], [-1, -1, -1, -1]])
# np.where(cond, x, y) — поэлементный выбор np.where(a > 5, a, -1)

Запомните: маска a[a > 5] возвращает значения, а np.argwhere(cond)индексы элементов.

Чем a[a > 3] отличается от np.where(a > 3, a, 0)?

Broadcasting — «растягивание» размерностей

Broadcasting — NumPy расширяет меньший массив до формы большего без копирования данных, чтобы выполнить поэлементную операцию.

Правило сравнения форм справа налево:

  1. равные размерности — совместимы;
  2. одна из размерностей равна 1 — она расширяется;
  3. не совпали и ни одна не 1 → ValueError.
Совместимы ли формы (3, 1) и (1, 4)? А (4,) и (3, 4)?

Broadcasting: примеры

# (3,1) + (1,4) → (3,4): «внешнее» суммирование np.arange(3).reshape(-1, 1) + np.arange(4) array([[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]]) # (5,) + (3,5): вычитание среднего по столбцам x - x.mean(axis=0) # форма (3,5) - (5,) → (3,5)

Строка-«центр» формы (5,) расширяется до (3, 5) — это нормализация признаков перед обучением модели.

Какой формы будет результат np.arange(3).reshape(-1,1) + np.arange(4)?

Broadcasting: несовместимые формы

# (4,) + (3,4) — совместимо (последняя 4=4) np.zeros((3, 4)) + np.arange(4) array([[0., 1., 2., 3.], [0., 1., 2., 3.], [0., 1., 2., 3.]])
# (3,) + (3,4) — НЕсовместимо: 3 vs 4 np.zeros((3, 4)) + np.arange(3) ValueError: operands could not be broadcast together with shapes (3,4) (3,)

Проверяйте .shape перед каждой операцией с матрицами разной формы — это нормальный рабочий процесс, не стыд.

Почему (3,) не расширяется до (3,4)?

Пример: нормализация по столбцам через broadcasting

x = np.array([[0., 2., 10.], [4., 2., 20.]]) x_min = x.min(axis=0) # → [0. 2. 10.] span = x.max(axis=0) - x_min # → [4. 0. 10.] result = (x - x_min) / span # (2,3) - (3,) → (2,3) result array([[0., nan, 0.], [1., nan, 1.]]) # столбец с span=0 → NaN

Константный столбец (span = 0) даёт nan — edge case, который нужно обработать через np.where (задача семинара).

Почему при вычитании (2,3) - (3,) форма не ломается?

ufuncs — универсальные функции

ufunc — функция, действующая поэлементно над массивами: np.add, np.multiply, np.exp, np.log, np.sqrt, np.sin, np.abs, np.sign.

np.sqrt(np.arange(4)) array([0. , 1. , 1.41421356, 1.73205081]) np.exp([0, 1, 2]) array([1. , 2.71828183, 7.3890561 ])

Методы: reduce (свёртка), accumulate (накопление), outer (внешнее произведение).

Что вернёт np.add.reduce([1,2,3,4])?

Агрегации и параметр axis

m = np.array([[1, 2, 3], [4, 5, 6]]) m.sum(axis=0) # → [5 7 9] вдоль столбцов m.sum(axis=1) # → [6 15] вдоль строк m.mean(axis=0) # → [2.5 3.5 4.5]

Как не путать оси: axis=0 схлопывает «вертикаль» (результат — по столбцам), axis=1 — «горизонталь» (результат — по строкам).

Другие: np.std, np.var, np.min/max, np.argmin/argmax, np.unique, np.count_nonzero.

Какой shape у результата m.sum(axis=0) для матрицы (3,4)?

Пример: таблица умножения через broadcasting

np.arange(1, 11).reshape(-1, 1) * np.arange(1, 11) array([[ 1, 2, 3, ...], [ 2, 4, 6, ...], [ 3, 6, 9, ...], ... [ 10, 20, 30, ..., 100]]) # форма (10,1) * (10,) → (10,10)

Одна операция вместо двойного цикла — наглядный пример силы broadcasting.

Какой формы результат и чему равен элемент [9, 9]?

Типичные ошибки недели 2

  1. a > 3 and a < 7and не работает с массивами; нужно & и скобки.
  2. Путаница axis=0 / axis=1 (по какой оси схлопывается).
  3. Неожиданная мутация исходного массива из-за view — там, где ожидали копию.
  4. Broadcasting: думают, что NumPy «сам разберётся», не проверяя формы — получают ValueError.
  5. np.where vs маска: np.where возвращает значения, np.argwhere — индексы.
Перед какой операцией всегда полезно вывести .shape?

Вопросы для проверки понимания

  1. Что произойдёт при broadcasting массивов форм (3,1) и (1,4)?
  2. Возвращает ли slice копию или view? Как получить копию?
  3. Как найти индексы элементов, больших порога, и заменить их на порог?
  4. Чем a[a > 3] отличается от np.where(a > 3, a, 0)?
Переход: на семинаре — нормализация данных, изображение как массив, маскирование выбросов и broadcasting-упражнения.

Что дальше

Готовы? Задача семинара: нормализация данных — edge case с константным столбцом ждёт вас.