Алла Тамбовцева
NumPy
¶Сегодня мы познакомимся с библиотекой NumPy
(сокращение от Numeric Python), которая часто используется в задачах, связанных с машинным обучением и построением статистических моделей.
Массивы NumPy
очень похожи на списки (даже больше на вложенные списки), только они имеют одну особенность: элементы массива должны быть одного типа. Либо все элементы целые числа, либо числа с плавающей точкой, либо строки. Для обычных списков это условие не является обязательным:
L = [1, 2, 4, 0]
E = [[1, 0, 3], [3, 6, 7], []]
D = [[1, 3, 6], ['a', 'b', 'c']]
# все работает
print(L)
print(E)
print(D)
[1, 2, 4, 0] [[1, 0, 3], [3, 6, 7], []] [[1, 3, 6], ['a', 'b', 'c']]
Чем хороши массивы NumPy
? Почему обычных списков недостаточно? Во-первых, обработка массивов занимает меньше времени (а их хранение меньше памяти), что очень актуально в случае работы с большими объемами данных. Во-вторых, функции NumPy
являются векторизованными ‒ их можно применять сразу ко всему массиву, то есть поэлементно. В этом смысле работа с массивами напоминает работу с векторами в R. Если в R у нас есть вектор c(1, 2, 5)
, то, прогнав строчку кода c(1, 2, 5)**2
, мы получим вектор, состоящий из квадратов значений: c(1, 4, 25)
. Со списками в Python такое проделать не получится: понадобятся циклы или списковые включения. Зато с массивами NumPy
‒ легко, и без всяких циклов! И в этом мы сегодня убедимся.
Для начала импортируем библиотеку (и сократим название до np
):
import numpy as np
Получить массив NumPy
можно из обычного списка, просто используя функцию array()
:
A = np.array(L)
A
array([1, 2, 4, 0])
A = np.array([1, 2, 4, 0])
A
array([1, 2, 4, 0])
Как видно из примера выше, список значений можно просто вписать в array()
. Главное не забыть квадратные скобки: Python не сможет склеить перечень элементов в список самостоятельно и выдаст ошибку:
A = np.array(1, 2, 4, 0)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-5-83a5f8fa93ae> in <module>() ----> 1 A = np.array(1, 2, 4, 0) ValueError: only 2 non-keyword arguments accepted
Посмотрим, какую информацию о массиве можно получить. Например, тип его элементов:
A.dtype # integer
dtype('int64')
Число измерений – число «маленьких» массивов внутри «большого» массива (здесь такой один).
A.ndim
1
«Форма» массива, о котором можно думать как о размерности матрицы – кортеж, включающий число строк и столбцов. Здесь у нас всего одна строка, поэтому NumPy
считает только число элементов внутри массива.
A.shape
(4,)
Так как массив A
одномерный, обращаться к его элементам можно так же, как и к элементам списка, указывая индекс элемента в квадратных скобках:
A[0]
1
Попытка использовать двойной индекс приведет к неудаче:
A[0][0] # index error
--------------------------------------------------------------------------- IndexError Traceback (most recent call last) <ipython-input-10-ac48a874bd97> in <module>() ----> 1 A[0][0] # index error IndexError: invalid index to scalar variable.
Общее число элементов в массиве можно получить с помощью метода size
(аналог len()
для списков):
A.size
4
Кроме того, по массиву можно получить разные описательные статистики:
A.max() # максимум
4
A.min() # минимум
0
A.mean() # среднее
1.75
О других полезных методах можно узнать, нажав Tab после np.
.
Наконец, массив NumPy
можно легко превратить в список:
A.tolist()
[1, 2, 4, 0]
А теперь перейдем к многомерным массивам.
Создадим многомерный массив, взяв за основу вложенный список:
S = np.array([[8, 1, 2], [2, 8, 9]])
S
array([[8, 1, 2], [2, 8, 9]])
Посмотрим на число измерений:
S.ndim # два массива внутри
2
S.shape # две строки (два списка) и три столбца (по три элемента в списке)
(2, 3)
Общее число элементов в массиве (его длина):
S.size
6
Когда в массиве больше одного измерения, при различных операциях нужно указывать, по какому измерению мы движемся (по строкам или по столбцам). Посмотрим еще раз на массив S
и подумаем о нем как о матрице, как о таблице с числами:
S
array([[8, 1, 2], [2, 8, 9]])
Можно найти максимальное значение по строкам или столбцам S
:
S.max(axis=0) # по столбцам - три столбца и три максимальных значения
array([8, 8, 9])
S.max(axis=1) # по строкам - две строки и два максимальных значения
array([8, 9])
S.mean(axis=0)
array([5. , 4.5, 5.5])
S.mean(axis=1)
array([3.66666667, 6.33333333])
Для того, чтобы обратиться к элементу двумерного массива, нужно указывать два индекса: сначала индекс массива, в котором находится нужный нам элемент, а затем индекс элемента внутри этого массива:
S[0][0]
8
S[1][2]
9
Если мы оставим один индекс, мы просто получим массив с соответствующим индексом:
S[0]
array([8, 1, 2])
Массивы – изменяемые объекты в Python. Обращаясь к элементу массива, ему можно присвоить новое значение:
S[1][2] = 6
S
array([[8, 1, 2], [2, 8, 6]])
Чтобы выбрать сразу несколько элементов, как и в случае со списками, можно использовать срезы. Рассмотрим массив побольше.
T = np.array([[1, 3, 7], [8, 10, 1], [2, 8, 9], [1, 0, 5]])
T
array([[ 1, 3, 7], [ 8, 10, 1], [ 2, 8, 9], [ 1, 0, 5]])
Как и при выборе среза из списка, правый конец не включается:
T[0:2] # массивы с индексами 0 и 1
array([[ 1, 3, 7], [ 8, 10, 1]])
Можно сделать что-то еще более интересное – выставить шаг среза. Другими словами, сообщить Python, что нужно брать? например, элементы, начиная с нулевого, с шагом 2: элемент с индексом 0, с индексом 2, с индексом 4, и так до конца массива.
T[0::2] # старт, двоеточие, двоеточие, шаг
array([[1, 3, 7], [2, 8, 9]])
В примере выше совершенно логично были выбраны элементы с индексами 0 и 2.
Способ 1
С первым способом мы уже отчасти познакомились: можно получить массив из готового списка, воспользовавшись функцие array()
:
np.array([10.5, 45, 2.4])
array([10.5, 45. , 2.4])
Кроме того, при создании массива из списка можно изменить его форму, используя метод .reshape()
.
old = np.array([[2, 5, 6], [9, 8, 0]])
old
array([[2, 5, 6], [9, 8, 0]])
old.shape # 2 на 3
(2, 3)
new = old.reshape(3, 2) # изменим на 3 на 2
new
array([[2, 5], [6, 9], [8, 0]])
new.shape # 3 на 2
(3, 2)
Конечно, такие преобразования разумно применять, если произведение чисел в .reshape()
совпадает с общим числом элементов в массиве. В нашем случае в массиве old
6 элементов, поэтому из него можно получить массивы 2 на 3, 3 на 2, 1 на 6, 6 на 1. Несоответствующее число измерений приведет к ошибке:
old.reshape(2, 4) # и Python явно пишет, что не так
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-39-a803468708c0> in <module>() ----> 1 old.reshape(2, 4) # и Python явно пишет, что не так ValueError: cannot reshape array of size 6 into shape (2,4)
Способ 2
Можно создать массив на основе промежутка, созданного с помощьюarange()
– функции из NumPy
, похожей на range()
, только более гибкую. Посмотрим, как работает эта функция.
np.arange(2, 9) # по умолчанию - как обычный range()
array([2, 3, 4, 5, 6, 7, 8])
По умолчанию эта функция создает массив, элементы которого начинаются со значения 2 и заканчиваются на значении 8 (правый конец промежутка не включается), следуя друг за другом с шагом 1. Но этот шаг можно менять:
np.arange(2, 9, 3) # с шагом 3
array([2, 5, 8])
И даже делать дробным!
np.arange(2, 9, 0.5)
array([2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. , 7.5, 8. , 8.5])
А теперь совместим arange()
и reshape()
, чтобы создать массив нужного вида:
np.arange(2, 9, 0.5).reshape(2, 7)
array([[2. , 2.5, 3. , 3.5, 4. , 4.5, 5. ], [5.5, 6. , 6.5, 7. , 7.5, 8. , 8.5]])
Получилось!
Способ 3
Еще массив можно создать совсем с нуля. Единственное, что нужно чётко представлять – это его размерность, его форму, то есть опять же, число строк и столбцов. Библиотека NumPy
позволяет создать массивы, состоящие из нулей или единиц, а также «пустые» массивы (на самом деле, не совсем пустые, как убедимся позже). Удобство заключается в том, что сначала можно создать массив, инициализировать его (например, заполнить нулями), а затем заменить нули на другие значения в соответствии с требуемыми условиями. Как мы помним, массивы ‒ изменяемые объекты, и использовать замену в цикле еще никто не запрещал.
Так выглядит массив из нулей:
Z = np.zeros((3, 3)) # размеры в виде кортежа - не теряйте еще одни круглые скобки
Z
array([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])
А так ‒ массив из единиц:
O = np.ones((4, 2))
O
array([[1., 1.], [1., 1.], [1., 1.], [1., 1.]])
С пустым (empty) массивом все более загадочно:
Emp = np.empty((3, 2))
Emp
array([[6.92198590e-310, 6.92198590e-310], [5.31021756e-317, 6.92194731e-310], [5.39590831e-317, 5.39790038e-317]])
Массив Emp
– не совсем пустой, в нем содержатся какие-то (псевдо)случайные элементы, которые примерно равны 0. Теоретически создавать массив таким образом можно, но не рекомендуется: лучше создать массив из "чистых" нулей, чем из какого-то непонятного «мусора».
Задание: Дан массив ages
(см. ниже). Напишите программу с циклом, которая позволит получить массив ages_bin
такой же размерности, что и ages
, состоящий из 0 и 1 (0 - младше 18, 1 – не младше 18).
Подсказка: используйте вложенный цикл.
ages = np.array([[12, 16, 17, 18, 14], [20, 22, 18, 17, 23], [32, 16, 44, 16, 23]])
Решение:
shape = ages.shape
ages_bin = np.zeros(shape)
ages_bin
for i in range(0, shape[0]):
for j in range(0, shape[1]):
if ages[i][j] >= 18:
ages_bin[i][j] = 1
ages_bin
array([[0., 0., 0., 1., 0.], [1., 1., 1., 0., 1.], [1., 0., 1., 0., 1.]])
NumPy
– это удобно?¶Как уже было отмечено в начале занятия, операции с массивами можно производить поэлементно, не используя циклы или их аналоги. Посмотрим на массив A
:
A
array([1, 2, 4, 0])
А теперь возведем все его элементы в квадрат:
A ** 2
array([ 1, 4, 16, 0])
Или вычтем из всех элементов единицу:
A - 1
array([ 0, 1, 3, -1])
Кроме того, так же просто к элементам массива можно применять свои функции. Напишем функцию, которая будет добавлять к элементу 1, а затем считать от него натуральный логарифм (здесь эта функция актуальна, так как в массиве A
есть 0).
def my_log(x):
return np.log(x + 1)
Применим:
my_log(A)
array([0.69314718, 1.09861229, 1.60943791, 0. ])
И никаких циклов и иных нагромождений.
Превратить многомерный массив в одномерный (как список) можно, воспользовавшись методами .flatten()
и .ravel()
.
ages.flatten() # "плоский" массив
array([12, 16, 17, 18, 14, 20, 22, 18, 17, 23, 32, 16, 44, 16, 23])
ages.ravel()
array([12, 16, 17, 18, 14, 20, 22, 18, 17, 23, 32, 16, 44, 16, 23])
NumPy
?¶1.Позволяет производить вычисления – нет необходимости дополнительно загружать модуль math
.
np.log(3) # натуральный логарифм
1.0986122886681098
np.sqrt(7) # квадратный корень
2.6457513110645907
np.exp(2) # e^2
7.38905609893065
2.Позволяет производить операции с векторами и матрицами. Пусть у нас есть два вектора a
и b
.
a = np.array([1, 2, 3])
b = np.array([0, 4, 7])
Если мы просто умножим a
на b
с помощью символа *
, мы получим массив, содержащий произведения соответствующих элементов a
и b
:
a * b
array([ 0, 8, 21])
А если мы воспользуемся функцией .dot()
, получится скалярное произведение векторов (dot product).
np.dot(a, b) # результат - число
29
При желании можно получить векторное произведение (cross product):
np.cross(a, b) # результат- вектор
array([ 2, -7, 4])
Теперь создадим матрицу и поработаем с ней. Создадим ее не самым интуитивным образов ‒ из строки (да, так тоже можно).
m = np.array(np.mat('2 4; 1 6')) # np.mat - матрица из строки, np.array - массив из матрицы
Самое простое и понятное, что можно сделать с матрицей ‒ транспонировать её, то есть поменять местами строки и столбцы:
m.T
array([[2, 1], [4, 6]])
Можно вывести её диагональные элементы:
m.diagonal()
array([2, 6])
И посчитать след матрицы – сумму её диагональных элементов:
m.trace()
8
Задание. Создайте единичную матрицу 3 на 3, создав массив из нулей, а затем заполнив ее диагональные элементы значениями 1.
Подсказка: функция fill_diagonal()
.
Решение:
I = np.zeros((3, 3))
np.fill_diagonal(I, 1)
I
array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])
Правда, для создания массива в виде единичной матрицы в NumPy
уже есть готовая функция (наряду с zeros
и ones
):
np.eye(3)
array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])
Найдем обратную матрицу:
np.invert(m)
array([[-3, -5], [-2, -7]])
Для других операций с матрицами (и вычислений в рамках линейной алгебры) можно использовать функции из подмодуля linalg
. Например, так можно найти определитель матрицы:
np.linalg.det(m) # вспоминаем истории про числа с плавающей точкой, это 8 на самом деле
7.999999999999998
И собственные значения:
np.linalg.eigvals(m)
array([1.17157288, 6.82842712])
Полный список функций с описанием см. в документации.
3.Библиотеку numpy
часто используют с библиотекой для визуализации matplotlib
.
Рассмотрим функцию linspace()
. Она возвращает массив одинаково (с одинаковым шагом) распределенных чисел из фиксированного интервала.
x = np.linspace(10, 100, 5) # 5 чисел из интервала от 10 до 100
x
array([ 10. , 32.5, 55. , 77.5, 100. ])
x = np.linspace(0, 1000, 100)
x
array([ 0. , 10.1010101 , 20.2020202 , 30.3030303 , 40.4040404 , 50.50505051, 60.60606061, 70.70707071, 80.80808081, 90.90909091, 101.01010101, 111.11111111, 121.21212121, 131.31313131, 141.41414141, 151.51515152, 161.61616162, 171.71717172, 181.81818182, 191.91919192, 202.02020202, 212.12121212, 222.22222222, 232.32323232, 242.42424242, 252.52525253, 262.62626263, 272.72727273, 282.82828283, 292.92929293, 303.03030303, 313.13131313, 323.23232323, 333.33333333, 343.43434343, 353.53535354, 363.63636364, 373.73737374, 383.83838384, 393.93939394, 404.04040404, 414.14141414, 424.24242424, 434.34343434, 444.44444444, 454.54545455, 464.64646465, 474.74747475, 484.84848485, 494.94949495, 505.05050505, 515.15151515, 525.25252525, 535.35353535, 545.45454545, 555.55555556, 565.65656566, 575.75757576, 585.85858586, 595.95959596, 606.06060606, 616.16161616, 626.26262626, 636.36363636, 646.46464646, 656.56565657, 666.66666667, 676.76767677, 686.86868687, 696.96969697, 707.07070707, 717.17171717, 727.27272727, 737.37373737, 747.47474747, 757.57575758, 767.67676768, 777.77777778, 787.87878788, 797.97979798, 808.08080808, 818.18181818, 828.28282828, 838.38383838, 848.48484848, 858.58585859, 868.68686869, 878.78787879, 888.88888889, 898.98989899, 909.09090909, 919.19191919, 929.29292929, 939.39393939, 949.49494949, 959.5959596 , 969.6969697 , 979.7979798 , 989.8989899 , 1000. ])
При чем тут matplotlib
? Представьте, что нам нужно построить обычный график функции $y=x^2$. Если в наши задачи не входит построение графика по определенным данным, можно спокойно создать массив x
с помощью linspace
, а затем просто возвести его в квадрат! И нанести полученные точки на график.
y = x ** 2
y
array([0.00000000e+00, 1.02030405e+02, 4.08121620e+02, 9.18273646e+02, 1.63248648e+03, 2.55076013e+03, 3.67309458e+03, 4.99948985e+03, 6.52994592e+03, 8.26446281e+03, 1.02030405e+04, 1.23456790e+04, 1.46923783e+04, 1.72431385e+04, 1.99979594e+04, 2.29568411e+04, 2.61197837e+04, 2.94867871e+04, 3.30578512e+04, 3.68329762e+04, 4.08121620e+04, 4.49954086e+04, 4.93827160e+04, 5.39740843e+04, 5.87695133e+04, 6.37690032e+04, 6.89725538e+04, 7.43801653e+04, 7.99918376e+04, 8.58075707e+04, 9.18273646e+04, 9.80512193e+04, 1.04479135e+05, 1.11111111e+05, 1.17947148e+05, 1.24987246e+05, 1.32231405e+05, 1.39679625e+05, 1.47331905e+05, 1.55188246e+05, 1.63248648e+05, 1.71513111e+05, 1.79981635e+05, 1.88654219e+05, 1.97530864e+05, 2.06611570e+05, 2.15896337e+05, 2.25385165e+05, 2.35078053e+05, 2.44975003e+05, 2.55076013e+05, 2.65381084e+05, 2.75890215e+05, 2.86603408e+05, 2.97520661e+05, 3.08641975e+05, 3.19967350e+05, 3.31496786e+05, 3.43230283e+05, 3.55167840e+05, 3.67309458e+05, 3.79655137e+05, 3.92204877e+05, 4.04958678e+05, 4.17916539e+05, 4.31078461e+05, 4.44444444e+05, 4.58014488e+05, 4.71788593e+05, 4.85766758e+05, 4.99948985e+05, 5.14335272e+05, 5.28925620e+05, 5.43720029e+05, 5.58718498e+05, 5.73921028e+05, 5.89327620e+05, 6.04938272e+05, 6.20752984e+05, 6.36771758e+05, 6.52994592e+05, 6.69421488e+05, 6.86052444e+05, 7.02887460e+05, 7.19926538e+05, 7.37169677e+05, 7.54616876e+05, 7.72268136e+05, 7.90123457e+05, 8.08182838e+05, 8.26446281e+05, 8.44913784e+05, 8.63585348e+05, 8.82460973e+05, 9.01540659e+05, 9.20824406e+05, 9.40312213e+05, 9.60004081e+05, 9.79900010e+05, 1.00000000e+06])
Подробнее этот пример мы рассмотрим позже, когда будем работать с matplotlib
.
4.Еще NumPy
можно использовать в статистике. Например, чтобы посчитать выборочную дисперсию, стандартное отклонение, медиану. Важный момент: здесь NumPy
чем-то похож на R, он не сможет выдать результат в случае, если в массиве присутствуют пропущенные значения. Проверим.
q = np.array([1., 0., 4.5, np.nan, 3.]) # np.nan - Not a number (пропущенное значение)
q
array([1. , 0. , 4.5, nan, 3. ])
np.var(q) # получаем nan вместо дисперсии
nan
np.nanvar(q) # если функция начинается с nan, все работает
3.046875
Аналогичная история с медианой:
np.median(q)
/usr/local/lib/python3.5/dist-packages/numpy/lib/function_base.py:4033: RuntimeWarning: Invalid value encountered in median r = func(a, **kwargs)
nan
np.nanmedian(q)
2.0
Еще с помощью NumPy
можно считать выборочный коэффициент корреляции Пирсона. Функция corrcoef()
возвращает корреляционную матрицу, из которой можно извлечь коэффициент линейной корреляции.
x = np.array([2, 6, 8, 10, 12])
y = np.array([4, 7, 14, 21, 19])
np.corrcoef(x, y)
array([[1. , 0.93307545], [0.93307545, 1. ]])
np.corrcoef(x, y)[0][1]
0.9330754546745095
Внимание: не путайте функцию corrcoef()
с correlate()
!
np.correlate(x, y)
array([600])
Функция correlate()
используется для нахождения кросс-корреляции.
help(np.correlate)
Help on function correlate in module numpy.core.numeric: correlate(a, v, mode='valid') Cross-correlation of two 1-dimensional sequences. This function computes the correlation as generally defined in signal processing texts:: c_{av}[k] = sum_n a[n+k] * conj(v[n]) with a and v sequences being zero-padded where necessary and conj being the conjugate. Parameters ---------- a, v : array_like Input sequences. mode : {'valid', 'same', 'full'}, optional Refer to the `convolve` docstring. Note that the default is 'valid', unlike `convolve`, which uses 'full'. old_behavior : bool `old_behavior` was removed in NumPy 1.10. If you need the old behavior, use `multiarray.correlate`. Returns ------- out : ndarray Discrete cross-correlation of `a` and `v`. See Also -------- convolve : Discrete, linear convolution of two one-dimensional sequences. multiarray.correlate : Old, no conjugate, version of correlate. Notes ----- The definition of correlation above is not unique and sometimes correlation may be defined differently. Another common definition is:: c'_{av}[k] = sum_n a[n] conj(v[n+k]) which is related to ``c_{av}[k]`` by ``c'_{av}[k] = c_{av}[-k]``. Examples -------- >>> np.correlate([1, 2, 3], [0, 1, 0.5]) array([ 3.5]) >>> np.correlate([1, 2, 3], [0, 1, 0.5], "same") array([ 2. , 3.5, 3. ]) >>> np.correlate([1, 2, 3], [0, 1, 0.5], "full") array([ 0.5, 2. , 3.5, 3. , 0. ]) Using complex sequences: >>> np.correlate([1+1j, 2, 3-1j], [0, 1, 0.5j], 'full') array([ 0.5-0.5j, 1.0+0.j , 1.5-1.5j, 3.0-1.j , 0.0+0.j ]) Note that you get the time reversed, complex conjugated result when the two input sequences change places, i.e., ``c_{va}[k] = c^{*}_{av}[-k]``: >>> np.correlate([0, 1, 0.5j], [1+1j, 2, 3-1j], 'full') array([ 0.0+0.j , 3.0+1.j , 1.5+1.5j, 1.0+0.j , 0.5+0.5j])