Hands-On Practice
Practice Project: Predicting Titanic Survival with Logistic Regression
8 min read
A famous first classification problem
The previous lesson used linear regression to predict a number — a house price. Logistic regression, despite the name, is for a different job: predicting which of two categories something belongs to.
This project is one of the most widely used first classification exercises in machine learning: given facts about a Titanic passenger — their ticket class, age, sex, how many relatives they were traveling with — predict whether they survived or died. Real historical data, real messiness, and a genuine yes/no outcome to predict.
Datasets: titanic_train.csv (labeled — has the Survived column, used to train and test the model) and titanic_test.csv (unlabeled — a bonus set to practice generating predictions on data with no answer key at all).
Full notebook: 01-logistic-regression-with-python.ipynb — download it and run every cell yourself.
Step 1: Load the data and look for gaps
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
train = pd.read_csv('titanic_train.csv')
train.head()
Real-world data is rarely as clean as the housing dataset from the last lesson. A fast way to spot missing values across every column at once is a heatmap of where the data is null:
sns.heatmap(train.isnull(), yticklabels=False, cbar=False, cmap='viridis')
Every colored streak in that heatmap marks a gap. Roughly 20% of Age values are missing — annoying, but fixable. The Cabin column is missing so much data that trying to impute it convincingly isn't realistic — it may be better to drop it, or reduce it to something coarser like "was cabin info known at all: yes or no."
Check your understanding
Age is missing for ~20% of passengers, and Cabin is missing for the large majority of passengers. Why treat those two columns differently instead of applying one rule to both?
Step 2: Explore before cleaning
Before fixing anything, it's worth looking at how survival relates to other columns — this is the same exploratory instinct as the pairplot and heatmap from the linear regression project.
sns.countplot(x='Survived', hue='Sex', data=train)
sns.countplot(x='Survived', hue='Pclass', data=train)
plt.figure(figsize=(12, 7))
sns.boxplot(x='Pclass', y='Age', data=train)
That last boxplot — age distribution broken out by passenger class — turns out to matter a lot for the next step: it shows that wealthier, higher-class passengers tended to be older. That's not just a curiosity. It becomes the basis for a smarter way to fill in the missing Age values.
Step 3: Impute missing ages using what the data already showed
Rather than filling every missing age with one overall average, the age boxplot suggests a better estimate — the average age within that passenger's own class:
def impute_age(cols):
Age = cols[0]
Pclass = cols[1]
if pd.isnull(Age):
if Pclass == 1:
return 37
elif Pclass == 2:
return 29
else:
return 24
else:
return Age
train['Age'] = train[['Age', 'Pclass']].apply(impute_age, axis=1)
This function checks each row: if Age is missing, it fills in the average age observed for that passenger's class (37 for first class, 29 for second, 24 for third) instead of just leaving a blank or using one global average. This is what "imputation" means in practice — not guessing randomly, but using a related, already-known column to make an informed estimate.
With Age patched and Cabin dropped, the remaining rows with any leftover missing values (a couple of blanks in Embarked) get dropped outright — small enough not to matter.
train.drop('Cabin', axis=1, inplace=True)
train.dropna(inplace=True)
Step 4: Convert categories into numbers
Just like Address had to be dropped from the housing dataset because it was text, columns like Sex and Embarked ("S", "C", "Q") need to become numeric before a model can use them. Pandas' get_dummies turns each category into its own 0/1 column:
sex = pd.get_dummies(train['Sex'], drop_first=True)
embark = pd.get_dummies(train['Embarked'], drop_first=True)
train.drop(['Sex', 'Embarked', 'Name', 'Ticket'], axis=1, inplace=True)
train = pd.concat([train, sex, embark], axis=1)
drop_first=True drops one of the resulting columns to avoid redundancy — if a passenger isn't female, isn't from Q, and isn't from S, the model can already infer they were male and embarked from C without needing a column that says so explicitly. Name and Ticket are dropped entirely; they're free text that isn't converted into a usable feature here.
Check your understanding
Why can't the raw Sex column ('male' / 'female' text) be fed straight into a logistic regression model?
Step 5: Split, train, and predict
With the data fully numeric and gap-free, this step should look familiar — it's the same train/test split and fit pattern from the linear regression project, just with a classification model instead of a regression one:
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
X_train, X_test, y_train, y_test = train_test_split(
train.drop('Survived', axis=1), train['Survived'], test_size=0.30, random_state=101
)
logmodel = LogisticRegression()
logmodel.fit(X_train, y_train)
predictions = logmodel.predict(X_test)
The only real difference from the housing project: LinearRegression() became LogisticRegression(), and instead of predicting a continuous price, predictions now holds a 0 or 1 for every passenger — died or survived.
Step 6: Evaluate with a classification report
MAE, MSE, and RMSE measured how far off a numeric prediction was. That doesn't make sense for a yes/no prediction — either the model got it right or it didn't. Classification uses different metrics:
from sklearn.metrics import classification_report
print(classification_report(y_test, predictions))
This prints precision, recall, and f1-score for each class (survived / died). At a high level: precision asks "of everyone the model predicted survived, how many actually did?"; recall asks "of everyone who actually survived, how many did the model catch?" A model can be excellent on one and mediocre on the other, which is exactly why classification problems are graded with more than one number.
Check your understanding
Why does this project use a classification report (precision, recall, f1-score) instead of RMSE like the housing project did?
Key takeaway
Logistic regression follows the same overall workflow as linear regression — load, explore, clean, split, fit, evaluate — but two things change because the goal is a category instead of a number: text and messy real-world data need more deliberate cleaning (imputing Age, dropping Cabin, converting Sex and Embarked into dummy columns), and evaluation shifts from distance-based metrics like RMSE to classification metrics like precision and recall.
What's next?
Between the two practice projects, you've now trained, evaluated, and interpreted both a regression model and a classification model — the two most common supervised learning tasks — using the exact same core Scikit-learn workflow. From here, the natural next step is exploring unsupervised and other learning types hands-on, or revisiting the titanic_test.csv file to practice generating predictions on completely unlabeled data.