© Семён Лукашевский сайт автора
Главный объект NumPy - это однородный многомерный массив.
Чаще всего это одномерная последовательность или двумерная таблица, заполненная элементами одного типа, как правило числами, которые проиндексированы кортежем положительных целых чисел.
Что бы перейти к примерам, сначала выполним импорт пакета:
import numpy as np
Импортирование numpy под псевдонимом np
уже стало общепринятой, негласной договоренностью, можно сказать, традицией.
Теперь мы може приступить к примерам. Способов создания массивов NumPy довольно много, но мы начнем с самого тривиального - создание массива из заполненного вручную списка Python:
a = np.array([11, 22, 33, 44, 55, 66, 77, 88, 99])
a
array([11, 22, 33, 44, 55, 66, 77, 88, 99])
Теперь у нас есть одномерный массив, т.е. у него всего одна ось вдоль которой происходит индексирование его элементов.
a[2]
33
В общем-то, можно подумать, что ничего интересного и нет в этих массивах, но на самом деле это только начало кроличьей норы.
Оцените:
a[[7, 0, 3, 3, 3, 0, 7]]
array([88, 11, 44, 44, 44, 11, 88])
Вместо одного индекса, указан целый список индексов. А вот еще любопытный пример, теперь вместо индекса укажем логическое выражение:
a[a > 50]
array([55, 66, 77, 88, 99])
Цель этих двух примеров - не устраивать головоломку, а продемонстрировать расширенные возможности индексирования массивов NumPy.
Что еще интересного можно продемонстрировать? Векторизованные вычисления:
2*a + 10
array([ 32, 54, 76, 98, 120, 142, 164, 186, 208])
np.sin(a)**2 + np.cos(a)**2
array([1., 1., 1., 1., 1., 1., 1., 1., 1.])
Векторизованные - означает, что все арифметические операции и математические функции выполняются сразу над всеми элементами массивов. А это в свою очередь означает, что нет никакой необходимости выполнять вычисления в цикле.
Давайте перейдем к двумерным массивам:
a = np.arange(12)
a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
a = a.reshape(3, 4)
a
array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])
Сейчас мы создали массив с помощью функции np.arange()
, которая во многом аналогична функции range()
языка Python.
Затем, мы изменили форму массива с помощью метода reshape()
, т.е. на самом деле создать этот массив мы могли бы и одной командой:
a = np.arange(12).reshape(3, 4)
a
array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])
Визуально, данный массив выглядит следующим образом:
Глядя на картинку, становится понятно, что первая ось (и индекс соответственно) - это строки, вторая ось - это столбцы. Т.е. получить элемент 9 можно простой командой:
a[2][1] # равносильно команде a[2, 1]
9
Снова можно подумать, что ничего нового - все как в стандартном Python. Да, так и есть, и, это круто!
Еще круто, то что NumPy добавляет к удобному и привычному синтаксису Python, весьма удобные трюки, например - транслирование массивов:
b = [2, 3, 4, 5]
a * b
array([[ 0, 3, 8, 15], [ 8, 15, 24, 35], [16, 27, 40, 55]])
В данном примере, без всяких циклов, мы умножили каждый столбец из массива a
на соответствующий элемент из массива b
.
Т.е. мы как бы транслировали (в какой-то степени можно сказать - растянули) массив b
по массиву a
.
То же самое мы можем проделать с каждой строкой массива a
:
c = [[10],
[20],
[30]]
a + c
array([[10, 11, 12, 13], [24, 25, 26, 27], [38, 39, 40, 41]])
В данном случае мы просто прибавили к массиву a
массив-столбец c
. И получили, то что хотели.
При работе с двумерными или трехмерными массивами, особенно с массивами большей размерности, становится очень важным удобство работы с элементами массива, которые расположены вдоль отдельных измерений - его осей.
Например, у нас есть двумерный массив и мы хотим узнать его минимальные элементы по строкам и столбцам.
Для начала создадим массив из случайных чисел и пусть, для нашего удобства, эти числа будут целыми:
a = np.random.randint(0, 15, size=(4, 6))
a
array([[ 2, 1, 12, 2, 3, 6], [10, 2, 6, 4, 13, 12], [ 2, 9, 8, 10, 6, 0], [ 4, 14, 8, 3, 3, 8]])
Минимальный элемент в данном массиве это:
a.min()
0
А вот минимальные элементы по столбцам и строкам:
a.min(axis=0) # минимальные элементы по столбцам
array([2, 1, 6, 2, 3, 0])
a.min(axis=1) # минимальные элементы по строкам
array([1, 2, 0, 3])
Такое поведение заложено практически во все функции и методы NumPy:
a.mean(axis=0) # среднее по столбцам
array([4.5 , 6.5 , 8.5 , 4.75, 6.25, 6.5 ])
np.std(a, axis=1) # стандартное отклонение по строкам
array([3.77123617, 4.09945796, 3.67045259, 3.90156664])
Что насчет вычислений, их скорости и занимаемой памяти?
Для примера, создадим трехмерный массив:
a = np.arange(48).reshape(4, 3, 4)
a
array([[[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]], [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]], [[24, 25, 26, 27], [28, 29, 30, 31], [32, 33, 34, 35]], [[36, 37, 38, 39], [40, 41, 42, 43], [44, 45, 46, 47]]])
Почему именно трехмерный?
На самом деле реальный мир вовсе не ограничивается таблицами, векторами и матрицами.
Еще существуют тензоры, кватернионы, октавы. А некоторые данные, гораздо удобнее представлять именно в трехмерном и четырехмерном представлении:
Визуализация (и хорошее воображение) позволяет сразу догадаться, как устроена индексация трехмерных массивов. Например, если нам нужно вытащить из данного массива число 31, то достаточно выполнить:
a[2][1][3] # или a[2, 1, 3]
31
В самом деле, у массивов есть целый ряд важных атрибутов. Например, количество осей массива (его размерность), которую при работе с очень большими массивами, не всегда легко увидеть:
a.ndim
3
Массив a действительно трехмерный.
Но иногда становится интересно, а на сколько же большой массив перед нами. Например, какой он формы, т.е. сколько элементов расположено вдоль каждой оси?
Ответить позволяет метод ndarray.shape
:
a.shape
(4, 3, 4)
Метод ndarray.size
просто возвращает общее количество элементов массива:
a.size
48
Еще может встать такой вопрос - сколько памяти занимает наш массив?
Иногда даже возникает такой вопрос - влезет ли результирующий массив после всех вычислений в оперативную память?
Что бы на него ответить надо знать, сколько "весит" один элемент массива:
a.itemsize # эквивалентно ndarray.dtype.itemsize
8
ndarray.itemsize
возвращает размер элемента в байтах.
Теперь мы можем узнать сколько "весит" наш массив:
a.size * a.itemsize
384
Итого - 384 байта. На самом деле, размер занимаемой массивом памяти, зависит не только от количества элементов в нем, но и от испльзуемого типа данных:
a.dtype
dtype('int64')
dtype('int64')
- означает, что используется целочисленный тип данных, в котором для хранения одного числа выделяется 64 бита памяти.
Но если мы выполним какие-нибудь вычисления с массивом, то тип данных может измениться:
b = a/3.14
b
array([[[ 0. , 0.31847134, 0.63694268, 0.95541401], [ 1.27388535, 1.59235669, 1.91082803, 2.22929936], [ 2.5477707 , 2.86624204, 3.18471338, 3.50318471]], [[ 3.82165605, 4.14012739, 4.45859873, 4.77707006], [ 5.0955414 , 5.41401274, 5.73248408, 6.05095541], [ 6.36942675, 6.68789809, 7.00636943, 7.32484076]], [[ 7.6433121 , 7.96178344, 8.28025478, 8.59872611], [ 8.91719745, 9.23566879, 9.55414013, 9.87261146], [10.1910828 , 10.50955414, 10.82802548, 11.14649682]], [[11.46496815, 11.78343949, 12.10191083, 12.42038217], [12.7388535 , 13.05732484, 13.37579618, 13.69426752], [14.01273885, 14.33121019, 14.64968153, 14.96815287]]])
b.dtype
dtype('float64')
Теперь у нас есть еще один массив - массив b
и его тип данных 'float64'
- вещественные числа (числа с плавающей точкой) длинной 64 бита.
А его размер:
b.size * b.itemsize
384
И так, массив может быть создан из обычного списка или кортежа Python с использованием функции array()
.
Причем тип полученного массива зависит от типа элементов последовательности:
import numpy as np
a = np.array([1, 2, 3])
a
array([1, 2, 3])
a.dtype
dtype('int64')
a = np.array([1.1, 2.2, 3.3])
a
array([1.1, 2.2, 3.3])
a.dtype
dtype('float64')
a = np.array([1 + 2j, 2 + 3j])
a.dtype
dtype('complex128')
a = np.array((1, 2, 3))
a
array([1, 2, 3])
Функция array()
преобразует последовательности последовательностей в двумерные массивы, а последовательности последовательностей, которые тоже состоят из последовательностей в трехмерные массивы.
То есть уровень вложенности исходной последовательности определяет размерность получаемого массива:
a = np.array([[2, 4], [6, 8], [10, 12]])
a
array([[ 2, 4], [ 6, 8], [10, 12]])
b = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
[[9, 10], [11, 12]]])
b
array([[[ 1, 2], [ 3, 4]], [[ 5, 6], [ 7, 8]], [[ 9, 10], [11, 12]]])
a.ndim # Количество осей массива
2
b.ndim
3
Очень часто возникает задача создания массива определенного размера, причем, чем заполнен массив абсолютно неважно.
В этом случае можно воспользоваться циклами или генераторами списков (кортежей), но NumPy для таких случаев предлагает более быстрые и менее затратные функции-заполнители.
Функция zeros
заполняет массив нулями, функция ones
- единицами, а функция empty
- случайными числами, которые зависят от состояния памяти.
По умолчанию, тип создаваемого массива - float64
.
np.zeros((3,3))
array([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])
np.ones((3,3))
array([[1., 1., 1.], [1., 1., 1.], [1., 1., 1.]])
np.ones((3,3), dtype=complex) # Можно изменить тип массива
array([[1.+0.j, 1.+0.j, 1.+0.j], [1.+0.j, 1.+0.j, 1.+0.j], [1.+0.j, 1.+0.j, 1.+0.j]])
np.empty([3, 3])
array([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])
Для создания последовательностей чисел NumPy предоставляет функцию arange
, которая возвращает одномерные массивы:
np.arange(10) # От 0 до указанного числа
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.arange(10, 20) # Диапазон
array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
np.arange(20, 100, 10) # Диапазон с заданным шагом
array([20, 30, 40, 50, 60, 70, 80, 90])
np.arange(0, 1, 0.1) # Аргументы могут иметь тип float
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
Если функция arange
используется с аргументами типа float
, то предсказать количество элементов в возвращаемом массиве не так-то просто.
Гораздо чаще возникает необходимость указания не шага изменения чисел в диапазоне, а количества чисел в заданном диапазоне.
Функция linspace
, так же как и arange
принимает три аргумента, но третий аргумент, как раз и указывает количество чисел в диапазоне.
np.linspace(0, 1, 5)
array([0. , 0.25, 0.5 , 0.75, 1. ])
np.linspace(0, 1, 7)
array([0. , 0.16666667, 0.33333333, 0.5 , 0.66666667, 0.83333333, 1. ])
np.linspace(10, 100, 5)
array([ 10. , 32.5, 55. , 77.5, 100. ])
Функция linspace
удобна еще и тем, что может быть использована для вычисления значений функций на заданном множестве точек:
x = np.linspace(0, 2*np.pi, 10)
x
array([0. , 0.6981317 , 1.3962634 , 2.0943951 , 2.7925268 , 3.4906585 , 4.1887902 , 4.88692191, 5.58505361, 6.28318531])
y1 = np.sin(x)
y1
array([ 0.00000000e+00, 6.42787610e-01, 9.84807753e-01, 8.66025404e-01, 3.42020143e-01, -3.42020143e-01, -8.66025404e-01, -9.84807753e-01, -6.42787610e-01, -2.44929360e-16])
y2 = np.cos(x)
y2
array([ 1. , 0.76604444, 0.17364818, -0.5 , -0.93969262, -0.93969262, -0.5 , 0.17364818, 0.76604444, 1. ])
Чтобы быстрее разобраться с примерами печати массивов воспользуемся методом ndarray.reshape()
, который позволяет изменять размеры массивов.
Одномерные массивы в NumPy печатаются в виде строк:
a = np.arange(10) # Одномерный массив
print(a)
[0 1 2 3 4 5 6 7 8 9]
Двумерные массивы печатаются в виде матриц:
b = np.arange(16).reshape(4, 4) # Двумерный массив
print(b)
[[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11] [12 13 14 15]]
Трехмерные массивы печатаются в виде списка матриц, которые разделены пустой строкой:
c = np.arange(30).reshape(5, 2, 3) # Трехмерный массив
print(c)
[[[ 0 1 2] [ 3 4 5]] [[ 6 7 8] [ 9 10 11]] [[12 13 14] [15 16 17]] [[18 19 20] [21 22 23]] [[24 25 26] [27 28 29]]]
Можете поэкспериментировать с печатью массивов большей размерности и вы убедитесь, что в ней довольно легко ориентироваться.
В случае, если массив очень большой (больше 1000 элементов), NumPy печатает только начало и конец массива, заменяя его центральную часть многоточием.
print(np.arange(1001))
[ 0 1 2 ... 998 999 1000]
print(np.arange(1000000))
[ 0 1 2 ... 999997 999998 999999]
print(np.arange(1000000).reshape(1000,1000))
[[ 0 1 2 ... 997 998 999] [ 1000 1001 1002 ... 1997 1998 1999] [ 2000 2001 2002 ... 2997 2998 2999] ... [997000 997001 997002 ... 997997 997998 997999] [998000 998001 998002 ... 998997 998998 998999] [999000 999001 999002 ... 999997 999998 999999]]
Если необходимо выводить весь массив целиком, то такое поведение печати можно изменить с помощью set_printoptions
.
np.set_printoptions(threshold=np.nan)
Занимаясь научными вычислениями, вы получаете результаты, которые должны быть обязательно сохранены.
Самый надежный способ хранения - это загрузка массивов с результатами в файл, так как их легко хранить и передавать.
Для данных нужд, NumPy предоставляет очень удобные инструменты, позволяющие производить загрузку и выгрузку массивов в файлы различных форматов, а также производить их сжатие, необходимое для больших массивов.
NumPy имеет два собственных формата файлов .npy
- для хранения массивов без сжатия и .npz
- для предварительного сжатия массивов.
Если массивы, которые необходимо сохранить являются небольшими, то можно воспользоваться функцией numpy.save()
. В самом простом случае, данная функция принимает всего два аргумента - имя файла в который будет сохранен массив и имя самого сохраняемого массива. Однако следует помнить, что файл будет сохранен, в той директории в которой происходит выполнение скрипта Python или в указанном месте:
import numpy as np
a = np.arange(12).reshape(3, 4)
a
array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])
b = np.arange(16).reshape(4, 4)
b
array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11], [12, 13, 14, 15]])
# Файл сохранится в той же папке что и исполняемый скрипт
np.save('example_1', a)
После того как массив сохранен, его можно загрузить из файла с помощью функции numpy.load()
, указав в виде строки имя необходимого файла, если он находится в той же директории, что и выполняемый скрипт Python, или путь к нему, если он располагается в другом месте:
import numpy as np
a = np.load('example_1.npy')
a
array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])
Файлы .npy
удобны для хранения одного массива, если в одном файле нужно сохранить несколько массивов, то необходимо воспользоваться функцией numpy.savez()
, которая сохранит их в несжатом виде в файле NumPy с форматом .npz
.
a = np.array([1, 2, 3])
b = np.array([[1, 1], [0, 0]])
c = np.array([[1], [2], [3]])
np.savez('example_2', a, b, c)
После сохранения массивов в файл .npz
они могут быть загружены с помощью, уже знакомой нам функции numpy.load()
. Однако, имена массивов теперь изменились с a
, b
и c
на arr_0
, arr_1
и arr_2
соответственно:
ex_2 = np.load('example_2.npz')
ex_2.files
['arr_0', 'arr_1', 'arr_2']
ex_2['arr_0']
array([1, 2, 3])
ex_2['arr_1']
array([[1, 1], [0, 0]])
ex_2['arr_2']
array([[1], [2], [3]])
Что бы вместе с массивами сохранялись их оригинальные имена, необходимо в функции numpy.savez()
указывать их как ключи словарей Python:
np.savez('example_2', a=a, b=b, c=c)
ex_2 = np.load('example_2.npz')
ex_2.files
['a', 'b', 'c']
ex_2['a']
array([1, 2, 3])
В случае очень больших массивов можно воспользоваться функцией numpy.savez_compressed()
.
a = np.arange(100000)
a
array([ 0, 1, 2, ..., 99997, 99998, 99999])
# Файл example_3.npy занимает 400 кБ на диске:
np.save('example_3', a)
# файл example_3.npynpz занимает всего 139 кБ на диске:
np.savez_compressed('example_3', a)
На самом деле, файлы .npz
это просто zip-архив который содержит отдельные файлы .npy
для каждого массива.
После того как файл был загружен с помощью функции numpy.savez_compressed()
его так же легко загрузить с помощью функции numpy.load()
:
ex_3 = np.load('example_3.npz')
ex_3.files
['arr_0']
ex_3['arr_0']
array([ 0, 1, 2, ..., 99997, 99998, 99999])