Занятие 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>