Занятие 5

Семинар EDA

Вступление

На семинаре мы будем заниматься построением различных визуализаций. Наша цель — провести разведочный анализ данных (exploratory data analysis, EDA), чтобы исследовать поведение данных и выявить в них закономерности. Мы продолжим работать с данными о пассажирах Титаника.

Цели:

  • познакомиться с библиотеками matplotlib, seaborn и визуализациями в pandas
  • научиться делать различные визуализации
  • заполнять пропуски в данных
  • делать однофакторный анализ
  • конструировать новые признаки

Как нужно строить графики:

  1. Если график стандартный, используйте matplotlib напрямую из pandas
  2. Если график нестандартный, используйте matplotlib
  3. Если график совсем нестандартный, то разделите его на несколько стандартных и используйте matplotlib
  4. Если нужны профильные красивые графики, график гистограммы с распределением, японские свечи, график pairplot или heatmap, то используйте seaborn (это всё тоже можно сделать в matplotlib, но будет дольше)
  5. Если у вас есть два часа времени, чтобы построить один график нормального качества, то используйте plotly
  6. Если вам нужно, чтобы получился один красивый график, за который вам очень хорошо заплатят, то используйте plotly

План семинара:

  1. Учимся строить графики
  2. Обрабатываем признаки
  3. Однофакторный анализ
In [ ]:
import warnings

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

warnings.filterwarnings("ignore")
plt.style.use("seaborn")

1. Учимся строить графики

Сперва загрузим данные и ещё раз изучим базовую информацию при помощи pandas.

In [ ]:
df = pd.read_csv(
    "https://raw.githubusercontent.com/iad34/seminars/master/materials/data_sem1.csv",
    sep=";",
)
In [ ]:
print(f"Data size: {df.shape}")
df.head()
In [ ]:
df.describe()
In [ ]:
df.isna().mean()  # df.isnull() — то же самое, что df.isna()

Теперь давайте посмотрим на данные глазами. По диагонали — маргинальное распределение каждой числовой переменной с учётом целевой переменной. В остальных ячейках — scatter plot каждой пары числовых переменных с учётом целевой переменной.

In [ ]:
sns.pairplot(df, hue="Survived");

Начинаем работу с графиками matplotlib и pandas (вызывает те же функции matplotlib).

In [ ]:
plt.figure(figsize=(15, 5), dpi=300)
plt.title("Survived")
df["Survived"].hist(bins=3);
In [ ]:
# То же самое, но только с matplotlib
plt.figure(figsize=(8, 3), dpi=300)
plt.title("Survived", fontsize=10)
plt.hist(df["Survived"], bins=3);

Построим гистаграмму по возрасту. Обратите внимание, что размер фигуры (и многие другие параметры, например, размер шрифтов) можно задавать один раз на весь нутбук при помощи plt.rc.Params.

In [ ]:
# plt.figure(figsize=(15, 5), dpi=300)
plt.rcParams["figure.figsize"] = (15, 5)
plt.title("Age")
df["Age"].hist(bins=50);

Для лёгкого разделения по группам выживших и не выживших, используем seaborn.

In [ ]:
sns.displot(df, x="Age", hue="Survived")
plt.show()

В matplotlib будет дольше и менее красиво :(

In [ ]:
plt.title("Age")
plt.hist(
    [df["Age"][df["Survived"] == 1], df["Age"][df["Survived"] == 0]],
    stacked=True,
    bins=50,
);

Давайте оценим корреляцию между столбцами числовых признаков.

In [ ]:
df.corr()

Сделаем её чуть более читаемой.

In [ ]:
sns.heatmap(df.corr());

И ещё более читаемой!

In [ ]:
sns.heatmap(df.corr(), cmap="vlag", annot=True, fmt="0.2f");

Неповторимый идеал.

In [ ]:
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.

In [ ]:
df.groupby("Pclass")["Name"].nunique().sort_values().plot(kind="barh");

Посмотрим на взаимосвязь разных переменных при помощи scatter plot.

Как scatter строить НЕ нужно:

In [ ]:
plt.scatter(df["Pclass"], df["Survived"]);

Как scatter строить нужно:

In [ ]:
plt.scatter(df["Age"], df["Fare"]);

Ещё на график можно что-то дорисовать. Например, дополнительные оси.

In [ ]:
plt.scatter(df["Age"], df["Fare"])
plt.axhline(10, c="y")
plt.axvline(10, c="y");

Вишенка на торте: рисуем несколько графиков на одной фигуре.

In [ ]:
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). Часто это не очень удобно, поэтому давайте посмотрим на другой способ взаимодействия с подграфиками. Этот способ позволит, например, удобно итерироваться в цикле по графикам и отрисовывать что-то.

In [ ]:
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 несколько шрифтов (см. ворнинги при запуске).

In [ ]:
with plt.xkcd():
    plt.title("Age")
    df["Age"].hist(bins=50);
In [ ]:
with plt.xkcd(scale=10):
    plt.title("Age")
    df["Age"].hist(bins=50);

2. Обрабатываем признаки

  1. Найдем признаки, у которых есть пропущенные значения, и обработаем эти пропущенные значения
  2. Переведём категориальные признаки в числовые

Обработаем признак Sex

Задание. Проверьте, есть ли пропуски в столбце Sex.

In [ ]:
# <YOUR CODE HERE>

Задание *. Подумайте, можно ли как-то узнать пол пассажиров, которые отнесены к категории unknown?

In [ ]:
# <YOUR CODE HERE>

Признак Sex является категориальным, то есть содержит нечисловые значения. Для работы большинства алгоритмов необходимо переводить категории в числа. Как это можно сделать?

Задание. Придумайте кодировку и сделайте её.

In [ ]:
# <YOUR CODE HERE>
Data = df

После первичной обработки можем посмотреть, как влияет пол на выживаемость.

In [ ]:
sns.barplot(x="is_male", y="Survived", data=Data, palette="summer")
plt.title("Sex - Survived")
plt.show()

Посмотрим, как влияет пол человека и класс билета (Pclass) на выживаемость

In [ ]:
sns.barplot(x="Sex", y="Survived", hue="Pclass", data=Data, palette="autumn")
plt.title("Sex - Survived")
plt.show()

Ещё один полезный вид визуализации — ящик с усами. Такой вид графиков позволяет визуально оценить моду и разброс распределения признака. Посмотрим на ящик с усами, отражающий распределение пассажиров по полу и возрасту.

In [ ]:
sns.catplot(x="Sex", y="Age", data=Data, kind="box")  # box plot (box-and-whiskers-plot)
plt.show()
In [ ]:
sns.catplot(x="Sex", y="Age", hue="Pclass", data=Data, kind="box")
plt.show()

Обработаем признак Embarked (порт посадки)

In [ ]:
print(f"Data size: {Data.shape}")
Data["Embarked"].value_counts(dropna=False)

Задание. Удалите из таблицы пассажиров, для которых неизвестен порт посадки.

In [ ]:
# <YOUR CODE HERE>

Задание. Преобразуем столбец Embarked методом OneHot-кодирования при помощи pd.get_dummies).

In [ ]:
# <YOUR CODE HERE>

Обработаем признак Age

Проверьте, если ли в Age пропущенные значения.

In [ ]:
# <YOUR CODE HERE>

Заполним пропуски медианным значением Age.

In [ ]:
median_age = Data["Age"].median()
Data["Age"].fillna(median_age, inplace=True)

Нарисуем распределение возраста пассажиров.

In [ ]:
sns.distplot(Data["Age"], kde=True)
plt.show()

Посмотрим на распределение Pclass по возрастам.

In [ ]:
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.

In [ ]:
# <YOUR CODE HERE>

Обработаем признак Pclass

Задание. Проверьте, если ли в Pclass пропущенные значения. Если пропущенные значения есть, заполните их самым частым значением Pclass.

In [ ]:
# <YOUR CODE HERE>

Задание. Нарисуйте гистограмму выживаемости в зависимости от Pclass.

In [ ]:
# <YOUR CODE HERE>

Обработаем признак SibSp (число братьев или сестер, мужей, жен)

Проверим, если ли в SibSp пропущенные значения.

In [ ]:
Data["SibSp"].value_counts()

Обработаем признак Parch (число братьев или сестер, мужей, жен)

Проверим, если ли в Parch пропущенные значения.

In [ ]:
Data["Parch"].value_counts()

Задание. Столбец PassengerId является категориальным и не несёт важной информации, удалите его.

In [ ]:
# <YOUR CODE HERE>
In [ ]:
Data.head()

3. Feature engineering

Нарисуем матрицу корреляций числовых признаков между собой и с целевой переменной.

Задание. Создайте таблицу NumericData, которая содержит только числовые столбцы из таблицы Data.

In [ ]:
# <YOUR CODE HERE>
NumericData = ...
In [ ]:
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,
);

Посмотрим на попарные зависимости между некоторыми признаками.

In [ ]:
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 — разбиение числовых (необязательно целочисленных) данных на несколько интервалов по квантилям
In [ ]:
Data["AgeBin"] = pd.cut(Data["Age"].astype(int), 5)
Data["AgeBin"].head()

Теперь переведём полученные интервалы в числа, используя LabelEncoder.

In [ ]:
from sklearn.preprocessing import LabelEncoder

label = LabelEncoder()
Data["AgeBin_Code"] = label.fit_transform(Data["AgeBin"])
Data[["Age", "AgeBin", "AgeBin_Code"]].head()

Задание. Бинаризуйте Fare, используя разбиение на 4 интервала.

In [ ]:
# <YOUR CODE HERE>

Бонус

Мы не используем всю информацию о данных, в частности, не используем текстовые данные. Также из матрицы корреляций мы видим, что признаки Parch и SibSp слабо коррелируют с выживаемостью (Survived). Сконструируем новые признаки, чтобы решить эти проблемы.

Задание.

1) Создайте признак NameLen и запишите в него длину имени (Name).

2) Создайте признак FamilySize, равный Parch + SibSp + 1. Зачем добавлять 1?

3) Создайте признак IsAlone, который показывает, путешествовал человек один или с семьей.

In [ ]:
# <YOUR CODE HERE>

Задание. Посмотрите, как коррелируют новые признаки (не забудьте про бинаризованные признаки) со столбцом Survived.

In [ ]:
# <YOUR CODE HERE>

Можно извлечь и другую полезную информацию из данных путём конструирования новых признаков.

Задание. Придумайте новые осмысленные признаки. Проверьте, как они коррелируют с выживаемостью.

In [ ]:
# <YOUR CODE HERE>

Задание. Верно ли, что если признак имеет маленькую по модулю корреляцию с выживаемостью, то он не влияет на неё и бесполезен для нашей задачи?

Мы провели довольно подробный однофакторный анализ данных и увидели, какие признаки сильно влияют на выживаемость, а какие нет.

Задание.

a) Попробуйте написать свою модель для предсказания выживаемости, используя обнаруженные закономерности

b) Оцените качество модели: вычислите долю правильных ответов алгоритма по всем данным

In [ ]:
def prediction(x):
    # x - один объект
    ...
In [ ]:
from sklearn.metrics import accuracy_score

pred = Data.apply(lambda x: prediction(x), axis=1)
accuracy_score(Data["Survived"], pred)
In [ ]:
# <YOUR CODE HERE>