Shanraq.org Shanraq.org
matplotlib: первый график и файл, а не окно
IT

Python: от данных до своей сводки Урок 33 из 56

matplotlib: первый график и файл, а не окно

Тридцать второй урок курса по Python. Рисуем ряд из прошлого урока: лист и оси, линия, подписи, сетка, легенда — и сохранение в файл без единого окна на экране. Плюс три вещи, которые ломают график у всех: `Agg`, порядок «сначала рисуем, потом сохраняем» и незакрытая фигура.

Зачем это нужно

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

За это придётся заплатить. График — самый убедительный и самый лёгкий для подделки способ показать данные: те же числа на другой шкале говорят другое. Этим займётся следующий урок — про шкалы, ноль и подписи. Сегодня — как вообще нарисовать и сохранить.

Библиотека ставится туда же, где стоит pandas:

pip install matplotlib==3.11.1

Сразу целиком

Файл grafik.py. Тот же ряд курса, что и в тридцать первом уроке, и обе линии: настоящая и сглаженная.

"""Урок 32: первый график.

Тот же ряд курса, что и в прошлом уроке. Рисуем линию, подписываем оси,
сохраняем в файл — и ни одного окна на экране.
"""

import matplotlib

# Без экрана: программа по расписанию рисует в файл, а не в окно. Эту строку
# ставят до импорта pyplot — иначе он успеет выбрать другой способ рисовать.
matplotlib.use("Agg")

import matplotlib.pyplot as plt
import pandas as pd
from pathlib import Path

HERE = Path(__file__).resolve().parent

days, rates = [], []
start = pd.Timestamp("2026-01-05")
for i in range(40):
    day = start + pd.Timedelta(days=i)
    if day.weekday() >= 5:
        continue
    value = 512.0 + i * 0.35 + ((i * 7) % 11 - 5) * 0.6
    if i == 21:
        value += 14.0
    days.append(day)
    rates.append(value)
rate = pd.Series(rates, index=pd.DatetimeIndex(days, name="day"), name="rate").round(2)

# Figure — лист бумаги, Axes — сами оси на нём. Всё рисование идёт через ax.
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(rate.index, rate.values, color="#b03a2e", linewidth=1.6, label="курс")
ax.plot(rate.index, rate.rolling(5).mean(), color="#7f8c8d", linewidth=1.2,
        linestyle="--", label="скользящее среднее, 5 дней")

ax.set_title("Курс доллара, январь — февраль 2026")
ax.set_xlabel("день")
ax.set_ylabel("тенге за доллар")
ax.grid(True, linewidth=0.4, alpha=0.5)
ax.legend()
fig.autofmt_xdate()

out = HERE / "kurs.png"
fig.savefig(out, dpi=150, bbox_inches="tight")
plt.close(fig)

print("точек на графике:", len(rate))
print("линий на осях:", len(ax.get_lines()))
print("подпись оси Y:", ax.get_ylabel())
print("файл:", out.name, "| создан:", out.exists(), "| не пустой:", out.stat().st_size > 0)

Выводит:

точек на графике: 30
линий на осях: 2
подпись оси Y: тенге за доллар
файл: kurs.png | создан: True | не пустой: True

А рядом с программой появляется kurs.png — та самая картинка.

Разбор

Agg: программа без экрана

matplotlib.use("Agg") — первая строка не случайно. По умолчанию matplotlib ищет способ показать окно, а у программы, запущенной по расписанию в четыре утра (двадцать третий урок), экрана нет. Agg — это «рисуй в файл», и с ним окно не нужно вовсе.

Ставить её надо до import matplotlib.pyplot: pyplot выбирает способ рисования при импорте, и после него менять уже поздно.

Поэтому в программе непривычный порядок импортов: сначала matplotlib, потом строка use, и только потом pyplot. Это единственное место в курсе, где импорты идут не подряд, и линтер на него ругается — а работает только так.

Лист и оси

fig, ax = plt.subplots(figsize=(8, 4)) создаёт две вещи:

  • fig — лист бумаги: у него есть размер в дюймах и на него сохраняют;
  • ax — оси на этом листе: на них рисуют, их подписывают, у них своя сетка.

Рисовать можно и короче — plt.plot(...), — но тогда вы рисуете на «текущих» осях, которые библиотека держит где-то у себя. Пока график один, разницы нет; как только их два, «текущие» оси оказываются не теми. Через ax такой ошибки не бывает, и в уроках курса используется только он.

figsize задаётся в дюймах, а не в пикселях: пиксели появятся при сохранении, когда к дюймам добавится dpi.

Линия и её вид

ax.plot(x, y) рисует линию. Что стоит задавать явно:

  • color — цветом отличают ряды; пусть он будет один и тот же для одного и того же ряда во всех ваших графиках;
  • linewidth — толщина; главное делают толще;
  • linestyle="--" — пунктир для вспомогательного: сглаженная линия в примере пунктирная не для красоты, а чтобы её не путали с данными;
  • label — подпись для легенды; без неё ax.legend() нечего показывать.

Подписи — не украшение

График без подписей осей — это картинка, а не данные. Четыре строки, которые пишут всегда:

ax.set_title(...)    что это вообще
ax.set_xlabel(...)   что по горизонтали
ax.set_ylabel(...)   что по вертикали, и в каких единицах
ax.legend()          где какой ряд

ax.grid(True, linewidth=0.4, alpha=0.5) добавляет сетку — бледную, чтобы она помогала считывать значения, а не спорила с линиями. fig.autofmt_xdate() поворачивает даты, когда они не помещаются.

savefig: формат по расширению

fig.savefig("kurs.png", dpi=150, bbox_inches="tight"):

  • формат берётся из расширения: .png — картинка для письма и мессенджера, .svg — для страницы (масштабируется без потери), .pdf — для печати;
  • dpi превращает дюймы в пиксели: 8 дюймов при 150 — это 1200 точек по ширине. Для экрана хватает 100–150, для печати берут 300;
  • bbox_inches="tight" обрезает пустые поля, иначе вокруг картинки остаётся воздух.

Главное правило — порядок: savefig фотографирует то, что нарисовано на момент вызова. Сохранили до plot — получили пустой лист, и никакой ошибки при этом не будет. Это третья разминка.

close: фигуры не исчезают сами

plt.close(fig) освобождает лист. Пока график один, это незаметно; программа, которая рисует по картинке в день и не закрывает их, на двадцать первый день получит предупреждение, а потом съест память. Привычка простая: нарисовали — сохранили — закрыли.

pandas умеет сам

У Series и DataFrame есть свой .plot(), и он рисует тем же matplotlib:

ax = rate.plot(figsize=(8, 4), color="#b03a2e")
ax.set_ylabel("тенге за доллар")

Он возвращает те же Axes, так что подписи и сохранение делаются как обычно: ax.figure.savefig(...). Это короче для быстрого взгляда на данные. Для отчёта, который кто-то увидит, обычно берут явный fig, ax — в нём видно всё, что происходит.

Карта урока

Карта урока: лист, оси и файл

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Зачем нужна строка matplotlib.use("Agg") и почему она стоит до импорта pyplot?
  2. Чем fig отличается от ax и почему в курсе рисуют через ax?
  3. Что получится, если вызвать savefig до plot, и будет ли при этом ошибка?

Разминка

Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.

1. Предскажите. Что напечатает эта программа?

import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(6, 3))
ax.plot([1, 2, 3], [10, 12, 11], label="ряд")
ax.set_title("проба")
ax.set_ylabel("тенге")
print("линий:", len(ax.get_lines()), "| заголовок:", ax.get_title(), "| ось Y:", ax.get_ylabel())
print("размер листа:", fig.get_size_inches().tolist(), "дюйма")
plt.close(fig)

2. Заполните пропуск. Вместо ... поставьте способ рисования, при котором программе не нужен экран.

# без этой строки программа по расписанию будет искать окно
import matplotlib

matplotlib.use(...)

import matplotlib.pyplot as plt

print("способ рисования:", matplotlib.get_backend())
fig, ax = plt.subplots()
ax.plot([1, 2], [3, 4])
plt.close(fig)

3. Почините. Файл создаётся, но картинка в нём пустая. Программа печатает, сколько линий было на осях в момент сохранения.

# сохранили раньше, чем нарисовали
import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
from pathlib import Path

HERE = Path(__file__).resolve().parent
fig, ax = plt.subplots()
out = HERE / "proba.png"
fig.savefig(out)
print("линий в момент сохранения:", len(ax.get_lines()))
ax.plot([1, 2, 3], [10, 12, 11])
print("линий в конце программы:", len(ax.get_lines()))
plt.close(fig)

Задание

Обязательное. Возьмите тот же ряд и нарисуйте его вместе со сглаженным. Отметьте точкой и подписью день скачка — тот самый, который сглаженная линия не показывает. Подпишите оси и заголовок, поставьте легенду и сетку. Сохраните два файла: kurs.png для письма и kurs.svg для страницы. Напечатайте: число точек, число линий на осях, заголовок, отмеченный день с его значением и по строке про каждый файл.

Ожидаемый вывод:

точек: 30 | линий на осях: 3
заголовок: Курс доллара: настоящий ряд и сглаженный
отмечен день: 2026-01-26 | значение: 532.75
kurs.png: создан True, не пустой True
kurs.svg: создан True, не пустой True

Готово, когда: вывод совпадает построчно; use("Agg") стоит до импорта pyplot; рисование идёт через ax, а не через plt; сохранение — после всего рисования; фигура закрыта; в обоих файлах картинка непустая (откройте и посмотрите).

На своих данных. Нарисуйте любой свой ряд — расходы по дням, шаги, вес. Обязательно подпишите ось Y с единицами. Потом покажите картинку человеку, который ваших данных не видел, и спросите, что он понял. Всё, чего он не понял, — это то, чего не хватает на графике.

По желанию.

  • Сохраните один и тот же график в png, svg и pdf и сравните размеры файлов.
  • Попробуйте dpi=72 и dpi=300 и посмотрите на текст на картинке.
  • Нарисуйте тот же ряд через rate.plot(ax=ax) и убедитесь, что подписи ставятся так же.

Куда это встанет в проекте

Тринадцатый шаг: у отчёта появляется картинка. sholu/suret.py рисует по линии на страну и кладёт файл рядом с CSV, а названия для легенды берёт из того же справочника, что и отчёт, — поэтому на картинке «Қазақстан», а не «KAZ».

Две строки в этом файле стоят там именно потому, что сводка работает по расписанию, а не перед человеком: matplotlib.use("Agg") до импорта pyplot — в четыре утра экрана нет; и plt.close(fig) после сохранения — иначе программа, рисующая по картинке в день, узнает про незакрытые фигуры на двадцать первый день.

Долги. Цвета заданы прямо в коде списком. Пока рядов три, это нормально; когда их станет больше, цвета захотят жить в одном месте вместе с остальным видом отчёта.

Ответы

Показать ответы

На вопросы

  1. Она говорит matplotlib рисовать в файл, а не в окно. Программе, запущенной по расписанию, экран недоступен, и без этой строки она попытается его найти. Ставится до импорта pyplot, потому что pyplot выбирает способ рисования при импорте — после него менять поздно.
  2. fig — лист: размер, поля, сохранение. ax — оси на листе: линии, подписи, сетка, легенда. Через ax рисуют потому, что plt.plot рисует на «текущих» осях, а когда графиков становится два, текущие оказываются не теми.
  3. Получится пустая картинка, и ошибки не будет. savefig сохраняет то, что нарисовано в момент вызова; всё, что нарисовали после, в файл не попадёт.

К разминке

  1. ax знает, что на нём нарисовано и как подписано, а fig — свой размер в дюймах, тот самый, что задан в figsize.
линий: 1 | заголовок: проба | ось Y: тенге
размер листа: [6.0, 3.0] дюйма
  1. matplotlib.use("Agg"). Имя способа рисования пишется строкой, и get_backend() потом возвращает его же.
import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt

print("способ рисования:", matplotlib.get_backend())
fig, ax = plt.subplots()
ax.plot([1, 2], [3, 4])
plt.close(fig)
способ рисования: Agg
  1. Перенести savefig после plot. Ноль линий в момент сохранения — это и есть ответ: в файле пусто, хотя программа отработала без единой жалобы.
import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
from pathlib import Path

HERE = Path(__file__).resolve().parent
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [10, 12, 11])
out = HERE / "proba.png"
fig.savefig(out)
print("линий в момент сохранения:", len(ax.get_lines()))
print("линий в конце программы:", len(ax.get_lines()))
plt.close(fig)
линий в момент сохранения: 1
линий в конце программы: 1

К заданию

Точка и подпись ставятся на настоящий ряд: ax.plot([день], [значение], marker="o") и ax.annotate(...). Смысл задания в том, чтобы на картинке было видно ровно то, о чём говорил прошлый урок, — сглаженная линия проходит мимо события, и рядом с ней это заметно с одного взгляда.

Два файла сохраняются из одной фигуры: savefig можно звать сколько угодно раз, фигура от этого не меняется. Закрывают её один раз, после всех сохранений.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.