Страницы

Поиск по вопросам

суббота, 22 сентября 2018 г.

NumPy Python

NumPy — это расширение языка Python, добавляющее поддержку больших многомерных массивов и матриц, вместе с большой библиотекой высокоуровневых математических функций для операций с этими массивами.


NumPy, часть 1: начало работы

NumPy — это библиотека языка Python, добавляющая поддержку больших многомерных массивов и матриц, вместе с большой библиотекой высокоуровневых (и очень быстрых) математических функций для операций с этими массивами.

Установка NumPy

На linux - пакет python3-numpy (или аналогичный для вашей системы), или через pip. Ну или же собирать из исходников https://sourceforge.net/projects/numpy/files/NumPy/.
На Windows на том же сайте есть exe установщики. Или, если возникают проблемы, рекомендую ещё хороший сборник библиотек http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy.

Начинаем работу

Основным объектом NumPy является однородный многомерный массив (в numpy называется numpy.ndarray). Это многомерный массив элементов (обычно чисел), одного типа.
Наиболее важные атрибуты объектов ndarray:
ndarray.ndim - число измерений (чаще их называют "оси") массива.
ndarray.shape - размеры массива, его форма. Это кортеж натуральных чисел, показывающий длину массива по каждой оси. Для матрицы из n строк и m столбов, shape будет (n,m). Число элементов кортежа shape равно ndim.
ndarray.size - количество элементов массива. Очевидно, равно произведению всех элементов атрибута shape.
ndarray.dtype - объект, описывающий тип элементов массива. Можно определить dtype, используя стандартные типы данных Python. NumPy здесь предоставляет целый букет возможностей, как встроенных, например: bool_, character, int8, int16, int32, int64, float8, float16, float32, float64, complex64, object_, так и возможность определить собственные типы данных, в том числе и составные.
ndarray.itemsize - размер каждого элемента массива в байтах.
ndarray.data - буфер, содержащий фактические элементы массива. Обычно не нужно использовать этот атрибут, так как обращаться к элементам массива проще всего с помощью индексов.

Создание массивов

В NumPy существует много способов создать массив. Один из наиболее простых - создать массив из обычных списков или кортежей Python, используя функцию numpy.array() (запомните: array - функция, создающая объект типа ndarray):
>>>
>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> a
array([1, 2, 3])
>>> type(a)
<class 'numpy.ndarray'>
Функция array() трансформирует вложенные последовательности в многомерные массивы. Тип элементов массива зависит от типа элементов исходной последовательности (но можно и переопределить его в момент создания).
>>>
>>> b = np.array([[1.5, 2, 3], [4, 5, 6]])
>>> b
array([[ 1.5,  2. ,  3. ],
       [ 4. ,  5. ,  6. ]])
Можно также переопределить тип в момент создания:
>>>
>>> b = np.array([[1.5, 2, 3], [4, 5, 6]], dtype=np.complex)
>>> b
array([[ 1.5+0.j,  2.0+0.j,  3.0+0.j],
       [ 4.0+0.j,  5.0+0.j,  6.0+0.j]])
Функция array() не единственная функция для создания массивов. Обычно элементы массива вначале неизвестны, а массив, в котором они будут храниться, уже нужен. Поэтому имеется несколько функций для того, чтобы создавать массивы с каким-то исходным содержимым (по умолчанию тип создаваемого массива — float64).
Функция zeros() создает массив из нулей, а функция ones() — массив из единиц. Обе функции принимают кортеж с размерами, и аргумент dtype:
>>>
>>> np.zeros((3, 5))
array([[ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.]])
>>> np.ones((2, 2, 2))
array([[[ 1.,  1.],
        [ 1.,  1.]],

       [[ 1.,  1.],
        [ 1.,  1.]]])
Функция eye() создаёт единичную матрицу (двумерный массив)
>>>
>>> np.eye(5)
array([[ 1.,  0.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.],
       [ 0.,  0.,  0.,  1.,  0.],
       [ 0.,  0.,  0.,  0.,  1.]])
Функция empty() создает массив без его заполнения. Исходное содержимое случайно и зависит от состояния памяти на момент создания массива (то есть от того мусора, что в ней хранится):
>>>
>>> np.empty((3, 3))
array([[  6.93920488e-310,   6.93920488e-310,   6.93920149e-310],
       [  6.93920058e-310,   6.93920058e-310,   6.93920058e-310],
       [  6.93920359e-310,   0.00000000e+000,   6.93920501e-310]])
>>> np.empty((3, 3))
array([[  6.93920488e-310,   6.93920488e-310,   6.93920147e-310],
       [  6.93920149e-310,   6.93920146e-310,   6.93920359e-310],
       [  6.93920359e-310,   0.00000000e+000,   3.95252517e-322]])
Для создания последовательностей чисел, в NumPy имеется функция arange(), аналогичная встроенной в Python range(), только вместо списков она возвращает массивы, и принимает не только целые значения:
>>>
>>> np.arange(10, 30, 5)
array([10, 15, 20, 25])
>>> np.arange(0, 1, 0.1)
array([ 0. ,  0.1,  0.2,  0.3,  0.4,  0.5,  0.6,  0.7,  0.8,  0.9])
Вообще, при использовании arange() с аргументами типа float, сложно быть уверенным в том, сколько элементов будет получено (из-за ограничения точности чисел с плавающей запятой). Поэтому, в таких случаях обычно лучше использовать функцию linspace(), которая вместо шага в качестве одного из аргументов принимает число, равное количеству нужных элементов:
>>>
>>> np.linspace(0, 2, 9)  # 9 чисел от 0 до 2 включительно
array([  0. ,  0.25,  0.5 ,  0.75, 1. , 1.25, 1.5 , 1.75, 2. ])
fromfunction(): применяет функцию ко всем комбинациям индексов
>>>
>>> def f1(i, j):
...     return 3 * i + j
...
>>> np.fromfunction(f1, (3, 4))
array([[ 0.,  1.,  2.,  3.],
       [ 3.,  4.,  5.,  6.],
       [ 6.,  7.,  8.,  9.]])
>>> np.fromfunction(f1, (3, 3))
array([[ 0.,  1.,  2.],
       [ 3.,  4.,  5.],
       [ 6.,  7.,  8.]])

Печать массивов

Если массив слишком большой, чтобы его печатать, NumPy автоматически скрывает центральную часть массива и выводит только его уголки.
>>>
>>> print(np.arange(0, 3000, 1))
[   0    1    2 ..., 2997 2998 2999]
Если вам действительно нужно увидеть весь массив, используйте функцию numpy.set_printoptions:
np.set_printoptions(threshold=np.nan)
И вообще, с помощью этой функции можно настроить печать массивов "под себя". Функция numpy.set_printoptions принимает несколько аргументов:
precision : количество отображаемых цифр после запятой (по умолчанию 8).
threshold : количество элементов в массиве, вызывающее обрезание элементов (по умолчанию 1000).
edgeitems : количество элементов в начале и в конце каждой размерности массива (по умолчанию 3).
linewidth : количество символов в строке, после которых осуществляется перенос (по умолчанию 75).
suppress : если True, не печатает маленькие значения в scientific notation (по умолчанию False).
nanstr : строковое представление NaN (по умолчанию 'nan').
infstr : строковое представление inf (по умолчанию 'inf').
formatter : позволяет более тонко управлять печатью массивов. Здесь я его рассматривать не буду, можете почитать здесь (на английском).
И вообще, пользуйтесь официальной документацией по numpy, а в этом пособии я постараюсь описать всё необходимое. В следующей части мы рассмотрим базовые операции над массивами.

NumPy, часть 2: базовые операции над массивами

Базовые операции

Математические операции над массивами выполняются поэлементно. Создается новый массив, который заполняется результатами действия оператора.
>>>
>>> import numpy as np
>>> a = np.array([20, 30, 40, 50])
>>> b = np.arange(4)
>>> a + b
array([20, 31, 42, 53])
>>> a - b
array([20, 29, 38, 47])
>>> a * b
array([  0,  30,  80, 150])
>>> a / b  # При делении на 0 возвращается inf (бесконечность)
array([         inf,  30.        ,  20.        ,  16.66666667])
<string>:1: RuntimeWarning: divide by zero encountered in true_divide
>>> a ** b
array([     1,     30,   1600, 125000])
>>> a % b  # При взятии остатка от деления на 0 возвращается 0
<string>:1: RuntimeWarning: divide by zero encountered in remainder
array([0, 0, 0, 2])
Для этого, естественно, массивы должны быть одинаковых размеров.
>>>
>>> c = np.array([[1, 2, 3], [4, 5, 6]])
>>> d = np.array([[1, 2], [3, 4], [5, 6]])
>>> c + d
Traceback (most recent call last):
  File "<input>", line 1, in <module>
ValueError: operands could not be broadcast together with shapes (2,3) (3,2)
Также можно производить математические операции между массивом и числом. В этом случае к каждому элементу прибавляется (или что вы там делаете) это число.
>>>
>>> a + 1
array([21, 31, 41, 51])
>>> a ** 3
array([  8000,  27000,  64000, 125000])
>>> a < 35  # И фильтрацию можно проводить
array([ True,  True, False, False], dtype=bool)
NumPy также предоставляет множество математических операций для обработки массивов:
>>>
>>> np.cos(a)
array([ 0.40808206,  0.15425145, -0.66693806,  0.96496603])
>>> np.arctan(a)
array([ 1.52083793,  1.53747533,  1.54580153,  1.55079899])
>>> np.sinh(a)
array([  2.42582598e+08,   5.34323729e+12,   1.17692633e+17,
         2.59235276e+21])
Полный список можно посмотреть здесь.
Многие унарные операции, такие как, например, вычисление суммы всех элементов массива, представлены также и в виде методов класса ndarray.
>>>
>>> a = np.array([[1, 2, 3], [4, 5, 6]])
>>> np.sum(a)
21
>>> a.sum()
21
>>> a.min()
1
>>> a.max()
6
По умолчанию, эти операции применяются к массиву, как если бы он был списком чисел, независимо от его формы. Однако, указав параметр axis, можно применить операцию для указанной оси массива:
>>>
>>> a.min(axis=0)  # Наименьшее число в каждом столбце
array([1, 2, 3])
>>> a.min(axis=1)  # Наименьшее число в каждой строке
array([1, 4])

Индексы, срезы, итерации

Одномерные массивы осуществляют операции индексирования, срезов и итераций очень схожим образом с обычными списками и другими последовательностями Python (разве что удалять с помощью срезов нельзя).
>>>
>>> a = np.arange(10) ** 3
>>> a
array([  0,   1,   8,  27,  64, 125, 216, 343, 512, 729])
>>> a[1]
1
>>> a[3:7]
array([ 27,  64, 125, 216])
>>> a[3:7] = 8
>>> a
array([  0,   1,   8,   8,   8,   8,   8, 343, 512, 729])
>>> a[::-1]
array([729, 512, 343,   8,   8,   8,   8,   8,   1,   0])
>>> del a[4:6]
Traceback (most recent call last):
  File "<input>", line 1, in <module>
ValueError: cannot delete array elements
>>> for i in a:
...     print(i ** (1/3))
...
0.0
1.0
2.0
2.0
2.0
2.0
2.0
7.0
8.0
9.0
У многомерных массивов на каждую ось приходится один индекс. Индексы передаются в виде последовательности чисел, разделенных запятыми (то бишь, кортежами):
>>>
>>> b = np.array([[  0, 1, 2, 3],
...               [10, 11, 12, 13],
...               [20, 21, 22, 23],
...               [30, 31, 32, 33],
...               [40, 41, 42, 43]])
...
>>> b[2,3]  # Вторая строка, третий столбец
23
>>> b[(2,3)]
23
>>> b[2][3]  # Можно и так
23
>>> b[:,2]  # Третий столбец
array([ 2, 12, 22, 32, 42])
>>> b[:2]  # Первые две строки
array([[ 0,  1,  2,  3],
       [10, 11, 12, 13]])
>>> b[1:3, : : ]  # Вторая и третья строки
array([[10, 11, 12, 13],
       [20, 21, 22, 23]])
Когда индексов меньше, чем осей, отсутствующие индексы предполагаются дополненными с помощью срезов:
>>>
>>> b[-1]  # Последняя строка. Эквивалентно b[-1,:]
array([40, 41, 42, 43])
b[i] можно читать как b[i, <столько символов ':', сколько нужно>]. В NumPy это также может быть записано с помощью точек, как b[i, ...].
Например, если x имеет ранг 5 (то есть у него 5 осей), тогда
  • x[1, 2, ...] эквивалентно x[1, 2, :, :, :],
  • x[... , 3] то же самое, что x[:, :, :, :, 3] и
  • x[4, ... , 5, :] это x[4, :, :, 5, :].
>>>
>>> a = np.array(([[0, 1, 2], [10, 12, 13]], [[100, 101, 102], [110, 112, 113]]))
>>> a.shape
(2, 2, 3)
>>> a[1, ...]  # то же, что a[1, : , :] или a[1]
array([[100, 101, 102],
       [110, 112, 113]])
>>> c[... ,2]  # то же, что a[: , : ,2]
array([[  2,  13],
       [102, 113]])
Итерирование многомерных массивов начинается с первой оси:
>>>
>>> for row in a:
...     print(row)
...
[[ 0  1  2]
 [10 12 13]]
[[100 101 102]
 [110 112 113]]
Однако, если нужно перебрать поэлементно весь массив, как если бы он был одномерным, для этого можно использовать атрибут flat:
>>>
>>> for el in a.flat:
...     print(el)
...
0
1
2
10
12
13
100
101
102
110
112
113

Манипуляции с формой

Как уже говорилось, у массива есть форма (shape), определяемая числом элементов вдоль каждой оси:
>>>
>>> a
array([[[  0,   1,   2],
        [ 10,  12,  13]],

       [[100, 101, 102],
        [110, 112, 113]]])
>>> a.shape
(2, 2, 3)
Форма массива может быть изменена с помощью различных команд:
>>>
>>> a.ravel()  # Делает массив плоским
array([  0,   1,   2,  10,  12,  13, 100, 101, 102, 110, 112, 113])
>>> a.shape = (6, 2)  # Изменение формы
>>> a
array([[  0,   1],
       [  2,  10],
       [ 12,  13],
       [100, 101],
       [102, 110],
       [112, 113]])
>>> a.transpose()  # Транспонирование
array([[  0,   2,  12, 100, 102, 112],
       [  1,  10,  13, 101, 110, 113]])
>>> a.reshape((3, 4))  # Изменение формы
array([[  0,   1,   2,  10],
       [ 12,  13, 100, 101],
       [102, 110, 112, 113]])
Порядок элементов в массиве в результате функции ravel() соответствует обычному "C-стилю", то есть, чем правее индекс, тем он "быстрее изменяется": за элементом a[0,0] следует a[0,1]. Если одна форма массива была изменена на другую, массив переформировывается также в "C-стиле". Функции ravel() и reshape() также могут работать (при использовании дополнительного аргумента) в FORTRAN-стиле, в котором быстрее изменяется более левый индекс.
>>>
>>> a
array([[  0,   1],
       [  2,  10],
       [ 12,  13],
       [100, 101],
       [102, 110],
       [112, 113]])
>>> a.reshape((3, 4), order='F')
array([[  0, 100,   1, 101],
       [  2, 102,  10, 110],
       [ 12, 112,  13, 113]])
Метод reshape() возвращает ее аргумент с измененной формой, в то время как метод resize() изменяет сам массив:
>>>
>>> a.resize((2, 6))
>>> a
array([[  0,   1,   2,  10,  12,  13],
       [100, 101, 102, 110, 112, 113]])
Если при операции такой перестройки один из аргументов задается как -1, то он автоматически рассчитывается в соответствии с остальными заданными:
>>>
>>> a.reshape((3, -1))
array([[  0,   1,   2,  10],
       [ 12,  13, 100, 101],
       [102, 110, 112, 113]])

Объединение массивов

Несколько массивов могут быть объединены вместе вдоль разных осей с помощью функций hstack и vstack.
hstack() объединяет массивы по первым осям, vstack() — по последним:
>>>
>>> a = np.array([[1, 2], [3, 4]])
>>> b = np.array([[5, 6], [7, 8]])
>>> np.vstack((a, b))
array([[1, 2],
       [3, 4],
       [5, 6],
       [7, 8]])
>>> np.hstack((a, b))
array([[1, 2, 5, 6],
       [3, 4, 7, 8]])
Функция column_stack() объединяет одномерные массивы в качестве столбцов двумерного массива:
>>>
>>> np.column_stack((a, b))
array([[1, 2, 5, 6],
       [3, 4, 7, 8]])
Аналогично для строк имеется функция row_stack().
>>>
>>> np.row_stack((a, b))
array([[1, 2],
       [3, 4],
       [5, 6],
       [7, 8]])

Разбиение массива

Используя hsplit() вы можете разбить массив вдоль горизонтальной оси, указав либо число возвращаемых массивов одинаковой формы, либо номера столбцов, после которых массив разрезается "ножницами":
>>>
>>> a = np.arange(12).reshape((2, 6))
>>> a
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11]])
>>> np.hsplit(a, 3)  # Разбить на 3 части
[array([[0, 1], [6, 7]]),
 array([[2, 3], [8, 9]]),
 array([[ 4,  5], [10, 11]])]
>>> np.hsplit(a, (3, 4))  # Разрезать a после третьего и четвёртого столбца
[array([[0, 1, 2], [6, 7, 8]]),
 array([[3], [9]]),
 array([[ 4,  5], [10, 11]])]
Функция vsplit() разбивает массив вдоль вертикальной оси, а array_split() позволяет указать оси, вдоль которых произойдет разбиение.

Копии и представления

При работе с массивами, их данные иногда необходимо копировать в другой массив, а иногда нет. Это часто является источником путаницы. Возможно 3 случая:

Вообще никаких копий

Простое присваивание не создает ни копии массива, ни копии его данных:
>>>
>>> a = np.arange(12)
>>> b = a  # Нового объекта создано не было
>>> b is a  # a и b это два имени для одного и того же объекта ndarray
True
>>> b.shape = (3,4)  # изменит форму a
>>> a.shape
(3, 4)
Python передает изменяемые объекты как ссылки, поэтому вызовы функций также не создают копий.

Представление или поверхностная копия

Разные объекты массивов могут использовать одни и те же данные. Метод view() создает новый объект массива, являющийся представлением тех же данных.
>>>
>>> c = a.view()
>>> c is a
False
>>> c.base is a  # c это представление данных, принадлежащих a
True
>>> c.flags.owndata
False
>>>
>>> c.shape = (2,6)  # форма а не поменяется
>>> a.shape
(3, 4)
>>> c[0,4] = 1234  # данные а изменятся
>>> a
array([[  0, 1, 2, 3],
       [1234, 5, 6, 7],
       [ 8, 9, 10, 11]])
Срез массива это представление:
>>>
>>> s = a[:,1:3]
>>> s[:] = 10
>>> a
array([[  0, 10, 10, 3],
       [1234, 10, 10, 7],
       [ 8, 10, 10, 11]])

Глубокая копия

Метод copy() создаст настоящую копию массива и его данных:
>>>
>>> d = a.copy()  # создается новый объект массива с новыми данными
>>> d is a
False
>>> d.base is a  # d не имеет ничего общего с а
False
>>> d[0, 0] = 9999
>>> a
array([[  0, 10, 10, 3],
       [1234, 10, 10, 7],
       [ 8, 10, 10, 11]])

Комментариев нет:

Отправить комментарий