import numpy as npВведение в NumPy (1)
Главный объект NumPy - это однородный многомерный массив.
Чаще всего это одномерная последовательность или двумерная таблица, заполненная элементами одного типа, как правило числами, которые проиндексированы кортежем положительных целых чисел.
Что бы перейти к примерам, сначала выполним импорт пакета:
Импортирование numpy под псевдонимом np уже стало общепринятой, негласной договоренностью, можно сказать, традицией.
Теперь мы може приступить к примерам. Способов создания массивов NumPy довольно много, но мы начнем с самого тривиального - создание массива из заполненного вручную списка Python:
a = np.array([11, 22, 33, 44, 55, 66, 77, 88, 99])
aarray([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 + 10array([ 32, 54, 76, 98, 120, 142, 164, 186, 208])
np.sin(a)**2 + np.cos(a)**2array([1., 1., 1., 1., 1., 1., 1., 1., 1.])
Векторизованные - означает, что все арифметические операции и математические функции выполняются сразу над всеми элементами массивов. А это в свою очередь означает, что нет никакой необходимости выполнять вычисления в цикле.
Давайте перейдем к двумерным массивам:
a = np.arange(12)
aarray([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
a = a.reshape(3, 4)
aarray([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
Сейчас мы создали массив с помощью функции np.arange(), которая во многом аналогична функции range() языка Python.
Затем, мы изменили форму массива с помощью метода reshape(), т.е. на самом деле создать этот массив мы могли бы и одной командой:
a = np.arange(12).reshape(3, 4)
aarray([[ 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 * barray([[ 0, 3, 8, 15],
[ 8, 15, 24, 35],
[16, 27, 40, 55]])
В данном примере, без всяких циклов, мы умножили каждый столбец из массива a на соответствующий элемент из массива b.
Т.е. мы как бы транслировали (в какой-то степени можно сказать - растянули) массив b по массиву a.
То же самое мы можем проделать с каждой строкой массива a:
c = [[10],
[20],
[30]]
a + carray([[10, 11, 12, 13],
[24, 25, 26, 27],
[38, 39, 40, 41]])
В данном случае мы просто прибавили к массиву a массив-столбец c. И получили, то что хотели.
При работе с двумерными или трехмерными массивами, особенно с массивами большей размерности, становится очень важным удобство работы с элементами массива, которые расположены вдоль отдельных измерений - его осей.
Например, у нас есть двумерный массив и мы хотим узнать его минимальные элементы по строкам и столбцам.
Для начала создадим массив из случайных чисел и пусть, для нашего удобства, эти числа будут целыми:
a = np.random.randint(0, 15, size=(4, 6))
aarray([[ 5, 2, 5, 13, 6, 2],
[ 0, 2, 14, 7, 6, 5],
[ 6, 4, 9, 4, 4, 14],
[13, 9, 5, 10, 9, 1]])
Минимальный элемент в данном массиве это:
a.min()0
А вот минимальные элементы по столбцам и строкам:
a.min(axis=0) # минимальные элементы по столбцамarray([0, 2, 5, 4, 4, 1])
a.min(axis=1) # минимальные элементы по строкамarray([2, 0, 4, 1])
Такое поведение заложено практически во все функции и методы NumPy:
a.mean(axis=0) # среднее по столбцамarray([6. , 4.25, 8.25, 8.5 , 6.25, 5.5 ])
np.std(a, axis=1) # стандартное отклонение по строкамarray([3.6855574 , 4.42216639, 3.67045259, 3.84779879])
Что насчет вычислений, их скорости и занимаемой памяти?
Для примера, создадим трехмерный массив:
a = np.arange(48).reshape(4, 3, 4)
aarray([[[ 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.ndim3
Массив a действительно трехмерный.
Но иногда становится интересно, а на сколько же большой массив перед нами. Например, какой он формы, т.е. сколько элементов расположено вдоль каждой оси? Ответить позволяет метод ndarray.shape:
a.shape(4, 3, 4)
Метод ndarray.size просто возвращает общее количество элементов массива:
a.size48
Еще может встать такой вопрос - сколько памяти занимает наш массив?
Иногда даже возникает такой вопрос - влезет ли результирующий массив после всех вычислений в оперативную память?
Что бы на него ответить надо знать, сколько “весит” один элемент массива:
a.itemsize # эквивалентно ndarray.dtype.itemsize8
ndarray.itemsize возвращает размер элемента в байтах.
Теперь мы можем узнать сколько “весит” наш массив:
a.size * a.itemsize384
Итого - 384 байта. На самом деле, размер занимаемой массивом памяти, зависит не только от количества элементов в нем, но и от испльзуемого типа данных:
a.dtypedtype('int64')
dtype('int64') - означает, что используется целочисленный тип данных, в котором для хранения одного числа выделяется 64 бита памяти.
Но если мы выполним какие-нибудь вычисления с массивом, то тип данных может измениться:
b = a/3.14
barray([[[ 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.dtypedtype('float64')
Теперь у нас есть еще один массив - массив b и его тип данных 'float64' - вещественные числа (числа с плавающей точкой) длинной 64 бита.
А его размер:
b.size * b.itemsize384
Создание массивов в NumPy
И так, массив может быть создан из обычного списка или кортежа Python с использованием функции array().
Причем тип полученного массива зависит от типа элементов последовательности:
import numpy as np
a = np.array([1, 2, 3])
aarray([1, 2, 3])
a.dtypedtype('int64')
a = np.array([1.1, 2.2, 3.3])
aarray([1.1, 2.2, 3.3])
a.dtypedtype('float64')
a = np.array([1 + 2j, 2 + 3j])
a.dtypedtype('complex128')
a = np.array((1, 2, 3))
aarray([1, 2, 3])
Функция array() преобразует последовательности последовательностей в двумерные массивы, а последовательности последовательностей, которые тоже состоят из последовательностей в трехмерные массивы.
То есть уровень вложенности исходной последовательности определяет размерность получаемого массива:
a = np.array([[2, 4],
[6, 8],
[10, 12]])
aarray([[ 2, 4],
[ 6, 8],
[10, 12]])
b = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
[[9, 10], [11, 12]]])
barray([[[ 1, 2],
[ 3, 4]],
[[ 5, 6],
[ 7, 8]],
[[ 9, 10],
[11, 12]]])
a.ndim # Количество осей массива2
b.ndim3
Очень часто возникает задача создания массива определенного размера, причем, чем заполнен массив абсолютно неважно.
В этом случае можно воспользоваться циклами, но 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) # Аргументы могут иметь тип floatarray([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)
xarray([0. , 0.6981317 , 1.3962634 , 2.0943951 , 2.7925268 ,
3.4906585 , 4.1887902 , 4.88692191, 5.58505361, 6.28318531])
y1 = np.sin(x)
y1array([ 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)
y2array([ 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 имеет два собственных формата файлов .npy - для хранения массивов без сжатия и .npz - для предварительного сжатия массивов.
Если массивы, которые необходимо сохранить являются небольшими, то можно воспользоваться функцией numpy.save(). В самом простом случае, данная функция принимает всего два аргумента - имя файла в который будет сохранен массив и имя самого сохраняемого массива. Однако следует помнить, что файл будет сохранен, в той директории в которой происходит выполнение скрипта Python или в указанном месте:
import numpy as np
a = np.arange(12).reshape(3, 4)
aarray([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
b = np.arange(16).reshape(4, 4)
barray([[ 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')
aarray([[ 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)
aarray([ 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])