Картиной можно выразить тысячу слов. В случае с библиотекой Python matplotlib, к счастью, понадобится намного меньше слов в коде для создания качественных графиков.
Однако, matplotlib это еще и массивная библиотека, и создание графика, который будет выглядеть «просто, нормально» обычно проходит через путь проб и ошибок. Использование однострочных линий для создания базовых графиков в matplotlib – весьма просто, но умело пользоваться остальными 98% библиотеки может быть сложно.
Содержание:
- Почему Matplotlib может быть сложным?
- Pylab: что это и нужно ли мне это?
- Иерархия объектов Matplotlib
- Структурированные и неструктурированные подходы
- Понимание нотации plt.subplots()
- «Фигуры» за кулисами
- Игра красок: imshow() и matshow()
- Построение графиков в Pandas
- Подведение итогов
- Дополнительные ресурсы
- Приложение А: Конфигурация и стилизация
- Приложение Б: Интерактивный режим
Эта статья – руководство для пользователей Python на начальном-среднем уровне по matplotlib, с использованием как теории, так и практических примеров. Обучение по практическим примерам может быть очень продуктивным, и дает возможность получить представление даже на поверхностном уровне понимания внутренней работы и макета библиотеки.
Что мы рассмотрим?
- Pylab и pyplot: кто есть кто?
- Ключевые концепции дизайна matplotlib;
- Понимание plt.subplots();
- Визуализация массивов при помощи matplotlib;
- Построение графиков с комбинацией pandas и matplotlib.
Эта статья подразумевает, что пользователь имеет хотя-бы минимальное представление о NumPy. Мы в основном будем пользоваться модулем numpy.random для создания «игрушечных» данных, рисовать примеры из различных статистических источников.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Если у вас еще не установлен matplotlib, рекомендуем ознакомиться с руководством по установке, перед тем как продолжить.
1 2 |
python -mpip install -U pip python -mpip install -U matplotlib |
Почему Matplotlib может быть сложным?
Изучение matplotlib временами может быть тяжелым процессом. Проблема не в нехватке документации (которая весьма обширная, между прочим). Сложности могут возникнуть со следующим:
- Размер библиотеки огромный сам по себе, около 70 000 строк кода;
- Matplotlib содержит несколько разных интерфейсов (способов построения фигуры) и может взаимодействовать с большим количеством бекендов. (Бекенды отвечают за то, как по факту будут отображаться диаграммы, не только за внутреннюю структуру);
- Несмотря на обширность, часть собственной документации matplotlib серьезно устарела. Библиотека все еще развивается, и множество старых примеров в сети могут включать на 70% меньше кода, чем в их современной версии;
Так что, перед тем как мы перейдем к сложным примерам, не помешает освоить корневые концепции дизайна matplotlib.
Pylab: что это и нужно ли мне это?
Немножко истории: Нейробиолог Джон Д. Хантер начал разрабатывать matplotlib в 2003 году, в основном вдохновляясь эмуляцией команд программного обеспечения Mathworks MATLAB. Джон отошел в мир иной трагически рано, в возрасте 44 лет в 2012 году, и matplotlib на сегодняшний день является целиком и полностью продуктом сообщества: развивается и поддерживается множеством людей. (Джон говорил об эволюции matplotlib на конференции SciPy в 2012, которую однозначно стоит посмотреть.)
Одной из важных особенностей MATLAB является его глобальный стиль. Концепция импорта Python не сильно используется в MATLAB, и большинство функций MATLAB легко доступны для пользователя на верхнем уровне.
Заказать свой собственный уникальный номер можно от Сим-Трейд.ру. Быстрая доставка в день заказа и красивые номера начиная от 300 руб. с выгодным тарифным планом. Свой уникальный номер это хороший признак для введения бизнеса с момента первого звонка.
Понимание того, что корни matplotlib растут из MATLAB, помогает объяснить существование pylab. pylab – это модуль внутри библиотеки matplotlib, который был встроен для подражания общего стиля MATLAB. Он существует только для внесения ряда функций классов из NumPy и matplotlib в пространство имен, что упрощает переход пользователей MATLAB, которые не сталкивались с необходимостью в операторах импорта. Бывшие пользователи MATLAB (которые очень хорошие люди, обещаем!) полюбили его функционал, потому что при помощи from pylab import * они могут просто вызывать plot() или array() напрямую также, как они это делали в MATLAB.
Проблема здесь может быть очевидной для некоторых пользователей Python: использование from pylab import * в сессии или скрипте – как правило, плохая идея. Matplotlib сегодня прямым текстом рекомендуют не делать этого в своих руководствах:
[pylab] все еще существует по историческим причинам, но его использование не рекомендуется. Он перегружает пространства имен функциями, которые оттеняют надстройки Python и может привести к скрытым багам. Для получения интеграции IPython без использования импортов, рекомендуется использовать %matplotlib.
В глубине своей, существует целая тонна потенциально конфликтных импортов, замаскированных в коротком источнике pylab. Фактически, использование ipython —pylab (из терминала или командной строки) или %pylab (из инструментов IPython/Jupyter) легко вызывает from pylab import *
Суть в том, что matplotlib забросили этот удобный модуль и рекомендуют не использовать pylab, подтверждая ключевое правило Python – явное лучше, чем неявное.
Без необходимости в использовании pylab, мы всегда можем обойтись всего одним каноничным импортом:
1 |
import matplotlib.pyplot as plt |
Кстати, давайте импортируем NumPy, пока мы здесь. Мы используем его для генерации данных в будущем и вызовем np.random.seed() для решения примеров со (псевдо) случайными воспроизводимыми данными:
1 2 |
import numpy as np np.random.seed(444) |
Иерархия объектов в Matplotlib
Одной из визитных карточек matplotlib является иерархия его объектов.
Если вы уже работали с вводным руководством matplotlib, вы, возможно, уже проводили вызов чего-то на подобии plt.plot([1, 2, 3]). Одна эта строка указывает на то, что график, на самом деле – это иерархия объектов Python. Под «иерархией» мы имеем ввиду, что каждый график основывается на древоподобной структуре объектов matplotlib.
Объект Figure – это самый важный внешний контейнер для графики matplotlib, который может включать в себя несколько объектов Axes. Причиной сложности в понимании может быть название: Axes (оси), на самом деле, превращаются в то, что мы подразумеваем под индивидуальным графиком или диаграммой (а не множественное число «оси», как вы можете ожидать).
Вы можете рассматривать объект Figure как похожий на ящик контейнер, содержащий один или несколько объектов Axes (настоящих графиков). Под объектами Axes, в порядке иерархии расположены меньшие объекты, такие как индивидуальные линии, отметки, легенды и текстовые боксы. Практически каждый «элемент» диаграммы – это собственный манипулируемый объект Python, вплоть до ярлыков и отметок:
Это изображение данной иерархии в действии. Не беспокойтесь о том, что вы не совсем понимаете эту часть, мы рассмотрим ее подробнее в дальнейшем.
1 2 |
fig, _ = plt.subplots() print(type(fig)) # <class 'matplotlib.figure.Figure'> |
Мы создали две переменные с plt.subplots(). Первый объект – это верхний объект Figure, второй – это второстепенная подчеркиваемая переменная, которая нам в данный момент не нужна. Использование нотации атрибута упрощает переход к иерархии фигур и позволяет увидеть, что первая отметка оси y – это первый объект Axes:
1 2 |
one_tick = fig.axes[0].yaxis.get_major_ticks()[0] print(type(one_tick)) # <class 'matplotlib.axis.YTick'> |
Вверху наш fig (экземпляр класса Figure) содержит множество объектов Axes (список, для которого мы берем первый элемент). Каждый объект Axes имеет ось х и ось у (xaxis и yaxis соответственно), каждая из которых имеет набор «основных отметок», и мы возьмем самую первую. Matplotlib представляет это как анатомию фигуры, а не явную иерархию:
(В родной стилистике matplotlib, фигура выше создана в документации matplotlib)
Структурированные и неструктурированные подходы
Что-ж, сейчас нам необходим еще один кусок теории, перед тем как мы приступим к созданию различных визуализаций – разница между структурированными (ориентированными на структуру) и неструктурированными (ориентированными на объект) интерфейсами.
Ранее, мы использовали import matplotlib.pyplot as plt для импорта модуля pyplot из matplotlib и назвали его plt.
Практически все функции pyplot, такие как plt.plot(), так или иначе, ссылаются на нынешний существующий объект Figure и нынешний объект Axes, или создают их, если какой-либо из них не существует. Рассмотрим полезный фрагмент, скрытый в документации matplotlib:
[pyplot], простые функции используются для добавления элементов графика (линии, изображения, текста, и т.д.) в текущие оси в нашей фигуре.
Хардкорные бывшие пользователи MATLAB могут определить это как: «plt.plot() — это интерфейс структурной машины, который неявно отслеживает текущую фигуру!». По-русски это будет звучать так:
- Структурный интерфейс делает свои вызовы с plt.plot() и другими высшими функциями pyplot. Существует только один объект Figure или Axes, который вы используете за данное время, и вам не нужна явная ссылка на этот объект;
- Модификация изложенных ниже объектов напрямую – это объектно-ориентированный подход. Обычно, мы делаем это, вызывая методы объекта Axes, который является объектом, который отображает целостный график.
Поток этих процессов на высшем уровне выглядит следующим образом:
Связав все это в месте, большая часть функций из pyplot также существует в качестве методов класса matplotlib.axes.Axes.
Для простоты понимания, мы можем взглянуть под капот. plt.plot() может быть сокращен до пяти строк кода:
1 2 3 4 5 6 7 8 |
def plot(*args, **kwargs): """An abridged version of plt.plot().""" ax = plt.gca() return ax.plot(*args, **kwargs) def gca(**kwargs): """Get the current Axes of the current Figure.""" return plt.gcf().gca(**kwargs) |
И это все. Вызов plt.plot() – это удобный способ получения текущего объекта Axes текущего объекта Figure и вызвать его метод plot(). Это тот случай, когда утверждается, что структурированный интерфейс всегда «неявно отслеживает» график, на который он хочет ссылаться.
pyplot содержит ряд функций, которые просто являются обертками объектно-ориентированного интерфейса. Например, при использовании plt.title() в рамках объектно-ориентированного подхода (далее ОО), существуют соответствующие методы получения данных и настроек: ax.set_title() и ax.get_title(). (Использование геттеров и сеттеров распространено в таких языках, как Java. Тем не менее, это основная функция ОО подхода matplotlib).
Вызов plt.title() преобразуется в одну следующую строку: gca().set_title(s, *args, **kwargs). Что она делает?
- gca() захватывает текущую ось и возвращает её
- set_title() – это метод сеттер, который указывает заголовок для отдельного объекта Axes. «Удобство» в данном случае заключается в том, что нам не нужно определять прозрачность объекта Axes при помощи plt.title().
Соответственно, если вы уделите немного времени, чтобы взглянуть на источник функций верхнего уровня, таких как plt.grid(), plt.legend(), и plt.ylabels(), вы увидите, что все они следуют общей структуре делегирования текущим объектам Axes с gca(), после чего вызывают определенный метод для текущего объекта Axes. (Это основа объектно-ориентированного подхода!).
Понимание нотации plt.subplots()
Ну что же, достаточно теории. Сейчас мы готовы к тому, чтобы связать все вместе и перейти к графикам. Начиная с этого момента, мы (по большому счету) будем ссылаться на неструктурированный (объектно-ориентированный) подход, который более гибкий и становится удобным, в то время как графики становятся все сложнее.
Предпочтительный способ создания объекта Figure с одним объектом Axes под ОО-подходом (не самый интуитивный способ) – это использовать plt.subplots(). (Это единственный раз, когда ОО — подход использует pyplot для создания объектов Figure и Axes).
1 |
fig, ax = plt.subplots() |
Только что мы воспользовались итерируемой распаковкой для назначения отдельной переменной к каждому из двух результатов plt.subplots(). Обратите внимание на то, что мы не передали аргументы к subplots() в данном случае. Вызов по умолчанию – это subplots(nrows=1, ncols=1). В результате, ax является единственным объектом AxesSubplot:
1 |
print(type(ax)) # <class 'matplotlib.axes._subplots.AxesSubplot'> |
Мы можем вызывать методы экземпляра для управления графиком так же, как когда мы вызываем функции pyplots. Давайте изобразим график с разбивкой по областям трех временных столбцов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import numpy as np import matplotlib.pyplot as plt rng = np.arange(50) rnd = np.random.randint(0, 10, size=(3, rng.size)) yrs = 1950 + rng fig, ax = plt.subplots(figsize=(5, 3)) ax.stackplot(yrs, rng + rnd, labels=['Eastasia', 'Eurasia', 'Oceania']) ax.set_title('Combined debt growth over time') ax.legend(loc='upper left') ax.set_ylabel('Total debt') ax.set_xlim(xmin=yrs[0], xmax=yrs[-1]) fig.tight_layout() plt.show() |
Что здесь происходит?
- После создания трех временных столбцов (рядов), мы определили одну фигуру (fig), содержащую один объект Axes (plot, ax);
- Мы вызываем методы ax напрямую для создания диаграммы с разбивкой по областям и добавлением легенды, заголовка и ярлыка оси y. С ОО-подходом, становится ясно, что все это – атрибуты ах;
- tight_layout() применяется к объекту Figure в целом для очистки пробелов;
Давайте рассмотрим пример, в котором содержится несколько малых графиков (Axes) внутри одного объекта Figure, формирующих два коррелированных массива, выводящихся из дискретного равномерного распределения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import numpy as np import matplotlib.pyplot as plt x = np.random.randint(low=1, high=11, size=50) y = x + np.random.randint(1, 5, size=x.size) data = np.column_stack((x, y)) fig, (ax1, ax2) = plt.subplots( nrows=1, ncols=2, figsize=(8, 4) ) ax1.scatter(x=x, y=y, marker='o', c='r', edgecolor='b') ax1.set_title('Scatter: $x$ versus $y$') ax1.set_xlabel('$x$') ax1.set_ylabel('$y$') ax2.hist( data, bins=np.arange(data.min(), data.max()), label=('x', 'y') ) ax2.legend(loc=(0.65, 0.8)) ax2.set_title('Frequencies of $x$ and $y$') ax2.yaxis.tick_right() plt.show() |
В данном примере происходит больше, чем на первый взгляд:
- Так как мы создаем фигуру «1×2», полученный результат от plt.subplots(1, 2) теперь является объектом Figure и массивом NumPy объектов Axes. (Вы можете проверить это при помощи fig, axs = plt.subplots(1, 2), и взглянуть на оси);
- Мы имеем дело с ax1 и ax2 индивидуально. (Случай, когда использование объектно-ориентированного подхода будет весьма трудным.) Последняя строка – это хорошее изображение иерархии объектов, где мы модифицируем принадлежащую второму объекту Axes ось y, разместив галочки и отметки справа;
- Текст внутри знака доллара использует разметку TeX для выделения переменных курсивом.
Обратите внимание на то, что несколько объектов Axes могут «принадлежать» или быть переданными данной фигуре. В данном примере (строчные, а не прописные объекты Axes – не спорим, терминология здесь слегка запутанная), дает нам список всех объектов Axes:
1 |
(fig.axes[0] is ax1, fig.axes[1] is ax2) # (True, True) |
Сделав еще один шаг вглубь, мы можем создать фигуру, которая содержит сетку объектов Axes 2х2 в качестве альтернативы:
1 |
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(7, 7)) |
Так, а это за «ax»? Теперь это не одиночный объект Axes, но их двухмерный массив NumPy:
1 2 3 |
print(type(ax)) # numpy.ndarray print(ax) |
Результат:
1 2 3 4 5 |
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x1106daf98>, <matplotlib.axes._subplots.AxesSubplot object at 0x113045c88>], [<matplotlib.axes._subplots.AxesSubplot object at 0x11d573cf8>, <matplotlib.axes._subplots.AxesSubplot object at 0x1130117f0>]], dtype=object) |
1 |
print(ax.shape) # (2, 2) |
Это подтверждается в документации:
«ax» может быть как одним объектом matplotlib.axes.Axes, так и массивом объектов Axes в более чем одном созданном малом графике.
Сейчас нам нужно вызвать методы создания графиков для каждого из этих объектов Axes (не массива NumPy, который является просто контейнером). Простой способ передать эти объекты, это использовать итеративную распаковку после выравнивания массива как одномерного:
1 2 |
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(7, 7)) ax1, ax2, ax3, ax4 = ax.flatten() |
Мы также могли бы выполнить это при помощи ((ax1, ax2), (ax3, ax4)) = ax, но первый подход более гибкий.
Для демонстрации более продвинутых функций меньших графиков, давайте добавим немного данных о макроэкономике недвижимости в Калифорнии, извлеченной из сжатого архива tar, пользуясь io, tarfile, и urllib из стандартной библиотеки Python.
1 2 3 4 5 6 7 8 9 10 |
from io import BytesIO import tarfile from urllib.request import urlopen url = 'http://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.tgz' b = BytesIO(urlopen(url).read()) fpath = 'CaliforniaHousing/cal_housing.data' with tarfile.open(mode='r', fileobj=b) as archive: housing = np.loadtxt(archive.extractfile(fpath), delimiter=',') |
Переменная «response» оси y, указанная ниже, использует термин из статистики: среднюю стоимость недвижимости. pop и age обозначают население и средний возраст недвижимости, соответственно.
1 2 |
y = housing[:, -1] pop, age = housing[:, [4, 7]].T |
Далее, давайте определим «вспомогательную функцию», которая размещает текстовый бокс внутри графика и выполняет функцию «заголовка внутри графика»:
1 2 3 4 5 6 7 |
def add_titlebox(ax, text): ax.text(.55, .8, text, horizontalalignment='center', transform=ax.transAxes, bbox=dict(facecolor='white', alpha=0.6), fontsize=12.5) return ax |
Теперь мы готовы к тому, чтобы сделать реальный график. Модуль gridspec нашей библиотеки matplotlib открывает расширенные возможности для настройки объектов под графиком. subplot2grid() отлично взаимодействует с этим модулем. Скажем, мы хотим создать такой макет:
Выше, у нас есть сетка 3х2. ax1 в два раза выше и шире ax2 и ax3. Это значит, что она занимает два ряда и две строки.
Второй аргумент, принадлежащий subplot2grid() – это (ряд, строка) локация объекта Axes со следующей сеткой:
1 2 3 4 5 6 7 8 9 |
import matplotlib.pyplot as plt gridsize = (3, 2) fig = plt.figure(figsize=(12, 8)) ax1 = plt.subplot2grid(gridsize, (0, 0), colspan=2, rowspan=2) ax2 = plt.subplot2grid(gridsize, (2, 0)) ax3 = plt.subplot2grid(gridsize, (2, 1)) plt.show() |
Теперь мы можем перейти к привычному режиму, настраивая каждый объект Axes индивидуально:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ax1.set_title( 'Home value as a function of home age & area population', fontsize=14 ) sctr = ax1.scatter(x=age, y=pop, c=y, cmap='RdYlGn') plt.colorbar(sctr, ax=ax1, format='$%d') ax1.set_yscale('log') ax2.hist(age, bins='auto') ax3.hist(pop, bins='auto', log=True) add_titlebox(ax2, 'Histogram: home age') add_titlebox(ax3, 'Histogram: area population (log scl.)') |
Выше, colorbar() (который отличается от ColorMap) вызывается напрямую к объекту Figure, а не к Axes. Его первый аргумент – это результат ax1.scatter(), который выполняет функцию отображения переменных оси y в ColorMap.
Визуально, разница в цветах незначительна, если мы двигаемся по оси y вверх-вниз, похоже, что возраст дома сильнее определяет его стоимость.
«Фигуры» за кулисами
Каждый раз, когда вы вызываете plt.subplots(), или реже используемую plt.figure() (которая создает объект Figure без объектов Axes), вы создаете новый объект Figure, который matplotlib по хитрому хранит в памяти. Ранее, мы упоминали концепцию текущего объекта Figure и Axes. По умолчанию, такие объекты Figure и Axes создаются чаще всего, это мы можем продемонстрировать при помощи встроенной функции id(), чтобы показать адрес объекта в памяти:
1 2 3 4 5 6 7 8 9 10 11 12 |
import matplotlib.pyplot as plt fig1, ax1 = plt.subplots() # fig1– это текущая фигура. print(id(fig1)) # 4525567840 print(id(plt.gcf())) # 4525567840 # текущая фигура была изменена на fig2. fig2, ax2 = plt.subplots() print(id(fig2) == id(plt.gcf())) # True |
(Мы также можем использовать встроенный оператор is в этом коде)
После показанной только что рутины, текущая фигура называется fig2, и является фигурой, которая создается чаще всего. Однако, обе фигуры висят в памяти, и обе носят соответствующий номер ID (с одним индексом, в духе MATLAB):
1 |
print(plt.get_fignums()) # [1, 2] |
1 2 3 4 |
def get_all_figures(): return [plt.figure(i) for i in plt.get_fignums()] print(get_all_figures()) |
Результат:
1 2 |
[<matplotlib.figure.Figure at 0x10dbeaf60>, <matplotlib.figure.Figure at 0x1234cb6d8>] |
Небольшая сноска: убедитесь в том, что используете скрипт, где вы создаете группу фигур. Вам нужно будет явно закрыть каждый из них, во избежание ошибки MemoryError. plt.close(), сам по себе, закрывает текущую фигуру, plt.close(num) закрывает фигуру под номером num, а plt.close(‘all’) закрывает все окна фигур.
1 2 |
plt.close('all') print(get_all_figures()) # [] |
Игра красок: imshow() и matshow()
В то время, как ax.plot() является одним из основных методов построения графиков на объектах Axes, существует целый ряд других (Мы использовали ax.stackplot() ранее, со списком остальных вы можете ознакомиться здесь).
Одна группа популярных использовании методов – это imshow() and matshow(), причем последний используется в качестве обертки первого. Они полезны в тех случаях, когда необработанный численный массив будет визуализирован как цветная таблица.
Для начала, давайте создадим две отдельные таблицы с какой-нибудь миловидной индексацией NumPy используя arange:
1 2 3 4 5 |
import numpy as np x = np.diag(np.arange(2, 12))[::-1] x[np.diag_indices_from(x[::-1])] = np.arange(2, 12) x2 = np.arange(x.size).reshape(x.shape) |
Далее, мы можем сопоставить их с собственными изображениями. В конкретном данном случае, мы выключаем (нажав “off”) все метки и штрихи осей, пользуясь пониманием словаря и передаем результат к ax.tick_params():
1 2 3 4 5 |
sides = ('left', 'right', 'top', 'bottom') nolabels = {s: False for s in sides} nolabels.update({'label%s' % s: False for s in sides}) print(nolabels) |
Результат:
1 2 |
{'left': False, 'right': False, 'top': False, 'bottom': False, 'labelleft': False, 'labelright': False, 'labeltop': False, 'labelbottom': False} |
Далее, мы можем использовать контекстный менеджер для отключения таблицы и вызвать matshow() в каждом объекте Axes. Наконец, нам нужно добавить цветную панель, в том, что технически является новым объектом Axes в fig. Для этого мы можем использовать одну специфическую функцию из глубин matplotlib:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable x = np.diag(np.arange(2, 12))[::-1] x[np.diag_indices_from(x[::-1])] = np.arange(2, 12) x2 = np.arange(x.size).reshape(x.shape) sides = ('left', 'right', 'top', 'bottom') nolabels = {s: False for s in sides} nolabels.update({'label%s' % s: False for s in sides}) with plt.rc_context(rc={'axes.grid': False}): fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) ax1.matshow(x) img2 = ax2.matshow(x2, cmap='RdYlGn_r') for ax in (ax1, ax2): ax.tick_params(axis='both', which='both', **nolabels) for i, j in zip(*x.nonzero()): ax1.text(j, i, x[i, j], color='white', ha='center', va='center') divider = make_axes_locatable(ax2) cax = divider.append_axes("right", size='5%', pad=0) plt.colorbar(img2, cax=cax, ax=[ax1, ax2]) fig.suptitle('Heatmaps with `Axes.matshow`', fontsize=16) plt.show() |
Построение графиков в Pandas
Библиотека pandas стала популярной не только благодаря обеспечению мощным анализом данных, но и благодаря удобным методам построения графиков. Это интересно тем, что методы построения графиков pandas – это просто удобные обертки существующих вызовов matplotlib.
Таким образом, метод plot() в Series и DataFrame pandas – это обертка для plt.plot(). Удобство заключается в том, что если индекс DataFrame состоит из дат, gcf().autofmt_xdate() вызывается pandas изнутри для получения текущего объекта Figure и красивого форматирования оси х в автоматическом режиме.
Помните о том, что plt.plot() (структурированный подход) неявно имеет представление о текущем объекте Figure и Axes, так что pandas следует структурированному подходу путем расширения.
Мы можем подтвердить эту «цепочку» вызовов функций при помощи небольшой внутренней проверки. Для начала, давайте создадим ванильную Series в pandas, предположив, что мы создаем свежую сессию интерпретатора:
1 2 3 4 5 6 7 8 |
import pandas as pd s = pd.Series(np.arange(5), index=list('abcde')) ax = s.plot() print(type(ax)) # <matplotlib.axes._subplots.AxesSubplot at 0x121083eb8> print(id(plt.gca()) == id(ax)) # True |
Такая внутренняя архитектура весьма полезна для понимания того, когда вы смешиваете методы построения графиков pandas с традиционными вызовами matplotlib. Что и продемонстрированно ниже в построении графика смещения среднего показателя широко просматриваемых финансовых временных отрезков. ma – это Series нашей библиотеки pandas, для которой мы можем вызвать ma.plot() (метод pandas) и затем провести настройку, извлекая объект Axes, который был создан этим вызовом (plt.gca()) для matplotlib для ссылки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import numpy as np import matplotlib.pyplot as plt import pandas as pd import matplotlib.transforms as mtransforms url = 'https://fred.stlouisfed.org/graph/fredgraph.csv?id=VIXCLS' vix = pd.read_csv(url, index_col=0, parse_dates=True, na_values='.', infer_datetime_format=True, squeeze=True).dropna() ma = vix.rolling('90d').mean() state = pd.cut( ma, bins=[-np.inf, 14, 18, 24, np.inf], labels=range(4) ) cmap = plt.get_cmap('RdYlGn_r') ma.plot( color='black', linewidth=1.5, marker='', figsize=(8, 4), label='VIX 90d MA' ) ax = plt.gca() # получаем текущий объект Axes, на который ссылается ma.plot() ax.set_xlabel('') ax.set_ylabel('90d moving average: CBOE VIX') ax.set_title('Volatility Regime State') ax.grid(False) ax.legend(loc='upper center') ax.set_xlim(xmin=ma.index[0], xmax=ma.index[-1]) trans = mtransforms.blended_transform_factory(ax.transData, ax.transAxes) for i, color in enumerate(cmap([0.2, 0.4, 0.6, 0.8])): ax.fill_between(ma.index, 0, 1, where=state == i, facecolor=color, transform=trans) ax.axhline( vix.mean(), linestyle='dashed', color='xkcd:dark grey', alpha=0.6, label='Full-period mean', marker='' ) plt.show() |
В этом коде происходит много чего:
- ma – это 90-дневное скользящее среднее число индекса VIX, который является показателем рыночных ожиданий в краткосрочной волатильности акций. state – это группирование скользящего среднего числа в разных состояниях режима. Высокий индекс VIX виден как сигнал о повышенном уровне страха на рынке.
- cmap (ColorMap) – объект matplotlib, который по сути своей, является отображением поплавков в цветах RGBA. Любая цветовая палитра может быть перевернута добавлением ‘_r’, где ‘RdYlGn_r‘ – перевернутая цветовая гамма красного, желтого и синего. Matplotlib поддерживает удобное справочное руководство по цветовым гаммам в своей документации.
- Единственный вызов pandas, который мы делаем в данном примере — это ma.plot(). Который проводит внутренний вызов plt.plot(), так что для интеграции объектно-ориентированного подхода нам нужно получить явную отсылку к текущему объекту Axes при помощи ax = plt.gca().
- Второй кусок кода создает закрашенные блоки, которые соответствуют всем регистрам state. cmap([0.2, 0.4, 0.6, 0.8]) как бы говорит нам: «Дайте нам последовательность RGBA в 20-х, 40-х, 60-х и 80-х «перцентилях» в спектре цветовой гаммы». Мы используем enumerate(), так как нам нужно сопоставить каждый цвет RGBA с состоянием.
Pandas также предоставляет небольшое количество более продвинутых графиков (обзор которых может представлять собой целое отдельное руководство). Однако, все они (включая их упрощенные аналоги) полагаются на внутренние механизмы matplotlib.
Подведем итоги
Как было показано в некоторых примерах, становится ясно, что matplotlib может быть технически и синтаксически сложной библиотекой. Чтобы создать готовую диаграмму, может уйти полчаса на один только поиск в Гугле и комбинирование всей этой мешанины для точной настройки графика.
Однако, понимание того, как интерфейсы matplotlib взаимодействуют друг с другом – это инвестиция, которая может окупиться. Совет Дэна Бадера (владельца Real Python):
Уделив время на анализ кода, а не на то, чтобы просто копипастить его, вы совершаете разумное решение с долгосрочной перспективой. Придерживаясь объектно-ориентированного подхода, вы можете сэкономить несколько часов страданий, делая свой график произведением искусства.
Дополнительные ресурсы
Из документации matplotlib:
- Примеры matplotlib;
- Часто задаваемые вопросы FAQ;
- Страница руководств, разбитая на начальный, средний и продвинутый уровни;
- Жизненный цикл графика, где затрагивается соотношение объектно-ориентированного и структурно-ориентированного подходов;
- Библиотека math которая предоставляет полный набор необходимых математических функций в Питоне.
Сторонние ресурсы
- Шпаргалка DataCamp в matplotlib;
- Вычислительная биология ПНБ (публичной научной библиотеки): десять простых правил для хороших фигур;
- Глава 9 (Построение графиков и Визуализация) из книги Python for Data Analysis (Python для анализа данных) Уеса МакКинни, второе издание;
- Глава 11 (Визуализация с Matplotlib, Pandas и Seaborn), Pandas Cookbook (Кулинарная книга Pandas) автора Теда Петроу;
- Раздел 1.4 (Matplotlib: Построение графиков) Scipy Lecture Notes;
- Цветовая палитра xkcd;
- Страница внешних ресурсов matplotlib;
- queirozf.com: Matplotlib, Pylab, Pyplot, и т.д.: Какая разница между ними и когда пользоваться каждым из них?;
- Страница визуализации в документации pandas.
Прочие библиотеки для построения графиков
- Библиотека seaborn, созданная на основе matplotlib и разработана для продвинутых статистических графиков, описание которой может стоить отдельного целого руководства;
- Datashader, графическая библиотека, ориентированная на большие наборы данных;
- Список всех остальных сторонних пакетов из документации matplotlib.
Приложение А: Конфигурация и стилизация
Если вы внимательно читали эту статью, то заметили, что различные графики отличаются стилистически от тех, что были предоставлены здесь.
Matplotlib предоставляет два способа индивидуальной настройки стиля в различных графиках:
- При помощи настройки файла matplotlibrc;
- Путем интерактивного изменения ваших параметров настройки или из скрипта .py.
Файл matplotlibrc (вариант №1) – это текстовый файл, определяющий пользовательские настройки, которые сохраняются между сессиями Python. В MacOSX он, как правило, находится в ~/.matplotlib/matplotlibrc.
Небольшой совет: GitHub – это отличное место для хранения файлов настроек. Только убедитесь в том, что они не содержат приватные данные, такие как пароли или ключи SSH!
В качестве альтернативы, вы можете изменить свои параметры настроек интерактивно (вариант №2). Когда вы импортируете import matplotlib.pyplot as plt, вы получаете доступ к объекту rcParams, который похож на словарь настроек Python. Все объекты модуля, названия которых начинаются с «rc» используются для взаимодействия со стилями вашего графика и настроек:
1 2 |
data = [attr for attr in dir(plt) if attr.startswith('rc')] print(data) # ['rc', 'rcParams', 'rcParamsDefault', 'rc_context', 'rcdefaults'] |
Из этого следует, что:
- plt.rcdefaults() восстанавливает параметры rc из внутренних настроек по умолчанию Matplotlib, которые находятся в списке plt.rcParamsDefault. Это вернет (перезапишет) что-либо из того, что вы уже настроили в файле matplotlibrc;
- plt.rc() используется для интерактивной настройки параметров;
- plt.rcParams – это словароподобный (изменяемый) объект, который позволяет вам управлять настройками напрямую. Если у вас есть измененные настройки в файле matplotlibrc, это отразится в данном словаре.
plt.rc() и plt.rcParams — два синтаксиса, которые являются эквивалентами для настройки параметров:
1 2 3 |
plt.rc('lines', linewidth=2, color='r') # Синтакс 1 plt.rcParams['lines.linewidth'] = 2 # Синтакс 2 plt.rcParams['lines.color'] = 'r' |
Обратите внимание на то, что класс Figure после этого использует некоторые из них в качестве своих аргументов по умолчанию.
Соответственно, стиль – это просто предопределенный кластер пользовательских настроек. Чтобы увидеть доступные стили, используйте:
1 2 |
import matplotlib.pyplot as plt print(plt.style.available) |
Результат:
1 2 3 4 5 6 |
['seaborn-dark', 'seaborn-darkgrid', 'seaborn-ticks', 'fivethirtyeight', 'seaborn-whitegrid', 'classic', '_classic_test', 'fast', 'seaborn-talk', 'seaborn-dark-palette', 'seaborn-bright', 'seaborn-pastel', 'grayscale', 'seaborn-notebook', 'ggplot', 'seaborn-colorblind', 'seaborn-muted', 'seaborn', 'Solarize_Light2', 'seaborn-paper', 'bmh', 'seaborn-white', 'dark_background', 'seaborn-poster', 'seaborn-deep'] |
Чтобы настроить стиль, вызовите:
1 |
plt.style.use('fivethirtyeight') |
Ваши графики теперь будут выглядеть по-новому:
Для вдохновения, в Matplotlib содержится несколько таблиц стилей для справки.
Приложение Б: Интерактивный режим
За кулисами, matplotlib также взаимодействует с различными бэкендами. Бэкенд – это рабочая лошадка фактического рендеринга диаграммы. (К примеру, в популярном дистрибутиве Anaconda бэкенд – это Qt5Agg). Некоторые бэкенды интерактивны, это значит, что они обновляются динамически и «выскакивают» перед пользователем, когда меняются.
Несмотря на то, что интерактивный режим по умолчанию отключен, вы можете проверить его статус при помощи plt.rcParams[‘interactive’] или plt.isinteractive и включать или выключать его при помощи plt.ion() и plt.ioff(), соответственно:
1 |
plt.rcParams['interactive'] # или: plt.isinteractive() |
1 2 |
plt.ioff() print(plt.rcParams['interactive']) # False |
В некоторых примерах вы можете заметить наличие plt.show() в последней части кода. Основная задача plt.show(), о чем и говорит название, это показывать (открывать) фигуру, когда вы работаете с отключенным интерактивным режимом. Иными словами:
- Если интерактивный режим включен, вам не нужен plt.show(), а изображения будут автоматически выскакивать перед пользователем и обновляться, когда вы ссылаетесь на них;
- Если интерактивный режим отключен, plt.show() нужен вам для показа фигуры и plt.draw() для обновления графика.
Ниже, мы отключаем интерактивный режим, что влечет за собой необходимость вызова plt.show() после построения графика.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import numpy as np import matplotlib.pyplot as plt plt.ioff() x = np.arange(-4, 5) y1 = x ** 2 y2 = 10 / (x ** 2 + 1) fig, ax = plt.subplots() ax.plot(x, y1, 'rx', x, y2, 'b+', linestyle='solid') ax.fill_between(x, y1, y2, where=y2>y1, interpolate=True, color='green', alpha=0.3) lgnd = ax.legend(['y1', 'y2'], loc='upper center', shadow=True) lgnd.get_frame().set_facecolor('#ffb19a') plt.show() |
Стоит отметить, что интерактивный режим не имеет ничего общего с используемой вами средой разработки, или если вы подключили встроенное построение графика при помощи: jupyter notebook —matplotlib inline или %matplotlib.
Оригинал статьи: https://realpython.com/python-matplotlib-guide/
Автор: Brad Solomon
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»