import numpy as np
a = [1, 2, 3] # список Python
b = np.array([1, 2, 3]) # массив NumPy
c = 7Введение в NumPy (2)
Пожалуй, первое с чего стоит начать, так это с того, что массивы NumPy могут быть обычными операндами в математических выражениях:
# Если мы умножим список на число, то...
a * c[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
# Если попытаемся к списку это число прибавить, то Python попытается выполнить конкатенацию, а не сложение.# Теперь умножим массив NumPy на число:
b * carray([ 7, 14, 21])
# Прибавим к массиву число:
b + carray([ 8, 9, 10])
Для выполнения таких операций на Python, мы были вынуждены писать циклы.
Писать такие циклы в NumPy, нет никакой необходимости, потому что все операции и так выполняются поэлементно:
a = np.array([[5, 7], [11, 13]])
a / 3 # обычное делениеarray([[1.66666667, 2.33333333],
[3.66666667, 4.33333333]])
a // 3 # целочисленное делениеarray([[1, 2],
[3, 4]])
a % 3 # остаток от деленияarray([[2, 1],
[2, 1]])
a ** 3 # возведение в степеньarray([[ 125, 343],
[1331, 2197]])
1 / a # частное 1 и каждого элемента массиваarray([[0.2 , 0.14285714],
[0.09090909, 0.07692308]])
-a # изменение знака элементов массиваarray([[ -5, -7],
[-11, -13]])
Точно так же обстоят дела и с математическими функциями:
a = np.arange(6)
aarray([0, 1, 2, 3, 4, 5])
np.sin(a) # синус каждого элемента массиваarray([ 0. , 0.84147098, 0.90929743, 0.14112001, -0.7568025 ,
-0.95892427])
np.log(a) # натуральный логарифм элементов массива/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in log
"""Entry point for launching an IPython kernel.
array([ -inf, 0. , 0.69314718, 1.09861229, 1.38629436,
1.60943791])
Такие операции как +=, -=, *=, /= и прочие подобные, могут применяться к массивам и так же выполняются поэлементно.
Они не создают новый массив, а изменяют старый:
a = np.array([1, 2, 3, 4, 5])
b = np.array([0.1, 0.2, 0.3, 0.4, 0.5])a += 2
aarray([3, 4, 5, 6, 7])
a *= 2
aarray([ 6, 8, 10, 12, 14])
# Вещественный тип ('float64') не может быть
# преобразован в целочисленный ('int32'):
# a += b# А вот преобразование целочисленного типа в вещественный возможно
b += a
barray([ 6.1, 8.2, 10.3, 12.4, 14.5])
При работе с массивами разного типа, тип результирующего массива приводится к более общему:
a = np.arange(5)
aarray([0, 1, 2, 3, 4])
b = np.linspace(0, 5, 5)
barray([0. , 1.25, 2.5 , 3.75, 5. ])
a.dtypedtype('int64')
b.dtypedtype('float64')
c = a + b
carray([0. , 2.25, 4.5 , 6.75, 9. ])
c.dtypedtype('float64')
Применение логических операций к массивам, так же возможно и так же выполняется поэлементно.
Результатом таких операций является массив булевых значений (True и False):
a = np.array([2, 3, 5, 7, 11, 13])
a > 5array([False, False, False, True, True, True])
a == 7array([False, False, False, True, False, False])
b = np.array([2, 2, 5, 5, 11, 11])
a > barray([False, True, False, True, False, True])
a == barray([ True, False, True, False, True, False])
Мы уже знаем что массив и число могут быть операндами самых разных математических выражений:
a = np.array([1, 2, 3])
(a + 3) * 7array([28, 35, 42])
Операндами могут быть даже несколько различных массивов, правда их размеры должны быть одинаковыми:
a = np.array([1, 2, 3])
b = np.array([3, 2, 1])
a + barray([4, 4, 4])
a ** barray([1, 4, 3])
a = np.arange(9).reshape(3, 3)
aarray([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
b = np.arange(9, 0, -1).reshape(3, 3)
barray([[9, 8, 7],
[6, 5, 4],
[3, 2, 1]])
a + barray([[9, 9, 9],
[9, 9, 9],
[9, 9, 9]])
a ** barray([[ 0, 1, 128],
[ 729, 1024, 625],
[ 216, 49, 8]])
Хотя, если честно, их размеры должны быть не равны, а должны быть совместимыми.
Если их размеры совместимы, т.е. один массив может быть растянут до размеров другого, то в дело включается механизм транслирования массивов NumPy.
Этот механизм очень прост, но имеет весьма специфичные нюансы.
Рассмотрим простой пример:
a = np.arange(9).reshape(3, 3)
aarray([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
b = np.array([1, 2, 3])
barray([1, 2, 3])
c = np.array([[1, 2, 3], [1, 2, 3], [1, 2, 3]])
carray([[1, 2, 3],
[1, 2, 3],
[1, 2, 3]])
a * barray([[ 0, 2, 6],
[ 3, 8, 15],
[ 6, 14, 24]])
a * carray([[ 0, 2, 6],
[ 3, 8, 15],
[ 6, 14, 24]])
В данном примере массив b может быть растянут до размеров массива a и станет абсолютно идентичен массиву c.
Транслирование массивов невероятно удобно, так как позволяет избежать создания множества вложенных и невложенных циклов.
К тому же в NumPy этот механизм реализован для максимально быстрого выполнения. Так что используйте транслирование везде, где это возможно в ваших вычислениях.
Вычисление суммы всех элементов в массиве и прочие унарные операции в NumPy реализованы как методы класса ndarray:
a = np.arange(10)
aarray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
a.sum()45
a.min()0
a.max()9
По умолчанию, эти операции применяются к массиву, как к обычному списку чисел, без учета его ранга (размерности).
Но если указать в качестве параметра одну из осей axis, то вычисления будут производиться именно по ней:
b = np.arange(16).reshape(4, 4)
barray([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
b.sum(axis=0) # Сумма элементов каждого столбцаarray([24, 28, 32, 36])
b.sum(axis=1) # Сумма элементов каждой строкиarray([ 6, 22, 38, 54])
b.min(axis=1) # Минимальный элемент каждой строкиarray([ 0, 4, 8, 12])
b.max(axis=0) # Максимальный элемент каждого столбцаarray([12, 13, 14, 15])
Значения -inf, inf и nan
Возможно вы обратили внимание на то, что когда мы вычисляли натуральный логарифм массива, среди значений которого был ноль, не появилось абсолютно никакой ошибки, а сам логарифм стал равен значению -inf (минус бесконечность).
Убедимся в этом еще раз:
np.log(0)/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in log
"""Entry point for launching an IPython kernel.
-inf
Более того, в NumPy мы даже можем делить на ноль:
a = np.array([0])
1 / a/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:2: RuntimeWarning: divide by zero encountered in true_divide
array([inf])
NumPy предупредил нас о том, что встретил деление на ноль, но тем не менее выдал ответ inf (плюс бесконечность).
Дело в том, что с математической точки зрения все абсолютно верно - если вы что-то делите на бесконечно малое значение, то в результате получете значение, которое окажется бесконечно большим.
Если результатом математической операции является плюс или минус бесконечность, то логичнее выдать значение inf или -inf, чем выдавать ошибку.
В NumPy есть еще одно специальное значение - nan. Данное значение выдается тогда, когда результат вычислений не удается определить:
a = np.array([0, 1, np.inf])
aarray([ 0., 1., inf])
np.cos(a)/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in cos
"""Entry point for launching an IPython kernel.
array([1. , 0.54030231, nan])
Заметьте, что NumPy нас просто предупредил о том, что ему попалось недопустимое значение, но ошибки не возникло.
Дело в том, что в реальных вычислениях значения nan, inf или -inf встречается очень часто, поэтому появление этого значения проще обрабатывать специальными методами (функции numpy.isnan() и numpy.isinf()), чем постоянно лицезреть сообщения об ошибках.
Новичкам, довольно трудно привыкнуть, к тому что в недрах компьютера вся арифметика на самом деле является двоичной и с этим связано очень много казусов.
Во-первых не совсем понятно, когда ждать появления значений -inf и inf:
np.cos(0)/np.sin(0)/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in double_scalars
"""Entry point for launching an IPython kernel.
inf
np.sin(np.pi/2)/np.cos(np.pi/2) # ожидаем значение 0, но...1.633123935319537e+16
Число 1.633123935319537e+16 появилось потому что в NumPy выполняются арифметические, а не символьные вычисления, т. е. число π хранится в памяти компьютера не как знание о том, что это математическая константа с бесконечным количеством десятичных знаков после запятой, а как обычное число с десятичной точкой (десятичная дробь) равная числу π с очень маленькой, но все же, погрешностью:
np.pi # значение числа pi в NumPy3.141592653589793
Если из-за самых незначительных погрешностей вычисления все же возможны, то NumPy их обязательно выполнит.
В этих случаях вместо значений -inf или inf у вас будут появляться самые маленькие или самые большие числа, которые возможно представить на вашем компьютере.
Если вам необходимы точные решения, то лучше обратиться к системам компьютерной алгебры и символьных вычислений, например пакету SymPy.
Если вы решили отправиться в самые дебри теории чисел, алгебры и криптографии, то лучшим решением окажется программа GAP.
Программа GAP не является программой Python, но имеет Python интерфейс в замечательной программе Sage, которая определенно заслуживает вашего внимания (см. книгу).
Линейная алгебра
Произведение одномерных массивов представляет собой скалярное произведение векторов:
a = np.array([1, 2])
b = np.array([3, 4])
np.dot(a, b)11
Произведение двумерных массивов по правилам линейной алгебры также возможно:
a = np.arange(2, 6).reshape(2, 2)
aarray([[2, 3],
[4, 5]])
b = np.arange(6, 10).reshape(2, 2)
barray([[6, 7],
[8, 9]])
np.dot(a, b)array([[36, 41],
[64, 73]])
При этом размеры матриц (массивов) должны быть либо равны, а сами матрицы квадратными, либо быть согласованными, т.е. если размеры матрицы А равны [m,k], то размеры матрицы В должны быть равны [k,n]:
a = np.arange(2, 8).reshape(2, 3)
aarray([[2, 3, 4],
[5, 6, 7]])
b = np.arange(4, 10).reshape(3, 2)
barray([[4, 5],
[6, 7],
[8, 9]])
np.dot(a,b)array([[ 58, 67],
[112, 130]])
Также по правилам умножения матриц, мы можем умножить матрицу на вектор (одномерный массив).
При этом в таком умножении вектор столбец должен находиться справа, а вектор строка слева:
a = np.array([1, 2, 3])
aarray([1, 2, 3])
b = np.arange(4, 10).reshape(3, 2)
barray([[4, 5],
[6, 7],
[8, 9]])
np.dot(a, b)array([40, 46])
a = np.arange(1, 3).reshape(2, 1)
aarray([[1],
[2]])
b = np.arange(4, 10).reshape(3, 2)
barray([[4, 5],
[6, 7],
[8, 9]])
np.dot(b, a)array([[14],
[20],
[26]])
Квадратные матрицы можно возводить в степень n т.е. умнажать сами на себя n раз:
a = np.arange(1, 5).reshape(2, 2)
aarray([[1, 2],
[3, 4]])
np.dot(a, a) # Равносильно a**2array([[ 7, 10],
[15, 22]])
np.linalg.matrix_power(a, 2)array([[ 7, 10],
[15, 22]])
np.linalg.matrix_power(a, 0)array([[1, 0],
[0, 1]])
Довольно часто приходится вычислять ранг матриц:
a = np.arange(1, 10).reshape(3, 3)
aarray([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
np.linalg.matrix_rank(a)2
b = np.arange(1, 24, 2).reshape(3, 4)
barray([[ 1, 3, 5, 7],
[ 9, 11, 13, 15],
[17, 19, 21, 23]])
np.linalg.matrix_rank(b)2
Еще чаще приходится вычислять определитель матриц, хотя результат вас может немного удивить:
a = np.array([[1, 3], [4, 3]])
aarray([[1, 3],
[4, 3]])
np.linalg.det(a)-8.999999999999998
1*3 - 3*4 # Результат должен быть целым числом-9
В данном случае, из-за двоичной арифметики, результат нецелое число и округлять до ближайшего целого придется вручную.
Это связано с тем, что алгоритм вычисления определителя использует LU-разложение - это намного быстрее, чем обычный алгоритм, но за скорость все же приходится немного заплатить ручным округлением (конечно, если таковое требуется):
np.linalg.det(a)-8.999999999999998
round(np.linalg.det(a))-9
b = np.arange(1, 48, 3).reshape(4, 4)
barray([[ 1, 4, 7, 10],
[13, 16, 19, 22],
[25, 28, 31, 34],
[37, 40, 43, 46]])
np.linalg.det(b)0.0
round(np.linalg.det(b))0
Транспонирование матриц:
aarray([[1, 3],
[4, 3]])
a.Tarray([[1, 4],
[3, 3]])
Вычисление обратных матриц:
aarray([[1, 3],
[4, 3]])
b = np.linalg.inv(a)
barray([[-0.33333333, 0.33333333],
[ 0.44444444, -0.11111111]])
np.dot(a, b)array([[1., 0.],
[0., 1.]])
Решение систем линейных уравнений:
# система из двух линейных уравнений:
# 1*x1 + 5*x2 = 11
# 2*x1 + 3*x2 = 8
a = np.array([[1, 5], [2, 3]])
b = np.array([11, 8])x = np.linalg.solve(a, b)
xarray([1., 2.])
np.dot(a, x)array([11., 8.])
Статистика
a = np.random.randint(20, size=(5, 5))
aarray([[ 2, 17, 7, 6, 4],
[ 2, 2, 1, 7, 13],
[17, 13, 0, 4, 15],
[11, 19, 12, 12, 14],
[13, 1, 12, 19, 2]])
np.amin(a) # Минимальный элемент массива0
np.amax(a) # максимальный элемент19
np.amin(a, axis=0) # минимальный элемент вдоль первой оси (столбцы)array([2, 1, 0, 4, 2])
np.amin(a, axis=1) # минимальный элемент вдоль второй оси (строки)array([ 2, 1, 0, 11, 1])
# Процентили:
np.percentile(a, 25)2.0
np.percentile(a, 50)11.0
np.percentile(a, 75)13.0
Средние значения элементов массива и их отклонения:
a = np.random.randint(13, size=(5, 5))
aarray([[ 2, 12, 0, 4, 12],
[ 5, 0, 10, 1, 1],
[ 5, 4, 3, 4, 12],
[12, 3, 10, 7, 5],
[ 6, 5, 3, 0, 4]])
np.median(a) # медиана элементов массива4.0
np.mean(a) # среднее арифметическое5.2
np.var(a) # дисперсия15.28
np.std(a) # стандартное отклонение3.9089640571384128
Корреляционные коэфициенты и ковариационные матрицы величин:
x = np.array([1, 4, 3, 7, 10, 8, 14, 21, 20, 23])
y = np.array([4, 1, 6, 9, 13, 11, 16, 19, 15, 22])
z = np.array([29, 22, 24, 20, 18, 14, 16, 11, 9, 10])# Линейный коэфициент корреляции Пирсона
# величин 'x' и 'y'
XY = np.stack((x, y), axis=0)
XYarray([[ 1, 4, 3, 7, 10, 8, 14, 21, 20, 23],
[ 4, 1, 6, 9, 13, 11, 16, 19, 15, 22]])
np.corrcoef(XY)array([[1. , 0.93158099],
[0.93158099, 1. ]])
# Кросс-корреляции:
np.correlate(x, y)array([1736])
np.correlate(x, z)array([1486])
# Ковариационные матрицы:
np.cov(XY)array([[63.65555556, 49.82222222],
[49.82222222, 44.93333333]])
np.cov(x)array(63.65555556)
Так же NumPy предоставляет функции для вычисления гистограмм наборов данных различной размерности и некоторые другие статистичские функции.
Генерация случайных значений
Получение простых случайных данных:
np.random.rand() # случайное число от 0 до 10.17448219339797233
np.random.rand(10) # одномерный массив случайных значенийarray([0.70252469, 0.67976413, 0.33093102, 0.71896991, 0.50738282,
0.85770526, 0.54996816, 0.20724896, 0.69961605, 0.72648606])
np.random.rand(3, 4) # двумерный массив случайных значенийarray([[0.09784915, 0.92849649, 0.57973627, 0.10194581],
[0.21016446, 0.24861077, 0.3003258 , 0.1775901 ],
[0.98115457, 0.64760134, 0.83332208, 0.09634793]])
np.random.randn(10) # случайные значения с нормальным распределениемarray([-0.18961292, -0.92561458, 0.04817766, -0.12435168, 0.76708842,
-0.14950132, 1.56869699, -0.19216652, 0.73780653, -0.90962697])
np.random.randint(10) # случайное целое число от 0 до 100
np.random.randint(10, 100) # случайное целое число от 10 до 10079
np.random.randint(10, size=7) # одномерный массив случайных целых чиселarray([9, 0, 3, 4, 7, 3, 4])
np.random.randint(10, size=(4, 4)) # двумерный массив случайных целых чиселarray([[9, 8, 4, 1],
[2, 5, 2, 8],
[6, 7, 4, 6],
[4, 0, 3, 2]])
Перестановки:
x = np.arange(7)
xarray([0, 1, 2, 3, 4, 5, 6])
np.random.shuffle(x) # перетасовывает содержимое массива
xarray([3, 4, 2, 6, 1, 0, 5])
np.random.permutation(7) # выполняет тоже самое, не требуя входного массиваarray([2, 5, 0, 6, 3, 4, 1])
y = np.arange(9).reshape(3, 3)
yarray([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
np.random.shuffle(y) # перестановки выполняются только по 1-й осиNumPy предоставляет порядка 30 функций, позволяющих генерировать случайные числа с самыми разными вероятностными распределениями:
np.random.beta(0.1, 0.6, size=5) # бета распределениеarray([2.84742109e-02, 1.01824328e-17, 9.25048785e-04, 3.64882719e-01,
3.25493988e-01])
np.random.gamma(shape=0.8, scale=1.7, size=5) # гамма распределениеarray([0.10021983, 0.16943748, 0.05614188, 0.55054814, 0.86767598])
np.random.pareto(3.5, size=5) # Паретто распределениеarray([1.46231788, 0.46808183, 0.1089563 , 1.08172899, 0.04363823])
np.random.chisquare(2.2, size=5) # хи-квадрат распределениеarray([ 0.21156785, 4.38347729, 10.16896532, 0.12034952, 4.05578084])
Вы так же имеете доступ к состоянию генератора случайных чисел, а так же можете управлять им:
#np.random.get_state() # Вы можете узнать состояние генератораnp.random.seed(123) # устанавливает состояние генератораnp.random.rand(5)array([0.69646919, 0.28613933, 0.22685145, 0.55131477, 0.71946897])
Множества
В случаях, когда массивы содержат очень много элементов, среди которых есть одинаковые, возникает задача поиска уникальных элементов, количества их вхождений в исходный массив:
x = np.array([1, 2, 1, 2, 3, 1])
np.unique(x)array([1, 2, 3])
np.unique(x, return_counts=True) # количество вхождений(array([1, 2, 3]), array([3, 2, 1]))
В двумерных и многомерных массивах уникальные массивы можно искать, как по всему множеству его значений, так и по отдельным осям:
x = np.array([[0, 1, 1, 2], [0, 1, 1, 2], [9, 1, 1, 2]])
xarray([[0, 1, 1, 2],
[0, 1, 1, 2],
[9, 1, 1, 2]])
np.unique(x, axis=0) # множество уникальных строкarray([[0, 1, 1, 2],
[9, 1, 1, 2]])
np.unique(x, axis=1) # множество уникальных столбцовarray([[0, 1, 2],
[0, 1, 2],
[9, 1, 2]])
Так же имеется ряд других полезных функций:
X = np.array([0, 2, 4, 6, 8])
Y = np.array([0, 3, 4, 6, 7])np.in1d(X, Y) # наличие элементов из X среди элементов Yarray([ True, False, True, True, False])
np.intersect1d(X, Y) # пересечение множеств элементов массивовarray([0, 4, 6])
np.setdiff1d(X, Y) # разность множествarray([2, 8])
np.setxor1d(X, Y) # симметрическая разность (исключающее или)array([2, 3, 7, 8])
np.union1d(X, Y) # объединение множествarray([0, 2, 3, 4, 6, 7, 8])
Логические операции
Логические функции NumPy, условно, можно разделить на два множества: первое - позволяет определять специфику элементов массива и самого массива; второе - обычные логические операции, которые действуют над массивами поэлементно.
Иногда, возникает потребность определить тип элементов:
A = np.array([-np.inf, np.inf, np.nan, -1, 0, 1, 1.47, 2, 3 + 2j])np.isfinite(A) # Все ли элементы в A числа?array([False, False, False, True, True, True, True, True, True])
np.isinf(A) # Есть ли в A бесконечности?array([ True, True, False, False, False, False, False, False, False])
np.isnan(A) # Есть ли в A значения nan?array([False, False, True, False, False, False, False, False, False])
np.iscomplex(A) # есть ли в A комплексные числа?array([False, False, False, False, False, False, False, False, True])
np.isreal(A) # Есть ли в A вещественные числа?array([ True, True, True, True, True, True, True, True, False])
Привычные нам логические операции выполняются над массивами булевых значений (массивы из значений True и False):
X = np.array([True, False, True, False])
Y = np.array([True, True, False, False])np.logical_and(X, Y) # логическое "И"array([ True, False, False, False])
np.logical_or(X, Y) # логическое "ИЛИ"array([ True, True, True, False])
np.logical_not(X) # логическое "НЕ"array([False, True, False, True])
np.logical_xor(X, Y) # исключающее "ИЛИ"array([False, True, True, False])
Помимо всего прочего, NumPy позволяет производить различные сравнения:
np.allclose([1, 2, 3], [1, 2, 2.99999]) # Являются ли значения массивов близкими?True
x = np.random.randint(4, size=5)
xarray([3, 1, 2, 1, 0])
y = np.random.randint(4, size=5)
yarray([1, 2, 3, 1, 0])
np.greater(x, y) # поэлементное x > yarray([ True, False, False, False, False])
np.less(x, y) # поэлементное x < yarray([False, True, True, False, False])
np.equal(x, y) # поэлементное x == yarray([False, False, False, True, True])