Мамкин Data Scientist представляет:

Titanic без цензуры

Ноутбук* посвящен соревнованию Титаник на платформе Kaggle.

*Ноутбук несет исключительно развлекательный характер и не является эталоном решения DS соревнований

tg: @mommyscience

Autor: @BoykoAA

In [2]:
from IPython.display import Image
Image("titanic_pic1.jpg")
Out[2]:

Что от нас хотят? В соревновании предлагается построить модель машинного обучения, которая бы предсказывала выживет ли человек в кораблекрушении Титаника или нет. Более подробное описание конкурса https://www.kaggle.com/c/titanic

Как обычно, в задачах машинного обучения к нам в распоряжение поступают определенные признаки, на основании которых и будут строиться выводы. Уже сейчас можно предполагать, что какие-нибудь личинки или большие шишки имеют шанс на спасение выше, чем простые работяги, как мы с вами.

Цель этой тетрадки - показать примерный ход решения подобных соревнований, а также показать, что это так же легко, как развести твою бывшую на вписоне за балтику девятку

Ну что, погнали

Импортим библиотеки и смотрим на данные

In [209]:
import pandas as pd
import numpy as np
import random as rnd

import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

import warnings
warnings.filterwarnings('ignore')

Pandas позволяет в одну строчку прочитать любой csv файл и потом уже вертеть его как захочется

In [147]:
train = pd.read_csv('data/train.csv')
test = pd.read_csv('data/test.csv')
In [8]:
display(train.head(5))
print(train.shape)
print(train.columns.values)
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
(891, 12)
['PassengerId' 'Survived' 'Pclass' 'Name' 'Sex' 'Age' 'SibSp' 'Parch'
 'Ticket' 'Fare' 'Cabin' 'Embarked']

Перед нами 12 признаков, некоторые из которых, точно повлияют на выживаемость. Например возраст -- явно личинке отдадут приоритет места в шлюпке. Или статус каюты, в которой плыл человек. Люди из первого класса спасались в первую очень

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

Обратите внимание, что не все признаки являются циферками, некоторые это слова, а некоторые хуй пойми что. С этой кухней тоже разберемся

In [10]:
train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
PassengerId    891 non-null int64
Survived       891 non-null int64
Pclass         891 non-null int64
Name           891 non-null object
Sex            891 non-null object
Age            714 non-null float64
SibSp          891 non-null int64
Parch          891 non-null int64
Ticket         891 non-null object
Fare           891 non-null float64
Cabin          204 non-null object
Embarked       889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB

Для тупых поясняю, int64 и float64 -- это циферки, а object -- это много букав

Даже слепошарый заметил бы три буквы в датасете. Ну не те три буквы, господи. Я про NaN. Это пропущенные значения в датасете, с ними тоже надо уметь работать.

Признак Survived является целевым, он принимает значение 0 или 1, 0 - смэрть, 1 - выжил, его то мы и будем предсказывать

In [13]:
train.describe(include=['object'])
Out[13]:
Name Sex Ticket Cabin Embarked
count 891 891 891 204 889
unique 891 2 681 147 3
top Gallagher, Mr. Martin male 1601 C23 C25 C27 S
freq 1 577 7 4 644

По этой таблице тоже можно сделать некоторые выводы, например, что имя у каждого человека на Титанике уникально (да ну нахуй), признак Sex принимает два значения, мужчина и женщина (а где квиры и трансы?). Каюты не уникальны, потому что многие плыли группами и делили одну каюту на несколько человек.

Вот видите, мы еще ничего не сделали, а сколько уже знаем про данные, дальше больше

Добавим немного красок, повизуализируем

In [17]:
plt.figure(figsize=(10,5))
train['Survived'].value_counts().plot(kind='barh')
plt.title('Целевая переменная')
plt.show()
In [70]:
plt, axes = plt.subplots(1, 2, figsize=(10, 5))

for ax, i in zip(axes.flatten(), range(0, 2)):
    sns.distplot(train[train['Survived'] == i]['Age'].dropna(), ax=ax, axlabel='Age of Survived ' + str(i))

Можно заметить, что младенцы имеют высокую вероятнсоть выжить, а люди в возрасте ~80 лет, спаслись в 100% случаев. Наша гипотеза подтвердилась

In [86]:
plt, axes = plt.subplots(1, 2, figsize=(10, 5))

for ax, j in zip(axes.flatten(), range(0, 2)):
    sns.distplot(train[train['Survived'] == j]['Pclass'].dropna(), ax=ax, axlabel='Pclass of Survived ' + str(j))

В первом классе шанс выжить был выше, чем в 3 классе, как раз в том, в которм ты остановился в развитии

In [87]:
train.head(5)
Out[87]:
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

Давайте немного почистим данные, уберем признаки, которые на первый взгляд не дадут информации для модели. Это будут признаки Ticket и Cabine (не забывайте указать inplace=True, для удаления на месте)

In [148]:
print('Размерность набора данных до удаления: {}'.format(train.shape))

train.drop(columns=['Ticket', 'Cabin'], inplace=True)
test.drop(columns=['Ticket', 'Cabin'], inplace=True)

print('Размерность набора данных после удаления: {}'.format(train.shape))
Размерность набора данных до удаления: (891, 12)
Размерность набора данных после удаления: (891, 10)

Признак имя на первый взгялд кажется бесполезным, но давайте все таки подумаем, какую пользу можно извлечь. Как видно ниже, в каждой строке есть приставка типа Мистер, Хуистер, Мисс и тд, давайте посмотрим на все возможные варианты, возможно если там будет какой-нибудь доктор, то шансы на выживания у него могут быть выше

In [91]:
train['Name'].head(5)
Out[91]:
0                              Braund, Mr. Owen Harris
1    Cumings, Mrs. John Bradley (Florence Briggs Th...
2                               Heikkinen, Miss. Laina
3         Futrelle, Mrs. Jacques Heath (Lily May Peel)
4                             Allen, Mr. William Henry
Name: Name, dtype: object

Опча

In [149]:
train['Name'].str.extract(' ([A-Za-z]+)\.', expand=False).unique()
Out[149]:
array(['Mr', 'Mrs', 'Miss', 'Master', 'Don', 'Rev', 'Dr', 'Mme', 'Ms',
       'Major', 'Lady', 'Sir', 'Mlle', 'Col', 'Capt', 'Countess',
       'Jonkheer'], dtype=object)

ля какие приставки к именам, у тебя только "зять нехуй взять" можно написать

Ну окей, добавим это как отдельный признак

In [150]:
train['title'] = train['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
test['title'] = test['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
In [98]:
train['title'].value_counts()
Out[98]:
Mr          517
Miss        182
Mrs         125
Master       40
Dr            7
Rev           6
Major         2
Col           2
Mlle          2
Countess      1
Mme           1
Sir           1
Jonkheer      1
Capt          1
Don           1
Ms            1
Lady          1
Name: title, dtype: int64

Только подсократим количество значений, а то больно много получается

Те, что встречаются редко, заменим на редкие, остальные назовем общепринятыми названиями

In [151]:
for dataset in [train, test]:
    dataset['title'] = dataset['title'].replace(['Lady', 'Countess','Capt', 'Col',\
    'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')

    dataset['title'] = dataset['title'].replace('Mlle', 'Miss')
    dataset['title'] = dataset['title'].replace('Ms', 'Miss')
    dataset['title'] = dataset['title'].replace('Mme', 'Mrs')

Другое дело

Проверим выживаемость каждой группы по старой схеме

In [152]:
train[['title', 'Survived']].groupby(['title'], as_index=False).mean().sort_values(by='Survived', ascending=False)
Out[152]:
title Survived
3 Mrs 0.793651
1 Miss 0.702703
0 Master 0.575000
4 Rare 0.347826
2 Mr 0.156673

Модели машинного обучения не умеют работать со словами, поэтому надо их закодировать, самый простой способ, это просто присвоить каждому title уникальное число, скажем так:

"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5

In [153]:
for dataset in [train, test]:
    dataset['title'] = dataset['title'].map({"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}).fillna(0)
    
train.head(5)
Out[153]:
PassengerId Survived Pclass Name Sex Age SibSp Parch Fare Embarked title
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 7.2500 S 1
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 71.2833 C 3
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 7.9250 S 2
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 53.1000 S 3
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 8.0500 S 1

Когда мы воспользовались признаком Name, так же как тобой пользуются телки, можно со спокойной душой его дропнуть. Так же поступим с PassengerId

In [154]:
print('Размерность набора данных до удаления: {}'.format(train.shape))

for dataset in [train, test]:
    dataset.drop(columns=['Name', 'PassengerId'], inplace=True)
    
print('Размерность набора данных после удаления: {}'.format(train.shape))
Размерность набора данных до удаления: (891, 11)
Размерность набора данных после удаления: (891, 9)
In [155]:
train.head(5)
Out[155]:
Survived Pclass Sex Age SibSp Parch Fare Embarked title
0 0 3 male 22.0 1 0 7.2500 S 1
1 1 1 female 38.0 1 0 71.2833 C 3
2 1 3 female 26.0 0 0 7.9250 S 2
3 1 1 female 35.0 1 0 53.1000 S 3
4 0 3 male 35.0 0 0 8.0500 S 1

Как уже стало понятно, даже последнему ебалну, с категориальными признаками надо что-то делать. Давайте закодируем признак Sex, так же присвоив мужланам 1, а бабам 0

In [156]:
for dataset in [train, test]:
    dataset['Sex'] = dataset['Sex'].map({'male': 1, 'female': 0})
In [119]:
train.head(5)
Out[119]:
Survived Pclass Sex Age SibSp Parch Fare Embarked title
0 0 3 1 22.0 1 0 7.2500 S 1
1 1 1 0 38.0 1 0 71.2833 C 3
2 1 3 0 26.0 0 0 7.9250 S 2
3 1 1 0 35.0 1 0 53.1000 S 3
4 0 3 1 35.0 0 0 8.0500 S 1

В признаке Age полно пропущеных значений, оставлять их пустыми нельзя

In [123]:
np.sum(train['Age'].isnull())
Out[123]:
177

Для заполнения пропущенных значений в данных существует куча способв, но замарачиваться сильно не будем и закроем пропущенные значения в признаке Age медианами

In [124]:
Image('mediana.png')
Out[124]: