Всем привет! В этой небольшой статье хочу поделиться своим первым опытом работы с 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()

Разведочный анализ данных (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 — если нет. В нашем случаи это будет выглядеть так:

После этого удаляю обычную колонку "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, а также визуализировал матрицу ошибок для анализа распределения правильных и ошибочных предсказаний модели.

Построение 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}')
Все аналогично, но используем другую модель.

Анализ результатов
Сравнительный анализ показывает компромисс между двумя подходами к классификации:
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. Модель выбираем опираясь на свой взгляд после анализа всех вариантов решений.
Итог
Есть и другие варианты решение, где будут уделять больше внимания обработке данных:
Извлечение титула из имени (
Mr,Mrs,Miss,Master,Rare).Создание FamilySize (SibSp + Parch + 1).
Флаг IsAlone (FamilySize == 1).
Палуба (Deck) из первой буквы Cabin.
И тд.
Так же можно рассмотреть варианты с другими моделями: Catboost, lightgbm, xgboost и другие.
Для первой работы с сетом я решил опираться на свои мысли и не углубляться в чужие решения, чтобы увидеть результат своих действий.
Как-то так выглядит титаник в 2026 году. Буду рад услышать критику и мнение со стороны.
