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

Введение в NumPy. Jupyter Notebook

Зачем NumPy · ndarray и dtype · создание массивов · векторизация vs циклы · Jupyter

2 ч теории. Далее — live-coding и семинар.

Почему вообще нужен ещё один тип данных? — прежде чем мы начнём, ответьте себе на этот вопрос.

Все downstream-курсы начинаются с import numpy as np

Какого типа элементы матрицы яркости чёрно-белого фото?

Не открывая конспект: что вернёт [1, 2, 3] + [4, 5, 6]?

Сколько в нём элементов?

А как посчитать поэлементную сумму без NumPy? Сколько строк кода это займёт?

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

В конце проверим себя списком вопросов — это не экзамен, а сигнал, где повторить.

Список хранит ссылки на объекты — цикл платит за каждый элемент

a = [1, 2, 3] b = [4, 5, 6] s = 0 for i in range(3): s += a[i] * b[i] # 5 строк, медленно
import numpy as np a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) np.dot(a, b) # → 32 ✓

NumPy переносит цикл из Python в C.

Что здесь платит за каждый шаг — и куда NumPy передаёт оба массива целиком?

ndarray — однородный массив; shape/ndim/size/dtype его описывают

ndarray — N-мерный однородный массив: все элементы одного типа (dtype), лежат подряд в одном блоке памяти, операции — на C.

АтрибутЧто означаетПример
shapeкортеж размерностей(3, 4)
ndimчисло осей (rank)2
sizeвсего элементов12
dtypeтип элементаint64
a = np.array([1, 2, 3]) a.shape, a.ndim, a.size, a.dtype ((3,), 1, 3, dtype('int64'))
Массив формы (2, 3, 4): сколько осей и сколько всего элементов?

Список — цепочка ссылок; ndarray — непрерывная лента ячеек

список [1, 2, 3] list ──→ int(1) (0x…A1) list ──→ int(2) (0x…B7) list ──→ int(3) (0x…C4)

каждый элемент — отдельный объект, адрес берётся по ссылке

123

элементы лежат вплотную: 3 ячейки по 8 байт = 24 байта

Непрерывность → процессор читает данные одним куском, кеш работает → скорость. Однородность → известный размер ячейки → компактность.

Почему элементы ndarray не могут быть разных типов — число и строка?

dtype — тип элемента; у целых результат операций целочисленный

Целые int8…int64, беззнаковые uint, вещественные float32/64 (по умолчанию float64), bool, complex.

np.zeros(3) # по умолчанию float64 np.zeros(3, dtype=int) # явно int64 np.array([1, 2]) / np.array([3, 3]) array([0., 0., 0.]) array([0, 0, 0]) array([0.33333333, 0.66666667])

У целочисленных данных результат операций тоже целочисленный; для деления и NaN нужен float (отсюда «точка» в 0.).

Чем np.zeros(3) отличается от np.zeros(3, dtype=int)?

Одна запись + делает разное: конкатенация vs поэлементно

a = [1, 2, 3] b = [4, 5, 6] print(a + b) c = np.array([1, 2, 3]) d = np.array([4, 5, 6]) print(c + d)
Не запуская: что напечатает каждая строка? Почему? Проверяйте, с чем работаете: type(x) и .shape.
Стоп-вопрос · Блок 1

Чем ndarray принципиально отличается от Python-списка? Назовите три отличия.

Бонус: что вернёт [1, 2, 3] * 3?

Фабрики создают готовые массивы; empty — быстрый «мусор»

np.array(список) создаёт массив из списка — уже знаем.

np.eye(3) # единичная матрица 3×3 array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])

zeros и ones гарантируют значения; empty — скорость. Для контроля формы всегда передавайте кортеж (строки, столбцы).

Чем empty((2, 2)) отличается от zeros((2, 2)) и когда он полезен?

arange — по шагу, linspace — по числу точек

# arange: шаг np.arange(0, 10, 2) array([0, 2, 4, 6, 8])
# linspace: N точек np.linspace(0, 1, 5) array([0. , 0.25, 0.5 , 0.75, 1. ])

arange(start, stop, step) — арифметическая прогрессия по шагу; linspace(start, stop, n) — ровно n точек от start до stop включительно.

Какая разница между arange(0, 1, 0.25) и linspace(0, 1, 5)?

Для равномерной сетки из N точек — linspace, не arange

np.arange(0, 1, 0.1)
Сколько элементов в результате? Будет ли в конце 1.0?

Форма меняется без изменения числа элементов

a = np.arange(12).reshape(3, 4) # 12 → (3, 4) a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])

Число элементов сохраняется: 12 = 3×4 = 12×1 = 6×2. Нельзя reshape в форму с другим size — будет ValueError.

Какой shape получится у np.arange(6).reshape(2, 3).T?

reshape(2, 3).T меняет порядок осей: (2, 3) → (3, 2)

b = np.arange(6).reshape(2, 3) b.T.shape
Какой shape вернёт b.T.shape?

Live-coding: создаём массивы и читаем их атрибуты

Создайте np.array, np.zeros, np.ones, np.full, np.eye, np.empty, np.arange(0, 10, 2), np.linspace(0, 1, 5) — и для каждого выведите shape, ndim, size, dtype.

# ожидаемые результаты np.eye(3).shape == (3, 3) np.linspace(0, 1, 5).shape == (5,) np.zeros((2, 3)).dtype # float64
Сколько способов создать массив вы сегодня знаете? Какие атрибуты читали у каждого?
Стоп-вопрос · Блок 2

Какая разница между arange и linspace? Приведите пример, когда arange подведёт.

Запомните формулу: «N точек → linspace».

Сначала почувствуй: один расчёт — 1.6 c и 0.06 c

x = np.arange(10**7) s = 0.0 for i in range(len(x)): s += x[i]**2 s # ≈ 1.6 c ✓
x = np.arange(10**7) s = np.sum(x**2) s # ≈ 0.06 c ✓

Тот же результат. Ускорение десятки раз, код короче. На семинаре измерите сами: типично 20–30× на 10⁶ (10⁵ ≈ 12×, 10⁶ ≈ 30×, 10⁷ ≈ 26×).

Что здесь быстрее и почему, как думаете?

Разрыв растёт с N: векторизация выигрывает тем сильнее, чем больше массив

0.1 1 10 100 10³ 10⁴ 10³ 10⁴ 10⁵ 10⁶ 10⁷ время, мс N точек цикл векторизация
Замер %timeit, Python 3.11 / NumPy 1.26. На 10⁷: цикл ≈ 1.57 c, векторизация ≈ 61 мс.
Почему на малых N линии почти совпадают, а с ростом N расходятся? (накладные расходы вызова C сопоставимы с выигрышем).

Векторизация — операция над всем массивом сразу, без явного for

Векторизация — применение операции ко всем элементам массива сразу, без явного цикла for. NumPy передаёт данные C-функции целиком; цикл же платит интерпретатору за каждый элемент.

Правило-шпаргалка: видишь for по элементам массива в научном стеке — ищи векторную альтернативу.

Честно о сложности: первая встреча с векторизацией может казаться «магией» — это нормально. Сегодня мы её разобрали на части: память, dtype, C-операции. Через неделю это станет рутиной.

Как проверить, с чем вы работаете: списком или массивом?

Операции поэлементны и возвращают НОВЫЙ массив (не меняют исходный)

x = np.arange(5) x**2 + 2*x + 1 array([ 1, 4, 9, 16, 25])
np.sqrt(np.arange(4)) np.exp([0, 1, 2]) x > 3 array([0. , 1. , 1.41421356, 1.73205081]) array([1. , 2.71828183, 7.3890561 ]) array([False, False, False, False, True])

Операции возвращают новый массив и не меняют исходный — кроме операторов +=, *= (in-place).

В чём разница между x = x + 1 и x += 1 для ndarray?

Сравнения возвращают булев массив, а не числа

x = np.arange(5) x > 3
Что напечатается? Числа 0/1, список, или что-то ещё?

Скалярное произведение: np.dot и @ — одно и то же

np.dot([1, 2, 3], [4, 5, 6]) np.array([1, 2, 3]) @ np.array([4, 5, 6]) 32 32

Для векторов @ и np.dot — одно и то же (скалярное произведение). Для матриц это матричное умножение — неделя 3. @ — современный оператор, запомните его.

Мы вернулись к слайду 5: что мы писали 5 строками цикла — теперь одна строка и 32. В чём разница dot и @ для векторов?

Live-coding: векторизация vs цикл + %timeit на 10⁶

Замерьте %timeit для цикла и для np.sum(x**2 + 2*x + 1); посчитайте отношение. Результаты должны совпасть — проверьте np.allclose.

x = np.arange(10**6) %timeit np.sum(x**2 + 2*x + 1) # быстрее цикла в 10–30 раз (эталон: 10⁵ ≈ 12×, 10⁶ ≈ 30×) # np.allclose(s_loop, s_vec) → True
Почему нельзя включать np.arange(...) внутрь таймера?

Jupyter: Shift+Enter выполняет ячейку; %timeit измеряет время

%%timeit np.sum(np.arange(10**6)**2) 1.25 ms ± 0.06 ms per loop

%timeit np.sum(np.arange(10**6)**2) — замер одной строки. Другие магии: %matplotlib inline, %whos (переменные), %debug (отладка после ошибки).

Чем %%timeit отличается от %timeit?

x**2 на списке → TypeError: ты думал — массив, а это список

x = [0, 1, 2, 3, 4] y = x**2 + 2*x + 1 TypeError: unsupported operand type(s) for ** or pow(): 'list' and 'int'

Последняя строка traceback — причина: ** не умеет работать с list. Исправление: x = np.array([0, 1, 2, 3, 4]).

Что говорит последняя строка traceback? Где ошибка?

Пять ошибок недели 1, которые вы совершите на семинаре

  1. Список вместо массива: a = [1, 2, 3], затем a + a — конкатенация, а не поэлементное сложение (слайд 9).
  2. Забыли np.: zeros(3) → NameError (слайд 16).
  3. Неверный dtype: деление целых без перевода в float (слайд 8).
  4. arange для дробной сетки: arange(0, 1, 0.1) — «кривой» конец; брать linspace (слайд 13).
  5. Мутация ожидается, а её нет: операции создают новый массив, исходный не меняется (кроме +=, *=).
Студент написал a + a и получил список длиной 6. Что он забыл?
Проверка понимания · перед семинаром
  1. Чем ndarray принципиально отличается от Python-списка?
  2. Что вернёт np.linspace(0, 1, 5) и какой у него shape?
  3. Почему x**2 на массиве из 10⁷ элементов быстрее цикла?
  4. Какая разница между arange и linspace?
Ответьте письменно за 2 минуты — это сигнал, что повторить перед семинаром, а не контроль.

Векторизация и dot — кирпичи линейной регрессии (проект модуля)

Какой приём сегодняшней лекции пригодится для матричных формул регрессии?

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

Два правила на память: «N точек → linspace»; «операция возвращает новый массив».

Какие два правила нужно запомнить на неделю?

One-minute paper: главное + один вопрос

Запишите одним-двумя предложениями: (1) самое главное, что вы сегодня поняли; (2) один вопрос, который остался.

Соберём 2–3 ответа вслух или анонимно — на оставшиеся вопросы отвечу на семинаре.

Что дальше: семинар, ДЗ, анонс недели 2

Готовы? Откройте семинар-1.md и выполните задачу базового уровня.