Files

48 KiB
Raw Permalink Blame History

Библиотеки Python для анализа данных и машинного обучения

Основы работы с библиотекой numpy

Знакомство с массивами

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

Массивы numpy очень похожи на списки (даже больше на вложенные списки), только они имеют одну особенность: элементы массива должны быть одного типа. Либо все элементы целые числа, либо числа с плавающей точкой, либо строки. Для обычных списков это условие не является обязательным:

In [1]:
L = [1, 2, 4, 0]
E = [[1, 0, 3], [3, 6, 7], []]
D = [[1, 3, 6], ['a', 'b', 'c']]

# все работает
print(L)
print(E)
print(D)
[1, 2, 4, 0]
[[1, 0, 3], [3, 6, 7], []]
[[1, 3, 6], ['a', 'b', 'c']]

Чем хороши массивы numpy? Почему обычных списков недостаточно? Во-первых, обработка массивов занимает меньше времени (а их хранение меньше памяти), что очень актуально в случае работы с большими объемами данных. Во-вторых, функции numpy являются векторизованными ‒ их можно применять сразу ко всему массиву, то есть поэлементно. В этом смысле работа с массивами напоминает работу с векторами в R. Если в R у нас есть вектор c(1, 2, 5), то, прогнав строчку кода c(1, 2, 5)**2, мы получим вектор, состоящий из квадратов значений: c(1, 4, 25). Со списками в Python такое проделать не получится: понадобятся циклы или списковые включения. Зато с массивами numpy ‒ легко, и без всяких циклов! И в этом мы сегодня убедимся.

Для начала импортируем библиотеку (и сократим название до np):

In [2]:
import numpy as np

Получить массив numpy можно из обычного списка, просто используя функцию array():

In [3]:
A = np.array(L)
A
Out [3]:
array([1, 2, 4, 0])
In [4]:
A = np.array([1, 2, 4, 0]) 
A
Out [4]:
array([1, 2, 4, 0])

Как видно из примера выше, список значений можно просто вписать в array(). Главное не забыть квадратные скобки: Python не сможет склеить перечень элементов в список самостоятельно и выдаст ошибку:

In [5]:
A = np.array(1, 2, 4, 0)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-83a5f8fa93ae> in <module>()
----> 1 A = np.array(1, 2, 4, 0)

ValueError: only 2 non-keyword arguments accepted

Посмотрим, какую информацию о массиве можно получить. Например, тип его элементов:

In [6]:
A.dtype # integer
Out [6]:
dtype('int64')

Число измерений ‒ число "маленьких" массивов внутри "большого" массива (здесь такой один).

In [7]:
A.ndim
Out [7]:
1

"Форма" массива, о котором можно думать как о размерности матрицы ‒ кортеж, включающий число строк и столбцов. Здесь у нас всего одна строка, поэтому numpy считает только число элементов внутри массива.

In [8]:
A.shape
Out [8]:
(4,)

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

In [9]:
A[0]
Out [9]:
1

Попытка использовать двойной индекс приведет к неудаче:

In [10]:
A[0][0] # index error
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-10-ac48a874bd97> in <module>()
----> 1 A[0][0] # index error

IndexError: invalid index to scalar variable.

Общее число элементов в массиве можно получить с помощью метода size (аналог len() для списков):

In [11]:
A.size
Out [11]:
4

Кроме того, по массиву можно получить разные описательные статистики:

In [12]:
A.max() # максимум
Out [12]:
4
In [13]:
A.min() # минимум
Out [13]:
0
In [14]:
A.mean() # среднее
Out [14]:
1.75

О других полезных методах можно узнать, нажав Tab после np..

Наконец, массив numpy можно легко превратить в список:

In [15]:
A.tolist()
Out [15]:
[1, 2, 4, 0]

А теперь перейдем к многомерным массивам.

Многомерные массивы

Создадим многомерный массив, взяв за основу вложенный список:

In [16]:
S = np.array([[8, 1, 2], [2, 8, 9]])
In [17]:
S
Out [17]:
array([[8, 1, 2],
       [2, 8, 9]])

Посмотрим на число измерений:

In [18]:
S.ndim # два массива внутри
Out [18]:
2
In [19]:
S.shape # две строки (два списка) и три столбца (по три элемента в списке)
Out [19]:
(2, 3)

Общее число элементов в массиве (его длина):

In [20]:
S.size
Out [20]:
6

Когда в массиве больше одного измерения, при различных операциях нужно указывать, по какому измерению мы движемся (по строкам или по столбцам). Посмотрим еще раз на массив S и подумаем о нем как о матрице, как о таблице с числами:

In [21]:
S
Out [21]:
array([[8, 1, 2],
       [2, 8, 9]])

Можно найти максимальное значение по строкам или столбцам S:

In [22]:
S.max(axis=0) # по столбцам - три столбца и три максимальных значения
Out [22]:
array([8, 8, 9])
In [23]:
S.max(axis=1) # по строкам - две строки и два максимальных значения
Out [23]:
array([8, 9])
In [24]:
S.mean(axis=0)
Out [24]:
array([5. , 4.5, 5.5])
In [25]:
S.mean(axis=1)
Out [25]:
array([3.66666667, 6.33333333])

Для того, чтобы обратиться к элементу двумерного массива, нужно указывать два индекса: сначала индекс массива, в котором находится нужный нам элемент, а затем индекс элемента внутри этого массива:

In [26]:
S[0][0]
Out [26]:
8
In [27]:
S[1][2]
Out [27]:
9

Если мы оставим один индекс, мы просто получим массив с соответствующим индексом:

In [28]:
S[0]
Out [28]:
array([8, 1, 2])

Массивы ‒ изменяемые объекты в Python. Обращаясь к элементу массива, ему можно присвоить новое значение:

In [29]:
S[1][2] = 6
S
Out [29]:
array([[8, 1, 2],
       [2, 8, 6]])

Чтобы выбрать сразу несколько элементов, как и в случае со списками, можно использовать срезы. Рассмотрим массив побольше.

In [30]:
T = np.array([[1, 3, 7], [8, 10, 1], [2, 8, 9], [1, 0, 5]])
In [31]:
T
Out [31]:
array([[ 1,  3,  7],
       [ 8, 10,  1],
       [ 2,  8,  9],
       [ 1,  0,  5]])

Как и при выборе среза из списка, правый конец не включается:

In [32]:
T[0:2] # массивы с индексами 0 и 1
Out [32]:
array([[ 1,  3,  7],
       [ 8, 10,  1]])

Можно сделать что-то еще более интересное ‒ выставить шаг среза. Другими словами, сообщить Python, что нужно брать? например, элементы, начиная с нулевого, с шагом 2: элемент с индексом 0, с индексом 2, с индексом 4, и так до конца массива.

In [33]:
T[0::2] # старт, двоеточие, двоеточие, шаг
Out [33]:
array([[1, 3, 7],
       [2, 8, 9]])

В примере выше совершенно логично были выбраны элементы с индексами 0 и 2.

Как создать массив?

Способ 1

С первым способом мы уже отчасти познакомились: можно получить массив из готового списка, воспользовавшись функцие array():

In [34]:
np.array([10.5, 45, 2.4])
Out [34]:
array([10.5, 45. ,  2.4])

Кроме того, при создании массива из списка можно изменить его форму, используя функцию reshape().

In [35]:
old = np.array([[2, 5, 6], [9, 8, 0]])
old 
Out [35]:
array([[2, 5, 6],
       [9, 8, 0]])
In [36]:
old.shape # 2 на 3
Out [36]:
(2, 3)
In [37]:
new = old.reshape(3, 2) # изменим на 3 на 2
new
Out [37]:
array([[2, 5],
       [6, 9],
       [8, 0]])
In [38]:
new.shape # 3 на 2
Out [38]:
(3, 2)

Конечно, такие преобразования разумно применять, если произведение чисел в reshape() совпадает с общим числом элементов в массиве. В нашем случае в массиве old 6 элементов, поэтому из него можно получить массивы 2 на 3, 3 на 2, 1 на 6, 6 на 1. Несоответствующее число измерений приведет к ошибке:

In [39]:
old.reshape(2, 4) # и Python явно пишет, что не так
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-39-a803468708c0> in <module>()
----> 1 old.reshape(2, 4) # и Python явно пишет, что не так

ValueError: cannot reshape array of size 6 into shape (2,4)

Способ 2

Можно создать массив на основе промежутка, созданного с помощьюarange() функции из numpy, похожей на range(), только более гибкую. Посмотрим, как работает эта функция.

In [40]:
np.arange(2, 9) # по умолчанию - как обычный range()
Out [40]:
array([2, 3, 4, 5, 6, 7, 8])

По умолчанию эта функция создает массив, элементы которого начинаются со значения 2 и заканчиваются на значении 8 (правый конец промежутка не включается), следуя друг за другом с шагом 1. Но этот шаг можно менять:

In [41]:
np.arange(2, 9, 3) # с шагом 3
Out [41]:
array([2, 5, 8])

И даже делать дробным!

In [42]:
np.arange(2, 9, 0.5)
Out [42]:
array([2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. , 7.5, 8. ,
       8.5])

А теперь совместим arange() и reshape(), чтобы создать массив нужного вида:

In [43]:
np.arange(2, 9, 0.5).reshape(2, 7)
Out [43]:
array([[2. , 2.5, 3. , 3.5, 4. , 4.5, 5. ],
       [5.5, 6. , 6.5, 7. , 7.5, 8. , 8.5]])

Получилось!

Способ 3

Еще массив можно создать совсем с нуля. Единственное, что нужно четко представлять ‒ это его размерность, его форму, то есть опять же, число строк и столбцов. Библиотека numpy позволяет создать массивы, состоящие из нулей или единиц, а также "пустые" массивы (на самом деле, не совсем пустые, как убедимся позже). Удобство заключается в том, что сначала можно создать массив, инициализировать его (например, заполнить нулями), а затем заменить нули на другие значения в соответствии с требуемыми условиями. Как мы помним, массивы ‒ изменяемые объекты, и использовать замену в цикле еще никто не запрещал.

Так выглядит массив из нулей:

In [44]:
Z = np.zeros((3, 3)) # размеры в виде кортежа - не теряйте еще одни круглые скобки
Z
Out [44]:
array([[0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.]])

А так ‒ массив из единиц:

In [45]:
O = np.ones((4, 2))
O
Out [45]:
array([[1., 1.],
       [1., 1.],
       [1., 1.],
       [1., 1.]])

С пустым (empty) массивом все более загадочно:

In [46]:
Emp = np.empty((3, 2))
Emp
Out [46]:
array([[6.92198590e-310, 6.92198590e-310],
       [5.31021756e-317, 6.92194731e-310],
       [5.39590831e-317, 5.39790038e-317]])

Массив Emp ‒ не совсем пустой, в нем содержатся какие-то (псевдо)случайные элементы, которые примерно равны 0. Теоретически создавать массив таким образом можно, но не рекомендуется: лучше создать массив из "чистых" нулей, чем из какого-то непонятного "мусора".

Задание: Дан массив ages (см. ниже). Напишите программу с циклом, которая позволит получить массив ages_bin такой же размерности, что и ages, состоящий из 0 и 1 (0 - младше 18, 1 - не младше 18).

Подсказка: используйте вложенный цикл.

In [47]:
ages = np.array([[12, 16, 17, 18, 14], [20, 22, 18, 17, 23], [32, 16, 44, 16, 23]])

Решение:

In [48]:
shape = ages.shape
ages_bin = np.zeros(shape)
ages_bin

for i in range(0, shape[0]):
    for j in range(0, shape[1]):
        if ages[i][j] >= 18:
            ages_bin[i][j] = 1
ages_bin
Out [48]:
array([[0., 0., 0., 1., 0.],
       [1., 1., 1., 0., 1.],
       [1., 0., 1., 0., 1.]])

Почему массивы numpy это удобно?

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

In [49]:
A
Out [49]:
array([1, 2, 4, 0])

А теперь возведем все его элементы в квадрат:

Warning:
Output truncated. This notebook contains too many cells to display efficiently.