Семестр 3 · Модуль 3 · Неделя 7

Matplotlib и Seaborn: честные графики на Titanic

Объектная модель Figure/Axes · виды графиков · subplots · подписи · сохранение · Seaborn: heatmap/pairplot · честная визуализация

2 ч теории · данные — titanic.csv (40 строк).

Прежде чем начать: откройте Jupyter и загрузите titanic.csv через pd.read_csv.

График — язык отчётов и научных статей

Какие данные вы считали в модуле 2? Как показать средний возраст по классу каюты человеку, который не читает таблицы?

Что вернул groupby('Pclass')['Survived'].mean() в модуле 2?

rate = df.groupby('Pclass')['Survived'].mean() rate
Не открывая конспект: это Series или DataFrame? Сколько в нём элементов? Какие у него индексы?

К концу лекции вы сможете: выбирать, строить, подписывать, сохранять

Риск недели: неподписанные графики. График без подписей — это не график, а картинка. Сегодня сделаем так, чтобы этот риск вас не касался.

Из чего соберётся первый блок проекта модуля 3?

Два API: pyplot — быстрый, объектный — управляемый

# pyplot: «последняя активная ось» import matplotlib.pyplot as plt plt.plot(x, y) plt.title('...') plt.show()
# объектный: явная ссылка на оси fig, ax = plt.subplots() ax.plot(x, y) ax.set_title('...') fig.savefig('plot.png')

Правило: pyplot — быстрый стиль для интерактива; объектный API (fig, ax) — полный контроль: несколько осей, подписи, сохранение. Для задач модуля предпочтителен объектный.

А что будет, если после fig, ax = plt.subplots() вызвать plt.xlabel(...)?

Figure — холст, Axes — подграфик: одна фигура, много осей

fig, axes = plt.subplots(2, 2, figsize=(9.5, 7)) # axes — массив 2×2; figsize — размер холста в дюймах axes[0, 0].set_title('левый верхний') axes[1, 1].set_title('правый нижний')
Сколько «графиков» в plt.subplots(2, 3) и какая форма у axes?

plt.xlabel после fig, ax — смешение стилей

fig, ax = plt.subplots() ax.plot(x, y) plt.xlabel('возраст, лет') # ← смешение стилей

Правило: выбрали объектный API — используем только ax./fig. методы.

Не запуская: что произойдёт? (а) ошибка, (б) подпись попадёт на последнюю активную ось, (в) ничего страшного.

plot — тренд/порядок: цена билета по списку

fig, ax = plt.subplots() ax.plot(df.index, df['Fare'], color='#7c3aed', marker='o', markersize=3) ax.set_title('Цена билета по списку') ax.set_xlabel('пассажир (номер в списке)') ax.set_ylabel('цена, у.е.') ax.grid(alpha=0.3) fig.savefig('figures/plot_fare.png', dpi=150, bbox_inches='tight')
050100 150200250 010203039 пассажир (номер в списке) цена, у.е. Цена билета по списку
Источник: titanic.csv, 40 пассажиров. Линия с маркерами скачет без выраженного тренда; пики ~230–250 у пассажиров №3, №30, №38. Одна серия — легенда не нужна.
Каким типом графика вы бы показали связь возраста и цены билета?

scatter — связь двух величин: возраст × цена

fig, ax = plt.subplots() ax.scatter(df['Age'], df['Fare'], s=40, alpha=0.75, color='#2563eb') ax.set_xlabel('возраст, лет') ax.set_ylabel('цена билета, у.е.') ax.set_title('Возраст и цена билета') ax.grid(alpha=0.3)
050100 150200250 10203040 возраст, лет цена билета, у.е. Возраст и цена билета
Источник: titanic.csv, 40 пассажиров (38 с известным возрастом; 2 пропуска отброшены автоматически). Облако широкое, без выраженного наклона — связь слабая.
Какая закономерность видна? Почему нельзя утверждать «цена растёт с возрастом»?

bar — сравнение категорий, ось Y с нуля

rate = df.groupby('Pclass')['Survived'].mean() fig, ax = plt.subplots() ax.bar([1, 2, 3], rate, color='#059669') ax.set_ylim(0, 1) # честная ось с нуля ax.set_xticks([1, 2, 3]) ax.set_xlabel('класс каюты') ax.set_ylabel('доля выживших') ax.set_title('Выживаемость по классу')
00.250.5 0.751 123 класс каюты доля выживших Выживаемость по классу 0.455 0.286 0.364
Источник: titanic.csv, доля = mean(Survived) по классам. Ось Y от 0 до 1; set_ylim(0, 0.5) «раздул» бы разницу между столбцами.
Почему set_ylim(0.3, 0.5) на этом графике — манипуляция? Что она скрывает?

hist — форма распределения: возраст

fig, ax = plt.subplots() ax.hist(df['Age'].dropna(), bins=12, color='#d97706', edgecolor='white') ax.set_xlabel('возраст, лет') ax.set_ylabel('число пассажиров') ax.set_title('Распределение возраста')
02468 10203040 возраст, лет число пассажиров Распределение возраста
Источник: titanic.csv, 38 пассажиров с известным возрастом (.dropna()); 12 корзин, интервалы 9.3–49.2. Мода — интервал 32.6–35.9 (7), вторичные пики 16–19 и 22.6–25.9 (по 6).
Сколько пассажиров младше ~20 лет? Как это понять из гистограммы?

boxplot — медиана, квартили, выбросы

import seaborn as sns fig, ax = plt.subplots() sns.boxplot(data=df, x='Sex', y='Age', ax=ax) ax.set_xlabel('пол') ax.set_ylabel('возраст, лет') ax.set_title('Возраст по полу')
51020 304050 malefemale пол возраст, лет Возраст по полу 25.8 32.4
Источник: titanic.csv, Age без пропусков. Мужчины (n=24): медиана 25.8; женщины (n=14): медиана 32.4. Разброс сопоставим, «усатых» выбросов почти нет.
Чем boxplot лучше hist для сравнения возраста по полу?

Какой график под какой вопрос

Вопрос к даннымТип графикаОсь X / Ось Y (с единицами)
Как меняется величина по порядку/времени?plotпорядок/время · значение
Связаны ли две количественные величины?scatterx · y
Сравнить категорииbar (ось Y с нуля!)категория · значение
Как распределена одна величина?histинтервалы · число наблюдений
Распределение по группам, выбросыboxplotгруппа · значение

Шпаргалка на весь семестр: выбор типа графика — половина честной визуализации.

Цена билета по классу каюты — какой тип и почему не scatter?

Live-coding 1: scatter «возраст × цена» с подписями

Задача: построить scatter возраста и цены билета на titanic.csv объектным API: fig, ax = plt.subplots() → ax.scatter(...) → подписи с единицами → ax.grid(alpha=0.3) → fig.savefig('figures/scatter_age_fare.png', dpi=150) → показать график. Комментируйте вслух каждое решение.

Что вернёт plt.subplots()? Зачем .dropna()? Какой единицы нет на оси Y?

plt.subplots(2, 2) — сетка одним вызовом

fig, axes = plt.subplots(2, 2, figsize=(9.5, 7)) # axes — массив 2×2; обращение по индексам axes[0, 0].hist(df['Age'].dropna(), bins=10, color='#d97706', edgecolor='white') axes[0, 0].set_title('Распределение возраста') axes[0, 0].set_xlabel('возраст, лет'); axes[0, 0].set_ylabel('число')

Правило: сетка создаётся одним вызовом; каждая ось настраивается независимо по индексу axes[i, j]. axes — это numpy-массив, индексация как в модуле 1; на сетке 2×2 запись axes[1] — ошибка.

Какой график вы разместите в axes[1, 0], если нужно показать связь возраста и цены билета?

Дашборд 2×2: четыре подписанных графика

rate = df.groupby('Pclass')['Survived'].mean() axes[0, 1].bar([1, 2, 3], rate, color='#059669') axes[0, 1].set_ylim(0, 1); axes[0, 1].set_xticks([1, 2, 3]) axes[0, 1].set_title('Выживаемость по классу') axes[0, 1].set_xlabel('класс'); axes[0, 1].set_ylabel('доля выживших') axes[1, 0].scatter(df['Age'], df['Fare'], s=30, alpha=0.7, color='#2563eb') axes[1, 0].set_title('Возраст × цена билета') axes[1, 0].set_xlabel('возраст, лет'); axes[1, 0].set_ylabel('цена, у.е.') sns.boxplot(data=df, x='Sex', y='Age', ax=axes[1, 1]) axes[1, 1].set_title('Возраст по полу') axes[1, 1].set_xlabel('пол'); axes[1, 1].set_ylabel('возраст, лет') fig.suptitle('Дашборд: Titanic', fontsize=14) fig.tight_layout() fig.savefig('figures/dashboard.png', dpi=110)
Дашборд: Titanic Распределение возраста Выживаемость по классу Возраст × цена билета Возраст по полу
Файл figures/dashboard.png (dpi=110): (0,0) гистограмма возраста · (0,1) bar долей выживших · (1,0) scatter возраст × цена · (1,1) boxplot по полу. Каждый подграфик подписан; общий заголовок fig.suptitle, fig.tight_layout() до savefig.
Зачем нужен fig.tight_layout() перед сохранением?

Настройка: title, оси, grid, xticks — чек-лист подписи

Норма: неподписанный график не считается сданным. Это пункт чек-листа ревью, а не пожелание.

Чего не хватает на графике с заголовком, но без подписей осей?

Легенда — только при >1 серии, label= в каждой

fig, ax = plt.subplots() ax.plot(x, y1, label='выжившие') ax.plot(x, y2, label='не выжившие') ax.set_xlabel('возраст, лет') ax.set_ylabel('число пассажиров') ax.legend() ax.set_title('Сравнение распределений возраста')
0246 1020304050 возраст, лет число пассажиров Сравнение распределений возраста выжившие не выжившие
Две серии (label='выжившие' / 'не выжившие') + ax.legend(). Легенда — только при >1 серии; без label= была бы пустой рамкой.
Чем ax.legend() без label= отличается от легенды с заданными метками?

Порядок: настроил → сохранил → показал

fig, ax = plt.subplots() ax.plot(df.index, df['Fare'], color='#7c3aed') ax.set_title('Цена билета по списку') ax.set_xlabel('пассажир (номер)'); ax.set_ylabel('цена, у.е.') fig.savefig('figures/fare.png', dpi=150, bbox_inches='tight') # ДО show!

Правила: порядок всегда — настроили → сохранили → показали. dpi=150 — качество; bbox_inches='tight' — без полей; PDF — векторный, для отчётов.

А что будет, если вызвать savefig ПОСЛЕ show?

savefig после show → пустой файл

fig, ax = plt.subplots() ax.plot(df.index, df['Fare']) plt.show() fig.savefig('fare.png') # ПОСЛЕ show! # результат: пустой/белый файл — figure уже закрыта

Разбор: plt.show() в интерактивном бэкенде завершает жизненный цикл фигуры; savefig после этого записывает пустой холст. Это «тихая» ошибка — проверяем результат, а не код:

from pathlib import Path Path('figures/fare.png').stat().st_size # 0 или несколько байт — пустой файл
Как проверить, что файл сохранился, не открывая его глазами?

Predict: «что внутри файла?»

fig, ax = plt.subplots() ax.hist(df['Age'].dropna()) fig.savefig('figures/a.png', dpi=150) plt.show() fig.savefig('figures/b.png', dpi=150) print(Path('figures/a.png').stat().st_size > 1000, Path('figures/b.png').stat().st_size > 1000)
Не запуская: что напечатается? (а) True True, (б) True False, (в) False False.

Обрезанная ось vs ось с нуля: одна история, две версии

456789 123 класс выживших Выжившие по классам (обрезанная ось) значение 2 — вне диапазона 5 8
ax.set_ylim(4, 9): столбец класса 2 (значение 2) вообще вне диапазона; разница 5 и 8 «раздута».
0510152025 123 класс выживших Выжившие по классам (честная ось) 5 2 8
ax.set_ylim(0, 25): честное соотношение 5 : 2 : 8. Источник: titanic.csv.
Что скрывает левый график? Какую историю рассказывает каждая версия?

Первые правила честности: бары с нуля, не обрезать, подписывать

Это требование к коду, а не этикет — на ревью масштаб осей проверяется. Полный разбор — неделя 8.

Почему честный график строится с нуля, даже если «некрасиво»?

Seaborn histplot/displot — распределение из DataFrame

import seaborn as sns sns.histplot(data=df, x='Age', bins=12) # гистограмма с ядровым сглаживанием sns.displot(data=df, x='Age', hue='Survived', kde=True) # распределения по группам
0246 1020304050 возраст, лет число наблюдений Распределение возраста по выживаемости 1 0
sns.displot(data=df, x='Age', hue='Survived', kde=True): два наложенных распределения с KDE; легенда «0»/«1». Источник: titanic.csv.
Что добавляет hue='Survived'?

Seaborn boxplot — группировка из коробки

sns.boxplot(data=df, x='Sex', y='Age')
51020 304050 malefemale пол возраст, лет Возраст по полу 25.8 32.4
Источник: titanic.csv, Age без пропусков. Мужчины (n=24): медиана 25.8; женщины (n=14): медиана 32.4. Разброс сопоставим, «усатых» выбросов почти нет.
Что изменится, если в x= поставить 'Pclass'?

sns.heatmap(df.corr()) — корреляции числовых колонок

import seaborn as sns fig, ax = plt.subplots(figsize=(6.2, 4.6)) sns.heatmap(df[['Age', 'Fare', 'Pclass', 'Survived']].corr(), annot=True, fmt='.2f', cmap='coolwarm', ax=ax) ax.set_title('Корреляции признаков Titanic')
Корреляции признаков Titanic AgeFarePclassSurvived AgeFarePclassSurvived +1.00 -0.22 -0.04 -0.07 -0.22 +1.00 -0.07 -0.22 -0.04 -0.07 +1.00 -0.07 -0.07 -0.22 -0.07 +1.00 -10+1
df[['Age','Fare','Pclass','Survived']].corr(); annot=True, fmt='.2f'. Ключевые: Fare–Survived −0.22, Age–Fare −0.22; диагональ 1.00. df.corr() «съедает» нечисловые колонки — Name и Sex не попали.
Почему в df.corr() не попали колонки Name и Sex? Что с ними сделать, чтобы учесть?

Какие корреляции реально есть: Fare–Survived r ≈ −0.22

Правильная формулировка: «связаны», а не «влияет». Знак «−» значит обратную связь, а не «плохую».

Можно ли по этой матрице утверждать «цена билета влияет на выживание»?

pairplot(hue='Survived') — разделимость классов

sns.pairplot(df[['Age', 'Fare', 'Pclass', 'Survived']].dropna(), hue='Survived', diag_kind='hist')
Попарные связи признаков, окраска по Survived 1 0 AgeFarePclassSurvivedAgeFarePclassSurvived
sns.pairplot(df[['Age','Fare','Pclass','Survived']].dropna(), hue='Survived', diag_kind='hist'). Сетка 4×4: вне диагонали — попарные scatter, на диагонали — гистограммы; видно, какой признак лучше разделяет классы. .dropna() обязателен.
Какой вопрос к данным можно быстро ответить, глядя на pairplot с hue='Survived'?

Типичные ошибки недели 7

  1. Неподписанные графики — нет title/xlabel/ylabel. График без подписей не считается сданным.
  2. savefig после show — пустой/белый файл.
  3. Смешение стилей — plt.xlabel после создания axes.
  4. Легенда без меток — label= не задан.
  5. Обрезанная ось Y у bar — «раздутая» разница между категориями.
  6. df.corr().plot() и heatmap без понимания, что корреляция — по числовым колонкам.
  7. Цикл строит график в одни оси — нужны новые subplot или цвет/метка каждой серии.
Студент показал график с заголовком, но без подписей осей. Примете ли вы его как «сданный»?

Вопросы для проверки понимания

  1. Зачем нужен объект Axes и чем ax.plot лучше plt.plot?
  2. Как создать сетку 2×2 и подписать каждый график?
  3. Как сохранить график в PNG высокого качества — и почему именно до show?
  4. Почему нельзя обрезать ось Y у bar-диаграммы?
  5. Какие колонки попадают в df.corr() и что показывает heatmap?
Ответьте письменно за 3 минуты — это мини-самооценка перед семинаром.

Проект модуля: дашборд + heatmap — блок 1

Проект как пазл: визуализация (неделя 7) + числа (неделя 8) + текст (неделя 9) → один ноутбук.

Что из сегодняшнего войдёт в финальный ноутбук module3_project.ipynb?

Итоги: 5 навыков · семинар · ДЗ · one-minute paper

Откройте семинар-7.md, скопируйте titanic.csv в data/ и начните с базовой задачи.