1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/it-ebooks-pyda-2e-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
a.md 25 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 28.11.2024 16:49 0ce2ae9

Приложение A. NumPy для продвинутых: работа с массивами

В этом приложении мы подробно рассмотрим работу с библиотекой NumPy и её возможностями для работы с многомерными массивами. Мы рассмотрим внутреннюю структуру объектов ndarray, а также их типы данных. Кроме того, мы обсудим более сложные операции над массивами, такие как изменение формы массива и управление порядком элементов в памяти.

A.1. Внутренняя структура объектов ndarray

NumPy предоставляет мощный инструмент для работы с однородными данными — объект ndarray. Он позволяет интерпретировать данные как многомерные массивы. Тип данных (dtype) определяет способ интерпретации данных, например, как целые числа, числа с плавающей точкой или логические значения.

Одной из ключевых особенностей ndarray является то, что все объекты представляют собой «промежуточный вид» на блок данных (strided view). Это позволяет выполнять различные операции над данными без копирования. Например, массив arr[::2,::-1] не копирует данные, потому что ndarray использует информацию о шагах (stride) для перемещения по памяти.

Внутренняя структура ndarray включает:

  • указатель на данные (обычно в памяти или в файле);
  • тип данных (dtype), описывающий размерность и тип значений;
  • кортеж, представляющий форму массива (shape);
  • кортеж шагов (stride), указывающий количество байтов, которые нужно пропустить для перехода к следующему элементу в каждом измерении.

На рисунке 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 порядке). Поэтому, если вам нужно использовать индексы на других осях, лучше использовать индексы.

A.3 Broadcasting

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 наглядно показывает этот процесс. Если следовать определённым правилам, то значения низкой размерности могут быть переданы в любую ось массива (например, чтобы нормализовать каждую строку путём вычитания среднего значения строки).

Рисунок A-4 Одномерный массив, транслируемый по оси 0

Тогда мы получим:

Хотя я опытный программист 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-5 Двумерный массив, транслируемый по оси 1

На рисунке A-6 показан ещё один случай, когда трёхмерный массив увеличивается на двумерный массив по оси 0.

Рисунок A-6 Трёхмерный массив, транслируемый по оси 0 ``` arr_3d.shape # (4, 1, 4)

arr_1d = np.random.normal(size=3)

arr_1d[:, np.newaxis]

array([[-2.3594],

[-0.1995],

[-1.542 ]])

arr_1d[np.newaxis, :]

array([[-2.3594, -0.1995, -1.542 ]])


Таким образом, если у нас есть трёхмерный массив и мы хотим сделать его двумерным, выровняв по второй оси, то достаточно написать следующий код:

arr = np.random.randn(3, 4, 5)

depth_means = arr.mean(2)

array([[-0.4735, 0.3971, -0.0228, 0.2001],

[-0.3521, -0.281 , -0.071 , -0.1586],

[ 0.6245, 0.6047, 0.4396, -0.2846]])

depth_means.shape

(3, 4)

demeaned = arr - depth_means[:, :, np.newaxis]

demeaned.mean(2)

array([[ 0., 0., -0., -0.],

[ 0., 0., -0., 0.],

[ 0., 0., -0., -0.]])

Некоторые читатели могут подумать, что при выравнивании по заданной оси существует ли универсальный и эффективный способ? На самом деле да, но для этого нужны некоторые навыки индексации:

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

array([[ 1.28, 1.28, 1.28],

[-0.42, -0.42, -0.42],

[ 0.44, 0.44, 0.44],

[ 1.6 , 1.6 , 1.6 ]])

arr[:2] = [[-1.37], [0.509]]

arr

array([[-1.37 , -1.37 , -1.37 ],

[ 0.509, 0.509, 0.509],

[ 0.44 , 0.44 , 0.44 ],

[ 1.6 , 1.6 , 1.6 ]])

A.4 Применение универсальных функций высокого уровня

Хотя многие пользователи 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.

A.5 Структурированные и записываемые массивы

Вы, возможно, уже заметили, что до сих пор обсуждаемые 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

Доступ к полям и вложенным dtype

При доступе к полю структурированного массива возвращается представление данных, поэтому данные не копируются:

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 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/it-ebooks-pyda-2e-zh.git
git@api.gitlife.ru:oschina-mirror/it-ebooks-pyda-2e-zh.git
oschina-mirror
it-ebooks-pyda-2e-zh
it-ebooks-pyda-2e-zh
master