Занятие 5
Семинар EDA¶
Вступление¶
На семинаре мы будем заниматься построением различных визуализаций. Наша цель — провести разведочный анализ данных (exploratory data analysis, EDA), чтобы исследовать поведение данных и выявить в них закономерности. Мы продолжим работать с данными о пассажирах Титаника.
Цели:¶
- познакомиться с библиотеками matplotlib, seaborn и визуализациями в pandas
- научиться делать различные визуализации
- заполнять пропуски в данных
- делать однофакторный анализ
- конструировать новые признаки
Как нужно строить графики:¶
- Если график стандартный, используйте matplotlib напрямую из pandas
- Если график нестандартный, используйте matplotlib
- Если график совсем нестандартный, то разделите его на несколько стандартных и используйте matplotlib
- Если нужны профильные красивые графики, график гистограммы с распределением, японские свечи, график pairplot или heatmap, то используйте seaborn (это всё тоже можно сделать в matplotlib, но будет дольше)
- Если у вас есть два часа времени, чтобы построить один график нормального качества, то используйте plotly
- Если вам нужно, чтобы получился один красивый график, за который вам очень хорошо заплатят, то используйте plotly
План семинара:¶
- Учимся строить графики
- Обрабатываем признаки
- Однофакторный анализ
import warnings
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
warnings.filterwarnings("ignore")
plt.style.use("seaborn")
1. Учимся строить графики¶
Сперва загрузим данные и ещё раз изучим базовую информацию при помощи pandas.
df = pd.read_csv(
"https://raw.githubusercontent.com/iad34/seminars/master/materials/data_sem1.csv",
sep=";",
)
print(f"Data size: {df.shape}")
df.head()
df.describe()
df.isna().mean() # df.isnull() — то же самое, что df.isna()
Теперь давайте посмотрим на данные глазами. По диагонали — маргинальное распределение каждой числовой переменной с учётом целевой переменной. В остальных ячейках — scatter plot каждой пары числовых переменных с учётом целевой переменной.
sns.pairplot(df, hue="Survived");
Начинаем работу с графиками matplotlib и pandas (вызывает те же функции matplotlib).
plt.figure(figsize=(15, 5), dpi=300)
plt.title("Survived")
df["Survived"].hist(bins=3);
# То же самое, но только с matplotlib
plt.figure(figsize=(8, 3), dpi=300)
plt.title("Survived", fontsize=10)
plt.hist(df["Survived"], bins=3);
Построим гистаграмму по возрасту. Обратите внимание, что размер фигуры (и многие другие параметры, например, размер шрифтов) можно задавать один раз на весь нутбук при помощи plt.rc.Params
.
# plt.figure(figsize=(15, 5), dpi=300)
plt.rcParams["figure.figsize"] = (15, 5)
plt.title("Age")
df["Age"].hist(bins=50);
Для лёгкого разделения по группам выживших и не выживших, используем seaborn.
sns.displot(df, x="Age", hue="Survived")
plt.show()
В matplotlib будет дольше и менее красиво :(
plt.title("Age")
plt.hist(
[df["Age"][df["Survived"] == 1], df["Age"][df["Survived"] == 0]],
stacked=True,
bins=50,
);
Давайте оценим корреляцию между столбцами числовых признаков.
df.corr()
Сделаем её чуть более читаемой.
sns.heatmap(df.corr());
И ещё более читаемой!
sns.heatmap(df.corr(), cmap="vlag", annot=True, fmt="0.2f");
Неповторимый идеал.
ax = sns.heatmap(df.corr(), cmap="vlag", annot=True, fmt="0.2f")
for t in ax.texts:
if float(t.get_text()) >= 0.3 or float(t.get_text()) <= -0.3:
t.set_text(t.get_text())
else:
t.set_text("")
Построим распредление количества билетов разных классов при помощи bar plot.
df.groupby("Pclass")["Name"].nunique().sort_values().plot(kind="barh");
Посмотрим на взаимосвязь разных переменных при помощи scatter plot.
Как scatter строить НЕ нужно:
plt.scatter(df["Pclass"], df["Survived"]);
Как scatter строить нужно:
plt.scatter(df["Age"], df["Fare"]);
Ещё на график можно что-то дорисовать. Например, дополнительные оси.
plt.scatter(df["Age"], df["Fare"])
plt.axhline(10, c="y")
plt.axvline(10, c="y");
Вишенка на торте: рисуем несколько графиков на одной фигуре.
plt.figure(figsize=(15, 5), dpi=300)
plt.subplot(1, 2, 1)
plt.title("1")
plt.hist(df["Age"])
plt.ylabel("Count")
plt.xlabel("Age")
plt.subplot(1, 2, 2)
plt.title("2")
plt.hist(df["Fare"])
plt.xlabel("Fare")
plt.show()
В способе выше мы должны каждый раз переключаться между тем, где мы рисуем, при помощи plt.subplot(n_rows, n_cols, idx)
. Часто это не очень удобно, поэтому давайте посмотрим на другой способ взаимодействия с подграфиками. Этот способ позволит, например, удобно итерироваться в цикле по графикам и отрисовывать что-то.
fig, axs = plt.subplots(1, 2, figsize=(15, 5), dpi=300)
axs[0].set_title("1")
axs[0].hist(df["Age"])
axs[0].set_ylabel("Count")
axs[0].set_xlabel("Age")
axs[1].set_title("2")
axs[1].hist(df["Fare"])
axs[1].set_xlabel("Fare")
plt.show()
Бонус! Строим самые красивые графики.
NB: вам понадобится установить на свою OS несколько шрифтов (см. ворнинги при запуске).
with plt.xkcd():
plt.title("Age")
df["Age"].hist(bins=50);
with plt.xkcd(scale=10):
plt.title("Age")
df["Age"].hist(bins=50);
# <YOUR CODE HERE>
Задание *. Подумайте, можно ли как-то узнать пол пассажиров, которые отнесены к категории unknown?
# <YOUR CODE HERE>
Признак Sex
является категориальным, то есть содержит нечисловые значения. Для работы большинства алгоритмов необходимо переводить категории в числа. Как это можно сделать?
Задание. Придумайте кодировку и сделайте её.
# <YOUR CODE HERE>
Data = df
После первичной обработки можем посмотреть, как влияет пол на выживаемость.
sns.barplot(x="is_male", y="Survived", data=Data, palette="summer")
plt.title("Sex - Survived")
plt.show()
Посмотрим, как влияет пол человека и класс билета (Pclass) на выживаемость
sns.barplot(x="Sex", y="Survived", hue="Pclass", data=Data, palette="autumn")
plt.title("Sex - Survived")
plt.show()
Ещё один полезный вид визуализации — ящик с усами. Такой вид графиков позволяет визуально оценить моду и разброс распределения признака. Посмотрим на ящик с усами, отражающий распределение пассажиров по полу и возрасту.
sns.catplot(x="Sex", y="Age", data=Data, kind="box") # box plot (box-and-whiskers-plot)
plt.show()
sns.catplot(x="Sex", y="Age", hue="Pclass", data=Data, kind="box")
plt.show()
Обработаем признак Embarked
(порт посадки)¶
print(f"Data size: {Data.shape}")
Data["Embarked"].value_counts(dropna=False)
Задание. Удалите из таблицы пассажиров, для которых неизвестен порт посадки.
# <YOUR CODE HERE>
Задание. Преобразуем столбец Embarked
методом OneHot-кодирования при помощи pd.get_dummies)
.
# <YOUR CODE HERE>
Обработаем признак Age
¶
Проверьте, если ли в Age
пропущенные значения.
# <YOUR CODE HERE>
Заполним пропуски медианным значением Age
.
median_age = Data["Age"].median()
Data["Age"].fillna(median_age, inplace=True)
Нарисуем распределение возраста пассажиров.
sns.distplot(Data["Age"], kde=True)
plt.show()
Посмотрим на распределение Pclass
по возрастам.
facet = sns.FacetGrid(data=Data, hue="Pclass", legend_out=True, height=5, aspect=1.5)
facet = facet.map(sns.kdeplot, "Age")
facet.add_legend(fontsize=20);
Обработаем признак Fare
¶
Задание. Проверьте, если ли в Fare
пропущенные значения. Если пропущенные значения есть, заполните их медианным значением Fare
.
# <YOUR CODE HERE>
Обработаем признак Pclass
¶
Задание. Проверьте, если ли в Pclass
пропущенные значения. Если пропущенные значения есть, заполните их самым частым значением Pclass
.
# <YOUR CODE HERE>
Задание. Нарисуйте гистограмму выживаемости в зависимости от Pclass
.
# <YOUR CODE HERE>
Обработаем признак SibSp
(число братьев или сестер, мужей, жен)¶
Проверим, если ли в SibSp
пропущенные значения.
Data["SibSp"].value_counts()
Обработаем признак Parch
(число братьев или сестер, мужей, жен)¶
Проверим, если ли в Parch
пропущенные значения.
Data["Parch"].value_counts()
Задание. Столбец PassengerId является категориальным и не несёт важной информации, удалите его.
# <YOUR CODE HERE>
Data.head()
3. Feature engineering¶
Нарисуем матрицу корреляций числовых признаков между собой и с целевой переменной.
Задание. Создайте таблицу NumericData
, которая содержит только числовые столбцы из таблицы Data
.
# <YOUR CODE HERE>
NumericData = ...
colormap = plt.cm.RdBu
plt.figure(figsize=(14, 12))
plt.title("Pearson Correlation of Features", y=1.05, size=18)
sns.heatmap(
NumericData.corr(),
linewidths=0.1,
vmax=1.0,
square=True,
cmap=colormap,
linecolor="white",
annot=True,
);
Посмотрим на попарные зависимости между некоторыми признаками.
g = sns.pairplot(
Data[["Survived", "Pclass", "Sex", "Age", "Parch", "Fare"]],
hue="Survived",
palette="seismic",
size=4,
diag_kind="kde",
diag_kws=dict(shade=True),
plot_kws=dict(s=50),
)
g.set(xticklabels=[]);
Зачастую признак может быть зашумлен или может содержать лишнюю (слишком детализированную, ведущую к переобучению) информацию. Возможным решением этой проблемы служит бинаризация признака.
Бинаризуем признаки Age
и Fare
.
pd.cut
— разбиение целочисленных данных на несколько интервалов по квантилямpd.qcut
— разбиение числовых (необязательно целочисленных) данных на несколько интервалов по квантилям
Data["AgeBin"] = pd.cut(Data["Age"].astype(int), 5)
Data["AgeBin"].head()
Теперь переведём полученные интервалы в числа, используя LabelEncoder
.
from sklearn.preprocessing import LabelEncoder
label = LabelEncoder()
Data["AgeBin_Code"] = label.fit_transform(Data["AgeBin"])
Data[["Age", "AgeBin", "AgeBin_Code"]].head()
Задание. Бинаризуйте Fare
, используя разбиение на 4 интервала.
# <YOUR CODE HERE>
Бонус¶
Мы не используем всю информацию о данных, в частности, не используем текстовые данные. Также из матрицы корреляций мы видим, что признаки Parch
и SibSp
слабо коррелируют с выживаемостью (Survived
). Сконструируем новые признаки, чтобы решить эти проблемы.
Задание.
1) Создайте признак NameLen
и запишите в него длину имени (Name).
2) Создайте признак FamilySize
, равный Parch
+ SibSp
+ 1. Зачем добавлять 1?
3) Создайте признак IsAlone
, который показывает, путешествовал человек один или с семьей.
# <YOUR CODE HERE>
Задание. Посмотрите, как коррелируют новые признаки (не забудьте про бинаризованные признаки) со столбцом Survived
.
# <YOUR CODE HERE>
Можно извлечь и другую полезную информацию из данных путём конструирования новых признаков.
Задание. Придумайте новые осмысленные признаки. Проверьте, как они коррелируют с выживаемостью.
# <YOUR CODE HERE>
Задание. Верно ли, что если признак имеет маленькую по модулю корреляцию с выживаемостью, то он не влияет на неё и бесполезен для нашей задачи?
Мы провели довольно подробный однофакторный анализ данных и увидели, какие признаки сильно влияют на выживаемость, а какие нет.
Задание.
a) Попробуйте написать свою модель для предсказания выживаемости, используя обнаруженные закономерности
b) Оцените качество модели: вычислите долю правильных ответов алгоритма по всем данным
def prediction(x):
# x - один объект
...
from sklearn.metrics import accuracy_score
pred = Data.apply(lambda x: prediction(x), axis=1)
accuracy_score(Data["Survived"], pred)
# <YOUR CODE HERE>