Мы можем научиться машинному обучению у Титаника

Кто еще не знал о знаменитом корабле «Титаник»? Титаник или полностью Royal Mail Ship (RMS) Титаник, британский роскошный пассажирский лайнер, затонувший 15 апреля 1912 года [1]. Одна из самых известных трагедий в современной истории, она вдохновила на создание множества рассказов, нескольких фильмов и мюзикла [1]. Этот «непотопляемый» корабль по иронии судьбы затонул во время путешествия из Саутгемптона, Англия, в Нью-Йорк, США, после столкновения с айсбергом. К сожалению, спасательных шлюпок на всех на борту не хватило, в результате чего из 2224 пассажиров и членов экипажа погибли 1502 человека [2].

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

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

Начнем

Предварительная обработка и анализ данных

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

Удалить ненужную функцию

train_df = train_df.drop(train_df[["Ticket"]], axis=1)
test_df = test_df.drop(test_df[["Ticket"]], axis=1)
combine_df = [train_df,test_df]

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

Найти нулевое значение

train_df.isnull().sum()

test_df.isnull().sum()

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

  • Возраст
for dataset in combine_df:
    dataset["Age"] = dataset["Age"].fillna(dataset["Age"].dropna().median())

Чтобы обработать нулевые значения в функции «Возраст», я заполняю нулевые значения, вставляя медиану данных функции «Возраст», чтобы заменить существующее значение Nan. Это потому, что возрастные данные не распределены нормально.

  • Тарифы
for dataset in combine_df:
    dataset["Fare"] = dataset["Fare"].fillna(dataset["Fare"].dropna().median())

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

  • Запущенная функция
for dataset in combine_df:
    dataset["Embarked"] = dataset["Embarked"].fillna(dataset["Embarked"].mode()[0])

Эта функция имеет три уникальных значения: S, C и Q. Эта функция показывает порт посадки. S для Саутгемптона, C для Шербура и Q для Квинстауна. Чтобы обработать нулевые значения в «Embarked Feature», я заполняю нулевые значения, вставляя режим данных «Embarked». Это потому, что эта функция является категориальной, поэтому мы можем использовать режим для обработки нулевых значений.

  • Кабина
for dataset in combine_df:
    dataset["Cabin"] = dataset["Cabin"].fillna("N/A")

Чтобы обработать нулевые значения в функции «Каюта», я заполняю нулевые значения, вставляя «Н/Д», чтобы заменить значения Нэн. Само «N/A» означает «Недоступно», поскольку данные были.

Создать новую функцию

Из текущих функций мы можем создать еще одну функцию, которая может дать нам больше информации. Особенностью является «Название». Мы извлекаем заголовок из функции «Имя» с помощью регулярного выражения. Вот код:

for dataset in combine_df:
    dataset["Title"] = dataset.Name.str.extract(" ([A-Za-z]+)\.", expand=False)

train_df["Title"].value_counts()

test_df["Title"].value_counts()

С помощью этих регулярных выражений мы можем извлечь слово в алфавитном порядке, которое в строке после этого слова имеет точку. Например, мы можем взять слово «господин». в строке «г. Фасоль». Но, поскольку мы видим данные в «Заголовке», у нас есть несколько категорий. В этой ситуации я хочу объединить все заголовки, содержащие менее 10 данных, в одну категорию «Редкие заголовки». Итак, это код, который я использовал:

for dataset in combine_df:
    dataset["Title"] = dataset["Title"].replace(["Dr","Rev","Mlle","Major","Col","Countess","Capt","Ms","Sir","Lady","Mme","Don","Jonkheer"], "Rare Title")

train_df["Title"].value_counts()

test_df["Title"].value_counts()

Как мы видим сейчас, у нас есть только 5 заголовков в функции «Заголовок», что позволяет модели легче предсказать выжившего, потому что не так много категорий с меньшим количеством участников.

Удалите другие ненужные функции

Поскольку у нас есть некоторые функции, которые больше не нужны, такие как «Имя» и «PassengerId», мы можем просто отказаться от них. Вот код:

train_df = train_df.drop(["PassengerId","Name"], axis=1)
test_df = test_df.drop(["PassengerId","Name"], axis=1)

Преобразование функций

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

temp = train_df.copy()
  • Возраст

Во-первых, нам нужно сначала найти возрастной диапазон, прежде чем создавать категории. Что касается возрастных данных, я хочу разделить их на 5 категорий.

После этого мы классифицируем возрастные данные по этим возрастным диапазонам.

for dataset in combine_df:
    dataset.loc[dataset["Age"] <= 16, "Age"] = 0
    dataset.loc[(dataset["Age"] > 16) & (dataset["Age"] <= 32), "Age"] = 1
    dataset.loc[(dataset["Age"] > 32) & (dataset["Age"] <= 48), "Age"] = 2
    dataset.loc[(dataset["Age"] > 48) & (dataset["Age"] <= 64), "Age"] = 3
    dataset.loc[dataset["Age"] > 64, "Age"] = 4

train_df["Age"] = train_df["Age"].astype(int)
test_df["Age"] = test_df["Age"].astype(int)
  • Запущенная функция

Как мы уже знаем, в этой функции у нас есть 3 разных значения: S, C и Q. Таким образом, мы можем преобразовать эти значения в числовые, выполнив сопоставление.

embarked_mapping = {"S": 1, "C": 2, "Q": 3}
for dataset in combine_df:
    dataset["Embarked"] = dataset["Embarked"].map(embarked_mapping).astype(int)
  • Секс-функция

Для функции «Пол» у нас есть только 2 уникальных значения: мужской и женский. Мы можем сделать то же самое, что и для функции «Embarked», чтобы преобразовать ее. Мы можем сделать отображение, чтобы преобразовать его.

sex_mapping = {"female": 0, "male": 1}
for dataset in combine_df:
    dataset["Sex"] = dataset["Sex"].map(sex_mapping).astype(int)
  • Заголовок

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

title_mapping = {"Mr": 0, "Miss": 1, "Mrs": 2, "Master": 3, "Rare Title": 4}
for dataset in combine_df:
    dataset["Title"] = dataset["Title"].map(title_mapping).astype(int)
  • Кабина

Для этой функции мы трансформируем ее немного иначе, чем другие функции, потому что эта функция имеет много разных значений. Чтобы преобразовать его в числовое, я стараюсь разделить все данные на 8 категорий, различающихся по первой букве в кабине от «А» до «Г». Кроме «A» до «G», они будут отнесены к одной категории со значением «N/A».

cabin_unique_train = list(train_df["Cabin"].unique())
cabin_unique_test = list(test_df["Cabin"].unique())
cabin_unique = cabin_unique_train + cabin_unique_test
cabon_unique = set(cabin_unique)

cabin_mapping = {}
for cabin in cabin_unique:
    if cabin != "N/A":
        if "A" in cabin:
            cabin_mapping[cabin] = 0
        elif "B" in cabin:
            cabin_mapping[cabin] = 1
        elif "C" in cabin:
            cabin_mapping[cabin] = 2
        elif "D" in cabin:
            cabin_mapping[cabin] = 3
        elif "E" in cabin:
            cabin_mapping[cabin] = 4
        elif "F" in cabin:
            cabin_mapping[cabin] = 5
        elif "G" in cabin:
            cabin_mapping[cabin] = 6
        else:
            cabin_mapping[cabin] = 7
    else:
        cabin_mapping[cabin] = 7

for dataset in combine_df:
    dataset["Cabin"] = dataset["Cabin"].map(cabin_mapping).astype(int)
  • Тарифы

Во-первых, нам нужно сначала найти диапазон тарифов, прежде чем создавать категории, как мы это делаем для функции «Возраст». Для данных о тарифах я хочу разделить на 3 категории.

После этого мы классифицируем данные о тарифах по этим диапазонам тарифов.

for dataset in combine_df:
    dataset.loc[dataset["Fare"] <= 171, "Fare"] = 1
    dataset.loc[(dataset["Fare"] > 171) & (dataset["Fare"] <= 342), "Fare"] = 2
    dataset.loc[dataset["Fare"] > 342, "Fare"] = 3

Найти корреляцию

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

Если мы посмотрим на матрицу корреляции выше, мы увидим, что есть некоторые функции, которые имеют высокую положительную корреляцию, например «Посадка», «Тариф» и «Название», а также имеют высокую отрицательную корреляцию, например «Pclass», «Пол». », и «Каюта». Те признаки, которые имеют высокую положительную корреляцию, показывают, что с увеличением значения этих признаков вероятность выживания выше. Потому что в «Выжил» значение 1 выживает, а 0 не выживает. Для тех признаков, высокая отрицательная корреляция которых показывает, что с увеличением значения этих признаков шансы на выживание снижаются. Отсюда мы можем сделать вывод, что:

  • У человека с более низким значением в «P-классе» больше шансов выжить. Это означает, что из существовавшего 3 класса у человека из 1 класса больше шансов выжить.
  • Самки, как правило, имеют больше шансов выжить, чем самцы. Это потому, что преобразование для функции «Пол» мы определяем как женщина = 0, а мужчина = 1.
  • У человека младшего возраста больше шансов выжить, чем у старшего. Это связано с тем, что функция «Возраст» имеет отрицательную корреляцию с «Выжившими» и показывает категорию возрастного диапазона, которую мы сделали в этом порядке по возрастанию.
  • У человека с меньшим количеством братьев, сестер или супругов на борту больше шансов выжить. Это потому, что «СибСп» имеет отрицательную корреляцию с «Выжил».
  • У человека с большим количеством родителей или детей на борту больше шансов выжить. Это потому, что «Парч» имеет положительную корреляцию с «Выжил».
  • Чем выше стоимость проезда у ответственного лица, тем выше шанс, что человек выживет. Это потому, что «Проезд» имеет положительную корреляцию с «Выжил».
  • У человека с кабиной «А» больше шансов выжить, чем у остальных. Это потому, что «Хижина» имеет отрицательную корреляцию с «Выжил».
  • У человека, высадившегося в Саутгемптоне, меньше шансов выжить, чем у человека, высадившегося в Шербуре или Квинстауне. Это связано с тем, что «Отправленные» имеют положительную корреляцию с «Выжившими», и мы преобразовали эту характеристику, определив Саутгемптон = 1, Шербур = 2 и Квинстаун = 3.
  • У человека с редким титулом больше шансов выжить, чем у другого титула. Это потому, что «Титул» имеет положительную корреляцию с «Выжившим», и мы преобразовали эту функцию, определив Мистер = 0, Мисс = 1, Миссис = 2, Мастер = 3 и Редкий Титул = 4.

Создание модели машинного обучения

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

Обучение и тестирование разделенных данных

В этом разделении мы разделим данные, которые у нас есть, на 80% данных поездов и 20% тестовых данных.

X = train_df.drop(["Survived"], axis=1)
y = train_df["Survived"]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)
print("Data split done")

После того, как мы разделили наши данные, теперь мы найдем лучшую модель машинного обучения для наших данных. Модели, которые мы собираемся сравнить, — это Extra Trees, SVM, Random Forest, Gradient Boosting, Adaboost, Decision Tree и Logistic Tree.

Найти лучшую модель

models = []
models.append(('Extra Trees', ExtraTreesClassifier()))
models.append(('SVM', SVC()))
models.append(('Random Forest', RandomForestClassifier()))
models.append(('Gradient Boosting', GradientBoostingClassifier()))
models.append(('Adaboost', AdaBoostClassifier()))
models.append(('Decision Tree', DecisionTreeClassifier()))
models.append(('Logistic Regression', LogisticRegression()))

results = []
names = []
scoring = 'accuracy'
for name, model in models:
 kfold = RepeatedStratifiedKFold(n_splits=3, n_repeats=2, random_state=42)
 cv_results = cross_val_score(model, X, y, cv=kfold, scoring=scoring)
 results.append(cv_results)
 names.append(name)
 msg = "%s: %f (%f)" % (name, cv_results.mean(), cv_results.std())
 print(msg)

fig = plt.figure(figsize=(12, 6))
fig.suptitle('Algorithm Comparison')
ax = fig.add_subplot(111)
plt.boxplot(results)
ax.set_xticklabels(names)
plt.grid()
plt.show()
Extra Trees: 0.811448 (0.013040)
SVM: 0.818743 (0.010179)
Random Forest: 0.809203 (0.005723)
Gradient Boosting: 0.826038 (0.015872)
Adaboost: 0.819865 (0.011125)
Decision Tree: 0.800224 (0.018713)
Logistic Regression: 0.808081 (0.014018)

Gradient Boosting здесь имеет лучшую производительность, чем другие алгоритмы со средним значением точности 0,826. Итак, мы будем использовать Gradient Boosting для прогнозирования выжившего.

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

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

param_grid = {
    "n_estimators":[5,50,250,500],
    "max_depth":[1,3,5,7,9],
    "learning_rate":[0.01,0.1,1,10,100]
}
grid = GridSearchCV(GradientBoostingClassifier(), param_grid, refit = True, verbose = 3, cv = 5)

grid.fit(X_train, y_train) 
print(grid.best_params_) 
{'learning_rate': 0.01, 'max_depth': 3, 'n_estimators': 500}

Там мы получаем лучший параметр для нашего повышения градиента: learning_rate = 0,01, max_depth = 3 и n_estimators = 500.

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

Отчет о классификации и матрица ошибок

classifier = GradientBoostingClassifier(learning_rate=grid.best_params_["learning_rate"], max_depth=grid.best_params_["max_depth"], n_estimators=grid.best_params_["n_estimators"])
classifier.fit(X_train, y_train)
y_pred = classifier.predict(X_test)
  • Отчет о классификации
print(classification_report(y_test, y_pred))

Из этой матрицы классификации мы видим, что точность нашей модели составляет 82%, что неплохо. Наша модель также имеет точность, полноту и показатель f1 в диапазоне 80%-82%.

  • Матрица путаницы
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm,annot=True, cmap='viridis')
plt.xlabel("Predicted label")
plt.ylabel("True label")
plt.title("Confusion Matrix")
plt.show()

Глядя на приведенную выше матрицу путаницы, мы видим, что наша модель имеет True Negative = 97, False Negative = 19, True Positive = 50 и False Positive = 13.

Предсказание выжившего на Титанике

После того, как мы уже создали нашу модель, мы будем использовать ее для прогнозирования выжившего с использованием данных test_df. Нам нужно выполнить настройку гиперпараметра так же, как мы делали это раньше, просто заменив X_train и y_train на X и y.

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

param_grid = {
    "n_estimators":[5,50,250,500],
    "max_depth":[1,3,5,7,9],
    "learning_rate":[0.01,0.1,1,10,100]
}
grid = GridSearchCV(GradientBoostingClassifier(), param_grid, refit = True, verbose = 3, cv = 5)

grid.fit(X, y) 
print(grid.best_params_) 
{'learning_rate': 0.1, 'max_depth': 1, 'n_estimators': 250}

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

Предсказать выжившего

prediction = classifier.predict(test_df)
submission = pd.read_csv("test.csv")
submission = new_test[["PassengerId"]]
submission["Survived"] = prediction
submission

submission["Survived"].value_counts()

Как видно из изображения выше, с Титаника не выжили 257 человек и 161 человек.

Сводка

Мы можем предсказать выжившего, используя алгоритм машинного обучения. В этом случае мы использовали алгоритм Gradient Boosting и получили показатель точности 82%.

Ссылки:

[1] А. Тикканен, «Титаник», Британская энциклопедия. 17 августа 2022 г.

[2] Титаник — машинное обучение на основе катастроф, Kaggle.com. [В сети]. Доступно: https://kaggle.com/competitions/titanic. [Проверено: 16 января 2023 г.].

Гитхаб:

https://github.com/adhityaprimandhika/Titanic-Dataset-Analysis-and-Prediction