Перейти к содержанию
Educora
Университет25 мин29 / 42

Основы NumPy: массивы

Познакомься с массивом NumPy (ndarray): тип и форма, создание массивов, воспроизводимые случайные числа, индексация, срезы, булевы маски и агрегация по осям.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять атрибуты ndarray (ndim, shape, size, dtype) и вычислять объём памяти массива
  • Создавать массивы с помощью arange, linspace, zeros и default_rng с зерном
  • Выбирать данные индексами, срезами и булевыми масками, отличать представление от копии
  • Вычислять статистики по строкам и столбцам с помощью параметра axis

Метеостанция измеряет температуру каждый час в течение года — это 8760 чисел. Среднее, самые жаркие дни и средние по месяцам можно посчитать и обычными списками Python с циклами, но медленно и длинным кодом. NumPy (Numerical Python) делает то же самое одной строкой и во много раз быстрее. pandas, matplotlib, SciPy, scikit-learn — все инструменты науки о данных в Python построены на массивах NumPy, поэтому модуль начинается именно с него.

Определение
ndarray (N-мерный массив)

Главный объект NumPy: многомерная таблица фиксированного размера из элементов одного типа. Элементы лежат в памяти подряд, а операции выполняются сразу над всем массивом в скомпилированном коде на C.

ndarray: форма, размер и тип

Список Python хранит каждый элемент как отдельный объект, а сам содержит лишь указатели (адреса) на них; массив же хранит «сырые» числа одним блоком. Поэтому массив занимает меньше памяти и обрабатывается быстрее. У каждого массива четыре главных атрибута: ndim — число измерений (осей), shape — кортеж длин по каждой оси, size — общее число элементов, dtype — тип элементов.

Python
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 раз.
size = d₁ · d₂ · … · dₙ nbytes = size · itemsize
где:
  • 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БайтДиапазон / точностьГде применяется
bool1True / Falseмаски, флаги
uint810 … 255пиксели изображений
int324примерно ±2,1 · 10⁹счётчики, индексы
int648примерно ±9,2 · 10¹⁸большие целые числа
float324~7 значащих цифрнейросети, GPU
float648~15–16 значащих цифртип по умолчанию для дробных чисел

Все элементы массива должны быть одного типа. Если типы смешаны, NumPy приводит всё к «самому широкому» типу: целые и дробные вместе дают float64, числа и строки вместе — строки. astype меняет тип явно; при переводе дробных чисел в целые дробная часть не округляется, а просто отбрасывается.

Python
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)генератор случайных чисел
Python
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 = (stop − start) / (num − 1) n = ⌈(stop − start) / step⌉h = (stop − start) / (num − 1) n = ⌈(stop − start) / step⌉
где:
  • 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:

Python
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, которые встречаются в старом коде, в новом коде используй генератор.

Python
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]]
Десять бросков кубика, случайный рост четырёх человек со средним ростом 170 см и случайная матрица 2 × 3. Запусти код ещё раз — результат не изменится.

Индексация, срезы и булевы маски

Одномерный массив индексируется как список: a[0], a[-1], a[2:5], a[::2]. В двумерном массиве индексы пишутся через запятую: m[строка, столбец]. Двоеточие : означает «всё по этой оси»: m[:, 1] — второй столбец, m[1] — вторая строка. Срезы работают по каждой оси отдельно, а отрицательный шаг меняет порядок на обратный.

Python
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] — каждая вторая строка, столбцы в обратном порядке.
Python
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» для каждого элемента за одну операцию. Вот дневные температуры за одну неделю:

Python
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.
Пример 1: индексы и оси вручную

Для a = np.arange(12).reshape(3, 4) найди, не запуская код: а) a[1:, ::2]; б) a.sum(axis=0) и a.sum(axis=1) и их формы; в) (a % 5 == 0).sum().

Показать решение
a = [[0 1 2 3], [4 5 6 7], [8 9 10 11]].
а) 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 студентов (строки) по математике, физике и химии (столбцы):

Python
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]
Средние баллы по предметам — 79,25; 79,75 и 81; средние студентов — 85, 74, 92 и 69. argmax(axis=0) даёт индекс лучшего студента по каждому предмету: студент 2 по математике и химии, студент 0 по физике.
x̄ = (1/n) · ∑ xᵢ σ = √( ∑ (xᵢ − x̄)² / (n − ddof) )x̄ = (1/n) · ∑ xᵢ σ = √( ∑ (xᵢ − x̄)² / (n − ddof) )
где:
  • x̄среднее арифметическое (mean)
  • nчисло значений
  • σстандартное отклонение (std): насколько значения в среднем удалены от среднего
  • ddof0 — для генеральной совокупности (по умолчанию в NumPy), 1 — для выборки (несмещённая дисперсия)

np.std по умолчанию делит на n, а учебники статистики и Series.std в pandas — на n − 1. Чтобы получить тот же результат, передай ddof=1.

Пример 2: стандартное отклонение вручную

x = [2, 4, 4, 4, 5, 5, 7, 9]. Что вернут np.std(x) и np.std(x, ddof=1)?

Показать решение
x̄ = 40 / 8 = 5.
Квадраты отклонений от среднего: 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. Не используй цикл.

Задание · Python
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 знака после запятой.

Задание · Python
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.

1 / 10
Какова форма результата np.zeros((3, 4)).sum(axis=0)?