Статьи

От стандартного линейного графика в Python до инфографики высокого качества

2025-02-06 10:16 Работа с данными
Каждый, кто хоть раз работал с Matplotlib, знает, насколько неэстетичными могут быть его графики по умолчанию. Давайте изучим несколько хитростей, которые помогут вашим визуализациям выделяться на фоне стандартных.
Мы начнем с простого линейного графика — одного из самых популярных типов визуализации. Основное внимание уделим добавлению градиентной заливки под графиком — задаче, реализация которой не так очевидна, как может показаться.

Для начала импортируем все необходимые библиотеки:
import pandas as pd
import numpy as np
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from matplotlib import rcParams
from matplotlib.path import Path
from matplotlib.patches import PathPatch

np.random.seed(38)  # Для воспроизводимости результатов
Теперь создадим данные для нашей визуализации: сгенерируем ряд, напоминающий то, как выглядит динамика цен на акции.
dates = pd.date_range(start='2024-02-01', periods=100, freq='D')
initial_rate = 75  
drift = 0.003  
volatility = 0.1 
returns = np.random.normal(drift, volatility, len(dates)) 
rates = initial_rate * np.cumprod(1 + returns)
x, y = dates, rates  
Посмотрим, как выглядит график с настройками Matplotlib по умолчанию. Только установим интервал для меток оси X (каждые 30 дней):
fig, ax = plt.subplots()
ax.plot(x, y)
ax.xaxis.set_major_locator(mdates.DayLocator(interval=30))
plt.show()
Не особо впечатляет, правда? Но мы постепенно его улучшим.

Начнем с улучшения базовых элементов графика: добавим заголовок, настроим размер и шрифт, переместим метки оси Y вправо и изменим цвет, стиль и толщину основной линии:
fig, ax = plt.subplots(figsize=(10, 6))  # Устанавливаем размер графика
plt.title("Ежедневное количество посетителей", fontsize=18, color="black")  # Добавляем заголовок
rcParams['font.family'] = 'DejaVu Sans'  # Меняем шрифт
rcParams['font.size'] = 14  # Меняем размер шрифтв

ax.yaxis.tick_right()  # Переносим метки оси Y на правую сторону
ax.yaxis.set_label_position("right")  # Перемещаем подпись оси Y вправо

ax.plot(x, y, color='#268358', linewidth=2)  # Изменяем цвет, стиль и толщину линии

x_interval = 30
ax.xaxis.set_major_locator(mdates.DayLocator(interval=x_interval))

plt.show()
Теперь график стал немного чище. Давайте ещё добавим минималистичную сетку на фон, уберем границы для более аккуратного вида и удалим деления с оси Y.

Вот как это можно сделать:
ax.grid(color="gray", linestyle=(0, (10, 10)), linewidth=0.5, alpha=0.6)  # Минималистичная сетка
ax.tick_params(axis="x", colors="black")  # Установка цвета меток оси X
ax.tick_params(axis="y", left=False, labelleft=False)  # Скрытие делений и меток слева

ax.spines["top"].set_visible(False)  # Убираем верхнюю границу
ax.spines["right"].set_visible(False)  # Убираем правую границу
ax.spines["bottom"].set_color("black")  # Цвет нижней границы
ax.spines["left"].set_color("white")  # Скрываем левую границу
ax.spines["left"].set_linewidth(1)  # Толщина левой границы

ax.tick_params(axis="y", length=0)  # Убираем деления с оси Y
После добавления этого кода график будет выглядеть уже так:
Теперь добавим небольшую эстетическую деталь — год рядом с первой меткой на оси X. Также сделаем цвет меток делений более приглушенным:
def custom_date_formatter(val_num, pos, dates_ref, interval):
    """
    Функция для форматирования меток оси X.
    Получает числовое представление даты 'val_num' и позицию метки 'pos'.
    Если это первая метка (pos=0), добавляем год, иначе только дату.
    """
    try:
        date = mdates.num2date(val_num) # Преобразуем число в дату
        # Проверяем, первая ли это метка по индексу pos
        if pos == 0:
            return date.strftime('%d %b \'%y')  # Формат: "01 Feb '24"
        else:
            return date.strftime('%d %b')      # Формат: "01 Feb"
    except (ValueError, IndexError):
        # В случае ошибки вернуть пустую строку
        return ""

# Применяем пользовательский форматтер к оси X
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
    lambda val, pos: custom_date_formatter(val, pos, dates_ref=x, interval=x_interval)
))
Мы подошли к самому сложному и интересному моменту — созданию градиентной заливки под кривой.

В Matplotlib нет встроенной функции для создания градиентной заливки, но мы можем смоделировать её, создав градиентное изображение и обрезав его по форме графика. Это требует некоторой магии с использованием `Path`, `PathPatch` и `imshow`:
# Преобразуем даты в числовой формат Matplotlib для Path и extent
x_num = mdates.date2num(x)

# Создаем координаты для Path, используя числовое представление
# Добавляем точки на оси X по краям для замыкания полигона
x_num_patch = np.concatenate(([x_num[0]], x_num, [x_num[-1]]))
# Добавляем нули по краям для Y, чтобы замкнуть полигон по оси X
y_patch = np.concatenate(([0], y, [0]))

# Создаем путь (Path) на основе числовых координат дат и значений Y
path_coords = np.array([x_num_patch, y_patch]).transpose()
path = Path(path_coords)
# Создаем PathPatch - невидимую область (facecolor='none'), которая будет маской для обрезки
patch = PathPatch(path, facecolor='none', lw=0)
ax.add_patch(patch) # Добавляем патч на оси

# Создаем градиентное изображение (здесь просто вертикальный градиент)
gradient_img = np.linspace(0, 1, 100).reshape(-1, 1)

# Отображаем градиентное изображение с помощью imshow
im = ax.imshow(
    gradient_img,
    interpolation="bicubic", # Сглаживание градиента
    cmap=plt.cm.Greens,      # Зеленая цветовая карта
    origin='lower',          # Начало координат внизу
    alpha=0.3,               # Прозрачность
    aspect="auto",           # Автоматическое соотношение сторон
    # extent задает границы изображения в координатах данных (числовые даты!)
    extent=[
        x_num[0],            # min x (первая дата в числовом формате)
        x_num[-1],           # max x (последняя дата в числовом формате)
        min(y_patch),        # min y (0)
        max(y) * 1.2         # max y (немного выше максимума данных)
    ],
    clip_path=patch,         # Обрезать изображение по этому пути (патчу)
    clip_on=True,            # Включить обрезку
    zorder=1                 # Слой градиента (ниже линии, выше сетки)
)

# Рисуем линию поверх градиента (zorder=2 по умолчанию)
ax.plot(x, y, color='#268358', linewidth=2, zorder=2)

# Убедимся, что пределы оси X соответствуют диапазону наших дат
ax.set_xlim(x[0], x[-1])
Теперь график выглядит чистым и стильным. Осталось добавить несколько финальных штрихов с помощью любого редактора (например, Google Slides): заголовок, скругленные углы границ и несколько числовых индикаторов.
Ниже полный код, который объединяет все шаги, описанные ранее 👇🏻
fig, ax = plt.subplots(figsize=(10, 6))  # Устанавливаем размер графика
plt.title("Ежедневное количество посетителей", fontsize=18, color="black")  # Добавляем заголовок
rcParams['font.family'] = 'DejaVu Sans'  # Меняем шрифт
rcParams['font.size'] = 14  # Меняем размер шрифтв

ax.yaxis.tick_right()  # Переносим метки оси Y на правую сторону
ax.yaxis.set_label_position("right")  # Перемещаем подпись оси Y вправо

ax.plot(x, y, color='#268358', linewidth=2)  # Изменяем цвет, стиль и толщину линии

# Определяем интервал для меток оси X (в днях)
x_interval = 30

# Устанавливаем локатор оси X (где будут стоять метки)
ax.xaxis.set_major_locator(mdates.DayLocator(interval=x_interval))

ax.grid(color="gray", linestyle=(0, (10, 10)), linewidth=0.5, alpha=0.6)  # Минималистичная сетка
ax.tick_params(axis="x", colors="black")  # Установка цвета меток оси X
ax.tick_params(axis="y", left=False, labelleft=False)  # Скрытие делений и меток слева

ax.spines["top"].set_visible(False)  # Убираем верхнюю границу
ax.spines["right"].set_visible(False)  # Убираем правую границу
ax.spines["bottom"].set_color("black")  # Цвет нижней границы
ax.spines["left"].set_color("white")  # Скрываем левую границу
ax.spines["left"].set_linewidth(1)  # Толщина левой границы

ax.tick_params(axis="y", length=0)  # Убираем деления с оси Y

def custom_date_formatter(val_num, pos, dates_ref, interval):
    """
    Функция для форматирования меток оси X.
    Получает числовое представление даты 'val_num' и позицию метки 'pos'.
    Если это первая метка (pos=0), добавляем год, иначе только дату.
    """
    try:
        date = mdates.num2date(val_num) # Преобразуем число в дату
        # Проверяем, первая ли это метка по индексу pos
        if pos == 0:
            return date.strftime('%d %b \'%y')  # Формат: "01 Feb '24"
        else:
            return date.strftime('%d %b')      # Формат: "01 Feb"
    except (ValueError, IndexError):
        # В случае ошибки вернуть пустую строку
        return ""

# Применяем пользовательский форматтер к оси X
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
    lambda val, pos: custom_date_formatter(val, pos, dates_ref=x, interval=x_interval)
))

# Преобразуем даты в числовой формат Matplotlib для Path и extent
x_num = mdates.date2num(x)

# Создаем координаты для Path, используя числовое представление дат
# Добавляем точки на оси X по краям для замыкания полигона
x_num_patch = np.concatenate(([x_num[0]], x_num, [x_num[-1]]))
# Добавляем нули по краям для Y, чтобы замкнуть полигон по оси X
y_patch = np.concatenate(([0], y, [0]))

# Создаем путь (Path) на основе числовых координат дат и значений Y
path_coords = np.array([x_num_patch, y_patch]).transpose()
path = Path(path_coords)
# Создаем PathPatch - невидимую область (facecolor='none'), которая будет маской для обрезки
patch = PathPatch(path, facecolor='none', lw=0)
ax.add_patch(patch) # Добавляем патч на оси

# Создаем градиентное изображение (здесь просто вертикальный градиент)
gradient_img = np.linspace(0, 1, 100).reshape(-1, 1)

# Отображаем градиентное изображение с помощью imshow
im = ax.imshow(
    gradient_img,
    interpolation="bicubic", # Сглаживание градиента
    cmap=plt.cm.Greens,      # Зеленая цветовая карта
    origin='lower',          # Начало координат внизу
    alpha=0.3,               # Прозрачность
    aspect="auto",           # Автоматическое соотношение сторон
    # extent задает границы изображения в координатах данных (числовые даты!)
    extent=[
        x_num[0],            # min x (первая дата в числовом формате)
        x_num[-1],           # max x (последняя дата в числовом формате)
        min(y_patch),        # min y (0)
        max(y) * 1.2         # max y (немного выше максимума данных)
    ],
    clip_path=patch,         # Обрезать изображение по этому пути (патчу)
    clip_on=True,            # Включить обрезку
    zorder=1                 # Слой градиента (ниже линии, выше сетки)
)

# Рисуем линию поверх градиента (zorder=2 по умолчанию)
ax.plot(x, y, color='#268358', linewidth=2, zorder=2)

# Убедимся, что пределы оси X соответствуют диапазону наших дат
ax.set_xlim(x[0], x[-1])

plt.show()

# Опционально: Сохраняем график
# plt.savefig('high_quality_plot_final_v3.png', dpi=300, bbox_inches='tight')

Источник: Medium