Программирование для всех

Алла Тамбовцева, НИУ ВШЭ

Краткое введение в массивы NumPy

Массивы NumPy

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

Чтобы мы смогли на конкретных примерах увидеть, зачем эта библиотека используется, давайте ее импортируем. Если вы уже устанавливали Anaconda, то библиотека NumPy также была установлена на ваш компьютер. Проверим: импортируем библиотеку с сокращенным названием, так часто делают, чтобы не «таскать» за собой в коде длинное название. Сокращение np для библиотеки numpy – общепринятое, его часто можно увидеть в документации или официальных тьюториалах.

In [1]:
import numpy as np

Основным объектом NumPy является Ndarray – это n-мерный массив (от n-dimensional array), структура данных, которая позволяет хранить набор элементов одного типа: либо целые числа, либо числа с плавающей точкой, либо строки, либо логические значения True и False. Массивы могут быть одномерными, то есть визуально ничем не отличаться от простого списка значений:

In [2]:
np.array([0, 2, 3, 4])
Out[2]:
array([0, 2, 3, 4])

А могут быть двумерными, то есть представлять собой таблицу, похожую на вложенный список или «список списков»):

In [3]:
np.array([[1, 2], 
          [1, 0]])
Out[3]:
array([[1, 2],
       [1, 0]])

Массивы могут быть и большей размерности (список таблиц или что-то более объемное – вкладывать списки в списки мы можем довольно долго), но на практике они нужны редко.

Зачем изучать массивы? Во-первых, с массивами гораздо приятнее работать, чем со списками, плюс, они занимают меньше памяти. Во-вторых, особенности массивов позволят нам лучше понять, как устроены столбцы в датафреймах (таблицах с данными), с которыми нам предстоит работать дальше.

Для того, чтобы увидеть, почему массивы удобнее списков, рассмотрим такую задачу. У нас есть список money_k, который содержит некоторые суммы в кнатах (волшебная валюта).

In [4]:
money_k = [210, 265, 570, 120, 180, 194]

Как получить новый список money_s, где те же суммы записаны в сиклях (1 сикль = 29 кнатов)? Либо создать пустой список и заполнить его через цикл for, либо использовать списковые включения (генераторы списков). Пойдем по второму пути:

In [5]:
money_s = [i/29 for i in money_k] 
money_s
Out[5]:
[7.241379310344827,
 9.137931034482758,
 19.655172413793103,
 4.137931034482759,
 6.206896551724138,
 6.689655172413793]

Вроде бы быстро, но без цикла все равно не обошлось. Поступим проще – сделаем из списка массив:

In [6]:
Money_k = np.array([210, 265, 570, 120, 180, 194])
Money_k
Out[6]:
array([210, 265, 570, 120, 180, 194])

А теперь просто разделим его на 29:

In [7]:
Money_s = Money_k / 29
Money_s
Out[7]:
array([ 7.24137931,  9.13793103, 19.65517241,  4.13793103,  6.20689655,
        6.68965517])

Почему такое возможно? Потому что подобные операции производятся поэлементно, то есть над каждым элементом массива в отдельности. Такие операции еще назвают векторизованными. То же будет работать и для нескольких массивов. Допустим, у нас есть два нюхлера (ниффлера), которые в течение 3 часов собирают монетки:

In [8]:
Niff_one = np.array([83, 73, 65]) 
Niff_two = np.array([34, 56, 40])

Посчитаем, сколько они насобирали вместе за каждый час:

In [9]:
Niff_sum = Niff_one + Niff_two
Niff_sum
Out[9]:
array([117, 129, 105])

Довольно быстро и удобно!

Важно! Запомните эту особенность массивов, нам она очень пригодится, когда будем работать с датафреймами pandas. Если мы решим сложить столбцы в таблице, они тоже будут складываться поэлементно.

Типы данных в массивах и преобразование типов

Чуть раньше мы зафиксировали, что массивы могут состоять только из элементов одного типа. Посмотрим, что это за типы:

In [10]:
# integer
Niff_sum.dtype  
Out[10]:
dtype('int64')
In [11]:
 # float
Money_s.dtype 
Out[11]:
dtype('float64')
In [12]:
# boolean
YN = np.array([True, False])
YN.dtype  
Out[12]:
dtype('bool')

Числа 64 или 32, дописанные в конце названия типа, зависят от системы (32-битная или 64-битная), на это можно не обращать внимания. А вот на что стоит обратить внимание, так это на то, что после .dtype нет круглых скобок. Раньше, когда мы дописывали что-то к объекту после точки, это «что-то» было методом (вспомните методы .lower() и .capitalize() на строках). Здесь dtype – это не метод, а атрибут массива, то есть какая-то его характеристика.

Три типа рассмотрели, остались строки. Создадим массив со строками:

In [14]:
creatures = np.array(["niffler", "dragon", "pixie"])
creatures.dtype
Out[14]:
dtype('<U7')

Получили таинственную запись. Но все просто. Буква U здесь означает Unicode (в этом формате кодируются строки), а 7 – это максимальное число символов в строке внутри массива. Поэтому можем считать это строковым типом, где все строки не длиннее 7 символов.

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

In [15]:
balls = np.array(["quaffle", 10, "snitch", 150])
balls
Out[15]:
array(['quaffle', '10', 'snitch', '150'], dtype='<U7')

Как и ожидалось, строковый тип оказался сильнее и вытеснил числа. Если это допустимо, можем один тип превратить в другой. Впомним про массив YN:

In [16]:
YN
Out[16]:
array([ True, False])

Превратим True и False в целые числа 1 и 0:

In [17]:
YN2 = YN.astype('int') 
YN2
Out[17]:
array([1, 0])

А теперь в обычные строки:

In [18]:
YN3 = YN.astype('str') 
YN3
Out[18]:
array(['True', 'False'], dtype='<U5')

Важно! Запомните этот полезный метод .astype(), он нам еще очень пригодится, когда будем работать с датафреймами.

Фильтрация значений по условиям и булевы массивы

Представим себе, что у нас есть массив points с числом очков, которые заработала команда за одну игру в квиддич:

In [19]:
points = np.array([150, 0, 20, 0, 30, 20, 0]) 

Убедимся, что число игроков в команде правильное – должно быть 7 человек. Вызовем атрибут size:

In [20]:
points.size  # все ок
Out[20]:
7

Теперь поинтересуемся, кто из участников набрал больше 0 очков:

In [21]:
points > 0
Out[21]:
array([ True, False,  True, False,  True,  True, False])

Неравенство выше было автоматически применено к каждому элементу массива, поэтому мы получили новый массив из True и False, которые сообщают нам, выполнено ли это условие для конкретного элемента или нет. Как посчитать число игроков, которые заработали больше 0 очков? Посчитать число True. А если учесть, что вместо True Python видит 1, а вместо False – 0? Посчитать сумму всех элементов массива:

In [22]:
(points > 10).sum()
Out[22]:
4

А как получить массив, в котором будут только те элементы points, которые удовлетворяют некоторому условию? Записать это условие в квадратных скобках, как раньше мы указывали индекс элемента:

In [23]:
points[points > 10]
Out[23]:
array([150,  20,  30,  20])

Запись выше означает, что из points Python должен выбрать те элементы, где points > 10 возвращает True.

Если условия сложные, то их нужно формулировать с помощью операторов & (одновременное выполнение условий) или | (хотя бы одно из условий верно).

In [24]:
points[(points > 10) & (points < 30)] 
Out[24]:
array([20, 20])

«Словесные» операторы and и or здесь не подойдут. Плюс, всегда нужно ставить скобки вокруг каждой части условия, иначе Python начнет «раскручивать» условие со знаков & или |, что закончится ошибкой:

In [25]:
points[points > 10 & points < 30]  # пытался сопоставить 10 и массив points
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-25-cbbd31fdcf4f> in <module>
----> 1 points[points > 10 & points < 30]  # пытался сопоставить 10 и массив points

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

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

In [26]:
np.where(points > 0)
Out[26]:
(array([0, 2, 4, 5]),)