- Объяснять атрибуты ndarray (
ndim,shape,size,dtype) и вычислять объём памяти массива - Создавать массивы с помощью
arange,linspace,zerosиdefault_rngс зерном - Выбирать данные индексами, срезами и булевыми масками, отличать представление от копии
- Вычислять статистики по строкам и столбцам с помощью параметра
axis
Метеостанция измеряет температуру каждый час в течение года — это 8760 чисел. Среднее, самые жаркие дни и средние по месяцам можно посчитать и обычными списками Python с циклами, но медленно и длинным кодом. NumPy (Numerical Python) делает то же самое одной строкой и во много раз быстрее. pandas, matplotlib, SciPy, scikit-learn — все инструменты науки о данных в Python построены на массивах NumPy, поэтому модуль начинается именно с него.
Главный объект NumPy: многомерная таблица фиксированного размера из элементов одного типа. Элементы лежат в памяти подряд, а операции выполняются сразу над всем массивом в скомпилированном коде на C.
ndarray: форма, размер и тип
Список Python хранит каждый элемент как отдельный объект, а сам содержит лишь указатели (адреса) на них; массив же хранит «сырые» числа одним блоком. Поэтому массив занимает меньше памяти и обрабатывается быстрее. У каждого массива четыре главных атрибута: ndim — число измерений (осей), shape — кортеж длин по каждой оси, size — общее число элементов, dtype — тип элементов.
import numpy as np
temps = np.array([12.5, 14.0, 9.8, 11.2])
print(temps)
print(temps.ndim, temps.shape, temps.size, temps.dtype)
print(temps.itemsize, temps.nbytes)
m = np.array([[1, 2, 3], [4, 5, 6]])
print(m)
print(m.ndim, m.shape, m.size)
print(m * 10)▸ Ожидаемый результат
[12.5 14. 9.8 11.2] 1 (4,) 4 float64 8 32 [[1 2 3] [4 5 6]] 2 (2, 3) 6 [[10 20 30] [40 50 60]]
m * 10 работает без цикла: операция применяется к каждому элементу. А для списка [1, 2] * 10 просто повторил бы список 10 раз.- nчисло осей (
ndim) - dₖдлина по k-й оси (элемент
shape) - itemsizeразмер одного элемента в байтах: 8 для
float64иint64, 4 дляfloat32иint32, 1 дляboolиint8
Для m формы (2, 3): size = 2 · 3 = 6. Для temps: nbytes = 4 · 8 = 32 байта.
| dtype | Байт | Диапазон / точность | Где применяется |
|---|---|---|---|
bool | 1 | True / False | маски, флаги |
uint8 | 1 | 0 … 255 | пиксели изображений |
int32 | 4 | примерно ±2,1 · 10⁹ | счётчики, индексы |
int64 | 8 | примерно ±9,2 · 10¹⁸ | большие целые числа |
float32 | 4 | ~7 значащих цифр | нейросети, GPU |
float64 | 8 | ~15–16 значащих цифр | тип по умолчанию для дробных чисел |
Все элементы массива должны быть одного типа. Если типы смешаны, NumPy приводит всё к «самому широкому» типу: целые и дробные вместе дают float64, числа и строки вместе — строки. astype меняет тип явно; при переводе дробных чисел в целые дробная часть не округляется, а просто отбрасывается.
import numpy as np
print(np.array([1, 2, 3.5]))
print(np.array([1, 2, '3']))
print(np.array([1.9, -1.9]).astype(np.int32))
small = np.array([100, 120], dtype=np.int8)
print(small + small)▸ Ожидаемый результат
[1. 2. 3.5] ['1' '2' '3'] [ 1 -1] [-56 -16]
Создание массивов
| Функция | Что создаёт |
|---|---|
np.array(data) | массив из списка или вложенных списков |
np.zeros(shape), np.ones(shape), np.full(shape, v) | массив, заполненный 0, 1 или v |
np.arange(start, stop, step) | последовательность с шагом, как range; stop не входит |
np.linspace(start, stop, num) | num равноотстоящих точек; оба конца включены |
np.eye(n) | единичная матрица n × n |
np.random.default_rng(seed) | генератор случайных чисел |
import numpy as np
print(np.zeros((2, 3)))
print(np.arange(0, 10, 2))
print(np.linspace(0, 1, 5))
print(np.full(3, 7.5))
print(np.eye(3))▸ Ожидаемый результат
[[0. 0. 0.] [0. 0. 0.]] [0 2 4 6 8] [0. 0.25 0.5 0.75 1. ] [7.5 7.5 7.5] [[1. 0. 0.] [0. 1. 0.] [0. 0. 1.]]
- hшаг между точками
linspace - nчисло элементов, которые даёт
arange - ⌈ ⌉округление вверх
np.linspace(0, 1, 5): h = 1 / 4 = 0,25. np.arange(0, 10, 2): n = ⌈10 / 2⌉ = 5 элементов.
Будь осторожен с arange и дробным шагом: 0,1 нельзя точно представить в двоичной системе, и ошибка округления может изменить, попадёт ли последний элемент в массив. Ниже 1,3 оказалось в массиве, хотя stop = 1.3:
import numpy as np
print(np.arange(0, 1, 0.1).size)
print(np.arange(1, 1.3, 0.1))
print(np.linspace(1, 1.3, 4))▸ Ожидаемый результат
10 [1. 1.1 1.2 1.3] [1. 1.1 1.2 1.3]
Современный способ получать случайные числа — создать генератор: rng = np.random.default_rng(42). Здесь 42 — зерно (seed): с одним и тем же зерном генератор каждый раз выдаёт одну и ту же последовательность. В науке это важно: коллега должен запустить твой код и получить тот же результат. integers(a, b) не включает верхнюю границу, normal(loc, scale) берёт числа из нормального распределения, а random() — из промежутка [0; 1). Вместо np.random.seed и np.random.rand, которые встречаются в старом коде, в новом коде используй генератор.
import numpy as np
rng = np.random.default_rng(42)
print(rng.integers(1, 7, size=10))
print(rng.normal(loc=170, scale=8, size=4).round(1))
print(rng.random((2, 3)).round(3))▸ Ожидаемый результат
[1 5 4 3 3 6 1 5 2 1] [159.6 171. 167.5 169.9] [[0.45 0.371 0.927] [0.644 0.823 0.443]]
Индексация, срезы и булевы маски
Одномерный массив индексируется как список: a[0], a[-1], a[2:5], a[::2]. В двумерном массиве индексы пишутся через запятую: m[строка, столбец]. Двоеточие : означает «всё по этой оси»: m[:, 1] — второй столбец, m[1] — вторая строка. Срезы работают по каждой оси отдельно, а отрицательный шаг меняет порядок на обратный.
import numpy as np
m = np.arange(1, 13).reshape(3, 4)
print(m)
print(m[1, 2], m[-1, -1])
print(m[:, 1])
print(m[0:2, 1:3])
print(m[::2, ::-1])▸ Ожидаемый результат
[[ 1 2 3 4] [ 5 6 7 8] [ 9 10 11 12]] 7 12 [ 2 6 10] [[2 3] [6 7]] [[ 4 3 2 1] [12 11 10 9]]
reshape(3, 4) раскладывает 12 элементов в 3 строки и 4 столбца (подробнее в следующем уроке). m[::2, ::-1] — каждая вторая строка, столбцы в обратном порядке.import numpy as np
m = np.arange(1, 13).reshape(3, 4)
row = m[0]
row[0] = 100
print(m[0])
safe = m[1].copy()
safe[0] = -1
print(m[1], safe)▸ Ожидаемый результат
[100 2 3 4] [5 6 7 8] [-1 6 7 8]
Сравнение, применённое к массиву, возвращает True или False для каждого элемента — это булева маска. Если передать маску как индекс, выберутся только элементы, где стоит True. Поскольку True считается как 1, а False как 0, mask.sum() даёт количество подходящих элементов, а mask.mean() — их долю. np.where(условие, a, b) делает выбор «если…, то a, иначе b» для каждого элемента за одну операцию. Вот дневные температуры за одну неделю:
import numpy as np
temps = np.array([21.4, 25.8, 31.2, 28.9, 33.5, 26.1, 19.7])
hot = temps > 28
print(hot)
print(temps[hot])
print(np.flatnonzero(hot))
print(hot.sum(), 'hot days,', f'{hot.mean():.0%} of the week')
print(temps[(temps > 20) & (temps < 27)])
print(np.where(temps > 30, 30.0, temps))▸ Ожидаемый результат
[False False True True True False False] [31.2 28.9 33.5] [2 3 4] 3 hot days, 43% of the week [21.4 25.8 26.1] [21.4 25.8 30. 28.9 30. 26.1 19.7]
np.flatnonzero возвращает индексы элементов со значением True: дни 2, 3 и 4, если считать с 0. Последняя строка заменяет значения выше 30 на 30.Для a = np.arange(12).reshape(3, 4) найди, не запуская код: а) a[1:, ::2]; б) a.sum(axis=0) и a.sum(axis=1) и их формы; в) (a % 5 == 0).sum().
Показать решениеСкрыть решение
а)
1: — строки 1 и 2, ::2 — столбцы 0 и 2: [[4 6], [8 10]].б) axis=0 «сжимает» строки, остаётся сумма каждого столбца: [0+4+8, 1+5+9, 2+6+10, 3+7+11] = [12 15 18 21], форма (4,).
axis=1 даёт сумму каждой строки: [6 22 38], форма (3,).
в) На 5 делятся 0, 5 и 10, в маске 3 значения
True: ответ 3.Агрегация и параметр axis
Функции sum, mean, min, max, std, argmax, cumsum «сворачивают» много чисел в одно число или в меньший массив — это называется агрегацией. Без axis вычисление идёт по всему массиву. axis=0 считает вдоль первой оси (вниз по строкам) и даёт по одному результату на столбец; axis=1 считает вдоль столбцов и даёт по одному результату на строку. Ниже баллы 4 студентов (строки) по математике, физике и химии (столбцы):
import numpy as np
scores = np.array([[85, 92, 78],
[67, 74, 81],
[93, 88, 95],
[72, 65, 70]])
print(scores.shape, scores.sum())
print(scores.mean(axis=0))
print(scores.mean(axis=1).round(2))
print(scores.max(axis=0), scores.argmax(axis=0))
print(scores.std(axis=0).round(2))
print(scores.std(axis=0, ddof=1).round(2))
print(scores.cumsum(axis=1)[0])▸ Ожидаемый результат
(4, 3) 960 [79.25 79.75 81. ] [85. 74. 92. 69.] [93 92 95] [2 0 2] [10.3 10.83 9.03] [11.9 12.5 10.42] [ 85 177 255]
argmax(axis=0) даёт индекс лучшего студента по каждому предмету: студент 2 по математике и химии, студент 0 по физике.- x̄среднее арифметическое (
mean) - nчисло значений
- σстандартное отклонение (
std): насколько значения в среднем удалены от среднего - ddof0 — для генеральной совокупности (по умолчанию в NumPy), 1 — для выборки (несмещённая дисперсия)
np.std по умолчанию делит на n, а учебники статистики и Series.std в pandas — на n − 1. Чтобы получить тот же результат, передай ddof=1.
x = [2, 4, 4, 4, 5, 5, 7, 9]. Что вернут np.std(x) и np.std(x, ddof=1)?
Показать решениеСкрыть решение
Квадраты отклонений от среднего: 9, 1, 1, 1, 0, 0, 4, 16; сумма 32.
ddof = 0: σ² = 32 / 8 = 4, σ = 2.
ddof = 1: s² = 32 / 7 ≈ 4,571, s ≈ 2,138.
С ростом n разница между двумя ответами уменьшается.
Где это пригодится? Столбцы таблиц pandas — это массивы NumPy; scikit-learn принимает данные как массив формы (n × d); обработка изображений, сигналы, финансовые ряды и физические модели — всё это операции над массивами. В следующем уроке ты научишься полностью заменять циклы векторизованным кодом и займёшься линейной алгеброй.
В массиве sales продажи небольшого кафе за 4 недели (строки — недели, столбцы — дни с понедельника по воскресенье). Выведи на отдельных строках: 1) сумму продаж каждой недели; 2) индекс дня недели с наибольшими продажами за 4 недели (0 = понедельник); 3) число дней, когда продажи были больше 100. Не используй цикл.
import numpy as np
sales = np.array([[80, 95, 110, 90, 130, 150, 70],
[85, 100, 105, 95, 140, 160, 75],
[90, 92, 120, 88, 125, 155, 80],
[78, 99, 115, 97, 135, 165, 72]])
# 1) total per week
# 2) index of the best weekday
# 3) number of days above 100▸ Ожидаемый результат
[725 760 750 761] 5 12
Отрицательные значения в показаниях датчика — ошибки. 1) Замени отрицательные значения на 0.0 с помощью np.where и выведи новый массив; 2) выведи среднее показаний исходного массива, которые больше 10, округлив до 1 знака после запятой.
import numpy as np
readings = np.array([12.5, -3.0, 8.4, 15.1, -1.2, 20.3, 9.9, 11.0])
# 1) negatives -> 0.0
# 2) mean of readings > 10, rounded to 1 decimal▸ Ожидаемый результат
[12.5 0. 8.4 15.1 0. 20.3 9.9 11. ] 14.7
Главное
- ndarray хранит элементы одного типа подряд в памяти; главные атрибуты —
ndim,shape,size,dtype, а nbytes = size · itemsize. arangeне включаетstop; для дробного шага надёжнееlinspace, включающий оба конца.- Для воспроизводимых случайных чисел используй генератор
np.random.default_rng(seed). - Срез — это представление: его изменение меняет исходный массив; для независимой копии —
.copy(). - Объединяй маски через
&,|,~и скобки;mask.sum()даёт количество,mask.mean()— долю. axis=kубирает ось k из формы результата;np.stdпо умолчанию используетddof=0.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
np.zeros((3, 4)).sum(axis=0)?