Візуалізація даних за допомогою matplotlib

Композиції та стилізація — subplots та багатопанельні дашборди

Побудова кількох графіків на одній фігурі, GridSpec для складних компоновок, кольорові схеми, теми, стилізація для публікацій та презентацій

Композиції та стилізація — subplots та багатопанельні дашборди

🎯 Мета підтеми

  • Навчитися створювати кілька графіків на одній фігурі (subplots)
  • Опанувати GridSpec для складних компоновок
  • Освоїти кольорові схеми та теми matplotlib
  • Навчитися створювати «дашборди» для аналізу даних
  • Підготувати графіки для публікацій, презентацій та звітів

🔑 Ключові терміни

  • Subplots — кілька осей (Axes) на одній фігурі (Figure)
  • GridSpec — гнучка сітка для нестандартних компоновок
  • Colormap — кольорова палітра для безперервних даних
  • Style — тема оформлення (seaborn, ggplot, dark_background)
  • Tight layout — автоматичне вирівнювання для уникнення перекриття

Плавний старт: Subplots за 3 рядки коду

Коли для повноти аналізу одного графіка недостатньо, інженери об'єднують кілька панелей на одній фігурі — створюють subplots. Це дозволяє порівнювати метрики різних систем в одному візуальному контексті.

Погляньмо, як від трьох мінімальних рядків перейти до повноцінного центру моніторингу:

hello_subplots.ipynb
Python 3 (ipykernel)
Code
md

Три рівні створення багатопанельних композицій

Від створення сітки 1×2 у 3 рядки до інженерного дашборду з пороговими лініями SLA

In [1]:
0.21s
import matplotlib.pyplot as plt
import numpy as np

# Синтетичні дані моніторингу сервера: 60 хвилин роботи
rng = np.random.default_rng(42)
t = np.linspace(0, 60, 60)
cpu = 35 + 20 * np.sin(t / 8) + rng.normal(0, 3, 60)
ram = 4 + 0.05 * t + rng.normal(0, 0.1, 60)

# --- РІВЕНЬ 0: Мінімальний код (3 рядки) ---
fig, (ax1, ax2) = plt.subplots(1, 2)
plt.show()

# --- РІВЕНЬ 1: Розділені осі та фізичні одиниці виміру ---
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(7, 4.5))
ax1.plot(t, cpu, color='#2563eb')
ax1.set_ylabel("CPU (%)")
ax1.set_title("Навантаження обчислювального вузла")
ax1.grid(True, linestyle=':', alpha=0.5)

ax2.plot(t, ram, color='#10b981')
ax2.set_xlabel("Час моніторингу (хв)")
ax2.set_ylabel("RAM (GB)")
ax2.grid(True, linestyle=':', alpha=0.5)
plt.tight_layout()
plt.show()

# --- РІВЕНЬ 2: Професійний NOC-дашборд з SLA та зонами заповнення ---
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 5))
fig.suptitle("NOC Оперативний моніторинг: Node Alpha", fontsize=13, fontweight='bold')

# Панель процесора
ax1.plot(t, cpu, color='#ef4444', linewidth=1.8, label="CPU Core 0")
ax1.axhline(50, color='#dc2626', linestyle='--', linewidth=1.2, label="SLA поріг (50%)")
ax1.fill_between(t, cpu, alpha=0.15, color='#ef4444')
ax1.set_ylabel("CPU (%)")
ax1.legend(loc='upper right', frameon=True, facecolor='#f8fafc')
ax1.grid(True, linestyle=':', alpha=0.5)

# Панель оперативної пам'яті
ax2.plot(t, ram, color='#3b82f6', linewidth=1.8, label="RAM Total")
ax2.fill_between(t, ram, alpha=0.15, color='#3b82f6')
ax2.set_xlabel("Час моніторингу (хв)")
ax2.set_ylabel("RAM (GB)")
ax2.legend(loc='lower right', frameon=True, facecolor='#f8fafc')
ax2.grid(True, linestyle=':', alpha=0.5)

plt.tight_layout()
plt.show()
Out[1]:
md

Чому багатопанельні композиції незамінні в інженерії?

  1. Єдиний контекст порівняння: зіставлення піку процесора з одночасним зростанням пам'яті або мережевого трафіку дозволяє за секунди знайти причину системного збою.
  2. Усунення плутанини одиниць виміру: замість нагромадження ліній із відсотками (%) та гігабайтами (GB) на одних осях, subplots надають кожній метриці власний чистий простір.
  3. Економія уваги людини: структурований звіт на одній сторінці читається в рази швидше, ніж перемикання між окремими вікнами чи вкладками.

🧭 Наскрізна інженерна історія: Побудова центру оперативного моніторингу (NOC Dashboard)

Уявіть, що ви керуєте надійністю інфраструктури (Lead SRE) у хмарному провайдері. Перед запуском нового регіону дата-центру команді потрібен централізований екран моніторингу (NOC Dashboard), який у реальному часі відображатиме здоров'я кластера:

  • Крок 1 (Subplots): Створити базову сітку синхронізованих панелей для ключових ресурсів сервера (CPU vs RAM).
  • Крок 2 (Анатомія plt.subplots та subplot_mosaic): Опанувати гнучку адресацію підграфіків у циклі через .flatten() та декларативний синтаксис мозаїки для швидкого прототипування.
  • Крок 3 (Асиметричний GridSpec): Побудувати професійний діагностичний Jointplot (основне поле розсіювання + бічні та верхні маргінальні гістограми розподілу).
  • Крок 4 (Спільні осі sharex / sharey): Забезпечити безшовну часову синхронізацію: масштабування одного графіка автоматично вирівнює всі інші панелі.
  • Крок 5 (Теми та стилізація): Налаштувати темну тему (dark_background) для моніторів чергової зміни та чистий стиль (seaborn-v0_8-whitegrid) для звітів керівництву.
  • Крок 6 (Інженерні анотації та зони axvspan / axhspan): Підсвітити часові інтервали DDoS-атак та порогові коридори SLA (Normal, Warning, Critical).

Розгляньмо кожен із цих аналітичних інструментів послідовно.


Історія про аналітика та квартальний звіт

Аналітик Ганна готує квартальний звіт для CEO. Їй потрібно показати:

  1. Динаміку продажів за 12 місяців
  2. Розподіл клієнтів за віком
  3. Топ-5 продуктів за виручкою
  4. Кореляцію між витратами на маркетинг та продажами

Вона може створити 4 окремі файли з графіками. Але CEO хоче одну сторінку — щоб порівняти все одним поглядом.

Рішення: subplots — 4 графіки на одній фігурі.

dashboard_example.ipynb
Python 3 (ipykernel)
Code
In [1]:
0.35s
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(seed=42)

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle("Квартальний звіт Q4 2026 — Ключові метрики", 
             fontsize=16, fontweight="bold")

# 1. Динаміка продажів (лінійний графік)
months = ["Січ", "Лют", "Бер", "Кві", "Тра", "Чер", 
          "Лип", "Сер", "Вер", "Жов", "Лис", "Гру"]
sales = [120, 135, 150, 145, 160, 180, 190, 185, 200, 220, 240, 260]

axes[0, 0].plot(months, sales, marker="o", linewidth=2.5, color="steelblue")
axes[0, 0].fill_between(range(len(months)), sales, alpha=0.3, color="steelblue")
axes[0, 0].set_title("Динаміка продажів (тис. грн)", fontweight="bold")
axes[0, 0].set_ylabel("Продажі")
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].tick_params(axis="x", rotation=45)

# 2. Розподіл клієнтів за віком (гістограма)
ages = rng.normal(35, 12, 500)
axes[0, 1].hist(ages, bins=25, color="coral", edgecolor="black", alpha=0.7)
axes[0, 1].axvline(np.mean(ages), color="red", linestyle="--", linewidth=2, 
                   label=f"Середнє: {np.mean(ages):.1f}")
axes[0, 1].set_title("Розподіл клієнтів за віком", fontweight="bold")
axes[0, 1].set_xlabel("Вік (роки)")
axes[0, 1].set_ylabel("Кількість клієнтів")
axes[0, 1].legend()
axes[0, 1].grid(axis="y", alpha=0.3)

# 3. Топ-5 продуктів (bar chart)
products = ["Продукт A", "Продукт B", "Продукт C", "Продукт D", "Продукт E"]
revenue = [450, 380, 320, 280, 210]

axes[1, 0].barh(products, revenue, color="mediumseagreen", edgecolor="black")
axes[1, 0].set_title("Топ-5 продуктів за виручкою (тис. грн)", fontweight="bold")
axes[1, 0].set_xlabel("Виручка")
axes[1, 0].grid(axis="x", alpha=0.3)

# 4. Кореляція маркетинг — продажі (scatter)
marketing = np.array([10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65])
sales_vs_marketing = 3 * marketing + 80 + rng.normal(0, 10, 12)

axes[1, 1].scatter(marketing, sales_vs_marketing, s=100, alpha=0.7, 
                   color="purple", edgecolor="black")

# Лінія тренду
z = np.polyfit(marketing, sales_vs_marketing, 1)
p = np.poly1d(z)
axes[1, 1].plot(marketing, p(marketing), "r--", linewidth=2, 
                label=f"r = {np.corrcoef(marketing, sales_vs_marketing)[0,1]:.2f}")

axes[1, 1].set_title("Маркетинг vs Продажі", fontweight="bold")
axes[1, 1].set_xlabel("Витрати на маркетинг (тис. грн)")
axes[1, 1].set_ylabel("Продажі (тис. грн)")
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
Out[1]:
md

Переваги композиції:

  1. Одна сторінка замість чотирьох окремих файлів
  2. Швидке порівняння — CEO бачить усі метрики одночасно
  3. Контекст — динаміка продажів зрозуміліша поруч з маркетингом
  4. Професійний вигляд — єдиний загальний заголовок, узгоджені кольори
Правило дашбордів:Кількість графіків на одній фігурі: 3–6 оптимально, 9 максимум. Більше — розділіть на кілька сторінок.Причина: когнітивне навантаження. Людина може утримати в уваіу ~5–7 об'єктів одночасно.

Базові subplots — регулярна сітка

Створення сітки 2×2

subplots_basic.ipynb
Python 3 (ipykernel)
Code
In [1]:
import matplotlib.pyplot as plt
import numpy as np

# Створюємо сітку 2x2
rng = np.random.default_rng(seed=42)
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# axes — це масив NumPy розміром (2, 2)
print(f"Тип axes: {type(axes)}")
print(f"Форма axes: {axes.shape}")

# Звертаємося до конкретних осей через індекси
axes[0, 0].plot([1, 2, 3], [1, 4, 9], 'r-o')
axes[0, 0].set_title("Графік 1: Квадрати")

axes[0, 1].hist(rng.normal(0, 1, 500), bins=30, color="green", alpha=0.7)
axes[0, 1].set_title("Графік 2: Гістограма")

axes[1, 0].scatter(rng.random(50), rng.random(50), s=100, alpha=0.6)
axes[1, 0].set_title("Графік 3: Scatter")

axes[1, 1].bar(["A", "B", "C"], [10, 25, 15], color=["red", "blue", "green"])
axes[1, 1].set_title("Графік 4: Bar Chart")

plt.tight_layout()
plt.show()
Out[1]:

Тип axes: <class 'numpy.ndarray'> Форма axes: (2, 2)

Основні параметри plt.subplots():
fig, axes = plt.subplots(nrows=2,        # Кількість рядків
                         ncols=3,        # Кількість стовпців
                         figsize=(12,8), # Розмір фігури (ширина, висота)
                         sharex=False,   # Спільна вісь X?
                         sharey=False,   # Спільна вісь Y?
                         squeeze=True)   # Якщо 1 Axes, повернути його (не масив)
Повернене значення:
  • Якщо nrows=1, ncols=1 → axes — один об'єкт Axes
  • Якщо один вимір > 1 → axes — одновимірний масив
  • Якщо обидва виміри > 1 → axes — двовимірний масив

Доступ до осей у різних конфігураціях

axes_access.ipynb
Python 3 (ipykernel)
Code
In [1]:
import matplotlib.pyplot as plt

# Конфігурація 1: один рядок, три стовпці
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# axes — одновимірний масив [ax0, ax1, ax2]
axes[0].plot([1, 2, 3])
axes[1].bar(["A", "B"], [5, 10])
axes[2].scatter([1, 2], [3, 4])

plt.tight_layout()
plt.show()

# Конфігурація 2: три рядки, один стовпець
fig, axes = plt.subplots(3, 1, figsize=(8, 10))

# axes — одновимірний масив [ax0, ax1, ax2]
axes[0].plot([1, 2, 3])
axes[1].bar(["A", "B"], [5, 10])
axes[2].scatter([1, 2], [3, 4])

plt.tight_layout()
plt.show()

# Конфігурація 3: 2x3 сітка
fig, axes = plt.subplots(2, 3, figsize=(15, 8))

# axes — двовимірний масив (2, 3)
axes[0, 0].plot([1, 2])
axes[1, 2].bar(["X"], [7])

plt.tight_layout()
plt.show()
Out[1]:
Пастка: flatten() для універсальностіЯкщо ви не знаєте заздалегідь розмірності axes (одновимірний чи двовимірний), використовуйте .flatten():
fig, axes = plt.subplots(2, 3)
axes_flat = axes.flatten()  # Тепер завжди одновимірний масив [ax0, ax1, ..., ax5]

for i, ax in enumerate(axes_flat):
    ax.text(0.5, 0.5, f"Графік {i+1}", ha="center", va="center", fontsize=14)
Це дозволяє ітерувати по осях у циклі без думок про форму масиву.

Повна сигнатура plt.subplots() та сучасний fig.subplot_mosaic()

Функція plt.subplots() є основним високорівневим фабричним методом створення фігури разом із сіткою осей:

plt.subplots(
    nrows=1,
    ncols=1,
    *,
    sharex=False,
    sharey=False,
    squeeze=True,
    width_ratios=None,
    height_ratios=None,
    subplot_kw=None,
    gridspec_kw=None,
    **fig_kw
) -> tuple[Figure, Axes | np.ndarray]
ПараметрТипЗа замовчуваннямОпис та інженерне призначення
nrows, ncolsint1, 1Кількість рядків і стовпців у регулярній сітці.
sharex, shareybool | {'all', 'row', 'col'}FalseСинхронізація меж та шкали осей X або Y між підграфіками.
squeezeboolTrueЯкщо True, скасовує зайві виміри (повертає окремий Axes для 1×1 або 1D масив для 1×N). Якщо False, завжди повертає 2D-масив форми (nrows, ncols).
width_ratiosSequence[float]NoneПропорції ширини стовпців (наприклад, [2, 1] робить перший стовпець удвічі ширшим).
height_ratiosSequence[float]NoneПропорції висоти рядків (наприклад, [3, 1] для головного графіка та підвалу).
subplot_kwdictNoneСловник аргументів, що передаються в кожен окремий Axes (наприклад, {'projection': 'polar'}).
gridspec_kwdictNoneСловник додаткових параметрів для базового GridSpec (wspace, hspace).
Революційний метод fig.subplot_mosaic() (Matplotlib 3.3+)Для побудови нестандартних екранів більше не обов'язково вручну вираховувати зрізи індексів gs[:, 0]! Метод subplot_mosaic() дозволяє задавати макет візуально у вигляді текстової матриці:
mosaic = [
    ["main", "main"],
    ["bar",  "hist"]
]
fig, ax_dict = plt.subplot_mosaic(mosaic, figsize=(10, 7))

# Звернення напряму за семантичними назвами:
ax_dict["main"].plot(t, cpu)
ax_dict["bar"].bar(cats, vals)
ax_dict["hist"].hist(errors)
Символ . у рядку mosaic означає порожній простір (пустий слот).
subplots_mosaic_deep_dive.ipynb
Python 3 (ipykernel)
Code
md

Дослідження компоновок: 1×3, 2×2, subplot_mosaic та ітерація flatten()

Порівняння одновимірних і двовимірних осей, декларативної мозаїки та пакетного опрацювання в циклі

In [1]:
0.27s
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(42)
t = np.linspace(0, 20, 40)

# Приклад 1: Використання subplot_mosaic для асиметричного NOC-екрана
mosaic = [
    ["monitor", "monitor"],
    ["summary", "events"]
]
fig, ax_dict = plt.subplot_mosaic(mosaic, figsize=(11, 7))
fig.suptitle("NOC Дашборд через fig.subplot_mosaic()", fontsize=14, fontweight='bold')

# Головний графік зверху
ax_dict["monitor"].plot(t, 40 + 15 * np.sin(t/3), color='#0284c7', linewidth=2, label="Cluster Load")
ax_dict["monitor"].set_title("Головний монітор: ax_dict['monitor']", fontweight='bold')
ax_dict["monitor"].legend(loc='upper right')
ax_dict["monitor"].grid(True, linestyle=':', alpha=0.5)

# Підсумкова діаграма зліва знизу
ax_dict["summary"].bar(["Ok", "Warn", "Err"], [120, 15, 2], color=['#10b981', '#f59e0b', '#ef4444'], width=0.5)
ax_dict["summary"].set_title("Статуси: ax_dict['summary']", fontweight='bold')
ax_dict["summary"].grid(axis='y', linestyle=':', alpha=0.5)

# Гістограма справа знизу
ax_dict["events"].hist(rng.normal(25, 5, 100), bins=15, color='#8b5cf6', edgecolor='black')
ax_dict["events"].set_title("Розподіл затримок: ax_dict['events']", fontweight='bold')
ax_dict["events"].grid(True, linestyle=':', alpha=0.5)

plt.tight_layout()
plt.show()
Out[1]:

GridSpec — нестандартні компоновки

Іноді потрібна асиметрична сітка: один великий графік зліва + два маленькі справа.

gridspec_example.ipynb
Python 3 (ipykernel)
Code
In [1]:
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np

rng = np.random.default_rng(seed=42)

fig = plt.figure(figsize=(12, 8))
gs = gridspec.GridSpec(2, 2, figure=fig, width_ratios=[2, 1], height_ratios=[1, 1])

# Лівий графік — займає два рядки
ax_left = fig.add_subplot(gs[:, 0])  # Всі рядки (:), перший стовпець (0)
x = np.linspace(0, 10, 100)
ax_left.plot(x, np.sin(x), label="sin(x)", linewidth=2)
ax_left.plot(x, np.cos(x), label="cos(x)", linewidth=2)
ax_left.set_title("Основний графік (великий)", fontsize=13, fontweight="bold")
ax_left.legend()
ax_left.grid(True, alpha=0.3)

# Правий верхній
ax_top_right = fig.add_subplot(gs[0, 1])
ax_top_right.hist(rng.normal(0, 1, 300), bins=20, color="coral", edgecolor="black")
ax_top_right.set_title("Деталь 1", fontweight="bold")

# Правий нижній
ax_bottom_right = fig.add_subplot(gs[1, 1])
ax_bottom_right.scatter(rng.random(50), rng.random(50), s=80, alpha=0.7, c="purple")
ax_bottom_right.set_title("Деталь 2", fontweight="bold")

plt.tight_layout()
plt.show()
Out[1]:
Синтаксис GridSpec:
gs = gridspec.GridSpec(nrows, ncols, 
                       figure=fig,
                       width_ratios=[2, 1],   # Співвідношення ширини стовпців
                       height_ratios=[1, 2],  # Співвідношення висоти рядків
                       wspace=0.3,            # Горизонтальний проміжок
                       hspace=0.4)            # Вертикальний проміжок

# Додавання осей:
ax = fig.add_subplot(gs[row, col])         # Одна клітинка
ax = fig.add_subplot(gs[row1:row2, col])   # Кілька рядків
ax = fig.add_subplot(gs[:, col1:col2])     # Кілька стовпців
Зрізи (slicing):
  • gs[0, 1] — рядок 0, стовпець 1
  • gs[:, 0] — всі рядки, стовпець 0
  • gs[1:3, :] — рядки 1–2, всі стовпці

Повна сигнатура GridSpec та архітектура інженерного Jointplot

Клас matplotlib.gridspec.GridSpec є низькорівневим координатором геометрії. Він дозволяє будувати довільні асиметричні компоновки, коли кожен підграфік займає кілька комірок віртуальної матриці.

matplotlib.gridspec.GridSpec(
    nrows,
    ncols,
    figure=None,
    left=None,
    bottom=None,
    right=None,
    top=None,
    wspace=None,
    hspace=None,
    width_ratios=None,
    height_ratios=None
)
ПараметрТипЗа замовчуваннямОпис та інженерне призначення
nrows, ncolsintобов'язковіКількість рядків і стовпців віртуальної координатної сітки.
width_ratiosSequence[float]NoneСпіввідношення ширини стовпців (наприклад, [4, 1] для великої зони аналізу та вузької бічної панелі).
height_ratiosSequence[float]NoneСпіввідношення висоти рядків (наприклад, [1, 4] для компактної шапки та головного полотна).
wspacefloatNone (rcParams)Горизонтальний проміжок між сусідніми стовпцями (для злитих графіків jointplot ставлять 0.05–0.08).
hspacefloatNone (rcParams)Вертикальний проміжок між сусідніми рядками.
left, right, bottom, topfloatNoneМежі розміщення сітки відносно країв полотна у частках від 0.0 до 1.0.
gridspec_jointplot_deep_dive.ipynb
Python 3 (ipykernel)
Code
md

Професійний патерн: Створення класичного Jointplot через GridSpec

Поєднання двовимірного розсіювання (Scatter) з маргінальними розподілами (Histogram) зверху та праворуч

In [1]:
0.29s
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np

rng = np.random.default_rng(42)
x = rng.normal(50, 15, 300)
y = 0.6 * x + rng.normal(20, 8, 300)

fig = plt.figure(figsize=(10, 8))
# Сітка 2x2: верхній рядок низький (height_ratio=1), лівий стовпець широкий (width_ratio=4)
gs = gridspec.GridSpec(2, 2, figure=fig, width_ratios=[4, 1], height_ratios=[1, 4],
                       wspace=0.08, hspace=0.08)

# 1. Центральний головний scatter plot
ax_main = fig.add_subplot(gs[1, 0])
ax_main.scatter(x, y, color='#059669', alpha=0.5, edgecolor='#064e3b', s=30)
ax_main.set_xlabel("Вхідний трафік (Mb/s)")
ax_main.set_ylabel("Затримка виконання (ms)")
ax_main.grid(True, linestyle=':', alpha=0.6)

# 2. Верхня маргінальна гістограма (спільна вісь X з ax_main)
ax_top = fig.add_subplot(gs[0, 0], sharex=ax_main)
ax_top.hist(x, bins=30, color='#34d399', edgecolor='#065f46', alpha=0.7)
ax_top.set_title("Архітектура Jointplot: Main + Marginal Distributions", fontweight='bold')
ax_top.grid(axis='y', linestyle=':', alpha=0.4)
# Приховуємо підписи осі X, щоб вони не дублювали головний графік
plt.setp(ax_top.get_xticklabels(), visible=False)

# 3. Права маргінальна гістограма (спільна вісь Y з ax_main)
ax_right = fig.add_subplot(gs[1, 1], sharey=ax_main)
ax_right.hist(y, bins=30, orientation='horizontal', color='#34d399', edgecolor='#065f46', alpha=0.7)
ax_right.grid(axis='x', linestyle=':', alpha=0.4)
# Приховуємо підписи осі Y
plt.setp(ax_right.get_yticklabels(), visible=False)

plt.show()
Out[1]:

Спільні осі (sharex, sharey)

Коли графіки порівнюються, корисно мати спільні осі для вирівнювання масштабу.

shared_axes.ipynb
Python 3 (ipykernel)
Code
In [1]:
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)

fig, axes = plt.subplots(3, 1, figsize=(10, 10), sharex=True, sharey=True)

# Три різні функції з однаковим діапазоном X
axes[0].plot(x, np.sin(x), color="blue")
axes[0].set_title("sin(x)")
axes[0].grid(True, alpha=0.3)

axes[1].plot(x, np.sin(2*x), color="red")
axes[1].set_title("sin(2x)")
axes[1].set_ylabel("Амплітуда (спільна вісь Y)", fontsize=12, fontweight="bold")
axes[1].grid(True, alpha=0.3)

axes[2].plot(x, np.sin(3*x), color="green")
axes[2].set_title("sin(3x)")
axes[2].set_xlabel("X (спільна вісь)", fontsize=12, fontweight="bold")
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
Out[1]:
Переваги sharex та sharey:
  1. Автоматична синхронізація масштабу — зум на одному графіку → зум на всіх
  2. Економія місця — мітки осі показуються лише на крайніх графіках
  3. Легше порівнювати — однаковий масштаб дозволяє бачити різницю у величині
Коли використовувати:
  • sharex — часові серії (усі мають однаковий діапазон дат)
  • sharey — графіки з однаковими одиницями виміру
  • Обидва — порівняння подібних метрик (продажі в різних регіонах)

Кольорові схеми та теми

Matplotlib підтримує готові теми (styles) для швидкої зміни вигляду графіків.

styles_demo.ipynb
Python 3 (ipykernel)
Code
In [1]:
import matplotlib.pyplot as plt
import numpy as np

styles = ["default", "seaborn-v0_8-darkgrid", "ggplot", "bmh"]
x = np.linspace(0, 10, 100)

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle("Порівняння стилів matplotlib", fontsize=16, fontweight="bold")

for ax, style in zip(axes.flat, styles):
    with plt.style.context(style):  # Тимчасово застосовуємо стиль
        ax.plot(x, np.sin(x), label="sin(x)", linewidth=2)
        ax.plot(x, np.cos(x), label="cos(x)", linewidth=2)
        ax.set_title(f"Стиль: {style}", fontweight="bold")
        ax.legend()
        ax.grid(True)

plt.tight_layout()
plt.show()
Out[1]:
Популярні стилі matplotlib:
# Застосувати стиль глобально
plt.style.use("seaborn-v0_8-darkgrid")

# Застосувати тимчасово (context manager)
with plt.style.context("ggplot"):
    plt.plot([1, 2, 3])
    plt.show()
СтильОписКоли використовувати
defaultКласичний matplotlibТехнічна документація
seaborn-v0_8-darkgridСвітлий фон + сіткаСтатистичні звіти
ggplotСтиль R ggplot2Наукові статті
bmhBayesian Methods for HackersПрезентації
fivethirtyeightСтиль сайту FiveThirtyEightЖурналістика даних
dark_backgroundТемний фонПрезентації в темряві
Список всіх: print(plt.style.available)

Кастомізація кольорових палітр

color_palettes.ipynb
Python 3 (ipykernel)
Code
In [1]:
import matplotlib.pyplot as plt
import numpy as np

# Створюємо дані: 5 категорій
categories = ["A", "B", "C", "D", "E"]
values = [23, 45, 56, 78, 32]

fig, axes = plt.subplots(2, 3, figsize=(16, 9))
fig.suptitle("Кольорові палітри для категорій", fontsize=15, fontweight="bold")

# 1. Дефолтні кольори matplotlib (C0, C1, ...)
axes[0, 0].bar(categories, values, color=[f"C{i}" for i in range(5)], edgecolor="black")
axes[0, 0].set_title("Default (C0–C9)")

# 2. Tab10 (10 розрізнюваних кольорів)
colors_tab10 = plt.cm.tab10(np.linspace(0, 1, 5))
axes[0, 1].bar(categories, values, color=colors_tab10, edgecolor="black")
axes[0, 1].set_title("Tab10 (рекомендовано)")

# 3. Viridis (послідовна палітра)
colors_viridis = plt.cm.viridis(np.linspace(0, 1, 5))
axes[0, 2].bar(categories, values, color=colors_viridis, edgecolor="black")
axes[0, 2].set_title("Viridis")

# 4. Pastel (м'які кольори)
colors_pastel = plt.cm.Pastel1(np.linspace(0, 1, 5))
axes[1, 0].bar(categories, values, color=colors_pastel, edgecolor="black")
axes[1, 0].set_title("Pastel1")

# 5. Set3 (категоріальні)
colors_set3 = plt.cm.Set3(np.linspace(0, 1, 5))
axes[1, 1].bar(categories, values, color=colors_set3, edgecolor="black")
axes[1, 1].set_title("Set3")

# 6. Кастомна палітра (власні HEX кольори)
custom_colors = ["#FF6B6B", "#4ECDC4", "#95E1D3", "#FFD93D", "#C7CEEA"]
axes[1, 2].bar(categories, values, color=custom_colors, edgecolor="black")
axes[1, 2].set_title("Кастомна (HEX)")

plt.tight_layout()
plt.show()
Out[1]:
Accessibility: Колірна сліпота~8% чоловіків та ~0.5% жінок мають порушення кольорового зору (найчастіше — червоний/зелений).Безпечні палітри (colorblind-safe):
  • viridis, plasma, cividis — послідовні
  • tab10, Paired — категоріальні
  • Використовуйте різні стилі ліній (linestyle) як додатковий канал
Уникайте: червоний + зелений для критичних розрізнень (як стоп/іти).Перевірити: Color Blindness Simulator

Системні налаштування plt.style та plt.rcParams для корпоративних звітів

У великих проєктах замість ручного задавання параметрів кожного графіка використовують глобальні файли конфігурації стилів або контекстні менеджери:

  1. Глобальне перемикання теми: plt.style.use("seaborn-v0_8-whitegrid") — застосовує набір кольорів, сіток та шрифтів до всіх наступних графіків у сесії.
  2. Локальне ізольоване застосування: конструкція with plt.style.context("dark_background"): змінює стиль виключно всередині блоку with, не впливаючи на інші візуалізації.
  3. Глибока кастомізація через plt.rcParams: глобальний словник параметрів Matplotlib, що керує абсолютно всіма властивостями рендерингу (розміри шрифтів, товщини ліній, роздільна здатність figure.dpi тощо).
# Приклад корпоративного налаштування rcParams
plt.rcParams.update({
    "font.family": "sans-serif",
    "font.size": 11,
    "figure.titlesize": 14,
    "axes.titlesize": 12,
    "axes.labelsize": 11,
    "axes.edgecolor": "#cbd5e1",
    "axes.grid": True,
    "grid.linestyle": ":",
    "grid.alpha": 0.6,
    "figure.dpi": 150
})
Параметр rcParamsТипПриклад значенняІнженерне призначення
font.familystr'sans-serif', 'Inter'Сімейство шрифтів для всіх текстових написів.
figure.dpifloat150 (екран), 300 (друк)Роздільна здатність полотна у точках на дюйм (DPI).
axes.edgecolorcolor'#cbd5e1', '#334155'Колір рамок координатних осей (spines).
axes.gridboolTrueАвтоматичне увімкнення сітки для всіх осей без виклику ax.grid().
savefig.bboxstr'tight'Автоматичне обрізання зайвих полів при збереженні через fig.savefig().
styles_comparison_deep_dive.ipynb
Python 3 (ipykernel)
Code
md

Порівняльне дослідження стилів: Seaborn, ggplot, Dark та BMH

Вплив теми на читабельність графіків у світлих і темних інтерфейсах

In [1]:
0.25s
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)

fig, axes = plt.subplots(2, 2, figsize=(13, 9.5))

styles = [
    ("seaborn-v0_8-whitegrid", "Стиль Seaborn: світлий фон, чітка сітка"),
    ("ggplot", "Стиль ggplot2: класичний сірий фон та біла сітка"),
    ("dark_background", "Стиль Dark: темна тема для нічного чергування"),
    ("bmh", "Стиль BMH: високий контраст для аналітичних звітів")
]

for idx, (st_name, st_desc) in enumerate(styles):
    ax = axes[idx // 2, idx % 2]
    with plt.style.context(st_name):
        ax.plot(x, y1, label="Signal A (sin)", linewidth=2)
        ax.plot(x, y2, label="Signal B (cos)", linewidth=2, linestyle='--')
        ax.set_title(f"Стиль: '{st_name}'\n{st_desc}", fontweight='bold')
        ax.set_xlabel("Час (с)")
        ax.set_ylabel("Амплітуда")
        ax.legend()

plt.tight_layout()
plt.show()
Out[1]:

Анотації та текстові підписи

annotations.ipynb
Python 3 (ipykernel)
Code
In [1]:
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
y = np.sin(x) * np.exp(-x/10)

fig, ax = plt.subplots(figsize=(12, 7))

ax.plot(x, y, linewidth=2.5, color="darkblue")

# Знаходимо максимум
max_idx = np.argmax(y)
max_x, max_y = x[max_idx], y[max_idx]

# Анотація зі стрілкою
ax.annotate("Локальний максимум", 
            xy=(max_x, max_y),                    # Точка, на яку вказуємо
            xytext=(max_x + 2, max_y + 0.3),      # Розташування тексту
            fontsize=12, fontweight="bold", color="darkred",
            arrowprops=dict(arrowstyle="->", color="red", lw=2),
            bbox=dict(boxstyle="round,pad=0.5", facecolor="yellow", alpha=0.8))

# Точка на максимумі
ax.plot(max_x, max_y, "ro", markersize=12, label=f"Max: ({max_x:.2f}, {max_y:.2f})")

# Горизонтальна лінія через максимум
ax.axhline(max_y, color="gray", linestyle="--", alpha=0.5)

# Вертикальна лінія
ax.axvline(max_x, color="gray", linestyle="--", alpha=0.5)

# Текст без стрілки
ax.text(7, -0.2, "Загасаюча амплітуда", fontsize=11, 
        style="italic", bbox=dict(boxstyle="round", facecolor="lightblue", alpha=0.6))

ax.set_title("Загасаюча синусоїда з анотаціями", fontsize=14, fontweight="bold")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.legend(loc="upper right")
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
Out[1]:
Методи додавання тексту:
# 1. Простий текст
ax.text(x, y, "Текст", fontsize=12, color="red", ha="center", va="top")

# 2. Анотація зі стрілкою
ax.annotate("Пояснення", xy=(x_point, y_point), xytext=(x_text, y_text),
            arrowprops=dict(arrowstyle="->", color="blue"))

# 3. Горизонтальна лінія з підписом
ax.axhline(y=value, color="gray", linestyle="--", label="Поріг")

# 4. Вертикальна лінія
ax.axvline(x=value, color="gray", linestyle=":")

# 5. Вертикальна зона (діапазон)
ax.axvspan(x1, x2, alpha=0.2, color="yellow")  # Підсвічує область між x1 та x2

# 6. Горизонтальна зона
ax.axhspan(y1, y2, alpha=0.2, color="lightblue")

Повна сигнатура ax.annotate() та інженерне зонування (axvspan / axhspan)

Метод ax.annotate() надає найбільш прецизійний контроль над інженерними підписами, дозволяючи зв'язувати пояснювальний текст із конкретною точкою даних за допомогою векторних стрілок із кастомною кривизною.

ax.annotate(
    text,
    xy,
    xytext=None,
    xycoords='data',
    textcoords=None,
    arrowprops=None,
    annotation_clip=None,
    **kwargs
) -> Annotation
ПараметрТипЗа замовчуваннямОпис та інженерне призначення
textstrобов'язковийТекст пояснення або числового значення.
xytuple[float, float]обов'язковийЦільова точка, на яку вказує стрілка (наприклад, координати локального максимуму).
xytexttuple[float, float]None (xy)Координати розташування тексту. Якщо задано, від xytext до xy малюється стрілка.
xycoordsstr | Transform'data'Система координат для точки xy: 'data' (шкала графіка), 'axes fraction' (від 0 до 1 відносно осі).
textcoordsstr | TransformxycoordsСистема координат для тексту: 'offset points' (зсув у пунктах від цілі) або 'data'.
arrowpropsdict | NoneNoneСловник параметрів стрілки: arrowstyle ('->', 'fancy'), connectionstyle ('arc3,rad=0.3'), color.
bboxdict | NoneNoneСловник рамки навколо тексту: boxstyle ('round', 'sawtooth'), facecolor, alpha.
Інженерне зонування та статусний коридор SLAДля візуалізації інцидентів та регламентів використовують комбінацію діапазонів і ліній:
  • ax.axvspan(t_start, t_end) — вертикальна зона: маркує часовий інтервал аварії, релізу або нічного резервного копіювання.
  • ax.axhspan(val_min, val_max) — горизонтальна зона: наочно показує допустимі діапазони метрики (зелена зона стабільності, жовта зона ризику, червона зона відмови).
  • ax.axvline(x) / ax.axhline(y) — точкові граничні лінії регламенту.
annotations_zoning_deep_dive.ipynb
Python 3 (ipykernel)
Code
md

Дослідження анотацій: стилі стрілок, кривизна, хронологія інциденту та зони SLA

Порівняння arrowstyle, connectionstyle, виділення вікна атаки через axvspan та коридорів через axhspan

In [1]:
0.28s
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(42)
time_hours = np.linspace(0, 24, 120)
latency = 40 + 5 * np.sin(time_hours / 3) + rng.normal(0, 3, 120)

# Моделюємо стрибок затримки під час інциденту з 14:00 по 18:00
mask_incident = (time_hours >= 14) & (time_hours <= 18)
latency[mask_incident] += 120 + 40 * np.sin((time_hours[mask_incident] - 14) * 0.8)

fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))

# 1. Хронологія інциденту з вертикальним зонуванням
axes[0].plot(time_hours, latency, color='#0f172a', linewidth=1.8, label="Latency P99")
axes[0].axvspan(14, 18, color='#fca5a5', alpha=0.35, label="Вікно інциденту (DDoS)")
axes[0].axvline(14, color='#dc2626', linestyle='--', linewidth=1.5)
axes[0].axvline(18, color='#16a34a', linestyle='--', linewidth=1.5)

# Додаємо анотацію зі стрілкою та рамкою
axes[0].annotate("Пікове навантаження: 215 ms\nПочаток міграції трафіку", 
                xy=(16, 215), xytext=(8, 230),
                arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=-0.2", color='#dc2626', lw=2),
                fontweight='bold', fontsize=9, color='#7f1d1d',
                bbox=dict(boxstyle="round,pad=0.4", facecolor='#fef2f2', edgecolor='#dc2626'))

axes[0].set_title("Хронологія збою: ax.axvspan() та ax.annotate()", fontweight='bold')
axes[0].set_xlabel("Час доби (години)")
axes[0].set_ylabel("Затримка (ms)")
axes[0].set_ylim(20, 260)
axes[0].legend(loc='upper left')
axes[0].grid(True, linestyle=':', alpha=0.5)

# 2. Коридори статусу системи через axhspan
axes[1].plot(time_hours, latency, color='#0f172a', linewidth=1.8)
axes[1].axhspan(0, 80, color='#bbf7d0', alpha=0.3, label="Нормальний стан SLA (< 80 ms)")
axes[1].axhspan(80, 150, color='#fef08a', alpha=0.3, label="Попередження Warning (80–150 ms)")
axes[1].axhspan(150, 260, color='#fecaca', alpha=0.3, label="Критична деградація Critical (> 150 ms)")
axes[1].axhline(80, color='#16a34a', linestyle=':', linewidth=1.5)
axes[1].axhline(150, color='#dc2626', linestyle=':', linewidth=1.5)

axes[1].set_title("Статусні коридори працездатності: ax.axhspan()", fontweight='bold')
axes[1].set_xlabel("Час доби (години)")
axes[1].set_ylabel("Затримка (ms)")
axes[1].set_ylim(20, 260)
axes[1].legend(loc='upper left')
axes[1].grid(True, linestyle=':', alpha=0.5)

plt.tight_layout()
plt.show()
Out[1]:

Практичні завдання

📝 Рівень 1: Базові навички

Завдання 1.1: Створіть сітку 2×3 з 6 різними типами графіків (line, scatter, bar, hist, box, pie). Додайте загальний заголовок.

Завдання 1.2: Побудуйте 3 графіки один під одним зі спільною віссю X (часові дані за рік).

Завдання 1.3: Використайте GridSpec: один великий графік зверху + два маленькі знизу (у співвідношенні 2:1 по висоті).

🧮 Рівень 2: Аналіз даних

Завдання 2.1: Завантажте датасет Iris. Створіть дашборд 2×2:

  • Scatter: sepal_length vs sepal_width (колір = species)
  • Hist: розподіл petal_length
  • Box plot: порівняння sepal_width між видами
  • Bar chart: середня petal_length для кожного виду

Завдання 2.2: Застосуйте 4 різні стилі (default, seaborn, ggplot, dark_background) до одного графіка. Порівняйте результати.

Завдання 2.3: Побудуйте scatter plot з анотаціями: позначте топ-3 викиди стрілками та підписами.

🚀 Рівень 3: Дослідницький проєкт

Проєкт: Створення повноцінного дашборду

Завантажте датасет Titanic. Створіть композицію 3×3 (або використайте GridSpec для складнішої компоновки):

  1. Загальний заголовок фігури: «Аналіз пасажирів Титаніка»
  2. Графік 1 (великий, займає 2×2): Scatter matrix для змінних age, fare, pclass
  3. Графік 2: Bar chart — виживання за класами
  4. Графік 3: Pie chart — співвідношення чоловіків/жінок
  5. Графік 4: Hist — розподіл віку
  6. Графік 5: Box plot — ціна квитка за класами

Вимоги:

  • Єдина кольорова схема для всіх графіків
  • Всі графіки мають заголовки, підписи осей
  • Використайте tight_layout() або constrained_layout=True
  • Додайте хоча б 2 анотації (наприклад, позначте середній вік)
  • Збережіть у форматі PNG (300 dpi) та PDF

Резюме підтеми

🎨 Subplots

  • plt.subplots(nrows, ncols) → регулярна сітка
  • GridSpec → асиметричні композиції
  • sharex, sharey → спільні осі для порівняння
  • tight_layout() → автоматичне вирівнювання
  • Оптимально: 3–6 графіків на одній фігурі

🎨 Стилізація

  • plt.style.use("seaborn") → готові теми
  • Кольорові палітри: tab10, viridis, Pastel1
  • Colorblind-safe: viridis, plasma, tab10
  • Анотації: ax.annotate(), ax.text()
  • Zones: ax.axvspan(), ax.axhspan()

🎯 Дашборди

  • Один заголовок для всієї фігури: fig.suptitle()
  • Єдина кольорова схема для узгодженості
  • Логічна компоновка: головне → великим, деталі → малим
  • Збереження: PNG (веб, звіти), PDF (друк, публікації)
  • Роздільна здатність: 150 dpi (екран), 300 dpi (друк)
Чек-лист професійного дашборду:
  1. Загальний заголовок фігури (fig.suptitle())
  2. Кожен графік має власний заголовок
  3. Підписи осей з одиницями виміру
  4. Єдина кольорова схема (максимум 3–4 кольори)
  5. Сітка (grid(True, alpha=0.3)) де доречно
  6. tight_layout() або constrained_layout=True
  7. Легенди з чіткими підписами
  8. Розмір шрифтів достатній (≥10pt для осей, ≥12pt для заголовків)
  9. Анотації для ключових точок
  10. Збережено у правильному форматі та роздільній здатності

Попередня підтема: Візуалізація категоріальних данихПовернутися до огляду модуля: Візуалізація даних за допомогою matplotlib

Copyright © 2026