Matplotlib: как строить графики в Python с примерами кода

Рабочее место разработчика: на мониторе линейный график рядом с окном редактора кода Программирование и разработка

Графики в Python чаще всего строят библиотекой matplotlib: ставим её командой pip install matplotlib, создаём фигуру и оси через fig, ax = plt.subplots(), рисуем ax.plot(x, y) и выводим окно plt.show(). Все примеры ниже проверены на Python 3.14.3 с matplotlib 3.11.2 и NumPy 2.5.3, каждый запускается как отдельный скрипт. Импортируем модуль pyplot под общепринятым коротким именем plt, NumPy под именем np: так их называют почти в любом коде и в документации.

Установка matplotlib

Библиотека matplotlib ставится из PyPI вместе с зависимостями, среди них NumPy и Pillow, отдельно их ставить не нужно:

pip install matplotlib

Если в системе несколько версий Python, надёжнее вызывать pip через нужный интерпретатор: python -m pip install matplotlib, а на Windows py -m pip install matplotlib. Проверить, что установка прошла:

import matplotlib
print(matplotlib.__version__)
3.11.2

В Jupyter Notebook ставят из ячейки командой %pip install matplotlib. В Google Colab matplotlib обычно уже установлена, импорт работает сразу. Для примеров ниже хватит списков, срезов и циклов; если со списками и строками пока трудно, сначала разберите задачу «Фруктовый салат» на Python.

Figure и Axes: из чего состоит график в matplotlib

Фигура (Figure) отвечает за всё окно или картинку целиком. Внутри неё лежат оси (Axes): прямоугольник, где рисуется график, со своими шкалами, подписями и заголовком. На одной фигуре могут быть одни оси или сетка из нескольких. Не путайте Axes с Axis: второе слово означает одну ось координат, x или y, с делениями и подписями.

У matplotlib два интерфейса. Неявный идёт через функции модуля pyplot: plt.plot(), plt.title(). Этот набор функций сделан по образцу MATLAB, и, как в MATLAB, pyplot сам помнит текущую фигуру и текущие оси. Явный, объектно-ориентированный подход: создаём объекты fig и ax с помощью plt.subplots(), затем вызываем их методы. Оба способа рисуют одно и то же. Пишите в объектном стиле всё, кроме быстрых проверок в консоли, и начинайте с fig, ax = plt.subplots() уже на первом графике: когда понадобится второй график рядом, достаточно поменять аргументы subplots и добавить индекс к ax, а вызовы методов останутся прежними.

Так выглядит простой график в стиле pyplot:

import matplotlib.pyplot as plt

plt.plot([1, 2, 3, 4], [1, 4, 9, 16], marker="o")
plt.title("Квадраты чисел")
plt.xlabel("n")
plt.ylabel("n²")
plt.show()

Те же действия в объектном стиле называются почти так же, но у части методов Axes есть приставка set_:

Функция pyplot Метод объекта
plt.plot(x, y) ax.plot(x, y)
plt.title("...") ax.set_title("...")
plt.xlabel("...") ax.set_xlabel("...")
plt.ylabel("...") ax.set_ylabel("...")
plt.xlim(0, 10) ax.set_xlim(0, 10)
plt.legend() ax.legend()
plt.grid(True) ax.grid(True)
plt.savefig("a.png") fig.savefig("a.png")

Если написать ax.xlabel("x") по аналогии с pyplot, программа упадёт:

AttributeError: 'Axes' object has no attribute 'xlabel'. Did you mean: 'clabel'?

Подсказка интерпретатора про clabel здесь мимо, нужен ax.set_xlabel.

Основные типы графиков в matplotlib

Для визуализации данных в большинстве задач хватает пяти методов Axes. Какой использовать, зависит от вопроса к данным:

Читайте также:  Парсер выражений: как разобрать математическое выражение из строки
Что нужно показать Тип графика Метод
Как величина меняется: функция, ряд по времени линейный график ax.plot()
Есть ли связь между двумя величинами диаграмма рассеяния ax.scatter()
Сравнение категорий столбчатая диаграмма ax.bar()
Распределение значений гистограмма ax.hist()
Доли целого круговая диаграмма ax.pie()

Линейный график: plot, подписи осей, легенда

Построим синус и косинус на одном графике. Массив x готовим функцией np.linspace(0, 10, 200): 200 точек от 0 до 10, между которыми plot проводит прямые отрезки, поэтому кривая выглядит гладкой.

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 200)          # 200 точек от 0 до 10

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, np.sin(x), color="blue", linewidth=2, label="sin(x)")
ax.plot(x, np.cos(x), color="red", linestyle="--", label="cos(x)")
ax.set_title("Синус и косинус", fontsize=14)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True)
ax.legend()
plt.show()

На экране две волны: синяя сплошная линия толщиной 2 и красная пунктирная, в углу легенда с подписями. figsize=(8, 4) задаёт ширину и высоту фигуры в дюймах, fontsize=14 размер шрифта заголовка. Параметр color принимает имя цвета линии ("red"), код вида "#1f77b4" или цвет из стандартной палитры "tab:blue". Кириллица в подписях работает без настройки: шрифт по умолчанию DejaVu Sans содержит русские буквы.

Легенда берёт текст из параметра label. Если вызвать ax.legend(), а подписи линиям не дать, легенда не появится, а matplotlib выведет предупреждение:

UserWarning: No artists with labels found to put in legend.  Note that artists whose label start with an underscore are ignored when legend() is called with no argument.

Вывод программы: синяя сплошная синусоида и красная пунктирная косинусоида на сетке, легенда в правом верхнем углу

Точечный график: scatter

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

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(42)
height = rng.normal(170, 8, 100)                     # рост, см
weight = height * 0.9 - 85 + rng.normal(0, 6, 100)   # вес, кг

fig, ax = plt.subplots()
ax.scatter(height, weight, s=20, alpha=0.6, color="green")
ax.set_xlabel("Рост, см")
ax.set_ylabel("Вес, кг")
plt.show()

Облако из 100 зелёных точек вытянуто снизу слева вверх направо: чем выше рост, тем больше вес. Параметр s задаёт размер маркера, alpha=0.6 делает точки полупрозрачными, и там, где они перекрываются, цвет гуще. С фиксированным default_rng(42) случайные числа при каждом запуске одинаковые, картинка не меняется.

Вывод программы: облако из ста зелёных точек, вес растёт вместе с ростом

Столбчатая диаграмма: bar

Столбчатая диаграмма удобна для сравнения категорий, например продаж по месяцам:

import matplotlib.pyplot as plt

months = ["Янв", "Фев", "Мар", "Апр", "Май", "Июн"]
sales = [120, 95, 140, 160, 150, 185]

fig, ax = plt.subplots()
bars = ax.bar(months, sales, color="tab:blue")
ax.bar_label(bars)                  # подписи значений над столбцами
ax.set_title("Продажи по месяцам")
ax.set_ylabel("Штук")
plt.show()

Получим шесть синих столбцов, над каждым число из списка sales, самый низкий в феврале (95), самый высокий в июне (185). Для горизонтальных столбцов есть ax.barh() с теми же аргументами: так проще читать длинные названия категорий.

Вывод программы: шесть синих столбцов продаж с января по июнь, над каждым его значение

Гистограмма: hist

Гистограмму путают со столбчатой диаграммой, но она показывает распределение и считает сама: делит диапазон значений на интервалы (bins) и рисует, сколько значений попало в каждый. Без параметра bins количество интервалов равно 10.

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(0)
data = rng.normal(loc=50, scale=10, size=1000)

fig, ax = plt.subplots()
counts, edges, patches = ax.hist(data, bins=20, edgecolor="white")
print(len(counts), counts.sum())
ax.set_xlabel("Значение")
ax.set_ylabel("Сколько раз встретилось")
plt.show()
20 1000.0

Метод возвращает высоты столбцов, границы интервалов и сами столбцы. Вывод подтверждает: интервалов 20, а сумма высот равна 1000, то есть каждое значение попало ровно в один столбец. На графике колокол с вершиной около 50.

Читайте также:  Async/await в JavaScript: как работает и где ошибаются

Вывод программы: гистограмма из 20 столбцов в форме колокола с вершиной около 50

Круговая диаграмма: pie

Круговая диаграмма показывает доли целого. Параметр autopct подписывает проценты внутри секторов, формат "%1.1f%%" даёт один знак после запятой:

import matplotlib.pyplot as plt

labels = ["Python", "JavaScript", "Java", "Другие"]
shares = [45, 30, 15, 10]

fig, ax = plt.subplots()
wedges, texts, autotexts = ax.pie(shares, labels=labels, autopct="%1.1f%%", startangle=90)
print([t.get_text() for t in autotexts])
ax.set_title("Языки в проекте")
plt.show()
['45.0%', '30.0%', '15.0%', '10.0%']

ax.pie() возвращает секторы, подписи и тексты процентов, по последним видно, что напечатано внутри секторов. startangle=90 поворачивает начало первого сектора на 90 градусов, то есть наверх, а дальше секторы идут против часовой стрелки: так по умолчанию, за это отвечает параметр counterclock=True. Больше пяти-шести секторов мы бы на круг не ставили: тонкие доли сливаются, и столбчатая диаграмма читается лучше.

Вывод программы: круговая диаграмма из четырёх секторов с процентами 45, 30, 15 и 10

Несколько графиков на одной фигуре: subplots

plt.subplots(nrows, ncols) создаёт сетку графиков на одной фигуре. По умолчанию nrows=1 и ncols=1, поэтому без аргументов получаются одни оси. Сетка 2 на 2 с графиками четырёх типов:

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 100)
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(8, 6), layout="constrained")
print(ax.shape)

ax[0, 0].plot(x, np.sin(x))
ax[0, 0].set_title("plot")
ax[0, 1].scatter(x, np.cos(x), s=5)
ax[0, 1].set_title("scatter")
ax[1, 0].bar(["A", "B", "C"], [3, 7, 5])
ax[1, 0].set_title("bar")
ax[1, 1].hist(np.random.default_rng(1).normal(size=500))
ax[1, 1].set_title("hist")
for a in ax.flat:
    a.grid(True)
plt.show()
(2, 2)

ax здесь уже не одни оси, а двумерный массив NumPy: к каждому графику обращаются по строке и столбцу. layout="constrained" раздвигает графики, чтобы заголовки и подписи осей не перекрывали соседей; похожую работу делает fig.tight_layout(), его вызывают перед plt.show(). Цикл for a in ax.flat: в конце перебирает все оси и включает сетку на каждом графике, подробнее в статье про циклы while и for в Python.

Форма массива зависит от сетки. При одной строке, plt.subplots(1, 3), массив одномерный, и привычный ax[0, 1] падает:

IndexError: too many indices for array: array is 1-dimensional, but 2 were indexed

Нужно писать ax[1]. Если код должен работать с любой сеткой, передайте squeeze=False: тогда массив всегда двумерный, даже при subplots(1, 1).

Вывод программы: сетка 2 на 2 с линейным графиком, точками, тремя столбцами и гистограммой

Как сохранить график в файл: savefig

fig.savefig() берёт формат из расширения имени файла: .png, .jpg, .pdf, .svg.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()          # размер по умолчанию 6.4 x 4.8 дюйма
ax.plot([1, 2, 3], [2, 1, 3])
print(fig.get_size_inches(), fig.dpi)

fig.savefig("chart.png")                              # 640 x 480 пикселей
fig.savefig("chart_300.png", dpi=300, bbox_inches="tight")
fig.savefig("chart.svg")                              # векторный формат
[6.4 4.8] 100.0

Размер в пикселях равен размеру в дюймах, умноженному на dpi. По умолчанию это 6.4 x 100 = 640 на 4.8 x 100 = 480. С dpi=300 выходит 1920 x 1440, а bbox_inches="tight" ещё и обрезает пустые поля: в нашем прогоне chart_300.png получился 1671 x 1239. Для печати и вёрстки берите PDF, для веба SVG: векторный файл не теряет чёткость при любом масштабе. На SVG из одних линий dpi не влияет: файлы с dpi=100 и dpi=300 у нас вышли одного размера, оба 460.8 x 345.6 pt и по 16 548 байт.

Читайте также:  TRY CATCH в SQL Server: обработка ошибок в T-SQL на примерах

Сохраняйте до plt.show(). В обычном скрипте show() останавливает программу, пока окно открыто, а после закрытия окна pyplot забывает текущую фигуру. Тогда plt.savefig() создаёт новую пустую фигуру и сохраняет её: на Windows с оконным бэкендом TkAgg у нас получился PNG 640 x 480, в котором все пиксели белые. fig.savefig() в том же опыте сохранил график нормально, потому что обращается к объекту фигуры напрямую.

Крупный план рук на клавиатуре, на экране рядом открыт сохранённый график в просмотрщике изображений

Предупреждения и ошибки matplotlib

На сервере или в контейнере без экрана matplotlib может работать через неинтерактивный бэкенд Agg (модуль отрисовки без окон). Тогда plt.show() ничего не покажет и предупредит:

UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown

В этом случае окно не нужно: выберите Agg явно и сохраняйте график в файл. Надёжнее вызывать matplotlib.use() до импорта pyplot; в 3.11.2 переключение сработало и после импорта, пока ни одно окно не открыто, но на это лучше не рассчитывать.

import matplotlib
matplotlib.use("Agg")               # до импорта pyplot, для надёжности
import matplotlib.pyplot as plt

for i in range(25):
    fig, ax = plt.subplots()
    ax.plot([0, i], [0, i])
    fig.savefig(f"agg_{i}.png")
    plt.close(fig)
print(plt.get_fignums())
[]

Скрипт сохранил 25 файлов без единого предупреждения, а пустой список в выводе показывает, что открытых фигур не осталось.

Фигуры, созданные через pyplot, живут в памяти, пока их не закрыть. Если строить графики в цикле, то, когда открытых фигур станет больше 20, появится предупреждение:

RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory.

Лечится вызовом plt.close(fig) после fig.savefig() в каждой итерации, как в цикле выше.

Частые вопросы

Нужен ли plt.show() в Jupyter Notebook?

Нет. Jupyter автоматически показывает под ячейкой все фигуры, созданные в ней, поэтому plt.show() там можно не писать. В обычном скрипте без него окно не откроется.

Как построить 3D-график?

Трёхмерные графики рисуются теми же осями, нужно передать projection="3d" при их создании:

import matplotlib.pyplot as plt
import numpy as np

x = y = np.linspace(-3, 3, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = plt.figure()
ax = fig.add_subplot(projection="3d")
ax.plot_surface(X, Y, Z, cmap="viridis")
plt.show()

Получится поверхность: кольцевая волна вокруг центра, раскрашенная по высоте от тёмно-фиолетового к жёлтому (палитра viridis).

Как построить график функции, которую пользователь ввёл строкой?

Строку сначала нужно разобрать в выражение, eval для чужого ввода не годится. Как это сделать безопасно, мы разбирали в статье про парсер математических выражений. Когда парсер вернул обычную функцию Python, остаётся посчитать её на массиве точек:

import matplotlib.pyplot as plt
import numpy as np

def f(x):                       # функция, которую вернул парсер
    return x**2 - 3 * x

x = np.linspace(-5, 5, 200)
fig, ax = plt.subplots()
ax.plot(x, f(x))
plt.show()

NumPy считает f(x) сразу для всех 200 точек, цикл не нужен. На графике парабола с минимумом в точке x = 1.5.

Чем matplotlib отличается от seaborn и Plotly?

Seaborn построена поверх matplotlib и даёт короткие функции для статистических графиков. Метод plot() у таблиц pandas (Series и DataFrame) тоже рисует через matplotlib, это обёртка над plt.plot(). Plotly рисует интерактивные графики для браузера. Начинать стоит с matplotlib: подписи, размер и сохранение у графика seaborn всё равно настраиваются через объекты matplotlib.

Оцените статью
bestprogrammer.ru
Добавить комментарий