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

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

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

2 ч теории: четыре блока, два live-coding, peer instruction, два перерыва.

Впереди самая сложная тема модуля — broadcasting. Здесь ошибаются все, включая практиков. Это не вы такие — тема такая.

Не открывая конспект: .T.shape и linspace(0,1,5)

Это не контроль, а припоминание: вспомнили сразу — отлично; нет — сигнал перечитать конспект.

Нормализация признаков — одна строка NumPy вместо 10 строк циклов

Без нормализации регрессия недели 3 даст бессмысленные веса.

К концу лекции вы сможете…

Пункт 6 — прямое ДЗ недели и подготовка к регрессии.

Индексация a[i,j] и отрицательные индексы работают как в строках

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

Правило: a[строка, столбец]; : — «все по этой оси»; отрицательные индексы считают с конца.

Что вернёт a[1, :]?

Срез по двум осям a[0:2, 0:2] — блок 2×2, а не 1D

a = np.arange(12).reshape(3, 4) a[0:2, 0:2]
Какой формы результат? Что внутри? Предскажите, не запуская код.

Срез возвращает view: изменение view меняет исходный массив

a = np.arange(12).reshape(3, 4) view = a[0:2, 0:2] view[0, 0] = 99 print(a[0, 0]) 99

Срез не копирует данные — он описывает «окно» на ту же ленту памяти. Запись через окно меняет исходный массив.

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

b = a[:, 1]; b[0]=100 — изменится ли a[0,1]?

a = np.arange(12).reshape(3, 4) b = a[:, 1] # 2-й столбец b[0] = 100 print(a[0, 1]) # что выведет?
Изменится ли a[0, 1]? Почему? Столбец — тоже view. Нужна независимая копия — .copy().

Копия — .copy(); правило: срез=view, fancy=копия

a = np.arange(12).reshape(3, 4) b = a[0:2, 0:2].copy() b[0, 0] = 99 print(a[0, 0]) 0
ПриёмЧто возвращает
срез a[0:2]view (те же данные)
fancy indexing a[[0,2]]копия
явная копия .copy()копия
Правило «срез=view» — источник неожиданной мутации; «fancy=копия» закрепим на следующем слайде.

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

a = np.arange(12).reshape(3, 4) a[[0, 2], [1, 3]] array([ 1, 11]) # a[0,1] и a[2,3] — параллельные списки
a[[2, 0, 1]] # строки в порядке 2, 0, 1 array([[ 8, 9, 10, 11], [ 0, 1, 2, 3], [ 4, 5, 6, 7]])

Правило: списки индексов идут параллельно: (0,1) и (2,3) — берутся пары; один список по оси 0 — перестановка строк.

Как выбрать a[0,1] и a[2,3] одной операцией?

Fancy indexing возвращает копию; для прямоугольных выборок — np.ix_

Запомните контраст: срез — view, fancy — копия.

Boolean mask: a[a>5] возвращает элементы, где условие истинно

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

Правило: a > 5 — булев массив той же формы (маска); a[mask] отдаёт элементы, где маска истинна. Результат — 1D: маска «выпрямляет».

Какой shape у a[a>5]?

Комбинирование условий: &, |, ~ — всегда со скобками

(a > 3) & (a < 7) array([[False, False, False, False], [ True, True, True, False], [False, False, False, False]])

Правило: & (И), | (ИЛИ), ~ (НЕ) — поэлементные; скобки обязательны, потому что приоритет & выше, чем >. Логического and у массивов нет.

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

Присваивание по маске a[a>5] = -1 меняет только отмеченные элементы

a = np.arange(12).reshape(3, 4) a[a > 5] = -1 a array([[ 0, 1, 2, 3], [ 4, 5, -1, -1], [-1, -1, -1, -1]])

Правило: a[mask] = value присваивает значение всем элементам, где маска истинна. Так заменяют выбросы, обнуляют, клампают значения.

Как заменить на -1 все элементы больше 5 одной строкой?

np.where(cond, x, y) — поэлементный выбор; np.argwhere — индексы

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

Правило: маска a[a>5] возвращает значения; np.where(cond, x, y) — массив той же формы (поэлементный выбор из x и y); np.argwhere(cond) — индексы истинных.

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

Live-coding: маскирование выбросов ±3σ

Задача: сгенерировать 1000×5 нормальных данных, отметить значения за пределами ±3σ как nan и посчитать их долю. Исходные данные не мутировать!

# ожидаемый результат mask.mean() # доля ≈ 0.0026 (0.26%) — правило 3σ
Почему result = d.copy(), а не result = d?

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)

Сверка углов: table[0,0] == 1, table[9,9] == 100. Одна операция вместо двойного цикла.

Как столбец (10,1) умножился на строку (10,) и получилась таблица? Что произошло с формами?

Правило сравнения форм справа налево: равные / 1 / иначе ValueError

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

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

(3,1)+(1,4) → (3,4): размерность 1 растягивается

np.arange(3).reshape(-1, 1) + np.arange(4) array([[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]])

Правило: (3,1) + (1,4): справа налево — 1=1, 3 vs 1 — расширяется до (3,4). Результат — таблица «строка + столбец».

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

(5,)+(3,5) — центрирование строкой; broadcasting — тема, где ошибаются все

x = np.array([[1., 2., 3., 4., 5.], [2., 3., 4., 5., 6.], [3., 4., 5., 6., 7.]]) x - x.mean(axis=0) # (3,5) - (5,) → (3,5) array([[-1., -1., -1., -1., -1.], [ 0., 0., 0., 0., 0.], [ 1., 1., 1., 1., 1.]])

Правило: вектор (5,) трактуется как «строка» и вычитается из каждой строки — это центрирование.

Честно о сложности: broadcasting — самая сложная тема модуля, здесь ошибаются все. Сложность — свойство темы, а не вас. Навык один: проверяйте .shape.

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

Несовместимые формы → ValueError; проверка .shape — навык, а не стыд

np.zeros((3, 4)) + np.arange(3) ValueError: operands could not be broadcast together with shapes (3,4) (3,)

Разбор: (3,4) и (3,) — справа налево: 4 vs 3, ни одна не 1 → несовместимо. А (3,4) + (4,) — совместимо: последняя ось должна совпасть или быть 1.

Ошибка совместимости форм — сигнал проверить .shape, а не повод стыдиться. Это рабочий процесс.

Чем (4,) отличается от (3,) в той же позиции?

Нормализация по столбцам: (x - min)/span; константный столбец → NaN

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

Правило: (2,3) - (3,) работает через broadcasting; константный столбец (span=0) даёт 0/0 = NaN — edge case, который обрабатывается через np.where(span == 0, 0.0, ...).

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

Live-coding: центрирование матрицы, keepdims

Задача: сгенерировать матрицу 5×3 и отцентрировать её по столбцам и по строкам — среднее каждого центрированного среза должно стать нулём.

# ожидаемые проверки np.allclose(c.mean(axis=0), 0) # True np.allclose(c2.mean(axis=1), 0) # True # ошибка без keepdims: ValueError: operands could not be broadcast together with shapes (5,3) (5,)
Почему центрирование по строкам требует keepdims=True, а по столбцам — нет?
Peer instruction · голосуем

Совместимы ли (3,1) и (1,4)? Какой формы результат?

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

Бонус: совместимы ли (3,) и (3,4)? А (4,) и (3,4)? Голосуем, обсуждаем в парах, голосуем снова.

ufunc — поэлементная функция: np.sqrt, np.exp, np.abs…

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

ufunc (универсальная функция) — функция, действующая поэлементно над массивами: np.add, np.multiply, np.exp, np.log, np.sqrt, np.sin, np.abs, np.sign. Запись +, *, ** — лишь синтаксический сахар над np.add, np.multiply, np.power.

Все поэлементные операции недели 1 — это ufuncs; теперь у них есть имена и методы.

Методы ufunc: reduce, accumulate, outer

np.add.reduce([1, 2, 3, 4]) # сумма — «свёртка» np.add.accumulate([1, 2, 3, 4]) # накопленные суммы np.add.outer(np.arange(3), np.arange(3)) # внешнее «сложение» 10 array([ 1, 3, 6, 10]) array([[0, 1, 2], [1, 2, 3], [2, 3, 4]])

Правило: reduce — последовательное свёртывание; accumulate — все промежуточные результаты; outer — «таблица» по двум осям (та же идея, что broadcasting-таблица на слайде 17).

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

Агрегации с axis: axis=0 схлопывает строки, axis=1 — столбцы

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

Правило: axis=0 схлопывает «вертикаль» — результат по столбцам (для (2,3) → длина 3); axis=1 — «горизонталь» (длина 2). Тест: какая размерность исчезает — та и ось.

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

Какая размерность исчезает при axis=0?

Какой shape у m.sum(axis=0) для (4,5)?

m = np.arange(20).reshape(4, 5) m.sum(axis=0).shape
Какой shape вернёт? Почему не (4,)? axis=0 удаляет ось строк — остаётся 5 чисел.

argmax без axis — плоский индекс; с axis — по строкам/столбцам

m = np.arange(20).reshape(4, 5) np.argmax(m) # плоский индекс глобального максимума np.argmax(m, axis=1) # индекс максимума в каждой строке np.count_nonzero(m > 15) # число элементов > 15 np.unique([1, 2, 2, 3]) # уникальные значения 19 array([4, 4, 4, 4]) 4 array([1, 2, 3])

Правило: без axis argmax ищет по «расплющенному» массиву (0..19); с axis — построчно/постолбцово. count_nonzero — число истинных в маске; unique — отсортированные уникальные значения.

Как найти индексы элементов, больших порога, и заменить их на порог?

a>3 and a<7 → ValueError; для массивов — только & со скобками

a = np.arange(12).reshape(3, 4) a > 3 and a < 7 ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

Разбор: and требует одного булева значения, а у массива таких значений много — результат неоднозначен. Исправление: (a > 3) & (a < 7).

Что говорит последняя строка traceback? Что значит «ambiguous»?

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

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

normalize/standardize — подготовка признаков к регрессии (проект)

def normalize(x: np.ndarray) -> np.ndarray: x_min = x.min(axis=0) span = x.max(axis=0) - x_min return np.where(span == 0, 0.0, (x - x_min) / span) normalize(np.array([[0., 2., 10.], [4., 2., 20.]])) array([[0., 0., 0.], [1., 0., 1.]])

Правило: нормализация — обязательная подготовка признаков перед регрессией (проект модуля); edge case константного столбца закрыт np.where. То же самое сделает sklearn.preprocessing.MinMaxScaler.

Это ровно задача ДЗ недели 2 и кирпич регрессии недели 3.

Сегодня вы научились…

One-minute paper: запишите (1) главное, что поняли; (2) один вопрос, что остался.

Что дальше

На этой неделе вы подготовили данные; на следующей — построите на них первую модель.

Готовы? Откройте семинар недели 2 и выполните базовый уровень.