Титаник глазами новичка в 2026. ai.. ai. data science.. ai. data science. kaggle.. ai. data science. kaggle. kaggle competition.. ai. data science. kaggle. kaggle competition. ml.. ai. data science. kaggle. kaggle competition. ml. titanic.. ai. data science. kaggle. kaggle competition. ml. titanic. машинное+обучение.

Всем привет! В этой небольшой статье хочу поделиться своим первым опытом работы с ML-моделями.

С чего все началось?

В начале 3 семестра я попал на проект ВУЗа, связанный с НС. Прошел курс по сеткам, пробежался по Pytorch и приступил к задачам на проекте. В процессе своего спринта решил параллельно изучать классический ML, где собственно выяснил, что “Hello world!” в мире машинного обучения является работа с датасетом титаник (предсказать выжил ли пассажир или нет). После этого ознакомился с Kaggle и полетел!

Titanic – Machine Learning from Disaster

При открытии “компетитив” сразу же наткнулся на тот самый кораблик и приступил к работе. Код писал в Jupyter-ноутбук.

Импортируем библиотеки

import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix, roc_auc_score, ConfusionMatrixDisplay

Читаем наши данные

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

train.head()
.head()

.head()

Разведочный анализ данных (EDA)

features = ['PassengerId', 'Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Cabin', 'Embarked']
target = 'Survived'

train_set = train[features + [target]].copy()
test_set = test[features].copy() 

plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)  
survival_by_sex = train_set.groupby('Sex')['Survived'].mean() * 100
plt.bar(['Male', 'Female'], survival_by_sex.values, color=['blue', 'red'])
plt.title('Survival by Sex')
plt.ylabel('Survival %')

plt.subplot(1, 2, 2) 
survival_by_pclass = train_set.groupby('Pclass')['Survived'].mean() * 100
plt.bar(['1st', '2nd', '3rd'], survival_by_pclass.values, color=['gold', 'silver', 'brown'])
plt.title('Survival by Class')
plt.ylabel('Survival %')

plt.tight_layout()
plt.show()

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

Анализ выживших по полу и классу

Анализ выживших по полу и классу

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

Обрабатываем пропуски в данных

train_set['Age'] = train_set['Age'].fillna(train_set['Age'].mean())
train_set['Sex'] = train_set['Sex'].map({'male': 0, 'female': 1})
# заполняем пропуски модой
train_set['Embarked'] = train_set['Embarked'].fillna(train_set['Embarked'].mode()[0])
# объединяю имеющийся датасет с one-hot-encoding, разделил embarked на embarked_c, embarkded_q и _s
train_set = pd.concat([train_set, pd.get_dummies(train_set['Embarked'], prefix='Embarked', dtype=int)], axis=1)
# удаляю
train_set = train_set.drop(['Embarked'], axis=1)
# создаю столбик, который указывает на наличие кабины у людей, проверяю с помощью notnal(не является ли nan?), который возвращается true/false (1/0 с помощью astype(int))
train_set['HasCabin'] = train_set['Cabin'].notna().astype(int)
# удаляю cabin
train_set = train_set.drop(['Cabin'], axis=1)

У некоторых пассажиров был пропущен возраст, поэтому заполняет пропуски средним значением по всем пассажирам. Колонка “Sex” содержала значения “male” и “female” – категориальные признаки, с которыми обычным модели не очень дружат. Переведем их в вещественные значения, а именно 0 и 1 (осуществил бинарное кодирование). Далее “Embarked” заполняем модой и производим one-hot-encoding. Суть метода: каждая уникальная категория становится отдельным бинарным признаком, принимающим значение 1, если объект принадлежит к этой категории, и 0 — если нет. В нашем случаи это будет выглядеть так:

Пример "OHE"
Пример “OHE”

После этого удаляю обычную колонку “Embarked”. Перейдем к колонке “Cabin”. В этом столбце достаточно много пропусков (около 70%). В начале думал просто удалить эту колонку, но при удалении мы теряем большое количество информации, которая может повлиять на результат нашей модели, поэтому принял решение извлечь какую-то пользу из этой колонки, а именно провел проверку на наличие информации о каюте. Преобразуем сложные категориальные признаки в бинарные (‘C85’, ‘C123’, ‘E46’ в 1 и 0). Для этого создал новую колонку “HasCabin”, а предыдущую удаляем.

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

test_set['Age'] = test_set['Age'].fillna(test_set['Age'].mean())
test_set['Sex'] = test_set['Sex'].map({'male': 0, 'female': 1})
test_set['Embarked'] = test_set['Embarked'].fillna(test_set['Embarked'].mode()[0])
test_set = pd.concat([test_set, pd.get_dummies(test_set['Embarked'], prefix='Embarked', dtype=int)], axis=1)
test_set = test_set.drop(['Embarked'], axis=1)
test_set['HasCabin'] = test_set['Cabin'].notna().astype(int)
test_set = test_set.drop(['Cabin'], axis=1)

Валидация

X_full = train_set.drop(['Survived', 'PassengerId'], axis=1)
y_full = train_set['Survived']

X_train, X_val, y_train, y_val = train_test_split(
    X_full, y_full, 
    test_size=0.2,  
    random_state=42,
    stratify=y_full  
)

Для валидации модели я подготовил данные, выделив отдельно признаки и целевую переменную. Затем разделил датасет на обучающую (80%) и валидационную (20%) выборки с сохранением исходного соотношения выживших и погибших. Это позволяет обучать модель на одной части данных, а проверять её качество — на другой, ранее не виденной модели.

Построение RandomForest

test_passenger_ids = test_set['PassengerId']
X_test_kaggle = test_set.drop(['PassengerId'], axis=1)

model = RandomForestClassifier(n_estimators=200, class_weight='balanced', random_state=42)
model.fit(X_train, y_train)

y_pred = model.predict(X_val)
y_pred_prob = model.predict_proba(X_val)[:, 1]

acc = accuracy_score(y_val, y_pred)
precision = precision_score(y_val, y_pred, zero_division=0)
recall = recall_score(y_val, y_pred, zero_division=0)
confm = confusion_matrix(y_val, y_pred)
roc_auc = roc_auc_score(y_val, y_pred_prob)

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

disp = ConfusionMatrixDisplay(confusion_matrix=confm, display_labels=['Погиб', 'Выжил'])
disp.plot(ax=axes[0], cmap='Blues')
axes[0].set_title('Матрица ошибок (валидация)')

print(f'Accuracy:  {acc:.3f}')
print(f'Precision: {precision:.3f}')
print(f'Recall:    {recall:.3f}')
print(f'AUC-ROC:   {roc_auc:.3f}')

Подготовил данные для отправки на Kaggle (об этом чуть ниже будет написано), сохранив идентификаторы пассажиров, и обучил RandomForestClassifier с балансировкой классов на тренировочных данных. Затем оценил модель на валидационной выборке, рассчитав ключевые метрики качества: точность, полноту, прецизионность и AUC-ROC, а также визуализировал матрицу ошибок для анализа распределения правильных и ошибочных предсказаний модели.

Результат RandomForest

Результат RandomForest

Построение LogisticRegression

X = test_set.drop(['Survived'], axis=1)
y = test_set['Survived']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LogisticRegression(class_weight='balanced', random_state=42, max_iter=1000)
model.fit(X_train, y_train)

y_pred = model.predict(X_test)
lg_y_pred_prob = model.predict_proba(X_test)[:, 1]

acc = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred, zero_division=0)
recall = recall_score(y_test, y_pred, zero_division=0)
conf_matr = confusion_matrix(y_test, y_pred)
log_roc_auc = roc_auc_score(y_test, lg_y_pred_prob)

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

disp1 = ConfusionMatrixDisplay(confusion_matrix=conf_matr, display_labels=['Погиб', 'Выжил'])
disp1.plot(ax=axes[0], cmap='Blues')
axes[0].set_title('Стандартная матрица ошибок')
disp1.plot
plt.show()

print(f'Accuracy: {acc:.3f}')
print(f'Precision: {precision:.3f}')
print(f'Recall: {recall:.3f}')
print(f'AUC: {log_roc_auc:.3f}')

Все аналогично, но используем другую модель.

Результат LogisticRegression

Результат LogisticRegression

Анализ результатов

Сравнительный анализ показывает компромисс между двумя подходами к классификации:

Random Forest демонстрирует более консервативную стратегию:

  • Общая точность выше на 1.7% (0.821 против 0.804)

  • Исключительно высокий Precision (0.828) означает, что 83% предсказанных выживших действительно выжили — модель очень осторожна в положительных предсказаниях

  • Однако низкий Recall (0.716) указывает на проблему: каждый четвертый реальный выживший был ошибочно классифицирован как погибший

  • Матрица ошибок подтверждает: всего 11 ложных отрицаний против 21 у логистики

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

  • Высокий Recall (0.811) показывает, что модель находит более 80% всех выживших

  • Лучший AUC-ROC (0.879 против 0.858) свидетельствует о более качественном вероятностном ранжировании

  • Модель совершает иной тип ошибок: меньше ложных отрицаний (14 против 21), но больше ложных срабатываний

Random Forest минимизирует ошибки первого рода (ложные надежды), а логистическая регрессия — ошибки второго рода (пропущенные жизни).

Оформление решения для kaggle

res = pd.DataFrame({
    'PassengerId': test_passenger_ids,  # сохранённые ранее ID
    'Survived': y_pred_kaggle,  # предсказания на реальных тестовых данных
})

# Сохранение для загрузки на Kaggle
res.to_csv('submission.csv', index=False)

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

Итог

Есть и другие варианты решение, где будут уделять больше внимания обработке данных:

  1. Извлечение титула из имени (MrMrsMissMasterRare).

  2. Создание FamilySize (SibSp + Parch + 1).

  3. Флаг IsAlone (FamilySize == 1).

  4. Палуба (Deck) из первой буквы Cabin.

  5. И тд.

Так же можно рассмотреть варианты с другими моделями: Catboost, lightgbm, xgboost и другие.

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

Как-то так выглядит титаник в 2026 году. Буду рад услышать критику и мнение со стороны.

Автор: L0ck21

Источник

Rambler's Top100