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

Тем не менее, благодаря обширным исследованиям математики и статистики, лежащим в основе кода, а также анализу процесса научного мышления, лежащего в основе машинного обучения, от Эдуарда Харриса из SharpestMinds, я думаю, что у меня есть что предложить другим новичкам. как я.

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

Мы начнем с довольно стандартного импорта. Основными библиотеками, которые я использовал в целях этого эксперимента, были Pandas и Numpy для обработки данных, Pandas и Seaborn. для визуализации и scikit-learn для машинного обучения.

# regular expressions
import re
# math and data utilities
import numpy as np
import pandas as pd
import scipy.stats as ss
import itertools
# data and statistics libraries
import sklearn.preprocessing as pre
from sklearn import model_selection
from sklearn import metrics
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
# visualization libraries
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
# Set-up default visualization parameters
mpl.rcParams[‘figure.figsize’] = [10,6]
viz_dict = {
 ‘axes.titlesize’:18,
 ‘axes.labelsize’:16,
}
sns.set_context(“notebook”, rc=viz_dict)
sns.set_style(“whitegrid”)

Начальная настройка

Мы можем загрузить данные из Kaggle в нашу папку данных с помощью командной строки:

kaggle competitions download -c titanic

unzip titanic.zip

После этого давайте поместим данные в несколько фреймов данных Pandas:

train_df = pd.read_csv('data/train.csv', index_col='PassengerId')
test_df = pd.read_csv('data/test.csv', index_col='PassengerId')

Исследовательский анализ данных:

Следующим нашим шагом будет задать следующие вопросы и ответить на них:

  1. Нам не хватает данных?
  2. В какой форме принимают наши данные?
  3. Какую дополнительную информацию мы можем извлечь из того, что у нас уже есть?
  4. Какие отношения мы можем найти между нашими переменными, особенно между входными и выходными переменными?
  5. Как мы можем использовать ответы на первые два вопроса, чтобы повысить ценность наших данных и моделей, которые будут их использовать?

Вопрос 1: что нам не хватает?

Давайте посмотрим на количество записей в наших обучающих данных, а также на то, что эти переменные содержат существенные недостающие данные. Ниже мы видим, что обучающие данные содержат 891 выборку пассажиров с 11 переменными, описывающими каждого пассажира. Ниже, используя using.info(), мы видим, что значительный объем данных для переменных Возраст, Посадка и Кабина отсутствует. Нам придется иметь дело с этими недостающими данными, либо найдя разумный способ заполнить пробелы, либо, возможно, полностью отбросив функции.

# Look for missing values
train_df.info()
# Output:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 891 entries, 1 to 891
Data columns (total 11 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   Survived  891 non-null    int64  
 1   Pclass    891 non-null    int64  
 2   Name      891 non-null    object 
 3   Sex       891 non-null    object 
 4   Age       714 non-null    float64
 5   SibSp     891 non-null    int64  
 6   Parch     891 non-null    int64  
 7   Ticket    891 non-null    object 
 8   Fare      891 non-null    float64
 9   Cabin     204 non-null    object 
 10  Embarked  889 non-null    object 
dtypes: float64(2), int64(4), object(5)
memory usage: 83.5+ KB

Вопрос 2: Какова форма наших данных?

Взглянув на нашу .info() распечатку, а также на несколько первых записей нашего фрейма данных ниже, мы видим, что наши данные поступают в основном в форме категориальных данных, за исключением возраста и Стоимость. Эти категории описываются строками Python, поэтому тип данных выше указан как «объект». Вот как Pandas работает с неопознанными типами данных. Позже мы скажем Pandas, что эти переменные являются строками.

# Look at the first few entries. 
train_df.head()

Вопрос 3: Какую дополнительную информацию мы можем извлечь из того, что у нас уже есть?

Название пассажира

Быстрый взгляд на переменную Имя показывает, что у каждого имени есть заголовок. Заголовок полезен для того, чтобы сообщить нам такие вещи, как социальный статус, карьера в семейном статусе и даже ранг в конкретной карьере. Поэтому может быть полезно иметь эту информацию под рукой. Давайте разберем заголовки из переменной Name и добавим ее как отдельную функцию:

# Question about what's going on here? Google "Regular Expressions"
train_df['Title'] = train_df['Name'].str.extract(r'([A-Za-z]+)\.')
train_df.Title.value_counts()

Выход:

Mr          517
Miss        182
Mrs         125
Master       40
Dr            7
Rev           6
Col           2
Mlle          2
Major         2
Don           1
Capt          1
Sir           1
Jonkheer      1
Countess      1
Mme           1
Ms            1
Lady          1
Name: Title, dtype: int64

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

# This dict will map redundant titles to their equivilent.
title_dict = {
    'Mrs': 'Mrs', 'Lady': 'Lady', 'Countess':'Lady',
    'Jonkheer':'Lord', 'Col': 'Officer', 'Rev': 'Rev',
    'Miss': 'Miss', 'Mlle': 'Miss', 'Mme': 'Mrs', 'Ms': 'Miss',
    'Dona': 'Lady', 'Mr': 'Mr', 'Dr': 'Dr', 'Major': 'Officer',
    'Capt': 'Officer', 'Sir': 'Lord', 'Don': 'Lord', 
    'Master': 'Master'
}
# Create new feature/variable in DataFrame.
train_df.Title = train_df.Title.map(title_dict)

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

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

# Use of Python lambda expressions is very useful for parsing data:
train_df['Alone'] = train_df.FamilySize.apply(lambda x: 1 if x==1 else 0)
# Display distribution of 'Alone' vs 'Not Alone'
plt.figure(figsize=(8,5))
sns.countplot(train_df.Alone)

Фамилия

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

# Once again, regular expressions come in handy.
train_df['LName'] = train_df.Name.str.extract(r'([A-Za-z]+),')

Имя Длина

У этого есть очень простое объяснение: просматривая ноутбуки на Kaggle, я увидел, что один конкурент обнаружил, что длина имени человека увеличивает производительность модели. Так почему бы не попробовать?

# Checkout DataFrame.apply() and Series.apply() in the docs.
train_df['NameLength'] = train_df.Name.apply(len)

Вопрос 4: Какие статистические взаимосвязи содержат наши данные?

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

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

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

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

Далее нам необходимо рассмотреть изучаемые нами типы категорий:

  • Порядковые переменные подразумевают базовый ранг или порядок. Примером могут служить классификации легкой, средней и тяжелой степени. Распространенный метод вычисления корреляции между порядковыми переменными называется Тау Кендалла ( 𝜏 ).
  • Номинальные переменные не имеют такого ранга или порядка. Примеры могут быть мужчиной или женщиной, кошкой или собакой. В этом случае мы будем использовать Cramer’s V для определения ассоциации.
# nominal variables (use Cramer's V)
nom_vars = ['Survived', 'Title', 'Embarked', 'Sex', 'Alone', 'LName']
# ordinal variables (nominal-ordinal, use Rank Biserial or Kendall's Tau)
ord_vars = ['Survived', 'Pclass', 'FamilySize', 'Parch', 'SibSp', 'NameLength']
# continuous variables (use Pearson's r)
cont_vars = ['Survived', 'Fare', 'Age']

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

Чтобы выполнить вычисления, мы должны преобразовать любые нечисловые данные в числа. Вы не можете вычислить слова, поэтому приступим:

# convert all string 'object' types to numeric categories
for i in train_df.columns:
    if train_df[i].dtype == 'object':
        train_df[i], _ = pd.factorize(train_df[i])

Далее нам нужен алгоритм для вычисления и отображения результатов измерений V-ассоциации Крамера:

# A method that creates a correlation matrix in the form of a Pandas DataFrame using Cramer's V.
def cramers_v_matrix(dataframe, variables):
    
    df = pd.DataFrame(index=dataframe[variables].columns,
                      columns=dataframe[variables].columns,
                      dtype="float64")
    
    for v1, v2 in itertools.combinations(variables, 2):
        
        # generate contingency table:
        table = pd.crosstab(dataframe[v1], dataframe[v2])
        n     = len(dataframe.index)
        r, k  = table.shape
        
        # calculate chi squared and phi
        chi2  = ss.chi2_contingency(table)[0]
        phi2  = chi2/n
        
        # bias corrections:
        r = r - ((r - 1)**2)/(n - 1)
        k = k - ((k - 1)**2)/(n - 1)
        phi2 = max(0, phi2 - (k - 1)*(r - 1)/(n - 1))
        
        # fill correlation matrix
        df.loc[v1, v2] = np.sqrt(phi2/min(k - 1, r - 1))
        df.loc[v2, v1] = np.sqrt(phi2/min(k - 1, r - 1))
        np.fill_diagonal(df.values, np.ones(len(df)))
        
    return df

Теперь, заставляя нашу новую функцию работать, давайте посмотрим, как складываются наши данные:

fig, axes = plt.subplots(1, 3, figsize=(20,6))
# nominal variable correlation
ax1 = sns.heatmap(cramers_v_matrix(train_df, nom_vars), annot=True, ax=axes[0], vmin=0)
# ordinal variable correlation: 
ax2 = sns.heatmap(train_df[ord_vars].corr(method='kendall'), annot=True, ax=axes[1], vmin=-1)
# Pearson's correlation:
ax3 = sns.heatmap(train_df[cont_vars].corr(), annot=True, ax=axes[2], vmin=-1)
ax1.set_title("Cramer's V Correlation")
ax2.set_title("Kendall's Tau Correlation")
ax3.set_title("Pearson's R Correlation")

Приведенные выше тепловые карты показывают нашу силу связи между каждой переменной. Несмотря на то, что нет жесткого стандарта для «сильно ассоциированных» или «слабо связанных», мы будем использовать пороговое значение | 0,1 | между нашими независимыми переменными и выживанием. Скорее всего, мы удалим объекты, ассоциация которых ниже | 0,1 |. Это совершенно произвольное предположение, и я могу вернуться, чтобы поднять или опустить планку позже (на самом деле, я решил оставить Age после того, как заметил улучшение производительности, когда я это сделал).

На данный момент критериям для исключения соответствует функция SibSp. Кроме того, я предпочитаю опустить Имя, Билет и Каюта, в основном потому, что они не добавляют особого смысла.

Следует отметить, что корреляция между независимыми (предикторами) переменных может означать избыточную информацию. Это может вызвать падение производительности некоторых алгоритмов. Некоторые могут отказаться от сильно коррелированных предикторов. У меня не было времени на это, но вы можете попробовать это дома!

todrop = ['SibSp', 'Ticket', 'Cabin', 'Name']
train_df = train_df.drop(todrop, axis=1)

Давайте посмотрим на наш преобразованный фрейм данных, изобилующий новыми функциями, категориями, преобразованными в числовые данные, и удаленными старыми функциями:

Настройка для машинного обучения:

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

  1. Разделение на тренировку / тест
  2. Нормализовать данные каждой группы
  3. Вменять отсутствующие значения

Пойдем.

Тренировка / тестовый сплит

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

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

# Split dependant and independant variables
X = train_df.drop(['Survived'], axis = 1)
Y = train_df.loc[:, 'Survived']
# Split data into training and validation sets
x_train, x_test, y_train, y_test = model_selection.train_test_split(X, Y, test_size=0.2, random_state=333)

Нормализация данных

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

# We normalize the training and testing data separately so as to avoid data leaks. Ask at the end!
x_train = pd.DataFrame(pre.scale(x_train), 
                       columns=x_train.columns, 
                       index=x_train.index)
x_test = pd.DataFrame(pre.scale(x_test), 
                      columns=x_test.columns, 
                      index=x_test.index)

Вменение отсутствующих данных

Как вы помните, в наших данных отсутствовало значительное количество значений возраста. Давайте заполним это средним возрастом:

# Again, applying changes to the now separate datasets helps us avoid data leaks.
x_train.loc[x_train.Age.isnull(), 'Age'] = x_train.loc[:, 'Age']
.median()
x_test.loc[x_test.Age.isnull(), 'Age'] = x_test.loc[:, 'Age']
.median()

Давайте удостоверимся, что наши недостающие данные заполнены:

x_train.info()
# Output: 
<class 'pandas.core.frame.DataFrame'>
Int64Index: 712 entries, 466 to 781
Data columns (total 11 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   Pclass      712 non-null    float64
 1   Sex         712 non-null    float64
 2   Age         712 non-null    float64
 3   Parch       712 non-null    float64
 4   Fare        712 non-null    float64
 5   Embarked    712 non-null    float64
 6   Title       712 non-null    float64
 7   FamilySize  712 non-null    float64
 8   Alone       712 non-null    float64
 9   LName       712 non-null    float64
 10  NameLength  712 non-null    float64
dtypes: float64(11)

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

Выбор модели

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

  • K-Ближайшие соседи
  • Машины опорных векторов
  • Деревья решений
  • Логистическая регрессия

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

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

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

# A function that evaluates each model and gives us the results:
def kfold_evaluate(model, folds=5):
    eval_dict = {}
    accuracy = 0
    f1       = 0
    AUC      = 0
    
    skf = model_selection.StratifiedKFold(n_splits=folds)
    
    # perform k splits on the training data. 
    for train_idx, test_idx in skf.split(x_train, y_train):
        xk_train, xk_test = 
        x_train.iloc[train_idx], x_train.iloc[test_idx]
        yk_train, yk_test = 
        y_train.iloc[train_idx], y_train.iloc[test_idx]
        
        # Test performance on this fold:        
        model.fit(xk_train, yk_train)
        y_pred = model.predict(xk_test)
        report = metrics.classification_report(yk_test,
                                               y_pred,
                                               output_dict=True)
        # Gather performance metrics for output
        prob_array = model.predict_proba(xk_test)
    
        fpr, tpr, huh = metrics.roc_curve(yk_test,
                        model.predict_proba(xk_test)[:,1])
        auc = metrics.auc(fpr, tpr)
        accuracy   += report['accuracy']
        f1         += report['macro avg']['f1-score']
        AUC        += auc
        
    # Average performance metrics over the k folds
    measures = np.array([accuracy, f1, AUC])
    measures = measures/folds
    # Add metric averages to dictionary and return.
    eval_dict['Accuracy']  = measures[0]
    eval_dict['F1 Score']  = measures[1]
    eval_dict['AUC']       = measures[2]  
    eval_dict['Model']     = model
    
    return eval_dict
# a function to pretty print our dictionary of dictionaries:
def pprint(web, level):
    for k,v in web.items():
        if isinstance(v, dict):
            print('\t'*level, f'{k}: ')
            level += 1
            pprint(v, level)
            level -= 1
        else:
            print('\t'*level, k, ": ", v)

Используем нашу оценочную функцию kfold:

# Perform evaluation on each model:
evals = {}
evals['KNN'] = kfold_evaluate(KNeighborsClassifier())
evals['Logistic Regression'] = kfold_evaluate(LogisticRegression(max_iter=1000))
evals['Random Forest'] = kfold_evaluate(RandomForestClassifier())
evals['SVC'] = kfold_evaluate(SVC(probability=True))
# Plot results for visual comparison:
result_df = pd.DataFrame(evals)
result_df
     .drop('Model', axis=0)
     .plot(kind='bar', ylim=(0.7, 0.9))
     .set_title("Base Model Performance")
plt.xticks(rotation=0)
plt.show()

Резюме базовой модели

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

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

Давайте настроим гиперпараметры нашего действующего чемпиона в надежде добиться чуть большей производительности. Мы будем использовать RandomizedSearchCV из scikit-learn, который имеет некоторые преимущества в скорости по сравнению с исчерпывающим GridSearchCV. Наш первый шаг - создать нашу сетку параметров, по которой мы будем случайным образом искать лучшие настройки:

# Number of trees in random forest
n_estimators = [int(x) for x in np.linspace(start = 200, stop = 2000, num = 10)]
# Number of features to consider at every split
max_features = ['auto', 'sqrt']
# Maximum number of levels in tree
max_depth = [int(x) for x in np.linspace(10, 110, num = 11)]
max_depth.append(None)
# Minimum number of samples required to split a node
min_samples_split = [2, 5, 10]
# Minimum number of samples required at each leaf node
min_samples_leaf = [1, 2, 4]
# Method of selecting samples for training each tree
bootstrap = [True, False]
# Create the random grid from above parameters
random_grid = {'n_estimators': n_estimators, 
               'max_features': max_features,
               'max_depth': max_depth,
               'min_samples_split': min_samples_split,
               'min_samples_leaf': min_samples_leaf,
               'bootstrap': bootstrap}
pprint(random_grid, 0)
#Output:
 n_estimators :  [200, 400, 600, 800, 1000,
                  1200, 1400, 1600, 1800, 2000]
 max_features :  ['auto', 'sqrt']
 max_depth :  [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, None]
 min_samples_split :  [2, 5, 10]
 min_samples_leaf :  [1, 2, 4]
 bootstrap :  [True, False]

Затем мы хотим создать наш объект RandomizedSearchCV, который будет использовать сетку, которую мы только что создали. Он случайным образом выберет 10 комбинаций параметров, проверит их более чем в 3 раза и вернет набор параметров, которые показали наилучшие результаты на наших обучающих данных.

# create RandomizedSearchCV object
searcher = model_selection.RandomizedSearchCV(
         estimator = RandomForestClassifier(),
         param_distributions = random_grid,
         n_iter = 10, # Number of parameter settings to sample
         cv     = 3,  # Number of folds for k-fold validation 
         n_jobs = -1, # Use all processors to compute in parallel
         random_state=0 # Fore reproducible results
) 
# Look for the best parameters
search = searcher.fit(x_train, y_train)
params = search.best_params_
params
#Output:
{'n_estimators': 1600,
 'min_samples_split': 10,
 'min_samples_leaf': 4,
 'max_features': 'auto',
 'max_depth': 30,
 'bootstrap': False}

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

tuning_eval = {}
tuned_rf = RandomForestClassifier(**params)
basic_rf = RandomForestClassifier()
tuning_eval['Tuned'] = kfold_evaluate(tuned_rf)
tuning_eval['Basic'] = kfold_evaluate(basic_rf)
result_df = pd.DataFrame(tuning_eval)
result_df.drop('Model', axis=0).plot(kind='bar', ylim=(0.7, 0.9)).set_title("Tuning Performance")
plt.xticks(rotation=0)
plt.show()
result_df

Заключительные шаги:

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

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

Наконец, мы сделаем наши прогнозы на немаркированных данных для подачи на конкурс.

Заключительный тест на хранимых данных

# Get tuned model predictions on held out data
y_pred = tuned_rf.predict(x_test)
# Compare predictions to actual answers and show performance
results = metrics.classification_report(y_test, y_pred,
                                        labels = [0, 1],
                                        target_names = ['Died', 'Survived'],
                                        output_dict = True)
pprint(results, 0)

А вот как работала наша модель:

Died: 
	 precision :  0.7815126050420168
	 recall :  0.8532110091743119
	 f1-score :  0.8157894736842106
	 support :  109
 Survived: 
	 precision :  0.7333333333333333
	 recall :  0.6285714285714286
	 f1-score :  0.6769230769230768
	 support :  70
 accuracy :  0.7653631284916201
 macro avg: 
	 precision :  0.757422969187675
	 recall :  0.7408912188728702
	 f1-score :  0.7463562753036437
	 support :  179
 weighted avg: 
	 precision :  0.7626715490665541
	 recall :  0.7653631284916201
	 f1-score :  0.761484178861421
	 support :  179

Похоже, мы столкнулись с переоборудованием. Эффективность нашей модели на тестовых данных примерно на 7–9% ниже по всем направлениям, но мы должны ожидать, что наша модель работает примерно так же хорошо на реальных данных, которых она никогда раньше не видела.

Объедините наборы данных для обучения и тестирования для окончательной подгонки модели

Теперь, когда мы убедились, что наша настроенная модель работает с точностью около 76% и имеет показатель f1 0,74 на новых данных, мы можем приступить к обучению нашей модели на всем помеченном обучающем наборе. Больше (хороших) данных почти всегда лучше для алгоритма.

X = pd.concat([x_train, x_test], axis=0).sort_index()
Y = pd.concat([y_train, y_test], axis=0).sort_index()
tuned_rf.fit(X, Y)

Форматирование и стандартизация немаркированных данных

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

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

# Feature Engineering:
test_df['Title'] = test_df.Name.str.extract(r'([A-Za-z]+)\.')
test_df['LName'] = test_df.Name.str.extract(r'([A-Za-z]+),')
test_df['NameLength'] = test_df.Name.apply(len)
test_df['FamilySize'] = 1 + test_df.SibSp + test_df.Parch
test_df['Alone'] = test_df.FamilySize.apply(lambda x: 1 if x==1 else 0)
test_df.Title = test_df.Title.map(title_dict)
# Feature Selection
test_df = test_df.drop(todrop, axis=1)
# Imputation of missing age and fare data
test_df.loc[test_df.Age.isna(), 'Age'] = test_df.Age.median()
test_df.loc[test_df.Fare.isna(), 'Fare'] = test_df.Fare.median()
# encode categorical data
for i in test_df.columns:
    if test_df[i].dtype == 'object':
        test_df[i], _ = pd.factorize(test_df[i])
        
# center and scale data 
test_df = pd.DataFrame(pre.scale(test_df), columns=test_df.columns, index=test_df.index)
# ensure columns of unlabeled data are in same order as training data.
test_df = test_df[x_test.columns]
test_df

Сделайте окончательные прогнозы и проверьте здравый смысл:

Около 32 процентов пассажиров «Титаника» жили. Мы сделаем последнюю проверку здравого смысла, чтобы увидеть, предсказывает ли наш алгоритм примерно такое же распределение выживших. Поскольку переменная Survived со значением 1 подразумевает выживаемость, мы можем просто сложить все случаи выживания и разделить на общее количество пассажиров, чтобы получить приблизительное представление о нашем прогнозируемом распределении.

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

# Make final predictions
final = tuned_rf.predict(test_df)
# Check the probability of survival according to our predictions. It should be roughly 32% (we get 36.6% which is a bit optimistic)
final.sum()/len(final)
# Get our predictions in the competition rules format:
submission = pd.DataFrame({'PassengerId':test_df.index,
                           'Survived':final})
# Output our submission data to a .csv file:
submission.to_csv('submission2.csv', index=False)

Резюме

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

Однако, если вам интересно, насколько хорошо работает эта настройка, она достигла точности 77%. Это далеко не идеально!

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

Спасибо!