Приложение A. NumPy для продвинутых: работа с массивами
В этом приложении мы подробно рассмотрим работу с библиотекой NumPy и её возможностями для работы с многомерными массивами. Мы рассмотрим внутреннюю структуру объектов ndarray, а также их типы данных. Кроме того, мы обсудим более сложные операции над массивами, такие как изменение формы массива и управление порядком элементов в памяти.
A.1. Внутренняя структура объектов ndarray
NumPy предоставляет мощный инструмент для работы с однородными данными — объект ndarray. Он позволяет интерпретировать данные как многомерные массивы. Тип данных (dtype) определяет способ интерпретации данных, например, как целые числа, числа с плавающей точкой или логические значения.
Одной из ключевых особенностей ndarray является то, что все объекты представляют собой «промежуточный вид» на блок данных (strided view). Это позволяет выполнять различные операции над данными без копирования. Например, массив arr[::2,::-1] не копирует данные, потому что ndarray использует информацию о шагах (stride) для перемещения по памяти.
Внутренняя структура ndarray включает:
На рисунке A-1 показана упрощённая схема внутренней структуры ndarray.
Например, массив 10×5 будет иметь форму (10, 5):
In [10]: np.ones((10, 5)).shape
Out[10]: (10, 5)
Массив 3×4×5 с типом float64 и шагом (160, 40, 8) будет выглядеть так:
In [11]: np.ones((3, 4, 5), dtype=np.float64).strides
Out[11]: (160, 40, 8)
Шаги могут быть отрицательными, что позволяет массиву перемещаться в обратном направлении в памяти, например, при использовании срезов obj[::-1] или obj[:,::-1].
A.2. Типы данных NumPy
Иногда может потребоваться проверить тип данных в массиве, например, чтобы узнать, содержит ли он целые числа, числа с плавающей точкой, строки или объекты Python. В NumPy есть суперклассы для всех типов данных, такие как np.integer и np.floating. Их можно использовать вместе с функцией issubdtype:
In [12]: ints = np.ones(10, dtype=np.uint16)
In [13]: floats = np.ones(10, dtype=np.float32)
In [14]: np.issubdtype(ints.dtype, np.integer)
Out[14]: True
In [15]: np.issubdtype(floats.dtype, np.floating)
Out[15]: True
Функция mro позволяет увидеть все родительские классы типа данных:
In [16]: np.float64.mro()
Out[16]:
[numpy.float64,
numpy.floating,
numpy.inexact,
numpy.number,
numpy.generic,
float,
object]
Большинство пользователей NumPy редко сталкиваются с необходимостью разбираться во внутренней структуре массивов и типах данных. Однако эти знания могут пригодиться в некоторых случаях. На рисунке A-2 представлена иерархия типов данных NumPy.
A.3. Продвинутые операции с массивами
Помимо базовых операций, таких как индексация, срезы и булевы условия, NumPy предлагает множество других возможностей для работы с массивами. Хотя pandas предоставляет мощные инструменты для анализа данных, иногда требуется написать собственные алгоритмы обработки данных.
Изменение формы массива
Обычно можно изменить форму массива без копирования данных. Для этого используется метод reshape, который принимает кортеж с новой формой массива. Например, одномерный массив можно преобразовать в матрицу:
In [18]: arr = np.arange(8)
In [19]: arr
Out[19]: array([0, 1, 2, 3, 4, 5, 6, 7])
In [20]: arr.reshape((4, 2))
Out[20]:
array([[0, 1],
[2, 3],
[4, 5],
[6, 7]])
Многомерные массивы также могут быть преобразованы:
In [21]: arr.reshape((4, 2)).reshape((2, 4))
Out[21]:
array([[0, 1, 2, 3],
[4, 5, 6, 7]])
Если один из размеров равен -1, его размер определяется автоматически:
In [22]: arr = np.arange(15)
In [23]: arr.reshape((5, -1))
Out[23]:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
Обратные операции обычно называются flattening или raveling:
In [27]: arr = np.arange(15).reshape((5, 3))
In [28]: arr
Out[28]:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [29]: arr.ravel()
Out[29]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
Метод ravel не создаёт копию исходных данных, если результат совпадает с ними. Метод flatten также возвращает копию данных. ``` array([[-2.0016, -0.3718, -2.0016, -0.3718], [ 1.669 , -0.4386, 1.669 , -0.4386], [-2.0016, -0.3718, -2.0016, -0.3718], [ 1.669 , -0.4386, 1.669 , -0.4386], [-2.0016, -0.3718, -2.0016, -0.3718], [ 1.669 , -0.4386, 1.669 , -0.4386]])
## Функция эквивалентности индексов take и put
В четвёртой главе мы говорили о том, что есть способ получить и установить подмножество массива с помощью целых чисел — это **индексы**:
```python
In [67]: arr = np.arange(10) * 100
In [68]: inds = [7, 1, 2, 6]
In [69]: arr[inds]
Out[69]: array([700, 100, 200, 600])
У ndarray есть и другие методы для получения отдельных участков вдоль оси:
In [70]: arr.take(inds)
Out[70]: array([700, 100, 200, 600])
In [71]: arr.put(inds, 42)
In [72]: arr
Out[72]: array([ 0, 42, 42, 300, 400, 500, 42, 42,800, 900])
In [73]: arr.put(inds, [40, 41, 42, 43])
In [74]: arr
Out[74]: array([ 0, 41, 42, 300, 400, 500, 43, 40, 800, 900])
Чтобы использовать take на другой оси, нужно просто передать axis=ключевое слово:
In [75]: inds = [2, 0, 2, 1]
In [76]: arr = np.random.randn(2, 4)
In [77]: arr
Out[77]:
array([[-0.5397, 0.477 , 3.2489, -1.0212],
[-0.5771, 0.1241, 0.3026, 0.5238]])
In [78]: arr.take(inds, axis=1)
Out[78]:
array([[ 3.2489, -0.5397, 3.2489, 0.477 ],
[ 0.3026, -0.5771, 0.3026, 0.1241]])
Put не принимает параметр axis, он работает только с плоским массивом (одномерным, в C порядке). Поэтому, если вам нужно использовать индексы на других осях, лучше использовать индексы.
Broadcasting — это способ выполнения арифметических операций между массивами разных форм. Это очень мощная функция, но она также может сбивать с толку, даже опытных программистов. Самый простой пример вещания — когда вы объединяете скалярное значение с массивом:
In [79]: arr = np.arange(5)
In [80]: arr
Out[80]: array([0, 1, 2, 3, 4])
In [81]: arr * 4
Out[81]: array([ 0, 4, 8, 12, 16])
Здесь мы говорим, что в этой операции умножения скалярное значение 4 было передано всем элементам массива.
Рассмотрим пример, где мы можем нормализовать каждый столбец массива путём вычитания среднего значения столбца. Эта задача решается очень просто:
In [82]: arr = np.random.randn(4, 3)
In [83]: arr.mean(0)
Out[83]: array([-0.3928, -0.3824, -0.8768])
In [84]: demeaned = arr - arr.mean(0)
In [85]: demeaned
Out[85]:
array([[ 0.3937, 1.7263, 0.1633],
[-0.4384, -1.9878, -0.9839],
[-0.468 , 0.9426, -0.3891],
[ 0.5126, -0.6811, 1.2097]])
In [86]: demeaned.mean(0)
Out[86]: array([-0., 0., -0.])
Рисунок A-4 наглядно показывает этот процесс. Если следовать определённым правилам, то значения низкой размерности могут быть переданы в любую ось массива (например, чтобы нормализовать каждую строку путём вычитания среднего значения строки).
Тогда мы получим:
Хотя я опытный программист NumPy, мне всё равно приходится останавливаться и рисовать диаграммы, чтобы вспомнить правила вещания. Давайте посмотрим на последний пример, предположим, вы хотите нормализовать каждую строку, вычитая среднее значение строки. Поскольку длина arr.mean(0) равна 3, его можно транслировать по оси 0: поскольку длина arr равна 3 по последней оси, они совместимы. Согласно этому правилу, для вычитания по оси 1 (то есть каждой строки) меньший массив должен иметь форму (4,1):
In [87]: arr
Out[87]:
array([[ 0.0009, 1.3438, -0.7135],
[-0.8312, -2.3702, -1.8608],
[-0.8608, 0.5601, -1.2659],
[ 0.1198, -1.0635, 0.3329]])
In [88]: row_means = arr.mean(1)
In [89]: row_means.shape
Out[89]: (4,)
In [90]: row_means.reshape((4, 1))
Out[90]:
array([[ 0.2104],
[-1.6874],
[-0.5222],
[-0.2036]])
In [91]: demeaned = arr - row_means.reshape((4, 1))
In [92]: demeaned.mean(1)
Out[92]: array([ 0., -0., 0., 0.])
Рисунок A-5 иллюстрирует эту операцию.
На рисунке A-6 показан ещё один случай, когда трёхмерный массив увеличивается на двумерный массив по оси 0.
```
arr_3d.shape # (4, 1, 4)
arr_1d = np.random.normal(size=3)
arr_1d[:, np.newaxis]
arr_1d[np.newaxis, :]
Таким образом, если у нас есть трёхмерный массив и мы хотим сделать его двумерным, выровняв по второй оси, то достаточно написать следующий код:
arr = np.random.randn(3, 4, 5)
depth_means = arr.mean(2)
depth_means.shape
demeaned = arr - depth_means[:, :, np.newaxis]
demeaned.mean(2)
Некоторые читатели могут подумать, что при выравнивании по заданной оси существует ли универсальный и эффективный способ? На самом деле да, но для этого нужны некоторые навыки индексации:
def demean_axis(arr, axis=0):
means = arr.mean(axis)
# This generalizes things like [:, :, np.newaxis] to N dimensions
indexer = [slice(None)] * arr.ndim
indexer[axis] = np.newaxis
return arr - means[indexer]
Арифметические операции, которые мы выполняем, также следуют принципу трансляции. Для самых простых случаев мы можем сделать следующее:
arr = np.zeros((4, 3))
arr[:] = 5
arr
# array([[ 5., 5., 5.],
# [ 5., 5., 5.],
# [ 5., 5., 5.],
# [ 5., 5., 5.]])
Но предположим, что мы хотим использовать одномерный массив для установки значений столбцов целевого массива, при условии, что формы совместимы:
col = np.array([1.28, -0.42, 0.44, 1.6])
arr[:] = col[:, np.newaxis]
arr
arr[:2] = [[-1.37], [0.509]]
arr
Хотя многие пользователи NumPy будут использовать только быстрые элементарные операции, предоставляемые универсальными функциями, универсальные функции фактически имеют несколько продвинутых применений, которые позволяют нам отказаться от циклов и писать более лаконичный код.
Каждая бинарная универсальная функция в NumPy имеет несколько методов, предназначенных для выполнения определённых векторизованных операций. Таблица A-2 суммирует эти методы, и я объясню их на нескольких конкретных примерах.
reduce принимает массив в качестве аргумента и объединяет его значения с помощью ряда бинарных операций (можно указать ось). Например, мы можем использовать np.add.reduce для суммирования элементов массива:
arr = np.arange(10)
np.add.reduce(arr)
# 45
arr.sum()
# 45
Начальное значение зависит от универсальной функции (для случая add это 0). Если указана ось, операция сокращения будет выполняться вдоль этой оси. Это позволяет вам получить ответы на некоторые вопросы более кратким способом. В следующем примере мы используем np.logical_and для проверки того, являются ли значения в каждой строке массива упорядоченными:
np.random.seed(12346) # для воспроизводимости
arr = np.random.randn(5, 5)
arr[::2].sort(1) # сортируем несколько строк
arr[:, :-1] < arr[:, 1:]
# array([[ True, True, True, True],
# [False, True, False, False],
# [ True, True, True, True],
# [ True, False, True, True],
# [ True, True, True, True]], dtype=bool)
np.logical_and.reduce(arr[:, :-1] < arr[:, 1:], axis=1)
# array([ True, False, True, False, True], dtype=bool)
Обратите внимание, что logical_and.reduce эквивалентен методу all. ``` [0, 2, 4, 6, 8], [0, 3, 6, 9, 12]]
In[135]: np.add.reduceat(arr, [0, 2, 4], axis=1) Out[135]: array([[0, 0, 0], [1, 5, 4], [2, 10, 8], [3, 15, 12]])
Таблица A-2 обобщает часть методов ufunc.
**Рисунок: таблица методов ufunc**
### Написание нового ufunc
Существует несколько способов позволить вам написать свои собственные функции NumPy ufuncs. Наиболее распространённым является использование NumPy C API, но это выходит за рамки этой книги. В этом разделе мы обсудим чисто Python ufunc.
numpy.frompyfunc принимает функцию Python и два параметра, представляющих количество входных и выходных параметров. Например, вот простая функция, которая может выполнять сложение элементов:
```python
In[136]: def add_elements(x, y):
.....: return x + y
In[137]: add_them = np.frompyfunc(add_elements, 2, 1)
In[138]: add_them(np.arange(8), np.arange(8))
Out[138]: array([0, 2, 4, 6, 8, 10, 12, 14], dtype=object)
Функции, созданные frompyfunc, всегда возвращают массив объектов Python, что очень неудобно. К счастью, есть ещё один способ — numpy.vectorize. Хотя он не так мощен, как frompyfunc, он позволяет вам указать тип вывода:
In[139]: add_them = np.vectorize(add_elements, otypes=[np.float64])
In[140]: add_them(np.arange(8), np.arange(8))
Out[140]: array([ 0., 2., 4., 6., 8., 10., 12., 14.])
Хотя эти две функции предоставляют способ создания функций типа ufunc, они очень медленные, потому что они выполняют вызов функции Python для каждого элемента, что намного медленнее, чем встроенные функции NumPy на основе C:
In[141]: arr = np.random.randn(10000)
In[142]: %timeit add_them(arr, arr)
4.12 ms +- 182 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
In[143]: %timeit np.add(arr, arr)
6.89 us +- 504 ns per loop (mean +- std. dev. of 7 runs, 100000 loops each)
В конце этой главы я расскажу об использовании Numba (http://numba.pydata.org/), чтобы создать быстрые функции Python ufunc.
Вы, возможно, уже заметили, что до сих пор обсуждаемые ndarrays представляют собой однородные контейнеры данных, то есть каждый элемент в блоке памяти, который они представляют, занимает одинаковое количество байтов (в зависимости от dtype). На первый взгляд кажется, что они не подходят для представления гетерогенных или табличных данных. Структурированный массив представляет собой особый вид ndarray, в котором каждый элемент можно рассматривать как структуру C (struct, отсюда и «структурированный») или строку таблицы SQL с несколькими именованными полями:
In[144]: dtype = [('x', np.float64), ('y', np.int32)]
In[145]: sarr = np.array([(1.5, 6), (np.pi, -2)], dtype=dtype)
In[146]: sarr
Out[146]:
array([( 1.5 , 6), ( 3.1416, -2)],
dtype=[('x', '<f8'), ('y', '<i4')])
Определение структурированного dtype (см. онлайн-документацию NumPy) имеет много способов. Самый типичный способ — использовать список кортежей, формат каждого кортежа — (field_name, field_data_type). Таким образом, элементы массива становятся объектами кортежного типа, к которым можно получить доступ, как к словарю:
In[147]: sarr[0]
Out[147]: ( 1.5, 6)
In[148]: sarr[0]['y']
Out[148]: 6
При доступе к полю структурированного массива возвращается представление данных, поэтому данные не копируются:
In[149]: sarr['x']
Out[149]: array([ 1.5 , 3.1416])
Поля имён хранятся в атрибуте dtype.names. При доступе к sarr['x'] вы получаете двумерный массив, а не одномерный массив из предыдущего примера:
In[150]: dtype = [('x', np.int64, 3), ('y', np.int32)]
In[151]: arr = np.zeros(4, dtype=dtype)
In[152]: arr
Out[152]:
array([([0, 0, 0], 0), ([0, 0, 0], 0), ([0, 0, 0], 0), ([0, 0, 0], 0)],
dtype=[('x', '<i8', (3,)), ('y', '<i4')])
Здесь каждый элемент записи поля x представляет собой массив длиной 3:
In[153]: arr[0]['x']
Out[153]: array([0, 0, 0])
Таким образом, доступ к arr['x'] даст вам двумерный массив вместо одномерного массива из предыдущего примера:
In[154]: arr['x']
Out[154]:
array([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
Это позволяет хранить сложные вложенные структуры в одном массиве памяти. Вы также можете вкладывать dtype для создания более сложных структур. Вот простой пример:
In[155]: dtype = [('x', [('a', 'f8'), ('b', 'f4')]), ('y', np.int32)]
In[156]: data = np.array([((1, 2), 5), ((3, 4), 6)], dtype=dtype)
In[157]: data['x']
Out[157]:
array([( 1., 2.), ( 3., 4.)],
dtype=[('a', '<f8'), ('b', '<f4')])
In[158]: data['y']
Out[158]: array([5, 6], dtype=int32)
In[159]: data['x']['a']
Out[159]: array([ 1., 3.])
Разделяемый индекс pandas похож на этот, хотя и не напрямую поддерживается.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )