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

RMS Titanic затонул 15 апреля 1912 года в северной части Атлантического океана, столкнувшись с айсбергом. На борту находились 2224 пассажира, в результате этой катастрофы погибло более 1500 человек.

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

Моя цель в этом посте - показать, как обрабатывать набор данных для таких новичков, как я. По этой причине я разделял почти все входы и выходы.

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

Набор данных

train_data = pd.read_csv("/kaggle/input/titanic/train.csv")
test_data = pd.read_csv("/kaggle/input/titanic/test.csv")
train_data.head()

train_data.shap

test_data.shape

У нас есть два набора данных, которые представляют собой наборы данных для обучения и тестирования. В нашем наборе данных о поездах есть 11 функций, за исключением целевого столбца (сохранившегося). Наборы данных для поездов и тестов содержат 891 и 418 строк соответственно.

train_data.info()
test_data.info()

Когда мы проверяем информацию о поездах и тестовых данных, мы видим типы функций и ненулевые значения в столбцах, как мы можем видеть в информации столбцов. В данных поезда отсутствуют значения в столбцах «Возраст», «Кабина» и «Посадка». В тестовых данных столбцы «Возраст», «Стоимость проезда» и «Кабина» содержат пропущенные значения. Мы разберемся с ними позже.

Исследование и визуализация данных

Давайте проанализируем особенности, используя несколько графиков.

Пол

train_data[['Sex', 'Survived']].groupby(['Sex'], as_index=False).mean()

sns.countplot(train_data["Survived"], hue=train_data["Sex"])

Когда мы смотрим на пол и выживаемость, результаты выглядят интересными. Коэффициент выживаемости у женщин намного больше, чем у мужчин. Согласно этим данным, существует сильная корреляция между столбцами выживших и полами.

Порт посадки

train_data[['Embarked', 'Survived']].groupby(['Embarked'], as_index=False).mean()

sns.countplot(train_data["Survived"], hue=train_data["Embarked"])

Функция «Посадка» показывает порты посадки: C = Шербур, Q = Квинстаун и S = ​​Саутгемптон. Согласно таблице, которая показывает коэффициент выживаемости в отношении этой особенности, пассажиры, прибывшие из Шербура, имеют более высокий коэффициент выживаемости.

Класс пассажиров

Столбец Pclass включает 3 разных значения. 1-й = верхний, 2-й = средний, 3-й = нижний.

train_data[['Pclass', 'Survived']].groupby(['Pclass'], as_index=False).mean()

sns.countplot(train_data["Survived"], hue=train_data["Pclass"])

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

Билет

sns.catplot(x="Survived",y="Fare",data=train_data, kind="boxen")

g = sns.FacetGrid(train_data, col='Survived')
g.map(plt.hist, 'Fare', bins=20)

Колонка тарифов на билеты - это одна из числовых функций. Когда мы смотрим на гистограмму и прямоугольные диаграммы, мы можем сделать пару замечаний о корреляции между тарифами на проезд и выживаемостью.
Средняя стоимость проезда для выживших пассажиров намного выше, чем для неживых пассажиров. Кроме того, гистограмма показывает, что выжило большинство пассажиров, которые заплатили больше 100.
Это ожидаемый результат, потому что мы можем предположить, что должна быть корреляция между pclass и тарифами.

Возраст

g = sns.FacetGrid(train_data, col='Survived')
g.map(plt.hist, 'Age', bins=15)

Когда мы строим гистограмму для «возраста», что является еще одной числовой характеристикой, мы можем получить несколько полезных советов. Дети (Возраст

Количество членов семьи

Возможности Parch и SibSp связаны с семейными связями. Перчин - это число родителей и детей. СибСп - количество братьев и сестер.

sns.countplot(train_data["Survived"], hue=train_data["Parch"])

sns.countplot(train_data["Survived"], hue=train_data["SibSp"])

train_data[['SibSp', 'Survived']].groupby(['SibSp'], as_index=False).mean().sort_values(by='Survived', ascending=False)

Похоже, одиночество - не преимущество для выживания.

Корреляция между функциями и целью

Мы можем построить график, чтобы увидеть все корреляции между числовыми характеристиками. Есть 2 похожие функции для создания этого графика. Один из них - парный график из sns, а другой - построение scatter_matrix из панд.

pd.plotting.scatter_matrix(train_data, figsize=(12,12));

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

train_data.groupby(train_data["Survived"]).hist(figsize=(6,8))

На самом деле, по моему мнению, лучший способ увидеть корреляцию между функциями - это тепловая карта. Используя функции corr из pandas и тепловую карту из sns, мы можем иметь очень четкое представление о корреляциях для числовых характеристик.

sns.heatmap(cor, annot=True, fmt=".2f")

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

Разработка и моделирование функций

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

X=train_data[["Pclass", "Age", "SibSp", "Parch", "Fare"]]
y=train_data["Survived"]

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

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

После разделения нашего набора данных на обучающие и тестовые тесты мы создаем нашу модель с помощью sklearn.RandomForestClassifier (). Затем мы подгоняем нашу модель к данным поезда, а затем делаем прогноз для тестовых данных.

from sklearn.ensemble import RandomForestClassifier
rfc=RandomForestClassifier()
rfc.fit(X_train, y_train)
rfc.score(X_test,y_test)

Это не работает должным образом, потому что нам нужно заполнить пропущенные значения перед развертыванием модели.

train_data.info()
test_data.info()

Как мы видим, отсутствуют значения для некоторых функций, таких как Возраст, Кабина, Стоимость проезда и Посадка. Во-первых, мы избавимся от значений NaN в числовых функциях.

train_data["Age"].fillna(train_data["Age"].mean(), inplace=True)
test_data["Age"].fillna(test_data["Age"].mean(), inplace=True)
test_data["Fare"].fillna(test_data["Fare"].mean(), inplace=True)

Значения NaN заменяются средним значением столбца с использованием метода панд fillna как в обучающих, так и в тестовых данных.
Теперь мы можем развернуть нашу модель.

X=train_data[["Pclass", "Age", "SibSp", "Parch", "Fare"]]
y=train_data["Survived"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
rfc=RandomForestClassifier(random_state=35)
rfc.fit(X_train, y_train)
print("test accuracy: ",rfc.score(X_test,y_test))

Мы могли бы достичь точности 68%, используя только числовые функции. Постараемся повысить точность. У нас есть некоторые другие функции, тип данных которых является объектом. Эти функции также имеют некоторую корреляцию с выживанием, которое мы уже видели в предыдущей части.
Мы должны преобразовать наши строковые значения в числовые значения. Здесь есть 2 основных метода. Первый метод - это горячее кодирование, которое создает столбец для каждого уникального значения и заполняет эти столбцы двоичными кодами 1 и 0. Другой метод - это кодирование меток, которое заменяет каждое уникальное строковое значение уникальным числом.
И pandas, и У sckit learn есть подходящие функции для этого процесса. Одно горячее кодирование возможно с помощью pandas.get_dummies и sklearn.preprocessing.OneHotEncoder. Для кодирования меток мы можем использовать функции pandas.factorize и sklearn.preprocessing.LabelEncoder.

train_data["Sex_encoded"]=pd.factorize(train_data["Sex"])[0]
train_data["Embarked_encoded"]=pd.factorize(train_data["Embarked"])[0]
test_data["Sex_encoded"]=pd.factorize(test_data["Sex"])[0]
test_data["Embarked_encoded"]=pd.factorize(test_data["Embarked"])[0]

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

train_data["Embarked"].unique()

train_data["Embarked_encoded"].unique()

Значения NaN заменены на -1, как вы можете видеть выше.

Теперь мы можем попробовать еще раз развернуть нашу модель с новыми функциями.

X=train_data[["Pclass", "Age", "SibSp", "Parch", "Fare", "Sex_encoded", "Embarked_encoded"]]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
rfc=RandomForestClassifier(random_state=35)
rfc.fit(X_train, y_train)
print("test accuracy: ",rfc.score(X_test,y_test))

Как видим, точность увеличилась почти на 10%.

Мы можем добавить еще одну функцию к нашему набору данных - столбец кабины. Однако похоже, что это невозможно напрямую использовать функцию panda.factorize, потому что существует слишком много разных значений. Во-первых, мы попытаемся получить только коды колоды.
Сначала мы заполняем нулевые значения, затем берем только первую букву каждой строки, используя функцию str.slice из pandas. Таким образом, мы можем получить коды каюты.

train_data["Cabin"].unique()

train_data["Cabin"].fillna("N", inplace=True)
train_data['Cabin_code'] = train_data["Cabin"].str.slice(0,1)
train_data['Cabin_code'].unique()

test_data["Cabin"].fillna("N", inplace=True)
test_data['Cabin_code'] = test_data["Cabin"].str.slice(0,1)

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

X=train_data[["Pclass", "Age", "SibSp", "Parch", "Fare", "Sex_encoded", "Embarked_encoded", "Cabin_code_encoded"]]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
rfc=RandomForestClassifier(random_state=35)
rfc.fit(X_train, y_train)
print("test accuracy: ",rfc.score(X_test,y_test))

Добавление этой новой функции в наш набор данных X не повлияло на точность.

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

from sklearn.preprocessing import StandardScaler
scaler=StandardScaler()
X_train_sc=scaler.fit_transform(X_train)
X_test_sc=scaler.transform(X_test)
rfc=RandomForestClassifier(random_state=35)
rfc.fit(X_train_sc, y_train)
print("train accuracy: ",rfc.score(X_train_sc, y_train))
print("test accuracy: ",rfc.score(X_test_sc,y_test))

Как видите, точность теста до и после масштабирования функций одинакова (79,1%). Таким образом, стандартизация функций не помогла повысить точность, как мы ожидали.

Переоснащение

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

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

rfc=RandomForestClassifier(random_state=35)
for x in X_train.columns:
    
    rfc.fit(X_train[[x]], y_train)
    print(x,"train accuracy: ",rfc.score(X_train[[x]], y_train)*100)
    print(x,"test accuracy: ",rfc.score(X_test[[x]],y_test)*100)

Мы можем прокомментировать эти результаты. Столбец пола - самая важная особенность нашей модели, только она сама имеет точность теста 79%. И он также имеет почти такую ​​же точность поезда и тестирования, что является очень идеальной ситуацией. Хотя разница между точностью поездов и тестов очень мала для некоторых характеристик, особенно числовые характеристики, такие как возраст и стоимость проезда, имеют большую разницу, которая составляет 6–10%.

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

Возраст

train_data["Age"].groupby(train_data["Survived"]).plot(kind="hist", bins=20, legend=True, alpha=0.5)

train_data.loc[train_data['Age'] <= 7.5, 'Age_encoded'] = 0
train_data.loc[(train_data['Age'] > 7.5) & (train_data['Age'] <= 15), 'Age_encoded'] = 1
train_data.loc[(train_data['Age'] > 15) & (train_data['Age'] <= 25), 'Age_encoded'] = 2
train_data.loc[(train_data['Age'] > 25) & (train_data['Age'] <= 30), 'Age_encoded'] = 3
train_data.loc[(train_data['Age'] > 30) & (train_data['Age'] <= 35), 'Age_encoded'] = 4
train_data.loc[(train_data['Age'] > 35) & (train_data['Age'] <= 50), 'Age_encoded'] = 5
train_data.loc[train_data['Age'] > 50, 'Age_encoded'] = 6
train_data["Age_encoded"].unique()

train_data[['Age_encoded', 'Survived']].groupby(['Age_encoded'], as_index=False).mean()

sns.countplot(train_data["Survived"], hue=train_data["Age_encoded"])

Стоимость

train_data["Fare"].groupby(train_data["Survived"]).plot(kind="hist", bins=20, legend=True, alpha=0.5)

train_data.loc[train_data['Fare'] <= 12.5, 'Fare_encoded'] = 0
train_data.loc[(train_data['Fare'] > 12.5) & (train_data['Fare'] <= 25), 'Fare_encoded'] = 1
train_data.loc[(train_data['Fare'] > 25) & (train_data['Fare'] <= 50), 'Fare_encoded'] = 2
train_data.loc[(train_data['Fare'] > 50) & (train_data['Fare'] <= 75), 'Fare_encoded'] = 3
train_data.loc[(train_data['Fare'] > 75) & (train_data['Fare'] <= 100), 'Fare_encoded'] = 4
train_data.loc[(train_data['Fare'] > 100) & (train_data['Fare'] <= 150), 'Fare_encoded'] = 5
train_data.loc[train_data['Fare'] > 150, 'Fare_encoded'] = 6
train_data["Fare_encoded"].unique()

train_data[['Fare_encoded', 'Survived']].groupby(['Fare_encoded'], as_index=False).mean()

sns.countplot(train_data["Survived"], hue=train_data["Fare_encoded"])

Как видно из приведенного выше, столбцы Age и Fare были преобразованы в дискретные значения из непрерывных. Я попытался определить критические точки и вырезать из них черты.

Я делал это вручную, но в пандах для этого есть некоторые функции. Это функции pandas.cut () и pandas.qcut (). Они сокращают значения согласно значениям и частотам соответственно.

Теперь мы можем опробовать нашу модель с новыми функциями.

X=train_data[["Pclass", "Age_encoded", "SibSp", "Parch", "Fare_encoded", "Sex_encoded", "Embarked_encoded", "Cabin_code_encoded"]]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
rfc=RandomForestClassifier(random_state=35)
rfc.fit(X_train, y_train)
print("train accuracy: ",rfc.score(X_train, y_train))
print("test accuracy: ",rfc.score(X_test,y_test))

Точность наших предыдущих тренировок и испытаний составляла 98,2% и 79,1 соответственно. Теперь точность поезда составляет 92,0%, а точность испытаний - 78,7%.

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

Настройка гиперпараметров

Теперь у нас есть модель и окончательный набор данных. Наша модель имеет множество аргументов (n_estimators, criterion, max_depth, min_samples_leaf и т. Д.), И нам нужно найти наилучшие значения для этих параметров. Для этого мы не будем пробовать параметры вручную, мы попробуем GridSearchCV и RandomizedSearchCV из sklearn и сравним их.
GridSearchCV - это исчерпывающий поиск по заданным параметрам. Он пробует каждую комбинацию в сетке значений гиперпараметров и может занять много времени, если у нас есть большой набор данных и слишком много гиперпараметров. С другой стороны, RandomizedSearchCV выбирает случайные комбинации и в большинстве случаев находит лучшие параметры за более короткое время.

from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
import time
rfc_parameters = { 
    'n_estimators': [100,200,500],
    'max_features': ['auto', 'sqrt', 'log2'],
    'max_depth' : [6,8,10],
    'criterion' :['gini', 'entropy'],
    'min_samples_split': [2, 4, 6]
}

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

Случайный поиск

start_time = time.time()

rand_search= RandomizedSearchCV(rfc, rfc_parameters, cv=5)
rand_search.fit(X_train, y_train)
print(rand_search.best_params_)
print("best accuracy :",rand_search.best_score_)

end_time = time.time()
print("Total execution time: {} seconds".format(end_time - start_time))

Поиск по сетке

start_time = time.time()

grid_search= GridSearchCV(rfc, rfc_parameters, cv=5)
grid_search.fit(X_train, y_train)
print(grid_search.best_params_)
print("best accuracy :",grid_search.best_score_)

end_time = time.time()
print("Total execution time: {} seconds".format(end_time - start_time))

Лучшие параметры и лучшие значения точности определяются как рандомизированным поиском, так и поиском по сетке. Хотя наилучшая точность в обоих случаях составляет около 83%, стоимость поиска по сетке намного выше. Существует огромная разница, которая более чем в десять раз превышает общее время выполнения (случайный поиск: 20 секунд, поиск по сетке: 517 секунд). Таким образом, использование алгоритма случайного поиска - хорошее решение как с точки зрения точности, так и с точки зрения затрат.

Наше лучшее значение точности составляет 83,1%, что неплохо.

Заключение

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