В этой лекции мы вкратце познакомимся с тремя основными библиотеками для решения научных задач. Они не являются частью стандартной библиотеки, и в общем случае должны устанавливаться вручную. Однако поскольку мы использовали дистрибутив Anaconda для установки Python, нам не потребуется ничего делать дополнительно, так как он включает в себя все нужное.
Библиотека NumPy предоставляет типы и функции для вычислений с многомерными массивами. Массивом (англ. array) в программировании называется контейнер, хранящий последовательно друг за другом множество элементов. Из тех контейнеров, с которыми мы познакомились с вами в одной из предыдущих лекций, он больше всего похож на список. Основное же отличие заключается в том, что массив может хранить только значения фиксированного типа - того, который был указан при его создании. Благодаря этому ограничению можно эффективнее организовать хранение в памяти элементов массива, и добиться хорошей производительности операций, выполняющихся над всеми элементами.
Элементами многомерного массива являются другие массивы. Классический пример - матрица, представляющая собой массив строк, каждая из которых является массивом чисел.
Основным типом данных, предоставляемым библиотекой NumPy, является класс ndarray
, который описывает многомерный массив. Перечислим наиболее важные атрибуты экземпляров этого класса:
ndim
- количество измерений или, как их принято называть, осей. Например, обычная матрица имеет две оси (строки и столбцы). Оси идентифицируются своим порядковым номером, причем как и для индексов последовательностей, нумерация начинается с нуля (у матрицы строки - это нулевая ось, а столбцы - первая).shape
- форма массива. Это кортеж, который для каждой оси содержит число элементов в ней. Например, если у нас есть матрица размерности $N \times M$, то shape
будет равно (N, M)
.size
- общее количество элементов в многомерном массиве. По сути, представляет собой произведение всех элементов shape
.dtype
- объект, содержащий информацию о типе данных элементов массива.В дальнейших примерах нам часто будет требоваться выводить информацию о массиве на экран, поэтому мы определим простую функцию для этого:
import numpy as np
def print_array(a):
print('ndim={}, shape={}, size={}, dtype={}'.format(a.ndim, a.shape, a.size, a.dtype))
print(a)
Существует несколько способов создать массив. Один из них - использовать функцию array
.
# создаем одномерный массив
a = np.array([1, 2, 3])
print_array(a)
ndim=1, shape=(3,), size=3, dtype=int32 [1 2 3]
# создаем двумерный массив (матрицу)
a = np.array([[0.1, 0.2, 0.3, 0.4],
[0.5, 0.6, 0.7, 0.8]])
print_array(a)
ndim=2, shape=(2, 4), size=8, dtype=float64 [[0.1 0.2 0.3 0.4] [0.5 0.6 0.7 0.8]]
Если при создании массива не указывается тип его элементов, то функция array
в качестве него выбирает такой, чтобы можно было хранить любой элемент из перечисленных в ее вызове:
a = np.array([1, 2, 3])
print_array(a)
ndim=1, shape=(3,), size=3, dtype=int32 [1 2 3]
a = np.array([1, 2, 3.1])
print_array(a)
ndim=1, shape=(3,), size=3, dtype=float64 [1. 2. 3.1]
# явно указываем тип элементов
a = np.array([1, 2, 3], dtype=complex)
print_array(a)
ndim=1, shape=(3,), size=3, dtype=complex128 [1.+0.j 2.+0.j 3.+0.j]
Библиотека NumPy для элементов массива использует собственные типы данных, которые можно использовать так же, как соответствующие встроенные. Особенность типов данных из NumPy в том, что для них четко определено количество бит, которое они занимают в памяти. Можно при создании массива указать и тип данных из библиотеки NumPy:
# используем 16битовые целые числа для хранения элементов
a = np.array([1, 2, 3], dtype=np.int16)
print_array(a)
ndim=1, shape=(3,), size=3, dtype=int16 [1 2 3]
Важно при этом понимать, какой диапазон значений можно хранить в том или ином типе (например, для int16 это $[-32768, 32767]$), потому что если впоследствии ваша программа присвоит элементу массива значение вне этого диапазона, оно будет сохранено неправильно!
Часто бывает так, что при создании массива известная его форма, но не значения элементов. В этом случае можно воспользоваться функциями zeros
, ones
или empty
, которые заполняют созданный массив нулями, единицами или случайными значениями. В качестве первого аргумента все эти функции принимают кортеж, описывающий форму массива:
# создаем массив с 3мя осями;
# он представляет собой массив из двух массивов, каждый из
# которых содержит 3 массива, каждый из которых содержит 4 элемента
a = np.zeros((2, 3, 4))
print_array(a)
ndim=3, shape=(2, 3, 4), size=24, dtype=float64 [[[0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.]] [[0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.]]]
Обратите внимание, что по умолчанию для элементов массива используется тип float64
. С помощью именованного параметра dtype
функции zeros
и других можно указать желаемый тип элементов.
Наконец, библиотека NumPy предоставляет функцию arange
для генерации числовой последовательности, аналогичную встроенной функции range
. Отличие заключается в том, что с помощью arange
можно генерировать и последовательности чисел с плавающей точкой.
# целые из интервала [0, 10) с шагом 1
a = np.arange(10)
print_array(a)
ndim=1, shape=(10,), size=10, dtype=int32 [0 1 2 3 4 5 6 7 8 9]
# действительные из интервала [0.0, 1.0) c шагом 0.1
a = np.arange(0, 1, 0.1)
print_array(a)
ndim=1, shape=(10,), size=10, dtype=float64 [0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]
Заметим, что использовать функцию arange
для получения действительных чисел, стоит осторожно, потому что размер полученного массива может отличаться от ожидаемого (это связано с неточным представлением чисел с плавающей точкой). Более безопасной с этой точки зрения является функция linspace
, которая возвращает указанное количество равноудаленных друг от друга чисел из интервала:
# 10 равноудаленных друг от друга чисел из интервала [0, 0.9]
a = np.linspace(0, 0.9, 10)
print_array(a)
ndim=1, shape=(10,), size=10, dtype=float64 [0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]
В случае одномерных массивов, получение срезов выполняется так же, как и для обычных последовательностей (списков, кортежей и т.д.). У многомерных массивов индексироваться может каждая ось. Если при этом отсутствует индекс для некоторой оси, то возвращаются все ее элементы. Срез объекта типа ndarray
также имеет тип ndarray
.
a_multi = np.array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[9, 10, 11]])
# выводи третий элемент второй строки (помните, что нумерация
# индексов начинается с нуля!)
print(a_multi[1, 2])
5
# получаем срез, состоящий из второй и третьей строки
result = a_multi[1:3]
print(type(result))
print(result)
<class 'numpy.ndarray'> [[3 4 5] [6 7 8]]
# получаем срез, состоящий из первого и третьего элемента
# второй и четвертой строки, взятых в обратном порядке
print(a_multi[3:0:-2, 0:3:2])
[[ 9 11] [ 3 5]]
Тип ndarray
является итерируемым, поэтому его можно использовать в цикле for ... in
. Итерация при этом происходит по первой оси (например, в случае матриц - по строкам):
for row in a_multi:
print(row)
[0 1 2] [3 4 5] [6 7 8] [ 9 10 11]
Для итерации по элементам нужно использовать атрибут flat
:
for item in a_multi.flat:
print(item)
0 1 2 3 4 5 6 7 8 9 10 11
Обратите внимание, что при итерации по элементам вначале изменяется последняя ось, потом предпоследняя и т.д. Например, для трехмерного массива $N_1 \times N_2 \times N_3$ элементы извлекались бы в такой последовательности:
a[0][0][0], ..., a[0][0][N3], a[0][1][0], ..., a[0][N2][N3], a[1][0][0], ..., a[N1][N2][N3]
Поскольку ndarray
относится к изменяемым типам данных, его элементы можно модифицировать:
a_multi[0, 0] = 100 # присваиваем первому элементу 100
a_multi[1] = 0 # присваиваем всем элементам второй строки 0
a_multi[2] = np.arange(20, 23) # заменяем третью строку на строку [20, 21, 22]
print(a_multi)
[[100 1 2] [ 0 0 0] [ 20 21 22] [ 9 10 11]]
Класс ndarray
предоставляет несколько методов и атрибутов, которые изменяют форму массива (количество и размер осей):
Название |
Описание |
---|---|
reshape(shape, ...) |
Возвращает новый массив с теми же данными, но с формой shape |
resize(shape, ...) |
Изменяет форму текущего массива на shape |
ravel(...) |
Возвращает "плоскую" версию массива с одним измерением |
При заполнении нового массива, его первому элементу присваивается первый элемент старого, второму - второй и т.д. Причем порядок элементов соответствует тому, в котором они возвращались бы при итерации по всем элементам (по атрибуту flat
).
Еще класс ndarray
предоставляет метод transpose
и атрибут T
, которые позволяют получить транспонированную версию многомерного массива.
a1 = np.arange(10)
a2 = a1.reshape((2, 5))
a3 = a2.T
a4 = a3.ravel()
print_array(a1)
ndim=1, shape=(10,), size=10, dtype=int32 [0 1 2 3 4 5 6 7 8 9]
print_array(a2)
ndim=2, shape=(2, 5), size=10, dtype=int32 [[0 1 2 3 4] [5 6 7 8 9]]
print_array(a3)
ndim=2, shape=(5, 2), size=10, dtype=int32 [[0 5] [1 6] [2 7] [3 8] [4 9]]
print_array(a4)
ndim=1, shape=(10,), size=10, dtype=int32 [0 5 1 6 2 7 3 8 4 9]
Для объединения нескольких многомерных массивов в один класс ndarray
предоставляет следующие функции:
Название |
Описание |
---|---|
hstack(arrays_seq) |
Возвращает новый массив, каждая строка которого является конкатенацией строк массивов из последовательности arrays_seq |
vstack(arrays_seq) |
Возвращает новый массив, каждый столбец которого является конкатенацией столбцов массивов из последовательности arrays_seq |
a1 = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
a2 = np.array([[11, 12],
[13, 14],
[15, 16]])
a3 = np.array([[11, 12, 13],
[14, 15, 16]])
print_array(np.hstack([a1, a2]))
ndim=2, shape=(3, 5), size=15, dtype=int32 [[ 1 2 3 11 12] [ 4 5 6 13 14] [ 7 8 9 15 16]]
print_array(np.vstack((a1, a3)))
ndim=2, shape=(5, 3), size=15, dtype=int32 [[ 1 2 3] [ 4 5 6] [ 7 8 9] [11 12 13] [14 15 16]]
Для разбиения многомерного массива на несколько меньших, используются функции hsplit
и vsplit
. Если им передается числовой аргумент, то он трактуется как количество массивов, на которое нужно разбить исходный, а если последовательность, то ее элементы трактуются как индексы, по которым нужно производить разбиение:
a = np.arange(18)
a.resize(2, 9)
print_array(a)
ndim=2, shape=(2, 9), size=18, dtype=int32 [[ 0 1 2 3 4 5 6 7 8] [ 9 10 11 12 13 14 15 16 17]]
arrays = np.hsplit(a, 3)
for array in arrays:
print_array(array)
ndim=2, shape=(2, 3), size=6, dtype=int32 [[ 0 1 2] [ 9 10 11]] ndim=2, shape=(2, 3), size=6, dtype=int32 [[ 3 4 5] [12 13 14]] ndim=2, shape=(2, 3), size=6, dtype=int32 [[ 6 7 8] [15 16 17]]
arrays = np.vsplit(a.T, [2, 5, 8])
for array in arrays:
print_array(array)
ndim=2, shape=(2, 2), size=4, dtype=int32 [[ 0 9] [ 1 10]] ndim=2, shape=(3, 2), size=6, dtype=int32 [[ 2 11] [ 3 12] [ 4 13]] ndim=2, shape=(3, 2), size=6, dtype=int32 [[ 5 14] [ 6 15] [ 7 16]] ndim=2, shape=(1, 2), size=2, dtype=int32 [[ 8 17]]
Обычные операции в Python перегружены для массива и при выполнении действуют на все его элементы:
a = np.arange(12).reshape((3, 4))
print_array(a)
ndim=2, shape=(3, 4), size=12, dtype=int32 [[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]]
a += 1
print(a)
[[ 1 2 3 4] [ 5 6 7 8] [ 9 10 11 12]]
Если в качестве операндов используются два массива одинаковой размерности, то операция выполняется для всех пар элементов на соответствующих позициях, например:
a1 = np.arange(7, 3, -1).reshape(2, 2)
a2 = np.arange(4).reshape(2, 2)
print_array(a1)
print_array(a2)
ndim=2, shape=(2, 2), size=4, dtype=int32 [[7 6] [5 4]] ndim=2, shape=(2, 2), size=4, dtype=int32 [[0 1] [2 3]]
print(a1 * a2)
print(a1 ** a2)
[[ 0 6] [10 12]] [[ 1 6] [25 64]]
Если вместо поэлементного умножения вы хотите матричное, то нужно воспользоваться функцией dot
:
a1 = np.arange(6).reshape(2, 3)
a2 = np.arange(6).reshape(3, 2)
print_array(a1)
print_array(a2)
print_array(np.dot(a1, a2))
ndim=2, shape=(2, 3), size=6, dtype=int32 [[0 1 2] [3 4 5]] ndim=2, shape=(3, 2), size=6, dtype=int32 [[0 1] [2 3] [4 5]] ndim=2, shape=(2, 2), size=4, dtype=int32 [[10 13] [28 40]]
Методы sum
, max
и min
, определенные для встроенных коллекций Python, также определены и для класса ndarray
. По умолчанию они работают со всеми элементами массива, но можно также указать конкретную ось, вдоль которой производить вычисления.
a = np.arange(6).reshape(3, 2)
print_array(a)
ndim=2, shape=(3, 2), size=6, dtype=int32 [[0 1] [2 3] [4 5]]
print('max={}, min={}'.format(a.max(), a.min()))
# вычисляем по столбцам (axis=1), следовательно в результате
# получим сумму элементов для каждой строки
print(a.sum(axis=1))
max=5, min=0 [1 5 9]
В модуле numpy.random
содержатся функции для генерации случайных массивов произвольной размерности. Например, функция random
из этого модуля создает массив указанной формы и заполняет его случайными числами, равномерно распределенными в интервале $[0, 1)$:
a = np.random.random((3, 3))
print_array(a)
ndim=2, shape=(3, 3), size=9, dtype=float64 [[0.77627538 0.82548991 0.27186471] [0.85898985 0.95649778 0.16657402] [0.16242193 0.62604804 0.69467224]]
В NumPy реализованы те же математические функции, что встречаются в стандартной библиотеке Python, однако они дополнительно умеют работать с массивами. В терминологии библиотеки NumPy эти функции назваются универсальными.
a = np.array([[1, 4, 9],
[16, 25, 36]])
print(np.sqrt(a))
[[1. 2. 3.] [4. 5. 6.]]
a = np.array([0, 30, 45, 60, 90], dtype=float) # градусы
a *= np.pi / 180 # преобразуем в радианы
# вычисляем синус для обычного числа и для массива
print(np.sin(a[3]))
print(np.sin(a))
0.8660254037844386 [0. 0.5 0.70710678 0.8660254 1. ]
a = np.random.random(10)
print(a)
print('mean:',np.mean(a)) # вычисляем математическое ожидание
print('variance:', np.var(a)) # вычисляем дисперсию
[0.72009011 0.50335114 0.09270624 0.58830524 0.96991841 0.52659037 0.0770578 0.28066821 0.67652392 0.83184407] mean: 0.5267055502037943 variance: 0.08048031288469937
Библиотека Matplotlib используется для создания различных 2D и 3D графиков и диаграмм, среди которых:
Функции и типы Matplotlib умеют работать с массивами NumPy, и более того, практически никогда не используются без них. В библиотеке Matplotlib релиазовано два интерфейса - один процедурный, другой объектно-ориентированный. Мы будем использовать процедурный, который реализован в модуле matplotlib.pyplot
. Давайте импортируем модули, которые мы будем использовать в примерах в этом разделе.
import numpy as np
import matplotlib.pyplot as plt
Обычный график создается с помощью функции plot
. Основными ее параметрами являются две последовательности: в первой содержатся абсциссы точек, а во второй их ординаты. Функция plot
строит график таким образом, чтобы он проходил через эти точки. Это означает, что чем сложнее форма графика, тем больше точек нужно передать в plot
. Например, для построения прямой достаточно двух точек, но этого явно мало, чтобы правильно нарисовать параболу.
Кроме координат точек, функция plot
имеет множество других параметров для настройки того, как график будет выглядеть. В наших примерах мы познакомим вас с некоторыми из них, а полную информацию можно получить в справочном руководстве.
Давайте начнем с простого примера и создадим график прямой линии $2x + 1$:
# двух точек достаточно для того, чтобы построить прямую
x = [0, 2] # ординаты
y = [1, 5] # абсциссы
plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x8970f98>]
Заметим, что сама функция plot
не рисует график, а просто инициализирует нужную информацию для этого. В среде Jupyter Notebook по умолчанию используется режим, при котором вывод графика на экран происходит автоматически при выполнении ячейки, но вообще говоря, за это отвечает отдельная функция show
. Если вы при работе с Matplotlib столкнетесь с ситуацией, когда график не отображается, в первую очередь стоит попробовать вызвать эту функцию.
Давайте немного иначе настроим наш график, чтобы продемонстрировать некоторые возможности Matplotlib:
x = np.array([0, 2])
y = np.array([1, 5])
# задаем дополнительно цвет линии и толщину
plt.plot(x, y, color='red', linewidth=2.5)
# установим границы осей X и Y так, чтобы график не
# выглядел "зажатым" осями координат
plt.xlim(x.min() - 0.5, x.max() + 0.5)
plt.ylim(y.min() - 0.5, y.max() + 0.5)
# определим, какие точки будут отмечены на осях
x_ticks = np.linspace(x.min(), x.max(), 2)
y_ticks = np.linspace(y.min(), y.max(), 2)
plt.xticks(x_ticks)
plt.yticks(y_ticks)
# определим, как будут подписаны координатные оси
plt.xlabel('abscissa')
plt.ylabel('ordinate')
# рисуем график
plt.show()
Можно в одной системе координат отобразить сразу несколько графиков. Давайте попробуем вывести графики синуса и косинуса.
x = np.linspace(-np.pi, np.pi, 200)
cos_x = np.cos(x)
sin_x = np.sin(x)
# добавляем два графика, устанавливаем для них цвет, тип линии
# и название
plt.plot(x, cos_x, color='red', linestyle='dashed', label='cosine')
plt.plot(x, sin_x, color='green', linestyle='dotted', label='sine')
# добавляем легеду (информацию о том, какая линия что означает)
plt.legend()
# определяем, какие точки будут отмечены на осях
# можно в качестве значения указывать LaTeX-формулу (используем
# это для того, чтобы вместо, например, 3.1415.. было написано pi)
x_ticks = np.array([-np.pi, -np.pi/2, 0, np.pi/2, np.pi])
x_ticks_name = [r'$-\pi$', r'$-\pi/2$', r'$0$', r'$\pi/2$', r'$\pi$']
y_ticks = np.array([-1, -0.5, 0, 0.5, 1])
y_ticks_name = [r'$-1$', r'$-0.5$', r'$0$', r'$0.5$', r'$1$']
plt.xticks(x_ticks, x_ticks_name)
plt.yticks(y_ticks, y_ticks_name)
# рисуем график
plt.show()
Теперь давайте попробуем настроить координатные оси таким образом, чтобы центр графика находился в точке $(0, 0)$, как мы привыкли со школы. Для этого нам потребуется метод gca
, возвращающий объект, который можно использовать для управления внешним видом координатных осей. На рисунке сверху есть четыре оси, образующих прямоугольник, внутри которого находятся графики. Нам нужно скрыть две из них (например, ту, что сверху и ту, что справа), а оставшиеся две поместить в точку $(0, 0)$:
x = np.linspace(-np.pi, np.pi, 200)
cos_x = np.cos(x)
sin_x = np.sin(x)
# добавляем два графика, устанавливаем для них цвет, тип линии
# и название
plt.plot(x, cos_x, color='red', linestyle='dashed', label='cosine')
plt.plot(x, sin_x, color='green', linestyle='dotted', label='sine')
# добавляем легеду (информацию о том, какая линия что означает)
plt.legend()
# определяем, какие точки будут отмечены на осях
# можно в качестве значения указывать LaTex-формулу (используем
# это для того, чтобы вместо, например, 3.1415.. было написано pi)
x_ticks = np.array([-np.pi, -np.pi/2, 0, np.pi/2, np.pi])
x_ticks_name = [r'$-\pi$', r'$-\pi/2$', r'$0$', r'$\pi/2$', r'$\pi$']
y_ticks = np.array([-1, -0.5, 0, 0.5, 1])
y_ticks_name = [r'$-1$', r'$-0.5$', r'$0$', r'$0.5$', r'$1$']
plt.xticks(x_ticks, x_ticks_name)
plt.yticks(y_ticks, y_ticks_name)
# меняем положение координатных осей
axes = plt.gca()
# скрываем две оси
axes.spines['top'].set_color(None)
axes.spines['right'].set_color(None)
# устаналиваем позицию левой и правой оси;
# data' означает, что 0 - это координата, через которую должна проходить ось
axes.spines['left'].set_position(('data', 0))
axes.spines['bottom'].set_position(('data', 0))
# рисуем график
plt.show()
Диаграмма разброса используется тогда, когда нужно оценить корреляцию между несколькими факторами. Для примера, в качестве одного фактора мы возьмем просто случайные, ничего не означающие данные. Второй фактор мы получим как сумму первого и некоторой случайной величины с достаточно большой дисперсией. Очевидно, что между этими двумя значениями мы должны увидеть положительную корреляцию - с ростом одного, увеличивается и второе. Проверим это на практике:
from random import uniform
from random import normalvariate
# используем генератор, чтобы создать массив случайных данных
x = np.array([uniform(0, 100) for i in range(1000)])
y = np.array([item + normalvariate(0, 20) for item in x])
# строим диаграмму разброса
plt.scatter(x, y)
plt.show()
Несмотря на то, что мы выбрали большое значение стандартного отклонения для случайной величины, с помощью которой мы оказывали влияние на первый фактор, положительная корреляция видна невооруженным глазом: с ростом $x$ возрастает $y$. Если стандартное отклонение продолжить увеличивать, то в определенный момент корреляция, очевидно, пропадет. Убедитесь в этом сами.
Гистограммы часто используются для того, чтобы сделать предположение о том, какому распределению подчиняется случайная выборка: по выборке строится гистограмма, а затем сравнивается, плотность распределения какого закона она напоминает больше всего. В качестве примера, рассмотрим выборку из нормального закона распределения, но вообразим, что нам это неизвестно:
from math import sqrt
from random import normalvariate
def norm_density(x, mean, std):
tmp1 = 1 / (std * np.sqrt(2 * np.pi))
tmp2 = -((x - mean)**2 / 2 * std**2)
return tmp1 * (np.e ** tmp2)
x = np.array([normalvariate(3, 1) for i in range(10000)])
# добавляем гистограмму на график (bins - количество столбиков в ней)
plt.hist(x, bins=15, density=True)
# добавим еще для наглядности график плотности распределения
x_mean = np.mean(x)
x_std = np.std(x) # стандартное отклонение
density_x = np.linspace(x.min(), x.max(), 200)
density_y = np.array([norm_density(item, x_mean, x_std) for item in density_x])
plt.plot(density_x, density_y, linewidth=2.5, color='red')
# рисуем графики
plt.show()
Как видите, гистограмма, построенная по выборке из "неизвестной" случайной величины, по форме похожа на плотность нормального распределения. Исходя из этого можно сделать вывод, что "неизвестная" случайная величина имеет закон распределения, близкий к нормальному.
В заключение дадим очень полезную ссылку на раздел документации библиотеки Matplotlib, по которой можно найти большое количество примеров графиков вместе с исходным кодом, строящим их. Примеры оттуда позволяют увидеть весь спектр возможностей Matplotlib.
Библиотека pandas предоставляет классы для быстрой обработки и анализа больших объемов данных. В своей реализации она использует библиотеку NumPy, с которой мы познакомились чуть выше. Двумя важнейшими классами библиотеки pandas являются Series
и DataFrame
. Оба они представляют собой массивы, элементам которых назначены специальные метки (англ. label), в совокупности образующие индекс этого массива. Термин "индекс" из pandas пересекается с тем, что мы использовали для обозначения позиции элемента в последовательности. Обычно путаницы из-за этого не возникает, но вам стоит иметь это в виду.
Все элеметы массивов pandas приводятся к одному и тому же типу данных (используются типы из библиотеки NumPy).
Перед тем как начать использование библиотеки pandas, подключим необходимый модуль:
import numpy as np
import pandas as pd
Series
представляет собой изменяемый одномерный массив, к каждому элементу которого прикреплена произвольная метка. Объект класса Series
можно создать несколькими способами:
# из обычного списка
s = pd.Series([1, 2, 3, 4, 5])
print(s)
0 1 1 2 2 3 3 4 4 5 dtype: int64
# из ndarray
s = pd.Series(np.random.random((3)))
print(s)
0 0.825980 1 0.605343 2 0.690893 dtype: float64
Обратите внимание на первый столбец в выведенных на экран объектах Series
- это и есть их индекс. Обратиться к нему можно с помощью атрибута index
:
print(s.index)
RangeIndex(start=0, stop=3, step=1)
По умолчанию индекс представляет собой последовательно возрастающие от нуля числа, но это легко можно изменить, явно указав метки:
s = pd.Series([1, 2, 3], index=['first', 'second', 'third'])
print(s.index)
print(s)
Index(['first', 'second', 'third'], dtype='object') first 1 second 2 third 3 dtype: int64
Одномерный массив можно создать с помощью словаря Python, при этом ключи становятся метками, а значения - элементами:
s = pd.Series({'a':100, 'b':200, 'c':300})
print(s)
a 100 b 200 c 300 dtype: int64
Объекты класса Series
могут использовать как ndarray
или как dict
:
s = pd.Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 'd', 'e'])
# получаем элемент по его позиции, как для ndarray
print(s[1])
# обращаемся к элементу по метке, как для dict
print(s['e'])
# определяем, есть ли метка в объекте Series
print('c' in s)
2 5 True
# получаем срез, как для ndarray;
# срез объекта Series тоже имеет тип Series
print(s[3:1:-1])
d 4 c 3 dtype: int64
Бинарные операции для одномерных массивов pandas работают так же, как и для массивов NumPy (применяются для каждого элемента). Универсальные функции NumPy могут в качестве аргумента принимать объекты Series
:
s1 = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
s2 = pd.Series([0.1, 0.2, 0.3], index=['a', 'b', 'c'])
print(s1, '\n')
print(s2)
a 1 b 2 c 3 dtype: int64 a 0.1 b 0.2 c 0.3 dtype: float64
print(s1 * 3, '\n')
print(s1 + s2, '\n')
print(np.exp(s1))
a 3 b 6 c 9 dtype: int64 a 1.1 b 2.2 c 3.3 dtype: float64 a 2.718282 b 7.389056 c 20.085537 dtype: float64
Особо стоит отметить случай, когда бинарная операция выполняется для массивов Series
, имеющих разные метки. В этом случае происходит следующее: если метка есть в обоих массивах, то операция выполняется и ее результат становится значением элемента в новом объекте, иначе - в новый объект записывается специальная константа Nan
, которая трактуется как отсутствие значения.
s2.index = ['x', 'a', 'c'] # меняем индекс у объекта s2
print(s2)
x 0.1 a 0.2 c 0.3 dtype: float64
result = s1 - s2
print(result)
a 0.8 b NaN c 2.7 x NaN dtype: float64
Любые бинарные арифметические операции со значениями Nan
будут давать Nan
:
print(result + 1)
a 1.8 b NaN c 3.7 x NaN dtype: float64
Объекты типа Series
поддерживают обращение сразу к нескольким элементам:
s = pd.Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 'd', 'e'])
print(s[['a', 'd', 'e']], '\n')
# изменяем сразу несколько элементов
s[['c', 'd']] = 0
print(s)
a 1 d 4 e 5 dtype: int64 a 1 b 2 c 0 d 0 e 5 dtype: int64
Объекты типа Series
можно сравнивать между собой и со скалярами. Сравнения, как и другие бинарные операции, выполняются для каждого элемента отдельно. Результатом становится новый объект, у которого в i-ой позиции стоит True
, если сравнение соответствующего элемента дало истину:
s1 = pd.Series([10, 5, 7])
s2 = pd.Series([1, 12, 3])
print(s1 > s2, '\n')
print(s2 == 3)
0 True 1 False 2 True dtype: bool 0 False 1 False 2 True dtype: bool
С помощью массива, состоящего из булевых элементов, можно отфильтровать элементы другого массива той же размерности. При этом в результирующий объект Series
попадают только элементы, для которых в соответствующей позиции фильтрующего массива находится True
:
print(s1[s1 > s2])
0 10 2 7 dtype: int64
В заключение скажем, что объекты класса Series
являются итерируемыми, т.е. могут использоваться в цикле for ... in
:
s = pd.Series({'aaa':0.1, 'bbb':0.2, 'ccc':0.3})
for item in s:
print(item)
0.1 0.2 0.3
# так выполняется итерация по меткам:
for item in s.index:
print(item)
aaa bbb ccc
Класс DataFrame
представляет двумерный изменяемый массив (матрицу), столбцами которого являются одномерные массивы Series
. С помощью атрибутов index
и columns
задаются метки для строк и столбцов. Как и для типа Series
, объекты DataFrame
можно создавать несколькими способами:
# из словаря Series (ключи словаря становятся метками столбцов,
# индексы Series объединяются и образуют индекс для строк)
df = pd.DataFrame({'col1': pd.Series([1, 2, 3], ['row1', 'row2', 'row3']),
'col2': pd.Series([0.1, 0.2, 0.3, 0.4], ['row1', 'row2', 'row5', 'row6'])})
print(df)
print(df.index)
print(df.columns)
col1 col2 row1 1.0 0.1 row2 2.0 0.2 row3 3.0 NaN row5 NaN 0.3 row6 NaN 0.4 Index(['row1', 'row2', 'row3', 'row5', 'row6'], dtype='object') Index(['col1', 'col2'], dtype='object')
# из словаря ndarray/list (ключи словаря становятся метками
# столбцов, для строк используется индекс по умолчанию)
df = pd.DataFrame({'c1': [1, 2, 3], 'c2':[4, 5, 6]})
df.index = ['r1', 'r2', 'r3']
print(df)
c1 c2 r1 1 4 r2 2 5 r3 3 6
Семантически объект DataFrame
может рассматриваться как словарь, ключом в котором являются метки столбцов, а значением - соответствующие объекты Series
. Операции, которые мы рассматривали для словаря, схожим образом выполняются и для объектов DataFrame
:
df = pd.DataFrame({'c1': pd.Series(np.random.random(3), ['r1', 'r2', 'r3']),
'c2': pd.Series(np.random.random(4), ['r1', 'r2', 'r3', 'r4']),
'c3': pd.Series(np.random.random(3), ['r1', 'r3', 'r4'])})
print(df)
c1 c2 c3 r1 0.308978 0.940771 0.988228 r2 0.778484 0.472404 NaN r3 0.570186 0.831688 0.568123 r4 NaN 0.513793 0.575264
# получение конкретного элемента
print(df['c1']['r1'], '\n')
# получение нескольких элементов столбца
print(df['c3'][['r1', 'r2']], '\n')
# получение столбца целиком
print(df['c2'])
0.30897794183961025 r1 0.988228 r2 NaN Name: c3, dtype: float64 r1 0.940771 r2 0.472404 r3 0.831688 r4 0.513793 Name: c2, dtype: float64
Очень удобной является возможность обращаться к столбцам и строкам как к атрибутам. Например, вот так мы можем переписать предыдущий блок кода:
print(df.c1.r1, '\n')
print(df.c3[['r1', 'r2']], '\n')
print(df.c2)
0.30897794183961025 r1 0.988228 r2 NaN Name: c3, dtype: float64 r1 0.940771 r2 0.472404 r3 0.831688 r4 0.513793 Name: c2, dtype: float64
# добавление и удаление столбца
df['c4'] = pd.Series(np.random.random(2), index=['r2', 'r4'])
print(df, '\n')
del df['c4']
print(df)
c1 c2 c3 c4 r1 0.308978 0.940771 0.988228 NaN r2 0.778484 0.472404 NaN 0.461306 r3 0.570186 0.831688 0.568123 NaN r4 NaN 0.513793 0.575264 0.369241 c1 c2 c3 r1 0.308978 0.940771 0.988228 r2 0.778484 0.472404 NaN r3 0.570186 0.831688 0.568123 r4 NaN 0.513793 0.575264
Существуют также удобные способы для обращения к строкам. В результате возвращаются объекты Series
, индекс которых состоит из меток столобцов объекта DataFrame
.
# получаем строку по метке и по позиции
print(df.loc['r2'], '\n')
print(df.iloc[1])
c1 0.778484 c2 0.472404 c3 NaN Name: r2, dtype: float64 c1 0.778484 c2 0.472404 c3 NaN Name: r2, dtype: float64
# получаем сразу несколько строк
print(df.loc[['r1', 'r3']], '\n')
# получаем несколько строк и несколько столбцов
print(df.loc[['r2', 'r3'], ['c1', 'c2']])
c1 c2 c3 r1 0.308978 0.940771 0.988228 r3 0.570186 0.831688 0.568123 c1 c2 r2 0.778484 0.472404 r3 0.570186 0.831688
Наконец, можно получить целый набор строк объекта DataFrame
одним из следующих способов (в результате получается новый объект DataFrame
, как и следовало ожидать):
# используем операцию взятия среза
df2 = df[3:1:-1]
print(type(df2))
print(df2)
<class 'pandas.core.frame.DataFrame'> c1 c2 c3 r4 NaN 0.513793 0.575264 r3 0.570186 0.831688 0.568123
# используем специфическую для массивов pandas операцию
print(df.loc[['r1', 'r2']])
c1 c2 c3 r1 0.308978 0.940771 0.988228 r2 0.778484 0.472404 NaN
С таблицами, можно выполнять арифметические операции, которые реализованы по тому же принципу, что и для объектов Series
:
df1 = pd.DataFrame({'c1': pd.Series([0, 1, 2], ['r1', 'r2', 'r3']),
'c2': pd.Series([3, 4, 5], ['r1', 'r2', 'r3'])})
df2 = pd.DataFrame({'c1': pd.Series([0.0, 0.1, 0.2], ['r1', 'r2', 'r3']),
'c2': pd.Series([0.3, 0.4, 0.5], ['r1', 'r2', 'r4']),
'c3': pd.Series([0.6, 0.7, 0.8], ['r1', 'r2', 'r4'])})
s = pd.Series(np.ones((3)), index=['c1', 'c2', 'c3'])
print(df1, '\n')
print(df2, '\n')
print(s)
c1 c2 r1 0 3 r2 1 4 r3 2 5 c1 c2 c3 r1 0.0 0.3 0.6 r2 0.1 0.4 0.7 r3 0.2 NaN NaN r4 NaN 0.5 0.8 c1 1.0 c2 1.0 c3 1.0 dtype: float64
# арифметическая операция со скаляром
print(df1 + 1)
c1 c2 r1 1 4 r2 2 5 r3 3 6
# арифметическая операция с двумя матрицами (результат состоит
# из объединения строк и столбцов операндов, значение Nan вставляется
# в позиции, где отсутствует элемент в одном из операндов)
print(df1 * df2)
c1 c2 c3 r1 0.0 0.9 NaN r2 0.1 1.6 NaN r3 0.4 NaN NaN r4 NaN NaN NaN
# арифметическая операция с объектом Series (выполняется по строкам,
# т.е. в индексе объекта Series должны быть указаны метки столбцов,
# для которых нужно применить операцию)
print(df2 - s)
c1 c2 c3 r1 -1.0 -0.7 -0.4 r2 -0.9 -0.6 -0.3 r3 -0.8 NaN NaN r4 NaN -0.5 -0.2
Покажем, как можно создать новый объект DataFrame
, применив фильтрацию к элементам существующего:
result = df2[df2.c2 > 0.3]
print(result)
c1 c2 c3 r2 0.1 0.4 0.7 r4 NaN 0.5 0.8
В заключение скажем, что объекты DataFrame
являются итерируемыми, и их можно использовать в циклах for ... in
. При этом стоит отметить, что итерация по массивам pandas выполняется медленно, и лучше ее избегать (например, подобрав нужную функцию, которая сделает все сама).
В pandas существует большое количество функций и методов, предназначенных для получения различных статистических данных об одномерных и двумерных массивах. В этом разделе мы рассмотрим некоторые из них.
s = pd.Series(np.random.random(5))
df = pd.DataFrame({'c1': pd.Series(np.random.random(3), index=['r1', 'r2', 'r3']),
'c2': pd.Series(np.random.random(3), index=['r1', 'r2', 'r3'])})
print(s, '\n')
print(df)
0 0.802763 1 0.881109 2 0.604589 3 0.606030 4 0.453705 dtype: float64 c1 c2 r1 0.679956 0.461280 r2 0.697153 0.006743 r3 0.152008 0.663765
# статистика для Series
print('sum={}, prod={}'.format(\
s.sum(), s.prod()))
print('mean={}, var={}, std={}'.format(\
s.mean(), s.var(), s.std()))
sum=3.3481964332404925, prod=0.11758321064167483 mean=0.6696392866480985, var=0.02933662862069077, std=0.1712793876118512
# статистика для DataFrame (методы все те же)
# сумма элементов по столбцам
print(df.sum(), '\n')
# сумма элементов по строкам
print(df.sum(1))
c1 1.529117 c2 1.131789 dtype: float64 r1 1.141236 r2 0.703896 r3 0.815773 dtype: float64
# основная статистика для DataFrame (для Series тот же метод)
print(df.describe())
c1 c2 count 3.000000 3.000000 mean 0.509706 0.377263 std 0.309894 0.336472 min 0.152008 0.006743 25% 0.415982 0.234012 50% 0.679956 0.461280 75% 0.688554 0.562523 max 0.697153 0.663765
На этом мы заканчиваем рассмотрение библиотеки pandas. Конечно, это лишь малая часть ее возможностей, однако, имея представление о работе с главными типами данных Series
и DataFrame
, можно легко продолжать самостоятельное изучение.