- Строить график объектно-ориентированным способом через
fig, ax = plt.subplots()и добавлять заголовок, подписи осей и легенду - Выбирать тип графика под вопрос: линия, столбцы, рассеяние, гистограмма
- Обосновывать число интервалов гистограммы по правилам Стёрджеса и Фридмана–Диакониса
- Распознавать вводящие в заблуждение графики и строить честные и понятные
В 1973 году статистик Фрэнсис Энскомб опубликовал четыре небольших набора данных, у которых средние, дисперсии, коэффициент корреляции и линия регрессии почти одинаковы. Но на графике один набор рассеян вокруг прямой, другой лежит на кривой, в третьем один выброс меняет всё, а в четвёртом все точки, кроме одной, стоят на вертикальной линии. Вывод: прежде чем верить числам, на данные нужно посмотреть. Главный инструмент для этого в Python — matplotlib.
Figure и Axes: как устроен график
Figure — всё изображение, «холст». Axes — одна область графика внутри него: оси, линии, заголовок, легенда. В одной Figure может быть несколько Axes. Большинство методов принадлежит Axes: ax.plot, ax.set_title, ax.legend.
У matplotlib два стиля: краткий стиль «pyplot», например plt.plot(...), и объектно-ориентированный. Всегда начинай со второго: fig, ax = plt.subplots() — такой код остаётся понятным и при нескольких графиках. В браузере картинка появляется под кодом автоматически; на компьютере plt.show() открывает окно, а fig.savefig('chart.png', dpi=200) сохраняет файл.
import matplotlib.pyplot as plt
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
baku = [279, 153, 426, 390, 455, 510]
ganja = [150, 306, 168, 210, 240, 265]
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(months, baku, marker='o', label='Baku')
ax.plot(months, ganja, marker='s', linestyle='--', label='Ganja')
ax.set_title('Monthly revenue by branch, 2026')
ax.set_xlabel('Month')
ax.set_ylabel('Revenue, AZN')
ax.grid(alpha=0.3)
ax.legend()
plt.show()- Заголовок — что показывает график, где и когда.
- Подписи осей с единицами: «Revenue, AZN», «Height, cm».
- Легенда (
legend) — если рядов больше одного. - Бледная сетка (
grid(alpha=0.3)) — облегчает чтение значений и не отвлекает.
Выбор типа графика
| Вопрос | График | matplotlib |
|---|---|---|
| Как меняется со временем? | линейный график | ax.plot |
| Как соотносятся категории? | столбчатая диаграмма | ax.bar, ax.barh |
| Связаны ли две числовые переменные? | диаграмма рассеяния | ax.scatter |
| Как распределены значения? | гистограмма, «ящик с усами» | ax.hist, ax.boxplot |
import numpy as np
import matplotlib.pyplot as plt
products = np.array(['Coffee', 'Cake', 'Tea', 'Juice', 'Sandwich'])
revenue = np.array([564, 540, 378, 212, 305])
order = np.argsort(revenue)
fig, ax = plt.subplots(figsize=(6, 3.5))
bars = ax.barh(products[order], revenue[order], color='#4C72B0')
ax.bar_label(bars, fmt='%d AZN', padding=3)
ax.set_xlim(0, 680)
ax.set_xlabel('Revenue, AZN')
ax.set_title('Revenue by product, Q1 2026')
print(products[order][-1], revenue.max())
plt.show()▸ Ожидаемый результат
Coffee 564
Диаграмма рассеяния показывает связь между двумя числовыми переменными. Цвет может передавать третью переменную (c=, cmap=), а np.polyfit(x, y, 1) находит линию тренда методом наименьших квадратов. Данные ниже выдуманы: площадь квартиры, число комнат и цена.
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(3)
area = rng.uniform(40, 140, 60)
rooms = np.clip(area // 30, 1, 4).astype(int)
price = 1.5 * area + 10 * rooms + rng.normal(0, 15, 60)
slope, intercept = np.polyfit(area, price, 1)
print(f'slope: {slope:.2f} thousand AZN per m2')
print(f'correlation r = {np.corrcoef(area, price)[0, 1]:.3f}')
fig, ax = plt.subplots(figsize=(6, 4))
points = ax.scatter(area, price, c=rooms, cmap='viridis', alpha=0.8)
xs = np.array([40, 140])
ax.plot(xs, slope * xs + intercept, color='red', label='trend line')
fig.colorbar(points, ax=ax, label='rooms')
ax.set_xlabel('Area, m²')
ax.set_ylabel('Price, thousand AZN')
ax.legend()
plt.show()▸ Ожидаемый результат
slope: 2.00 thousand AZN per m2 correlation r = 0.960
viridis читается и при чёрно-белой печати, и людьми с нарушениями цветового зрения. Коэффициент корреляции r подробно разбирается в следующем уроке.Гистограмма и число интервалов
Гистограмма делит ось значений на равные интервалы (bins) и считает наблюдения, попавшие в каждый. Если интервалов слишком мало, форма распределения теряется; если слишком много — график превращается в шумную «расчёску». Два известных правила дают отправную точку:
- kчисло интервалов (правило Стёрджеса)
- hширина интервала (правило Фридмана–Диакониса), в единицах данных
- nчисло наблюдений
- IQRмежквартильный размах Q3 − Q1
Правило Стёрджеса хорошо для примерно нормальных и не слишком больших данных; правило Фридмана–Диакониса устойчиво к выбросам. ax.hist(x, bins='fd') применяет второе автоматически.
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
heights = rng.normal(170, 8, 500)
k = int(np.ceil(np.log2(heights.size))) + 1
print('Sturges bins:', k)
print(f'mean = {heights.mean():.1f} cm, std = {heights.std(ddof=1):.1f} cm')
fig, ax = plt.subplots(figsize=(6, 4))
ax.hist(heights, bins=k, edgecolor='white')
ax.axvline(heights.mean(), color='red', linestyle='--', label='mean')
ax.set_xlabel('Height, cm')
ax.set_ylabel('Number of people')
ax.legend()
plt.show()▸ Ожидаемый результат
Sturges bins: 10 mean = 169.9 cm, std = 7.7 cm
n = 500, рост распределён примерно нормально, σ ≈ 8 см. Найди интервалы по правилам Стёрджеса и Фридмана–Диакониса. Для нормального распределения IQR ≈ 1,35σ.
Показать решениеСкрыть решение
Фридман–Диаконис: IQR ≈ 1,35 · 8 = 10,8 см; n^(1/3) = 500^(1/3) ≈ 7,94.
h = 2 · 10,8 / 7,94 ≈ 2,7 см.
Размах около 170 ± 3σ, то есть ~48 см → 48 / 2,7 ≈ 18 интервалов.
FD показывает больше деталей; с ростом n правило Стёрджеса добавляет интервалы очень медленно.
- nᵢнаблюдения в i-м интервале
- fᵢвысота столбца при
density=True(плотность) - hширина интервала
ax.hist(x, density=True) делает сумму площадей столбцов равной 1 — такую гистограмму можно сравнивать с кривой плотности вероятности.
200 измерений разбиты на интервалы ширины h = 2. В одном интервале 30 измерений. Какова высота этого столбца при density=True? Что показывает его площадь?
Показать решениеСкрыть решение
Площадь = f · h = 0,075 · 2 = 0,15, то есть в этом интервале 15% измерений (30 / 200).
Сумма площадей всех столбцов равна 1 (100%).
Несколько графиков на одном рисунке
plt.subplots(2, 2) возвращает массив Axes размера 2 × 2; к каждому графику обращаются через axes[строка, столбец]. layout='constrained' не даёт заголовкам и осям налезать друг на друга, а sharex=True делает оси общими.
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)
x = np.linspace(0, 2 * np.pi, 100)
fig, axes = plt.subplots(2, 2, figsize=(8, 6), layout='constrained')
axes[0, 0].plot(x, np.sin(x))
axes[0, 0].set_title('Line: sin x')
axes[0, 1].bar(['A', 'B', 'C'], [5, 3, 7])
axes[0, 1].set_title('Bar')
axes[1, 0].scatter(rng.random(30), rng.random(30))
axes[1, 0].set_title('Scatter')
axes[1, 1].hist(rng.normal(size=300), bins=15)
axes[1, 1].set_title('Histogram')
fig.suptitle('Four basic chart types')
print(axes.shape)
plt.show()▸ Ожидаемый результат
(2, 2)
В pandas есть собственный метод .plot(), построенный на matplotlib: каждый столбец DataFrame становится рядом, а индекс уходит на ось X. Метод возвращает Axes, поэтому график можно доработать обычными командами matplotlib.
import pandas as pd
import matplotlib.pyplot as plt
df = pd.DataFrame({'month': ['Jan', 'Feb', 'Mar'],
'Baku': [279, 153, 426],
'Ganja': [150, 306, 168]}).set_index('month')
ax = df.plot(kind='bar', figsize=(6, 3.5), rot=0, title='Revenue by month, AZN')
ax.set_ylabel('AZN')
print(df.sum())
plt.show()▸ Ожидаемый результат
Baku 858 Ganja 624 dtype: int64
Правила хорошего графика
- Один график — одна мысль. Пиши вывод в заголовке: «В марте лидирует бакинский филиал».
- На столбчатой диаграмме ось Y должна начинаться с 0, потому что глаз сравнивает длины столбцов.
- Цвет должен нести смысл: выделяемому ряду — яркий цвет, остальным — серый.
- Избегай 3D-эффектов, теней и круговых диаграмм со множеством секторов — они искажают значения.
import matplotlib.pyplot as plt
branches = ['Baku', 'Ganja']
satisfied = [96, 98]
fig, (bad, good) = plt.subplots(1, 2, figsize=(8, 3.5), layout='constrained')
bad.bar(branches, satisfied, color=['gray', 'orange'])
bad.set_ylim(95, 98.5)
bad.set_title('Misleading: axis starts at 95')
good.bar(branches, satisfied, color=['gray', 'orange'])
good.set_ylim(0, 100)
good.set_title('Honest: axis starts at 0')
for ax in (bad, good):
ax.set_ylabel('Satisfied customers, %')
plt.show()Построй горизонтальную столбчатую диаграмму (ax.barh) по словарю revenue, задай заголовок 'Revenue by product' и подпиши ось X как 'AZN'. Затем выведи на отдельных строках: 1) заголовок графика (ax.get_title()); 2) число столбцов на графике (len(ax.patches)); 3) товар с наибольшей выручкой.
import matplotlib.pyplot as plt
revenue = {'Coffee': 564, 'Cake': 540, 'Tea': 378, 'Juice': 212, 'Sandwich': 305}
fig, ax = plt.subplots()
# draw the bars, set the title and the X label, then print the three results▸ Ожидаемый результат
Revenue by product 5 Coffee
Построй гистограмму баллов за тест с 5 интервалами. ax.hist возвращает три объекта: количества, границы интервалов и столбцы. Выведи на отдельных строках: 1) количество в каждом интервале списком целых чисел; 2) ширину одного интервала, округлённую до 2 знаков после запятой.
import matplotlib.pyplot as plt
scores = [55, 62, 68, 70, 71, 75, 78, 80, 82, 85, 88, 90, 94, 97]
fig, ax = plt.subplots()
# counts, edges, _ = ax.hist(...)▸ Ожидаемый результат
[2, 3, 3, 3, 3] 8.4
Главное
- Figure — всё изображение, Axes — график внутри него; начинай с
fig, ax = plt.subplots(). - Время → линия, категории → столбцы, две числовые переменные → рассеяние, распределение → гистограмма.
- На каждом графике нужны заголовок, подписи осей с единицами и при необходимости легенда.
- Интервалы: Стёрджес k = ⌈log₂ n⌉ + 1, Фридман–Диаконис h = 2 · IQR · n^(−1/3).
- Ось столбчатой диаграммы начинается с 0; для цветовой шкалы выбирай
viridis.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.