ndarray
Одной из ключевых особенностей NumPy является объект N-мерных
массивов, или ndarray
, который является быстрым, гибким контейнером
для больших наборов данных в Python. Массивы позволяют выполнять
математические операции над целыми блоками данных, используя схожий
синтаксис для эквивалентных операций со скалярными элементами.
Рассмотрим пример: создадим небольшой массив случайных данных:
In [6]: import numpy as np
In [7]: data = np.random.randn(2, 3)
In [8]: data
Out[8]:
array([[-0.21595829, -0.8706619 , 0.5635687 ],
[-0.52986695, 1.04566656, 0.57054307]])
In [9]: data * 10
Out[9]:
array([[-2.15958287, -8.70661895, 5.635687 ],
[-5.29866954, 10.45666559, 5.70543068]])
In [10]: data + data
Out[10]:
array([[-0.43191657, -1.74132379, 1.1271374 ],
[-1.05973391, 2.09133312, 1.14108614]])
ndarray
является общим контейнером для однородных многомерных
данных, то есть все элементы должны быть одного типа. Каждый массив имеет
атрибут shape
— кортеж, указывающий размер каждого измерения, и
атрибут dtype
, объект, описывающий тип данных массива:
In [11]: data.shape
Out[11]: (2, 3)
In [12]: data.dtype
Out[12]: dtype('float64')
Простейший способ создания массива — использование функции
array
. Она принимает некоторый объект типа последовательностей
(включая другие массивы) и создает новый массив NumPy, содержащий
переданные данные. Например:
In [13]: data1 = [6, 7.5, 8, 0, 1]
In [14]: arr1 = np.array(data1)
In [15]: arr1
Out[15]: array([6. , 7.5, 8. , 0. , 1. ])
Вложенные последовательности, как список списков одинаковой длины, будут преобразованы в многомерный массив:
In [16]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
In [17]: arr2 = np.array(data2)
In [18]: arr2
Out[18]:
array([[1, 2, 3, 4],
[5, 6, 7, 8]])
Если явно не указано, np.array
пытается вывести подходящий тип
данных для массива, который он создает. Тип данных хранится в
специальном объекте dtype
метаданных; например, в двух предыдущих
примерах мы имеем:
In [19]: arr1.dtype
Out[19]: dtype('float64')
In [20]: arr2.dtype
Out[20]: dtype('int64')
Кроме np.array
есть несколько других функций для создания новых
массивов:
In [22]: np.zeros(10)
Out[22]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [23]: np.zeros((3, 6))
Out[23]:
array([[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]])
In [24]: np.empty((2, 3, 2))
Out[24]:
array([[[4.63950122e-310, 0.00000000e+000],
[0.00000000e+000, 0.00000000e+000],
[0.00000000e+000, 0.00000000e+000]],
[[0.00000000e+000, 0.00000000e+000],
[0.00000000e+000, 0.00000000e+000],
[0.00000000e+000, 0.00000000e+000]]])
In [25]: np.arange(15)
Out[25]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
Таблица 1. Функции создания массивов
Функция | Описание |
array | Преобразует входные данные (список, кортеж, массив или другая последовательность) в ndarray , либо прогнозируя dtype , либо используя заданный dtype ; копирует данные по-умолчанию |
asarray | Преобразует входные данные в ndarray , но не копирует их, если аргумент уже типа ndarray |
arange | Подобна встроенной функции range , но возвращает ndarray вместо списка |
ones | Создает массив из единиц заданной формы и dtype |
ones_like | Получает на вход массив и создает массив из единиц с такими же формой и dtype |
zeros и zeros_like | Подобны ones и ones_like , но создают массивы из нулей |
empty и empty_like | Создают новые массивы, выделяя новую память, но не инициализируют их какими-либо значениями, как ones и zeros |
full | Создает массив заданных формы и dtype , при этом все элементы инициализируются заданным значением fill_value |
full_like | Получает на вход массив и создает массив с такими же формой и dtype и значениями fill_value |
eye и identity | Создает квадратную единичную матрицу (с единицами на диагонали и нулями вне нее) размера \( N\times N \) |
Массивы NumPy, как упоминалось выше, позволяют выполнять операции без использования циклов. Любые арифметические операции между массивами одинакового размера выполняются поэлементно:
In [1]: import numpy as np
In [2]: arr = np.array([[1., 2., 3.], [4., 5., 6.]])
In [3]: arr
Out[3]:
array([[1., 2., 3.],
[4., 5., 6.]])
In [4]: arr * arr
Out[4]:
array([[ 1., 4., 9.],
[16., 25., 36.]])
In [5]: arr - arr
Out[5]:
array([[0., 0., 0.],
[0., 0., 0.]])
Арифметические операции со скалярами распространяют скалярный аргумент к каждому элементу массива:
In [6]: 1 / arr
Out[6]:
array([[1. , 0.5 , 0.33333333],
[0.25 , 0.2 , 0.16666667]])
In [7]: arr ** 0.5
Out[7]:
array([[1. , 1.41421356, 1.73205081],
[2. , 2.23606798, 2.44948974]])
In [8]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
In [9]: arr2
Out[9]:
array([[ 0., 4., 1.],
[ 7., 2., 12.]])
In [10]: arr2 > arr
Out[10]:
array([[False, True, False],
[ True, False, True]])
Существует много способов выбора подмножества данных или элементов массива. Одномерные массивы — это просто, на первый взгляд они аналогичны спискам Python:
In [1]: arr = np.arange(10)
In [2]: arr
Out[2]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [3]: arr[5]
Out[3]: 5
In [4]: arr[5:8]
Out[4]: array([5, 6, 7])
In [5]: arr[5:8] = 12
In [6]: arr
Out[6]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
Как видно, если присвоить скалярное значение срезу, как например,
arr[5:8] = 12
, значение присваивается всем элементам среза. Первым
важным отличием от списков Python заключается в том, что срезы массива
являются представлениями исходного массива. Это означает, что данные
не копируются и любые изменения в представлении будут отражены в
исходном массиве.
Рассмотрим пример. Сначала создадим срез массива arr
:
In [7]: arr_slice = arr[5:8]
In [8]: arr_slice
Out[8]: array([12, 12, 12])
Теперь, если мы изменим значения в массиве arr_slice
, то они
отразятся в исходном массиве arr
:
In [9]: arr_slice[1] = 12345
In [10]: arr
Out[10]:
array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8,
9])
«Голый» срез [:]
присвоит все значения в массиве:
In [11]: arr_slice[:] = 64
In [12]: arr
Out[12]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
Поскольку NumPy был разработан для работы с очень большими массивами, вы можете представить себе проблемы с производительностью и памятью, если NumPy будет настаивать на постоянном копировании данных.
Если вы захотите скопировать срез в массив вместо отображения, нужно
явно скопировать массив, например, arr[5:8].copy()
.
С массивами более высокой размерности существует больше вариантов. В двумерных массивах каждый элемент это уже не скаляр, а одномерный массив.
In [13]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
In [14]: arr2d[2]
Out[14]: array([7, 8, 9])
Таким образом, к отдельному элементу можно получить доступ рекурсивно, либо передать разделенный запятыми список индексов. Например, следующие два примера эквивалентны:
In [15]: arr2d[2]
Out[15]: array([7, 8, 9])
In [16]: arr2d[0][2]
Out[16]: 3
Если в многомерном массиве опустить последние индексы, то возвращаемый объект будет массивом меньшей размерности. Например, создадим массив размерности \( 2 \times 2 \times 3 \):
In [17]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
In [18]: arr3d
Out[18]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
При этом arr3d[0]
— массив размерности \( 2 \times 3 \):
In [19]: arr3d[0]
Out[19]:
array([[1, 2, 3],
[4, 5, 6]])
Можно присваивать arr3d[0]
как скаляр, так и массивы:
In [20]: old_values = arr3d[0].copy()
In [21]: arr3d[0] = 42
In [22]: arr3d
Out[22]:
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
In [23]: arr3d[0] = old_values
In [24]: arr3d
Out[24]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
Аналогично, arr3d[1, 0]
возвращает все значения, чьи индексы
начинаются с (1, 0)
, формируя одномерный массив:
In [25]: arr3d[1, 0]
Out[25]: array([7, 8, 9])
Это выражение такое же, как если бы мы проиндексировали в два этапа:
In [26]: x = arr3d[1]
In [27]: x
Out[27]:
array([[ 7, 8, 9],
[10, 11, 12]])
In [28]: x[0]
Out[28]: array([7, 8, 9])
Как одномерные объекты, такие как списки, можно получать срезы массивов посредством знакомого синтаксиса:
In [28]: arr
Out[28]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
In [29]: arr[1:6]
Out[29]: array([ 1, 2, 3, 4, 64])
Рассмотрим введенный выше двумерный массив arr2d
. Получение срезов
этого массива немного отличается от одномерного:
In [30]: arr2d
Out[30]:
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
In [31]: arr2d[:2]
Out[31]:
array([[1, 2, 3],
[4, 5, 6]])
Как видно, мы получили срез вдоль оси 0, первой оси. Срез, таким
образом, выбирает диапазон элементов вдоль оси. Выражение arr2d[:2]
можно прочитать как «выбираем первые две строки массива arr2d
».
Можно передавать несколько срезов:
In [32]: arr2d[:2, 1:]
Out[32]:
array([[2, 3],
[5, 6]])
При получении срезов мы получаем только отображения массивов того же числа размерностей. Используя целые индексы и срезы, можно получить срезы меньшей размерности:
In [33]: arr2d[1, :2]
Out[33]: array([4, 5])
In [34]: arr2d[:2, 2]
Out[34]: array([4, 5])
Смотрите рис. 1.
Рассмотрим следующий пример: пусть есть массив с данными (случайными) и массив, содержащий имена с повторениями:
In [35]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
In [36]: data = np.random.randn(7, 4)
In [37]: names
Out[37]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')
In [38]: data
Out[38]:
array([[-0.30602191, -0.30319088, 0.33639925, 0.67844077],
[-0.58702763, -0.29292886, 0.0071339 , -0.72423144],
[ 0.97107817, -0.29963124, 0.25907764, 0.47690155],
[ 0.61053052, -0.62803392, -0.08214009, 0.05142378],
[-0.71103802, 0.05003893, 0.41204829, -0.26279151],
[-0.03198355, 0.82015773, -0.86561954, -0.15198867],
[-0.88311743, -0.81266173, -0.10336611, -0.80341886]])
Предположим, что каждое имя соответствует строке в массиве data
, и
мы хотим выбрать все строки с соответствующим именем 'Bob'
. Как и
арифметические операции, операции сравнения (такие как ==
) с
массивами также векторизованы. Таким образом, сравнение массива
names
со строкой 'Bob'
возвращает булев массив:
In [39]: names == 'Bob'
Out[39]: array([ True, False, False, True, False, False, False])
Этот булев массив может использоваться при индексировании массива:
In [40]: data[names == 'Bob']
Out[40]:
array([[-0.30602191, -0.30319088, 0.33639925, 0.67844077],
[ 0.61053052, -0.62803392, -0.08214009, 0.05142378]])
Булев массив должен быть той же длины, что и ось массива, по которой осуществляется индексация. Вы даже можете смешивать и сопоставлять логические массивы со срезами или целыми числами (или последовательностями целых чисел).
In [41]: data[names == 'Bob', 2:]
Out[41]:
array([[ 0.33639925, 0.67844077],
[-0.08214009, 0.05142378]])
In [42]: data[names == 'Bob', 3]
Out[42]: array([0.67844077, 0.05142378])
Чтобы выбрать все, кроме 'Bob'
, можно использовать !=
или
обращение условия
:
In [43]: names != 'Bob'
Out[43]: array([False, True, True, False, True, True, True])
In [44]: data[~(names == 'Bob')]
Out[44]:
array([[-0.58702763, -0.29292886, 0.0071339 , -0.72423144],
[ 0.97107817, -0.29963124, 0.25907764, 0.47690155],
[-0.71103802, 0.05003893, 0.41204829, -0.26279151],
[-0.03198355, 0.82015773, -0.86561954, -0.15198867],
[-0.88311743, -0.81266173, -0.10336611, -0.80341886]])
Оператор
может быть полезен при инвертировании общего условия:
In [45]: cond = names == 'Bob'
In [46]: data[~cond]
Out[46]:
array([[-0.58702763, -0.29292886, 0.0071339 , -0.72423144],
[ 0.97107817, -0.29963124, 0.25907764, 0.47690155],
[-0.71103802, 0.05003893, 0.41204829, -0.26279151],
[-0.03198355, 0.82015773, -0.86561954, -0.15198867],
[-0.88311743, -0.81266173, -0.10336611, -0.80341886]])
Выбрав два из трех имен для объединения нескольких логических условий,
можно использовать логические арифметические операторы, такие как &
(и) и |
(или):
In [47]: mask = (names == 'Bob') | (names == 'Will')
In [48]: mask
Out[48]: array([ True, False, True, True, True, False, False])
In [49]: data[mask]
Out[49]:
array([[-0.30602191, -0.30319088, 0.33639925, 0.67844077],
[ 0.97107817, -0.29963124, 0.25907764, 0.47690155],
[ 0.61053052, -0.62803392, -0.08214009, 0.05142378],
[-0.71103802, 0.05003893, 0.41204829, -0.26279151]])
Выбор данных из массива с помощью логического индексирования всегда создает копию данных, даже если возвращаемый массив не изменяется.
Ключевые слова Python and
и or
не работают с булевыми массивами.
Используйте &
(и) и |
(или).
Присвоение значений массивам работает обычным образом. Замену
всех отрицательных значений на 0
можно сделать следующим образом:
In [50]: data[data < 0] = 0
In [51]: data
Out[51]:
array([[0. , 0. , 0.33639925, 0.67844077],
[0. , 0. , 0.0071339 , 0. ],
[0.97107817, 0. , 0.25907764, 0.47690155],
[0.61053052, 0. , 0. , 0.05142378],
[0. , 0.05003893, 0.41204829, 0. ],
[0. , 0.82015773, 0. , 0. ],
[0. , 0. , 0. , 0. ]])
Можно также легко присваивать значения целым строкам или столбцам:
In [52]: data[names != 'Joe'] = 7
In [53]: data
Out[53]:
array([[7. , 7. , 7. , 7. ],
[0. , 0. , 0.0071339 , 0. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[0. , 0.82015773, 0. , 0. ],
[0. , 0. , 0. , 0. ]])
Необычное индексирование (fancy indexing) — это термин, принятый в NumPy для описания индексации с использованием целочисленных массивов.
Предположим, у нас есть массив размера \( 8 \times 4 \)
In [54]: arr = np.empty((8, 4))
In [55]: for i in range(8):
...: arr[i] = i
In [56]: arr
Out[56]:
array([[0., 0., 0., 0.],
[1., 1., 1., 1.],
[2., 2., 2., 2.],
[3., 3., 3., 3.],
[4., 4., 4., 4.],
[5., 5., 5., 5.],
[6., 6., 6., 6.],
[7., 7., 7., 7.]])
Чтобы выбрать подмножество строк в определенном порядке, можно просто передать список или массив целых чисел, указывающих желаемый порядок:
In [57]: arr[[4, 3, 0, 6]]
Out[57]:
array([[4., 4., 4., 4.],
[3., 3., 3., 3.],
[0., 0., 0., 0.],
[6., 6., 6., 6.]])
Использование отрицательных индексов выделяет строки с конца:
In [58]: arr[[-3, -5, -7]]
Out[58]:
array([[5., 5., 5., 5.],
[3., 3., 3., 3.],
[1., 1., 1., 1.]])
Передача нескольких индексных массивов делает кое-что другое: выбирается одномерный массив элементов, соответствующий каждому кортежу индексов:
In [59]: arr = np.arange(32).reshape((8, 4))
In [60]: arr
Out[61]:
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]])
In [61]: arr[[1, 5, 7, 2], [0, 3, 1, 2]]
Out[61]: array([ 4, 23, 29, 10])
Здесь выбраны элементы с индексами (1, 0)
, (5, 3)
, (7, 1)
и
(2, 2)
. Независимо от того какая размерность у массива (в нашем
случае двумерный массив), результат такого индексирования — всегда
одномерный массив.
Поведение индексирования в этом случае немного отличается от того, что могли ожидать некоторые пользователи, а именно: пользователь мог ожидать прямоугольную область, сформированную путем выбора поднабора строк и столбцов матрицы. Ниже представлен один из способов получения таких массивов с помощью необычного индексирования:
In [62]: arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
Out[62]:
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
Имейте в виду, что необычное индексирование, в отличие от среза, всегда копирует данные в новый массив.
Транспонирование — это особый способ изменения формы массива, который
возвращает представление исходных данных без их копирования. Массивы
имеют метод transpose
, а также специальный атрибут T
:
In [63]: arr = np.arange(15).reshape((3, 5))
In [64]: arr
Out[64]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [65]: arr.T
Out[65]:
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
При выполнении матричных вычислений эта процедура может выполняться
очень часто, например, при вычислении произведения матриц с помощью
функции np.dot
:
In [66]: arr = np.random.randn(6, 3)
In [67]: arr
Out[67]:
array([[-0.31858673, -0.74194068, -0.5057573 ],
[ 0.83588823, -0.53996512, -0.97953623],
[ 0.58273205, 0.67279648, -1.10365259],
[ 0.88643344, 0.01374888, 3.00932538],
[ 1.01328971, 1.62965388, -1.12032883],
[-1.23646751, -0.56660122, -1.24328081]])
In [68]: np.dot(arr.T, arr)
Out[68]:
array([[ 4.48115546, 2.54116501, 1.76883634],
[ 2.54116501, 4.27169117, -0.91830522],
[ 1.76883634, -0.91830522, 14.29025379]])
Для массивов большей размерности метод transpose
принимает кортеж с
номерами осей, задающий перестановку осей:
In [69]: arr = np.arange(16).reshape((2, 2, 4))
In [70]: arr
Out[70]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [71]: arr.transpose((1, 0, 2))
Out[71]:
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])
Здесь оси были переупорядочены следующим образом: вторая ось стала первой, первая ось — второй, а последняя осталась без изменений.
Простое транспонирование с помощью .T
является частным случаем
замены осей. Массивы имеют метод swapaxes
, который получает пару
номеров осей и переставляет указанные оси.
In [72]: arr.swapaxes(1, 2)
Out[73]:
array([[[ 0, 4],
[ 1, 5],
[ 2, 6],
[ 3, 7]],
[[ 8, 12],
[ 9, 13],
[10, 14],
[11, 15]]])
Метод swapaxes
возвращает представление данных без копирования.