Шаг 53.
Python: сборник рецептов.
Числа, даты и время. Вычисления на больших массивах чисел

    На этом шаге мы познакомимся с модулем numpy.

Задача

    Вам нужно произвести вычисления на больших объемах числовых данных, таких как массивы или решетки.

Решение

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

>>> # Списки Python 
>>> x = [1, 2, 3, 4]
>>> у = [5, 6, 7, 8]
>>> x * 2
[1, 2, 3, 4, 1, 2, 3, 4]
>>> x + 10
Traceback (most recent call last):
 .   .   .   .
TypeError: can only concatenate list (not "int") to list 
>>> x + y
[1, 2, 3, 4, 5, 6, 7, 8]
>>> # массивы NumPy
>>> import numpy as np
>>> ax = np.array([1, 2, 3, 4])
>>> ay = np.array([5, 6, 7, 8])
>>> ax * 2 array([2, 4, 6, 8])
>>> ax + 10
array([11, 12, 13, 14])
>>> ax + ay 
array([ 6, 8, 10, 12])
>>> ax * ay
array([ 5, 12, 21, 32])
>>>

    Как вы можете видеть, базовые математические операции с использованием массивов выполняются по-разному. Конкретно скалярные операции (например, ax * 2 или ax + 10) применяют операцию элемент за элементом. Также отметим, что выполнение таких математических операций, где каждый из операндов является массивом, применяет операцию ко всем элементам и создает новый массив.

    Тот факт, что математические операции применяются одновременно ко всем элементам, позволяет очень просто и быстро применить функции ко всему массиву. Например, если вы хотите вычислить значение многочлена:

>>> def f(x):
      return 3*x**2 - 2*x + 7 
>>> f(ax)
array([ 8, 15, 28, 47])
>>>

    NumPy предоставляет набор "универсальных функций", которые также работают для операций над массивами. Они подменяют похожие функции, доступные в модуле math. Например:

>>> np.sqrt(ax)
array([ 1.	, 1.41421356, 1.73205081, 2.	])
>>> np.cos(ax)
array([ 0.54030231, -0.41614684, -0.9899925 , -0.65364362])
>>>

    Использование универсальных функций позволяет выполнить вычисление в сотни раз быстрее, чем проход по массиву и применение функций из math к каждому элементу. Так что используйте их при любой возможности.

    "Под капотом" массивы NumPy устроены похожим на массивы C или Fortran образом. А именно они представляют собой большие смежные области памяти, состоящие из однородных типов данных. Это дает возможность делать массивы намного более крупными, чем позволяет обычный список Python. Например, если вы хотите создать двумерную решетку размером 10 000*10 000 чисел с плавающей точкой, это не проблема:

>>> grid = np.zeros(shape=(10000,10000), dtype=float)
>>> grid
array([[ 0., 0., 0., ..., 0., 0., 0.],
       [ 0., 0., 0., ..., 0., 0., 0.],
       [ 0., 0., 0., ..., 0., 0., 0.],
       ...,
       [ 0., 0., 0., ..., 0., 0., 0.],
       [ 0., 0., 0., ..., 0., 0., 0.],
       [ 0., 0., 0., ..., 0., 0., 0.]])
>>>

    Все обычные операции все еще применяются к элементам одновременно:

>>> grid += 10 
>>> grid
array([[10. 10. 10. ... 10. 10. 10.]
       [10. 10. 10. ... 10. 10. 10.]
       [10. 10. 10. ... 10. 10. 10.]
       ...
       [10. 10. 10. ... 10. 10. 10.]
       [10. 10. 10. ... 10. 10. 10.]
       [10. 10. 10. ... 10. 10. 10.]])
>>> np.sin(grid)	
array([[-0.54402111 -0.54402111 -0.54402111 ... -0.54402111 -0.54402111 -0.54402111]
       [-0.54402111 -0.54402111 -0.54402111 ... -0.54402111 -0.54402111 -0.54402111]
       [-0.54402111 -0.54402111 -0.54402111 ... -0.54402111 -0.54402111 -0.54402111]
       ...
       [-0.54402111 -0.54402111 -0.54402111 ... -0.54402111 -0.54402111 -0.54402111]
       [-0.54402111 -0.54402111 -0.54402111 ... -0.54402111 -0.54402111 -0.54402111]
       [-0.54402111 -0.54402111 -0.54402111 ... -0.54402111 -0.54402111 -0.54402111]])
>>>

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

>>> a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
>>> a
array([[ 1, 2, 3, 4],
       [ 5, 6, 7, 8],
       [ 9, 10, 11, 12]])
>>> # Выбираем строку 1 
>>> a[1]
array([5, 6, 7, 8])
>>> # Выбираем колонку 1 
>>> a[:,1] 
array([ 2, 6, 10])
>>> # Выбираем и изменяем субрегион 
>>> a[1:3, 1:3] 
array([[ 6, 7],
      [10, 11]])
>>> a[1:3, 1:3] += 10
>>> a
array([[ 1, 2, 3, 4],
       [ 5, 16, 17, 8],
       [ 9, 20, 21, 12]])
>>> # Транслируем строковый вектор на операции со всеми строками 
>>> a + [100, 101, 102, 103] 
array([[101, 103, 105, 107],
       [105, 117, 119, 111],
       [109, 121, 123, 115]])
>>> a
array([[ 1, 2, 3, 4],
       [ 5, 16, 17, 8],
       [ 9, 20, 21, 12]])
>>> # Условное присваивание в массиве 
>>> np.where(a < 10, a, 10) 
array([[ 1, 2, 3, 4],
       [ 5, 10, 10, 8],
       [ 9, 10, 10, 10]])
>>>
Архив с приведенными примерами можно взять здесь.

Обсуждение

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

    Стоит отметить, что часто используется конструкция import numpy as np, как и показано в нашем примере. Это сокращает название, чтобы было удобно вводить его снова и снова в вашей программе.

    Прочую информацию вы найдете на https://numpy.org/.

    На следующем шаге мы рассмотрим вычисления с матрицами.




Предыдущий шаг Содержание Следующий шаг