Задача Титаника одна из самых известных платформы Kaggle. Рано или поздно, любой начинающий специалист по данным возьмется за ее решение. Здесь я покажу на пальцах: как проверить гипотезы, найти зависимости и реализовать предсказание только на основе аналитики.

Описание задачи

Всем знакома история, связанная с этим кораблем. Посмотрели одноименную картину и имеем представление о чем пойдет речь.

Кадр крушения Титаника из фильма.
Кадр крушения Титаника из фильма.

На борту находилось 2200 человек, после крушения выжило ( по некоторым данным) 565=( Предполагается, что у кого-то на борту шансов выжить было больше. Может если это был мужчина, или богач из первого класса... Мы изучим различные гипотезы, проверим их с помощью математической статистики и смоделируем свой вектор предсказания выживания на судне, а потом сравним с тем, как отработают знаменитые алгоритмы.

Обработка данных

Прежде чем начать обработку, необходимо эти данные получить. Любой желающий может зарегистрироваться на Kaggle и принять участие в соревновании. Для удобства прилагаю ссылку, после регистрации нажмите кнопку "Принять участие" и скачет два файла gender_submission.csv, train.csv и test.csv. Для начала импортируем необходимые библиотеки.

import pandas as pd
import numpy as np 
import missingno as msno
import seaborn as sns
import matplotlib.pyplot as plt 
from scipy.stats import mannwhitneyu 
from sklearn.neighbors import KNeighborsClassifier 
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import GradientBoostingClassifier

Чтение файлов обычно выглядит так:

df = pd.read_csv('gender_submission.csv') 
test = pd.read_csv('test.csv') 
train = pd.read_csv('train.csv')
msno.matrix(train)
msno.matrix(test)

Из указанного выше следует, что в train и test содержатся несовпадающий столбцы ( их названия по оси x) в train - 891 наблюдение, в test- 418. В обоих датасетах много пропущенных значений в столбцах Age и Cabin. df содержит 418 наблюдений с номером пассажира и предсказанием Survived в котором 1- спасен, 0 нет. В нем также 418 значений выживания для пассажиров из test. 

Для изучения данных соединим train и test.

y = test.merge(df,on='PassengerId', how = 'right')
df = train.append(y)

Мужчины-женщины на борту

df.groupby('Sex')['PassengerId'].count()
Sex
female    466
male      843

Классы билетов

df.groupby('Pclass')['PassengerId'].count()
Pclass
1    323
2    277
3    709

Описательная статистика возраста пассажиров.

df.Age.describe()
count    1046.000000
mean       29.881138
std        14.413493
min         0.170000
25%        21.000000
50%        28.000000
75%        39.000000
max        80.000000
df[train.Age==0.17]

Survived

Pclass

Sex

Age

354

1

3

female

0.17

Ура, малыш спасен:) Описательная статистика тарифов билетов

df.Fare.describe()
count    1308.000000
mean       33.295479
std        51.758668
min         0.000000
25%         7.895800
50%        14.454200
75%        31.275000
max       512.329200

Братьев и сестер на борту

df.SibSp.unique()
array([1, 0, 3, 4, 2, 5, 8])
df.SibSp.describe()
count    1309.000000
mean        0.498854
std         1.041658
min         0.000000
25%         0.000000
50%         0.000000
75%         1.000000
max         8.000000

Родителей- детей на борту

df.Parch.unique()
array([0, 1, 2, 5, 3, 4, 6, 9])
df.Parch.describe()
count    891.000000
mean       0.381594
std        0.806057
min        0.000000
25%        0.000000
50%        0.000000
75%        0.000000
max        6.000000

Большинство пассажиров путешествовавали в одиночку. но некоторые семьи насчитывали до 9 человек. Мужчин почти в два раза больше женщин. Интересно сравнить отличался ли возраст пасажиров в группах мужчины- женщины, была ли разница для них в тарифах билетов.

men = df[df.Sex == "male"] women = df[df.Sex == "female"]
stat, p = mannwhitneyu(men.Age, women.Age)
print('Критерий значимости = %.3f' % (p))

alpha = 0.1
if p > alpha:
  print('Возраст не отличался')
else:
  print('Действительно разный возраст, мужчины ', 
        men.Age.mean(),', женщины ', women.Age.mean())
Критерий значимости = 0.001
Действительно разный возраст, мужчины  30.58 , женщины  28.68

Интересно,а на первый взгляд так и не скажешь)

stat, p = mannwhitneyu(men.Fare, women.Fare)
print('Критерий значимости = %.3f' % (p))
alpha = 0.1
if p > alpha:
  print('Возраст не отличался')
else: 
  print('Действительно разный тариф, мужчины ',
        men.Fare.mean(),', женщины ', women.Fare.mean())
Критерий значимости = 0.000
Действительно разный тариф, мужчины  25.52 , женщины  44.47

А вот тариф однозначно отличается, женщинам путешествовать обходилось дороже. Посмотрим как они распределились по классам кают.

s = pd.DataFrame(df.groupby(['Sex', 'Pclass'])['PassengerId'].count().
                 reset_index())
f = s[s.Sex == 'female'] 
f['ratio'] = f.PassengerId/f.PassengerId.sum()*100
m = s[s.Sex == 'male']
m['ratio'] = m.PassengerId/m.PassengerId.sum()*100

Sex

Pclass

PassengerId

ratio

0

female

1

144

30.901288

1

female

2

106

22.746781

2

female

3

216

46.351931

Sex

Pclass

PassengerId

ratio

3

male

1

179

21.233689

4

male

2

171

20.284698

5

male

3

493

58.481613

Женщины предпочли первый клас чаще мужчин.

Заменим пропуски возраста средним значением ( так как среднее и медиана рядом)

df.Age = df.Age.fillna(df.Age.mean())
df.Age.describe()
count    1309.000000
mean       29.881138
std        12.883193
min         0.170000
25%        22.000000
50%        29.881138
75%        35.000000
max        80.000000

Стандартное отклонение изменилось на 2. В колонке кабин мало значений, удалим их. Также удалим имя, порт посадки на борт и номер билета, так как такая информация не несет предсказательной силы.

df = df.drop(labels=['Cabin','Name','Ticket','Embarked'], axis=1)

Визуализация параметра Survived

sns.catplot(data = df,y='Survived',x='Sex',col='Pclass',
            kind='bar', saturation=0.5)

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

sns.catplot(data = df,hue = 'Survived', x = 'Sex',
            kind='count', saturation=0.5)

На этом графике хорошо видно соотношение выживших мужчин и женщин после крушения. Предположим, что люди 'богатый' сегмент спасались чаще чем остальные.

sns.catplot(data = df,hue = 'Survived', x = 'Pclass',
            kind='count', saturation=0.5)

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

Возможно, дети выживали реже взрослых, в связи с невозможностью дееспособности.Поделим возраст на 7 перцентилей.

df.Age.hist()

Из распределения, видно, что людей 30 лет погибло больше остальных, но и среднее у нас в этом значении. Разобьем всех на 7 групп с помощью категоризации.

df['Age_cat'] = pd.qcut(df.Age,7)
sns.catplot(data = df,hue = 'Survived', x = 'Age_cat',
            kind='count', saturation=0.5) plt.xticks(rotation=45)
child= df[df.Age<6]
sns.catplot(data = child,hue = 'Survived', 
            x = 'Sex',kind='count', saturation=0.5)
grand= df[df.Age>50]
sns.catplot(data = grand,hue = 'Survived', 
            x = 'Sex',kind='count', saturation=0.5)

В целом на корабле предпочли спасать женщин и детей в первую очередь. Зависимости от возраста наблюдается. Например, погибнуть у мужчины за 50 шансов было больше. Проверим как влиял размер сеиьи на выживаемость. Ранее на графике наблюдалось аномальное количество погибших в 29 лет, (конечно, мы меняли пропуски на среднее, и медиана находится примерно здесь)

h = df[(df.Age>=29)&(df.Age<=30)]
sns.catplot(data = h,hue = 'Survived',
            x = 'Sex',kind='count', saturation=0.5)

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

df = df.drop('Age_cat', axis=1)
df['family'] = df['Parch'] + df['SibSp']

Обычно мы можем добавлять новые наблюдений, с помощью арифметических операций ( как в ячейке выше). Такая переменная будет отражать на общее количество человек семьи на борту.

sns.catplot(data = df,hue = 'Survived', x = 'family',
            kind='count', saturation=0.5)

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

Чем меньше семья- тем больше шансов выжить.

sns.catplot(data = df,hue = 'Survived', x = 'family',
            col = 'Sex',kind='count', saturation=0.5)

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

Модель "Baseline" на основе логических выводов

df['result'] = 1
df.loc[(df.Sex == 'male')&(df.Pclass == 3), 'result'] = 0
df.loc[(df.Sex == 'male')&(df.Age > 50), 'result'] = 0
df.loc[df['family']>3, 'result'] = 0

Здесь я присвоила "смерть" всем мужчинам в третьем классе, всем мужчинам старше 50, всем семьям, в которых больше 3 человек. Теперь оценим точность.

df['errors'] = (df.Survived - df.result)**2
1 - df.errors.sum() / df.shape[0]
0.7203972498090145

Отличный результат для моделей такого типа, в 72 случаях из 100 мы правильно предсказываем событие, а это гораздо выше случайности. Попробуем добавить еще один критерий

df.loc[(df.Sex == 'male')&(df.Age >=29)&(df.Age <=39), 'result'] = 0
df['errors'] = (df.Survived - df.result)**2
1 - df.errors.sum() / df.shape[0]
0.7815126050420168

Предсказательный вектор улучшил точность до 78,9%! Тут мы предсказывали смерти, что если мы попробуем предсказать спасение?

df['alive'] = 0
df.loc[(df.Sex == 'female')&((df.Pclass == 1)&
                             (df.Pclass == 2)), 'alive'] = 1
df.loc[df.Age < 6, 'alive'] = 1
df.loc[(df.Sex == 'female')&
       (df['family'] < 2), 'alive'] = 1
df.loc[(df.Sex == 'male')& (df['family'] < 2)&
       (df.Pclass == 1), 'alive'] = 1
df['errors_2'] = (df.Survived - df.alive)**2
1 - df.errors_2.sum() / df.shape[0]
0.7524828113063406

В коде выше мы спасли всех женщин 1-2 класса, детей младше 6 лет, женщин у кого семья меньше 2 человек и мужчин в первом классе с семьей меньше двух.

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

Комментарии (1)


  1. KatrinDynev Автор
    16.03.2022 22:44

    Вижу ошибки) но это моя первая публикация)