NumPy - библиотека Python для вычислении на массивах
NumPy -- базовая библиотека Python для быстрых вычислении на массивах. В этом уроке мы разберем, как устроены массивы NumPy, как их создавать и преобразовывать, как применять функции по осям, как сохранять и загружать данные, и где в NumPy находятся инструменты линеинои алгебры и статистики. В конце будут задачи с решениями и чек-лист.
Главная мысль: если вы умеете уверенно работать с ndarray (массивом NumPy), то дальше почти все в NumPy становится понятным: создание массивов, изменение формы, операции по осям, линеиная алгебра, статистика и ввод-вывод.
Содержание
- 1. Цели урока
- 2. Что такое NumPy и как читать документацию
- 3. Массивы NumPy: создание и базовые свойства
- 4. Форма массива и склеивание: reshape, transpose, concatenate, stack, split
- 5. Функциональные приемы: apply_along_axis, vectorize, piecewise, broadcasting
- 6. Ввод-вывод, линеиная алгебра, статистика и формат вывода
- 7. Практика: задачи (с решениями)
- 8. Чек-лист самопроверки
1. Цели урока
- Понимать, что такое
ndarrayи чем он отличается от обычного списка Python. - Создавать массивы разными способами:
array,zeros,ones,full,empty,arange,linspace,eye. - Менять форму массива и ориентироваться в осях:
shape,axis,reshape,transpose. - Склеивать и разделять массивы:
concatenate,stack,vstack,hstack,array_split. - Применять функции по осям и по элементам:
apply_along_axis,vectorize,piecewise. - Понимать базовые инструменты
numpy.linalgи статистики (mean,std,percentile). - Делать "специальный вывод" массивов через
set_printoptions.
shape, axis или dtype.2. Что такое NumPy и как читать документацию
Новые термины и определения
NumPy-- библиотека Python для вычислении на многомерных массивах и матрицах, с большим набором математических функции.ndarray-- основной тип NumPy: многомерный массив фиксированного типа элементов.shape-- форма массива: кортеж размеров по осям. Пример:(2, 3).axis-- номер оси, вдоль которой выполняется операция. Часто:axis=0-- по строкам вниз (то есть "по столбцам"),axis=1-- по столбцам вправо (то есть "по строкам").dtype-- тип данных элементов массива (напримерint64,float64).векторизация-- стиль программирования, когда вы пишете операции над целыми массивами, а не циклы по элементам.broadcasting(распространение размеров) -- правило NumPy, по которому массивы разных форм могут автоматически "растягиваться" для поэлементных операций, если их размеры совместимы.матрица-- двумерный массив (размер \(m \times n\)).скалярное произведениедвух векторов \(a\) и \(b\) -- число \(a \cdot b\), которое можно записать формулой:
$$ a \cdot b = \sum_{i=1}^{n} a_i b_i $$
Про версию и совместимость
версия библиотеки-- номер релиза пакета (например1.25.0), который помогает понять, какие функции и поведение доступны.релиз-- выпуск новой версии.
В этом уроке примеры ориентированы на NumPy версии 1.25 (как в исходном материале). Важно понимать: NumPy развивается, и в новых версиях могут меняться детали (например предупреждения, типы, поведение печати). Поэтому полезная привычка -- проверять версию в коде.
import numpy as np
print(np.__version__)
# 1.25.0
1.25.0, это не ошибка. Это означает, что у вас установлена другая версия. Большинство примеров все равно будет работать, но иногда вывод может немного отличаться.Частая путаница
shape, dtype и операции по осям. Список чаще всего хранит "что угодно", а NumPy старается хранить "однотипные числа" и работать с ними быстро.import numpy as np
lst = [1, 2, 3]
arr = np.array([1, 2, 3])
print(type(lst))
# <class 'list'>
print(type(arr))
# <class 'numpy.ndarray'>
^ К оглавлению3. Массивы NumPy: создание и базовые свойства
ndarray, shape, dtype
Самое важное, что нужно уметь делать с массивом NumPy -- это смотреть его форму и тип.
import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6]])
print(a)
# [[1 2 3]
# [4 5 6]]
print(a.shape)
# (2, 3)
print(a.dtype)
# int64
кортеж -- неизменяемая последовательность Python, записывается в круглых скобках: (2, 3).int64 в выводе -- типичный вариант на 64-битных системах. На некоторых системах вы можете увидеть int32. Если вам нужна строго фиксированная разрядность, задавайте dtype явно.array, zeros, ones, full, empty, arange, linspace, eye
np.array преобразует список (или список списков) в NumPy-массив.
import numpy as np
a = np.array([10, 20, 30], dtype=np.int64)
print(a)
# [10 20 30]
print(a.dtype)
# int64
zeros, ones, full создают массивы заданной формы.
import numpy as np
z = np.zeros((2, 3), dtype=np.int64)
o = np.ones((2, 3), dtype=np.int64)
f = np.full((2, 3), fill_value=7, dtype=np.int64)
print(z)
# [[0 0 0]
# [0 0 0]]
print(o)
# [[1 1 1]
# [1 1 1]]
print(f)
# [[7 7 7]
# [7 7 7]]
empty создает массив без инициализации элементов. Это значит: содержимое "какое было в памяти", то и может оказаться. Поэтому содержимое такого массива нельзя использовать, пока вы не заполнили его сами. Чтобы пример был честным и стабильным, мы будем выводить только форму и тип, а не содержимое.
import numpy as np
e = np.empty((2, 3), dtype=np.float64)
print(e.shape)
# (2, 3)
print(e.dtype)
# float64
arange делает "диапазон" значений. В отличие от range он может работать с числами с плавающей точкой (но есть нюанс про округления).
import numpy as np
a1 = np.arange(0, 5, 1)
a2 = np.arange(0.0, 1.0, 0.2)
print(a1)
# [0 1 2 3 4]
print(a2)
# [0. 0.2 0.4 0.6 0.8]
число с плавающей точкой (float) -- число, которое хранится в формате, где возможны дробные части, но из-за двоичного представления иногда возникают маленькие ошибки округления.linspace создает заданное количество точек на отрезке \([start, stop]\). Это часто удобнее, чем шаг в arange, если вам важны именно "N точек".
import numpy as np
x = np.linspace(0, 1, num=5)
print(x)
# [0. 0.25 0.5 0.75 1. ]
eye и identity создают единичные матрицы. Единичная матрица \(I\) это квадратная матрица, где на главной диагонали стоят 1, а остальные элементы 0:
$$ I = \begin{pmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix} $$
import numpy as np
m1 = np.eye(3, dtype=np.float64)
m2 = np.identity(3, dtype=np.float64)
print(m1)
# [[1. 0. 0.]
# [0. 1. 0.]
# [0. 0. 1.]]
print(m2)
# [[1. 0. 0.]
# [0. 1. 0.]
# [0. 0. 1.]]
Типичные ошибки
Ошибка 1: ожидать, что empty() заполнит нулями
empty не инициализирует значения. Если вам нужны нули, используйте zeros.
import numpy as np
e = np.empty((2, 2), dtype=np.int64)
z = np.zeros((2, 2), dtype=np.int64)
print(z)
# [[0 0]
# [0 0]]
Ошибка 2: arange с float дает "странные" числа
Это эффект двоичного представления float. Если вам важна сетка из N точек, используйте linspace.
import numpy as np
a = np.arange(0.0, 0.3, 0.1)
print(a)
# [0. 0.1 0.2]
4. Форма массива и склеивание: reshape, transpose, concatenate, stack, split
reshape, flatten, transpose
reshape-- изменение формы массива без изменения последовательности элементов.flatten-- получение одномерной копии массива.transpose-- перестановка осей. Для матрицы это транспонирование: строки становятся столбцами.
import numpy as np
a = np.arange(1, 7)
print(a)
# [1 2 3 4 5 6]
b = a.reshape((2, 3))
print(b)
# [[1 2 3]
# [4 5 6]]
c = b.flatten()
print(c)
# [1 2 3 4 5 6]
t = b.T
print(t)
# [[1 4]
# [2 5]
# [3 6]]
concatenate, stack, vstack, hstack, array_split
axis в склеивании означает "вдоль какой оси мы склеиваем". Для двумерного массива:
axis=0-- склеиваем по строкам (увеличиваем число строк).axis=1-- склеиваем по столбцам (увеличиваем число столбцов).
import numpy as np
a = np.array([[1, 2],
[3, 4]])
b = np.array([[10, 20],
[30, 40]])
print(np.concatenate([a, b], axis=0))
# [[ 1 2]
# [ 3 4]
# [10 20]
# [30 40]]
print(np.concatenate([a, b], axis=1))
# [[ 1 2 10 20]
# [ 3 4 30 40]]
stack добавляет новую ось, то есть увеличивает размерность.
import numpy as np
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
s0 = np.stack([a, b], axis=0)
s1 = np.stack([a, b], axis=1)
print(s0)
# [[ 1 2 3]
# [10 20 30]]
print(s1)
# [[ 1 10]
# [ 2 20]
# [ 3 30]]
print(s0.shape)
# (2, 3)
print(s1.shape)
# (3, 2)
vstack и hstack это удобные обертки для "вертикального" и "горизонтального" склеивания.
import numpy as np
a = np.array([[1, 2],
[3, 4]])
b = np.array([[10, 20],
[30, 40]])
print(np.vstack([a, b]))
# [[ 1 2]
# [ 3 4]
# [10 20]
# [30 40]]
print(np.hstack([a, b]))
# [[ 1 2 10 20]
# [ 3 4 30 40]]
array_split делит массив на части. Это полезно, когда нужно "порезать" данные по блокам.
import numpy as np
a = np.arange(1, 11)
parts = np.array_split(a, 3)
print(parts[0])
# [1 2 3 4]
print(parts[1])
# [5 6 7]
print(parts[2])
# [ 8 9 10]
delete, insert, append, unique
delete, insert, append обычно создают новый массив. Это не как "list.append" по смыслу. В NumPy чаще стараются заранее выделять нужную форму и заполнять ее, потому что частые вставки и удаления могут быть дорогими.import numpy as np
a = np.array([1, 2, 3, 2, 1])
print(np.delete(a, 1))
# [1 3 2 1]
print(np.insert(a, 2, 99))
# [ 1 2 99 3 2 1]
print(np.append(a, [7, 8]))
# [1 2 3 2 1 7 8]
print(np.unique(a))
# [1 2 3]
Типичные ошибки
Ошибка 1: reshape на несовместимый размер
Число элементов должно совпадать. Поймаем тип ошибки через try/except.
import numpy as np
a = np.arange(6)
try:
b = a.reshape((4, 2))
print(b)
except Exception as e:
print("ERROR:", type(e).__name__)
# ERROR: ValueError
Ошибка 2: неправильный axis при concatenate
Если формы не совпадают по всем осям, кроме указанной, NumPy не сможет склеить массивы.
import numpy as np
a = np.zeros((2, 2), dtype=np.int64)
b = np.zeros((3, 2), dtype=np.int64)
try:
print(np.concatenate([a, b], axis=1))
except Exception as e:
print("ERROR:", type(e).__name__)
# ERROR: ValueError
5. Функциональные приемы: apply_along_axis, vectorize, piecewise, broadcasting
axis: как думать про оси
Если у вас матрица размера \(m \times n\), то:
axis=0означает "вдоль строк вниз". Обычно это "операция по столбцам" (мы берем столбец и что-то с ним делаем).axis=1означает "вдоль столбцов вправо". Обычно это "операция по строкам".
import numpy as np
a = np.array([[1, 2, 3],
[10, 20, 30]])
print(np.sum(a, axis=0))
# [11 22 33]
print(np.sum(a, axis=1))
# [ 6 60]
apply_along_axis
apply_along_axis(func1d, axis, arr) применяет функцию func1d к одномерным срезам массива вдоль оси axis. То есть NumPy будет "доставать" кусочки (векторы) и передавать их в вашу функцию.import numpy as np
def sum_of_squares(v):
return np.sum(v**2)
a = np.array([[1, 2, 3],
[4, 5, 6]])
res0 = np.apply_along_axis(sum_of_squares, axis=0, arr=a)
res1 = np.apply_along_axis(sum_of_squares, axis=1, arr=a)
print(res0)
# [17 29 45]
print(res1)
# [14 77]
vectorize и piecewise
vectorize-- обертка, которая позволяет применить обычную Python-функцию к массиву "как будто поэлементно". Это удобно, но не всегда дает ускорение.piecewise-- поэлементно вычисляет кусочную функцию: выбирает формулу по условию.кусочная функция-- функция, заданная разными формулами на разных участках.
Пример кусочнои функции:
$$ f(x) = \begin{cases} x^2, & x < 0 \\ x, & x \ge 0 \end{cases} $$
import numpy as np
x = np.array([-2, -1, 0, 1, 2])
y = np.piecewise(
x,
[x < 0, x >= 0],
[lambda t: t**2, lambda t: t]
)
print(y)
# [4 1 0 1 2]
import numpy as np
def f(x):
return x*x + 1
vf = np.vectorize(f)
a = np.array([1, 2, 3])
print(vf(a))
# [ 2 5 10]
broadcasting (распространение размеров)
broadcasting -- правило, по которому NumPy может автоматически "растянуть" массив меньшей размерности, чтобы выполнить поэлементную операцию с массивом большей размерности. Это работает, если размеры совместимы (например 3 и 1).Пример: к строке длины 3 прибавим "столбец" размера \(2 \times 1\). Результат будет матрица \(2 \times 3\).
import numpy as np
row = np.array([1, 2, 3]) # shape (3,)
col = np.array([[10],
[100]]) # shape (2, 1)
res = col + row
print(res)
# [[ 11 12 13]
# [101 102 103]]
print(res.shape)
# (2, 3)
Типичные ошибки
Ошибка 1: ожидать, что vectorize ускоряет код
vectorize делает удобный интерфейс, но часто выполняет Python-функцию много раз. Для скорости обычно используют "настоящие" векторные операции NumPy (ufunc), а не vectorize.
import numpy as np
a = np.array([1, 2, 3])
# Векторная операция (обычно быстрее):
print(a*a + 1)
# [ 2 5 10]
Ошибка 2: broadcasting не сработал из-за несовместимых форм
Поймаем тип ошибки.
import numpy as np
a = np.zeros((2, 3), dtype=np.int64)
b = np.zeros((2, 2), dtype=np.int64)
try:
print(a + b)
except Exception as e:
print("ERROR:", type(e).__name__)
# ERROR: ValueError
6. Ввод-вывод, линеиная алгебра, статистика и формат вывода
save/load, savetxt/loadtxt, tolist
.npy-- бинарный формат NumPy для одного массива..npz-- архив, который может хранить несколько массивов.сериализация-- сохранение объекта в файл так, чтобы его можно было восстановить.
Мини пример: сохранить в .npy и загрузить обратно.
import numpy as np
import tempfile
import os
a = np.array([[1, 2],
[3, 4]], dtype=np.int64)
tmpdir = tempfile.mkdtemp()
path = os.path.join(tmpdir, "a.npy")
np.save(path, a)
b = np.load(path)
print(b)
# [[1 2]
# [3 4]]
tolist() превращает массив в обычные Python-списки.
import numpy as np
a = np.array([[1, 2],
[3, 4]])
print(a.tolist())
# [[1, 2], [3, 4]]
dot, matmul (@), linalg.solve, det, inv, norm
dot-- обобщенное произведение: для 1D это скалярное произведение, для 2D это матричное умножение.matmulи оператор@-- матричное умножение.linalg.solve-- решение системы линейных уравнений \(A x = b\).det-- определитель матрицы: \(\det(A)\).inv-- обратная матрица \(A^{-1}\), если она существует.norm-- норма вектора или матрицы (мера "длины" или "размера").
Система линейных уравнений в матричном виде:
$$ A x = b $$
import numpy as np
A = np.array([[2.0, 1.0],
[5.0, 3.0]])
b = np.array([1.0, 2.0])
x = np.linalg.solve(A, b)
print(x)
# [ 1. -1.]
Проверка подстановкой: \(A x\) должно дать \(b\).
import numpy as np
A = np.array([[2.0, 1.0],
[5.0, 3.0]])
x = np.array([1.0, -1.0])
print(A @ x)
# [1. 2.]
Определитель и обратная матрица.
import numpy as np
A = np.array([[2.0, 1.0],
[5.0, 3.0]])
print(np.linalg.det(A))
# 1.0
print(np.linalg.inv(A))
# [[ 3. -1.]
# [-5. 2.]]
Норма вектора \(v=(v_1,\dots,v_n)\) в одном из частых вариантов (евклидова норма):
$$ \lVert v \rVert_2 = \sqrt{\sum_{i=1}^{n} v_i^2} $$
import numpy as np
v = np.array([3.0, 4.0])
print(np.linalg.norm(v))
# 5.0
mean, var, std, percentile, corrcoef, histogram
mean-- среднее арифметическое.var-- дисперсия.std-- стандартное отклонение (корень из дисперсии).percentile-- персентиль (квантиль), значение, ниже которого лежит заданный процент данных.corrcoef-- матрица корреляции (обычно корреляция Пирсона).histogram-- гистограмма: разбиение значений на интервалы и подсчет количества в каждом интервале.
Среднее арифметическое для набора \(x_1,\dots,x_n\):
$$ \bar{x} = \frac{1}{n}\sum_{i=1}^{n} x_i $$
import numpy as np
a = np.array([1, 2, 3, 4], dtype=np.float64)
print(np.mean(a))
# 2.5
print(np.var(a))
# 1.25
print(np.std(a))
# 1.118033988749895
import numpy as np
a = np.array([1, 2, 3, 4, 100], dtype=np.float64)
print(np.median(a))
# 3.0
print(np.percentile(a, 50))
# 3.0
import numpy as np
x = np.array([1, 2, 3], dtype=np.float64)
y = np.array([1, 5, 7], dtype=np.float64)
print(np.corrcoef(x, y))
# [[1. 0.98198051]
# [0.98198051 1. ]]
import numpy as np
a = np.array([0, 0, 1, 1, 1, 2, 3, 3], dtype=np.int64)
counts, bins = np.histogram(a, bins=4)
print(counts)
# [2 3 1 2]
print(bins)
# [0. 0.75 1.5 2.25 3. ]
set_printoptions: специальное форматирование вывода
set_printoptions -- настройка того, как NumPy печатает массивы (точность, научная нотация, подавление очень маленьких чисел и т.д.).import numpy as np
np.set_printoptions(precision=3, suppress=True)
a = np.array([1.234567, 0.000012345, 1000.0], dtype=np.float64)
print(a)
# [ 1.235 0. 1000. ]
Типичные ошибки
Ошибка 1: использовать * вместо @ для матриц
Оператор * делает поэлементное умножение. Для матричного умножения используйте @ или np.matmul.
import numpy as np
A = np.array([[1, 2],
[3, 4]])
B = np.array([[10, 20],
[30, 40]])
print(A * B)
# [[ 10 40]
# [ 90 160]]
print(A @ B)
# [[ 70 100]
# [150 220]]
Ошибка 2: решить систему с вырожденной матрицей
Если \(\det(A)=0\), система может не иметь единственного решения. Покажем тип ошибки безопасно.
import numpy as np
A = np.array([[1.0, 2.0],
[2.0, 4.0]])
b = np.array([1.0, 2.0])
print(np.linalg.det(A))
# 0.0
try:
x = np.linalg.solve(A, b)
print(x)
except Exception as e:
print("ERROR:", type(e).__name__)
# ERROR: LinAlgError
7. Практика: задачи (с решениями)
Блок 1: создание и форма массивов
Задача 1: создать массив и вывести shape и dtype
Создайте массив 2 x 3 и выведите сам массив, shape и dtype.
import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6]])
print(a)
# [[1 2 3]
# [4 5 6]]
print(a.shape)
# (2, 3)
print(a.dtype)
# int64
Задача 2: построить сетку из 5 точек на [0, 2]
Используйте linspace. Должно получиться 5 чисел, включая границы.
import numpy as np
x = np.linspace(0, 2, num=5)
print(x)
# [0. 0.5 1. 1.5 2. ]
Задача 3: сделать reshape из 12 элементов в 3 x 4
Создайте arange от 1 до 12 включительно и преобразуйте в матрицу 3 x 4.
import numpy as np
a = np.arange(1, 13)
m = a.reshape((3, 4))
print(m)
# [[ 1 2 3 4]
# [ 5 6 7 8]
# [ 9 10 11 12]]
Задача 4: транспонировать матрицу
Возьмите матрицу 2 x 3 и выведите ее транспонированную форму 3 x 2.
import numpy as np
m = np.array([[1, 2, 3],
[4, 5, 6]])
print(m.T)
# [[1 4]
# [2 5]
# [3 6]]
Блок 2: оси, линеиная алгебра, статистика, ввод-вывод
Задача 5: сумма по axis=0 и axis=1
Посчитайте суммы по столбцам и по строкам.
import numpy as np
a = np.array([[1, 2, 3],
[10, 20, 30]])
print(np.sum(a, axis=0))
# [11 22 33]
print(np.sum(a, axis=1))
# [ 6 60]
Задача 6: решить систему Ax=b
Решите \(A x = b\), где:
$$ A = \begin{pmatrix} 2 & 1 \\ 5 & 3 \end{pmatrix}, \quad b = \begin{pmatrix} 1 \\ 2 \end{pmatrix} $$
import numpy as np
A = np.array([[2.0, 1.0],
[5.0, 3.0]])
b = np.array([1.0, 2.0])
x = np.linalg.solve(A, b)
print(x)
# [ 1. -1.]
Задача 7: сравнить A*B и A@B
Покажите, что * это поэлементно, а @ это матрично.
import numpy as np
A = np.array([[1, 2],
[3, 4]])
B = np.array([[10, 20],
[30, 40]])
print(A * B)
# [[ 10 40]
# [ 90 160]]
print(A @ B)
# [[ 70 100]
# [150 220]]
Задача 8: mean, median, percentile
Посчитайте среднее, медиану и 50-й персентиль для массива.
import numpy as np
a = np.array([1, 2, 3, 4, 100], dtype=np.float64)
print(np.mean(a))
# 22.0
print(np.median(a))
# 3.0
print(np.percentile(a, 50))
# 3.0
8. Чек-лист самопроверки
Отметьте пункты, которые вы реально понимаете и можете повторить без подсказок.
| +/- | Навык | Проверка |
|---|---|---|
| Понимаю ndarray | Могу объяснить разницу между list и ndarray | |
| Читаю shape и dtype | Могу по выводу понять форму и тип массива | |
| Создаю массивы разными способами | Могу использовать array, zeros, ones, full, empty, arange, linspace, eye | |
| Меняю форму массива | Могу сделать reshape и объяснить, когда возникает ValueError | |
| Понимаю transpose | Могу получить A.T и объяснить, что изменилось | |
| Склеиваю и делю массивы | Могу применить concatenate/stack и array_split | |
| Понимаю axis | Могу посчитать сумму по axis=0 и axis=1 и объяснить разницу | |
| Отличаю * от @ | Могу показать поэлементное и матричное умножение | |
| Решаю Ax=b | Могу применить np.linalg.solve и проверить результат через A@x | |
| Считаю базовую статистику | Могу посчитать mean, median, percentile и объяснить смысл | |
| Делаю специальный вывод | Могу настроить np.set_printoptions и получить нужную точность |