#!/usr/bin/env python # coding: utf-8 # # Основы программирования в 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) # Чем хороши массивы `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 # In[4]: A = np.array([1, 2, 4, 0]) A # Как видно из примера выше, список значений можно просто вписать в `array()`. Главное не забыть квадратные скобки: Python не сможет склеить перечень элементов в список самостоятельно и выдаст ошибку: # In[5]: A = np.array(1, 2, 4, 0) # Посмотрим, какую информацию о массиве можно получить. Например, тип его элементов: # In[6]: A.dtype # integer # Число измерений ‒ число "маленьких" массивов внутри "большого" массива (здесь такой один). # In[7]: A.ndim # "Форма" массива, о котором можно думать как о размерности матрицы ‒ кортеж, включающий число строк и столбцов. Здесь у нас всего одна строка, поэтому `numpy` считает только число элементов внутри массива. # In[8]: A.shape # Так как массив `A` одномерный, обращаться к его элементам можно так же, как и к элементам списка, указывая индекс элемента в квадратных скобках: # In[9]: A[0] # Попытка использовать двойной индекс приведет к неудаче: # In[10]: A[0][0] # index error # Общее число элементов в массиве можно получить с помощью метода `size` (аналог `len()` для списков): # In[11]: A.size # Кроме того, по массиву можно получить разные описательные статистики: # In[12]: A.max() # максимум # In[13]: A.min() # минимум # In[14]: A.mean() # среднее # О других полезных методах можно узнать, нажав *Tab* после `np.`. # Наконец, массив `numpy` можно легко превратить в список: # In[15]: A.tolist() # А теперь перейдем к многомерным массивам. # ### Многомерные массивы # Создадим многомерный массив, взяв за основу вложенный список: # In[16]: S = np.array([[8, 1, 2], [2, 8, 9]]) # In[17]: S # Посмотрим на число измерений: # In[18]: S.ndim # два массива внутри # In[19]: S.shape # две строки (два списка) и три столбца (по три элемента в списке) # Общее число элементов в массиве (его длина): # In[20]: S.size # Когда в массиве больше одного измерения, при различных операциях нужно указывать, по какому измерению мы движемся (по строкам или по столбцам). Посмотрим еще раз на массив S и подумаем о нем как о матрице, как о таблице с числами: # In[21]: S # Можно найти максимальное значение по строкам или столбцам S: # In[22]: S.max(axis=0) # по столбцам - три столбца и три максимальных значения # In[23]: S.max(axis=1) # по строкам - две строки и два максимальных значения # In[24]: S.mean(axis=0) # In[25]: S.mean(axis=1) # Для того, чтобы обратиться к элементу двумерного массива, нужно указывать два индекса: сначала индекс массива, в котором находится нужный нам элемент, а затем индекс элемента внутри этого массива: # In[26]: S[0][0] # In[27]: S[1][2] # Если мы оставим один индекс, мы просто получим массив с соответствующим индексом: # In[28]: S[0] # Массивы ‒ изменяемые объекты в Python. Обращаясь к элементу массива, ему можно присвоить новое значение: # In[29]: S[1][2] = 6 S # Чтобы выбрать сразу несколько элементов, как и в случае со списками, можно использовать срезы. Рассмотрим массив побольше. # In[30]: T = np.array([[1, 3, 7], [8, 10, 1], [2, 8, 9], [1, 0, 5]]) # In[31]: T # Как и при выборе среза из списка, правый конец не включается: # In[32]: T[0:2] # массивы с индексами 0 и 1 # Можно сделать что-то еще более интересное ‒ выставить шаг среза. Другими словами, сообщить Python, что нужно брать? например, элементы, начиная с нулевого, с шагом 2: элемент с индексом 0, с индексом 2, с индексом 4, и так до конца массива. # In[33]: T[0::2] # старт, двоеточие, двоеточие, шаг # В примере выше совершенно логично были выбраны элементы с индексами 0 и 2. # ### Как создать массив? # **Способ 1** # С первым способом мы уже отчасти познакомились: можно получить массив из готового списка, воспользовавшись функцие `array()`: # In[34]: np.array([10.5, 45, 2.4]) # Кроме того, при создании массива из списка можно изменить его форму, используя функцию `reshape()`. # In[35]: old = np.array([[2, 5, 6], [9, 8, 0]]) old # In[36]: old.shape # 2 на 3 # In[37]: new = old.reshape(3, 2) # изменим на 3 на 2 new # In[38]: new.shape # 3 на 2 # Конечно, такие преобразования разумно применять, если произведение чисел в `reshape()` совпадает с общим числом элементов в массиве. В нашем случае в массиве `old` 6 элементов, поэтому из него можно получить массивы 2 на 3, 3 на 2, 1 на 6, 6 на 1. Несоответствующее число измерений приведет к ошибке: # In[39]: old.reshape(2, 4) # и Python явно пишет, что не так # **Способ 2** # # Можно создать массив на основе промежутка, созданного с помощью`arange()` ‒ функции из `numpy`, похожей на `range()`, только более гибкую. Посмотрим, как работает эта функция. # In[40]: np.arange(2, 9) # по умолчанию - как обычный range() # По умолчанию эта функция создает массив, элементы которого начинаются со значения 2 и заканчиваются на значении 8 (правый конец промежутка не включается), следуя друг за другом с шагом 1. Но этот шаг можно менять: # In[41]: np.arange(2, 9, 3) # с шагом 3 # И даже делать дробным! # In[42]: np.arange(2, 9, 0.5) # А теперь совместим `arange()` и `reshape()`, чтобы создать массив нужного вида: # In[43]: np.arange(2, 9, 0.5).reshape(2, 7) # Получилось! # **Способ 3** # Еще массив можно создать совсем с нуля. Единственное, что нужно четко представлять ‒ это его размерность, его форму, то есть опять же, число строк и столбцов. Библиотека `numpy` позволяет создать массивы, состоящие из нулей или единиц, а также "пустые" массивы (на самом деле, не совсем пустые, как убедимся позже). Удобство заключается в том, что сначала можно создать массив, инициализировать его (например, заполнить нулями), а затем заменить нули на другие значения в соответствии с требуемыми условиями. Как мы помним, массивы ‒ изменяемые объекты, и использовать замену в цикле еще никто не запрещал. # # Так выглядит массив из нулей: # In[44]: Z = np.zeros((3, 3)) # размеры в виде кортежа - не теряйте еще одни круглые скобки Z # А так ‒ массив из единиц: # In[45]: O = np.ones((4, 2)) O # С пустым (*empty*) массивом все более загадочно: # In[46]: Emp = np.empty((3, 2)) Emp # Массив *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 # ### Почему массивы `numpy` ‒ это удобно? # Как уже было отмечено в начале занятия, операции с массивами можно производить поэлементно, не используя циклы или их аналоги. Посмотрим на массив `A`: # In[49]: A # А теперь возведем все его элементы в квадрат: # In[50]: A ** 2 # Или вычтем из всех элементов единицу: # In[51]: A - 1 # Кроме того, так же просто к элементам массива можно применять свои функции. Напишем функцию, которая будет добавлять к элементу 1, а затем считать от него натуральный логарифм (здесь эта функция актуальна, так как в массиве `A` есть 0). # In[52]: def my_log(x): return np.log(x + 1) # Применим: # In[53]: my_log(A) # И никаких циклов и иных нагромождений. # Превратить многомерный массив в одномерный (как список) можно, воспользовавшись методами `flatten()` и `ravel()`. # In[55]: ages.flatten() # "плоский" массив # In[56]: ages.ravel() # ### Чем еще хорош `numpy`? # 1.Позволяет производить вычисления ‒ нет необходимости дополнительно загружать модуль `math`. # In[57]: np.log(3) # натуральный логарифм # In[58]: np.sqrt(7) # квадратный корень # In[59]: np.exp(2) # e^2 # 2.Позволяет производить операции с векторами и матрицами. Пусть у нас есть два вектора `a` и `b`. # In[60]: a = np.array([1, 2, 3]) b = np.array([0, 4, 7]) # Если мы просто умножим `a` на `b` с помощью символа `*`, мы получим массив, содержащий произведения соответствующих элементов `a` и `b`: # In[61]: a * b # А если мы воспользуемся функцией `dot()`, получится [скалярное произведение](https://ru.wikipedia.org/wiki/%D0%A1%D0%BA%D0%B0%D0%BB%D1%8F%D1%80%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B8%D0%B7%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5) векторов (*dot product*). # In[62]: np.dot(a, b) # результат - число # При желании можно получить [векторное произведение](https://ru.wikipedia.org/wiki/%D0%92%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B8%D0%B7%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5) (*cross product*): # In[63]: np.cross(a, b) # результат- вектор # Теперь создадим матрицу и поработаем с ней. Создадим ее не самым интуитивным образов ‒ из строки (да, так тоже можно). # In[64]: m = np.array(np.mat('2 4; 1 6')) # np.mat - матрица из строки, np.array - массив из матрицы # Самое простоеи понятное, что можно сделать с матрицей ‒ транспонировать ее, то есть поменять местами строки и столбцы: # In[65]: m.T # Можно вывести ее диагональные элементы: # In[66]: m.diagonal() # И посчитать след матрицы ‒ сумму ее диагональных элементов: # In[67]: m.trace() # **Задание.** Создайте [единичную матрицу](https://ru.wikipedia.org/wiki/%D0%95%D0%B4%D0%B8%D0%BD%D0%B8%D1%87%D0%BD%D0%B0%D1%8F_%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86%D0%B0) 3 на 3, создав массив из нулей, а затем заполнив ее диагональные элементы значениями 1. # # *Подсказка:* функция `fill_diagonal()`. # *Решение:* # In[70]: I = np.zeros((3, 3)) np.fill_diagonal(I, 1) I # Правда, для создания массива в виде единичной матрицы в `numpy` уже есть готовая функция (наряду с `zeros` и `ones`): # In[71]: np.eye(3) # Найдем [обратную матрицу](https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%80%D0%B0%D1%82%D0%BD%D0%B0%D1%8F_%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86%D0%B0): # In[72]: np.invert(m) # Для других операций с матрицами (и вычислений в рамках линейной алгебры) можно использовать функции из подмодуля `linalg`. Например, так можно найти определитель матрицы: # In[73]: np.linalg.det(m) # вспоминаем истории про числа с плавающей точкой, это 8 на самом деле # И собственные значения: # In[74]: np.linalg.eigvals(m) # Полный список функций с описанием см. в [документации](https://docs.scipy.org/doc/numpy-1.13.0/reference/routines.linalg.html). # 3.Библиотеку `numpy` часто используют с библиотекой для визуализации `matplotlib`. # # Рассмотрим функцию `linspace()`. Она возвращает массив одинаково (с одинаковым шагом) распределенных чисел из фиксированного интервала. # In[76]: x = np.linspace(10, 100, 5) # 5 чисел из интервала от 10 до 100 x # In[78]: x = np.linspace(0, 1000, 100) x # При чем тут `matplotlib`? Представьте, что нам нужно построить обычный график функции $y=x^2$. Если в наши задачи не входит построение графика по определенным данным, можно спокойно создать массив `x` с помощью `linspace`, а затем просто возвести его в квадрат! И нанести полученные точки на график. # In[79]: y = x ** 2 y # Подробнее этот пример мы рассмотрим позже, когда будем работать с `matplotlib`. # 4.Еще `numpy` можно использовать в статистике. Например, чтобы посчитать выборочную дисперсию, стандартное отклонение, медиану. Важный момент: здесь `numpy` чем-то похож на R, он не сможет выдать результат в случае, если в массиве присутствуют пропущенные значения. Проверим. # In[81]: q = np.array([1., 0., 4.5, np.nan, 3.]) # np.nan - Not a number (пропущенное значение) q # In[85]: np.var(q) # получаем nan вместо дисперсии # In[84]: np.nanvar(q) # если функция начинается с nan, все работает # Аналогичная история с медианой: # In[86]: np.median(q) # In[87]: np.nanmedian(q) # Еще с помощью `numpy` можно считать выборочный коэффициент корреляции Пирсона. Функция `corrcoef()` возвращает корреляционную матрицу, из которой можно извлечь коэффициент линейной корреляции. # In[88]: x = np.array([2, 6, 8, 10, 12]) y = np.array([4, 7, 14, 21, 19]) np.corrcoef(x, y) # In[89]: np.corrcoef(x, y)[0][1] # **Внимание:** не путайте функцию `corrcoef()` с `correlate()`! # In[90]: np.correlate(x, y) # Функция `correlate()` используется для нахождения кросс-корреляции. # In[91]: help(np.correlate)