Titanic survival prediction

What can passenger records help us predict?

Imagine opening a historical passenger table. Each row records facts such as age, ticket fare and passenger class, along with whether that passenger survived the Titanic disaster. Our question is: can a model learn from some of these records to estimate the recorded survival outcome for other, held-out records?

This is binary classification: the answer is one of two labels, 0 for did not survive and 1 for survived. A model learns associations between inputs and labels. It does not recover a complete account of the disaster or explain what caused an individual person’s outcome.

This project brings together training/test separation, missing-value preparation, category encoding and random forests. Review those lessons if these ideas are unfamiliar. We will use the original forest settings as a fixed teaching experiment, not choose settings by repeatedly checking the test set.

What you’ll learn

  • Separate passenger information from the survival label and recognize a column that reveals the answer.
  • Fit numeric and categorical preparation on training rows, then trace one input through the fitted pipeline.
  • Compare the forest with a constant baseline and explain its correct decisions, false positives and missed survivors.
  • Describe what this historical experiment can show, and what additional evaluation a different research question would require.

179 held-out records: where the forest is right and wrong

Rows: actual label · Columns: predicted label
Actual ↓Did not survive (0)Survived (1)
Did not survive (0)98True negativesCorrectly unflagged12False positivesWrongly flagged
Survived (1)25False negativesMissed positives44True positivesCorrectly flagged
Actual labels form the rows; predicted labels form the columns. These are checked results from the fixed dataset, split and pipeline below. Positive means survived, not a value judgment.

Read the columns before training anything

We use the Seaborn example dataset, which contains 891 passenger records in this checked version. This teaching table is a sample of historical records, not a list of everyone aboard. The selected columns are age in years, fare as a recorded ticket fare, pclass for passenger class (1, 2 or 3), sex as recorded in the dataset, and embarked for embarkation port: C = Cherbourg, Q = Queenstown and S = Southampton. The target is survived.

Some passengers shared tickets, families or travel groups, so a row is not necessarily independent of every other row. Fare is also not a universal measure of an individual person’s wealth. We should explain the meaning and limits of a column before interpreting what a model does with it.

Download the actual CSV used here and keep your local copy with the project. The repository can change: a different dataset version or library version may produce different results. This lesson reports the displayed dataset and fixed settings.

Inspect three rows and count the missing values

Prepare and run

Save the Seaborn Titanic CSV as titanic.csv in your script or notebook’s working directory. Use Python with pandas and scikit-learn in the course environment. Each program below runs independently.

Python · complete runnable example

import pandas as pd

data = pd.read_csv("titanic.csv")
features = ["age", "fare", "pclass", "sex", "embarked"]
print("Rows:", len(data))
print(data[features + ["survived"]].head(3).to_string(index=False))
print("Missing feature values:")
for column in features:
    print(column + ":", int(data[column].isna().sum()))
print("Missing targets:", int(data["survived"].isna().sum()))
print("Survived:", int((data["survived"] == 1).sum()))
print("Did not survive:", int((data["survived"] == 0).sum()))

Expected output

Rows: 891
 age    fare  pclass    sex embarked  survived
22.0  7.2500       3   male        S         0
38.0 71.2833       1 female        C         1
26.0  7.9250       3 female        S         1
Missing feature values:
age: 177
fare: 0
pclass: 0
sex: 0
embarked: 2
Missing targets: 0
Survived: 342
Did not survive: 549

head(3) selects the first three records, while to_string(index=False) prints them without pandas’ row index. Each displayed survival label belongs to the passenger on that same row. isna() marks missing cells; summing those marks counts them.

Age is missing in 177 records, and embarkation port in two. Fare, passenger class and sex have no missing values in this copy. No survival label is missing. A missing age does not mean the passenger was zero years old. If the target were missing, we could not use that row as a labeled training example without obtaining its true outcome.

Keep the answer out of the inputs

The CSV also includes alive, containing “yes” or “no”. It restates survived. Including it as an input would let the model read the answer we are supposed to predict. This is target leakage: information from the outcome reaches the prediction procedure.

Ask of each proposed input: would this information be available at the point described by our prediction question? Age and ticket information are pre-outcome attributes in this historical exercise. An outcome label is not. A high score with alive would measure access to the answer, not useful prediction from passenger information.

We choose just five input columns explicitly rather than accepting every other column automatically. Fields such as class repeat passenger class, and embark_town repeats embarkation port in words. Repeated information is not automatically target leakage, but adding it without understanding its origin makes the experiment harder to audit.

Reserve test passengers before learning replacements

Split the selected inputs and matching labels together. With 20% reserved for testing, this run has 712 training rows and 179 test rows. stratify=y keeps the survival-label proportions approximately similar in both sets: training has 439 non-survivors and 273 survivors; testing has 110 non-survivors and 69 survivors.

random_state=7 makes the shuffled split repeatable for the same inputs. Stratification balances label proportions; it does not guarantee balanced passenger groups or remove family overlap. Row indexes stay attached to their records even though their order changes.

Only training rows determine missing-value replacements, category columns and forest rules. The test rows go through those already-fitted transformations. They must not help fit the pipeline. See cross-validation for how preparation must be refitted inside each development fold.

Give numeric and category columns different preparation

A ColumnTransformer sends selected columns to different preparation procedures and joins their resulting columns. Our numeric route replaces missing age, fare or passenger-class values with the respective training median. Passenger class is a numeric, ordered input here; a tree can split at thresholds between classes. Encoding it as separate categories is also a possible design choice to compare on development data.

The category route replaces missing values with each column’s most common training value, then uses one-hot encoding. Each known category gets a 0/1 indicator column. Thus sex produces two columns and embarkation port produces three. Together with the three numeric columns, the forest receives eight numbers per row.

A Pipeline connects the transformer to the forest. Calling fit(train_X, train_y) fits the preparation, transforms the training rows, and trains the forest on those numbers. Calling predict(test_X) reuses the preparation and passes transformed test rows to the fitted forest. The ColumnTransformer documentation explains this mixed-column arrangement.

Train the complete pipeline and make held-out predictions

Prepare and run

Save the Seaborn Titanic CSV as titanic.csv in your script or notebook’s working directory. Use Python with pandas and scikit-learn in the course environment. Each program below runs independently.

Python · complete runnable example

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

# Download the Seaborn example CSV as titanic.csv before running.
data = pd.read_csv("titanic.csv").dropna(subset=["survived"])
X = data[["age", "fare", "pclass", "sex", "embarked"]]
y = data["survived"].astype(int)
train_X, test_X, train_y, test_y = train_test_split(X, y, stratify=y, test_size=0.2, random_state=7)
numeric = make_pipeline(SimpleImputer(strategy="median"))
categorical = make_pipeline(SimpleImputer(strategy="most_frequent"), OneHotEncoder(handle_unknown="ignore"))
prepare = ColumnTransformer([("numbers", numeric, ["age", "fare", "pclass"]), ("categories", categorical, ["sex", "embarked"])])
model = make_pipeline(prepare, RandomForestClassifier(n_estimators=80, max_depth=5, random_state=7))
model.fit(train_X, train_y)
print("Training rows:", len(train_X))
print("Test rows:", len(test_X))
print("Held-out accuracy:", round(accuracy_score(test_y, model.predict(test_X)), 3))

Expected output

Training rows: 712
Test rows: 179
Held-out accuracy: 0.793

X contains the five input columns; y contains the matching integer survival labels. dropna(subset=["survived"]) would exclude rows whose answer is unknown; none are removed in this checked file. make_pipeline connects steps in their written order.

n_estimators=80 creates 80 trees, and max_depth=5 limits each tree’s depth. These are supplied settings, not the learned split rules. The forest averages its trees’ class probabilities and selects the class with the highest average. Scikit-learn’s forest combines class probabilities from its trees; it does not simply average the class labels as numerical survival scores.

The accuracy rounds to 0.793: 142 of 179 predictions match the recorded labels. That is a result for this experiment, not a promise about other passengers or datasets. The next examples reproduce this full setup before inspecting its preparation and errors.

Follow a missing age into eight model inputs

Consider an illustrative input with missing age, fare 8.4583, passenger class 3, sex “male” and embarkation code “Q”. These values illustrate preparation; we are not attributing a new prediction to a named passenger. The missing age becomes 28, the median learned from the training ages. A known age would remain unchanged.

One row, two preparation routes, eight numbers

Numeric route

Age: missing → 28
Fare: 8.4583 → 8.4583
Class: 3 → 3

Category route

Sex [female, male] → [0, 1]
Port [C, Q, S] → [0, 1, 0]

Joined forest input[28, 8.4583, 3 | 0, 1 | 0, 1, 0]
The separators show the three column groups; they are not values in the array. The survival label is kept outside this input.

The numeric medians are [28, 14.47915, 3] in age/fare/class order. The most common training categories are “male” and “S”. Our known “Q” is retained; the replacement “S” would apply only when embarkation is missing. Median imputation preserves a usable input shape, but cannot recover the passenger’s actual age.

Inspect the actual fitted replacements and feature order

Prepare and run

Save the Seaborn Titanic CSV as titanic.csv in your script or notebook’s working directory. Use Python with pandas and scikit-learn in the course environment. Each program below runs independently.

Python · complete runnable example

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

# Download the Seaborn example CSV as titanic.csv before running.
data = pd.read_csv("titanic.csv").dropna(subset=["survived"])
X = data[["age", "fare", "pclass", "sex", "embarked"]]
y = data["survived"].astype(int)
train_X, test_X, train_y, test_y = train_test_split(X, y, stratify=y, test_size=0.2, random_state=7)
numeric = make_pipeline(SimpleImputer(strategy="median"))
categorical = make_pipeline(SimpleImputer(strategy="most_frequent"), OneHotEncoder(handle_unknown="ignore"))
prepare = ColumnTransformer([("numbers", numeric, ["age", "fare", "pclass"]), ("categories", categorical, ["sex", "embarked"])])
model = make_pipeline(prepare, RandomForestClassifier(n_estimators=80, max_depth=5, random_state=7))
model.fit(train_X, train_y)

import numpy as np

# An illustrative input, not a claim about a named passenger.
passenger = pd.DataFrame([{
    "age": np.nan, "fare": 8.4583, "pclass": 3,
    "sex": "male", "embarked": "Q"
}])
preprocessor = model.steps[0][1]
print("Training numeric medians:",
      preprocessor.named_transformers_["numbers"].steps[0][1].statistics_.tolist())
print("Training category replacements:",
      preprocessor.named_transformers_["categories"].steps[0][1].statistics_.tolist())
for name, value in zip(preprocessor.get_feature_names_out(),
                       preprocessor.transform(passenger)[0]):
    print(name + ":", float(value))

Expected output

Training numeric medians: [28.0, 14.47915, 3.0]
Training category replacements: ['male', 'S']
numbers__age: 28.0
numbers__fare: 8.4583
numbers__pclass: 3.0
categories__sex_female: 0.0
categories__sex_male: 1.0
categories__embarked_C: 0.0
categories__embarked_Q: 1.0
categories__embarked_S: 0.0

This program repeats the full training setup so the transformation can be inspected independently. model.steps[0][1] retrieves the fitted first step, the ColumnTransformer. Its named_transformers_ entries expose the two fitted routes; each route’s first step is its imputer.

get_feature_names_out() supplies the exact output-column order, and transform(passenger) applies the fitted preparation without changing it. zip pairs each name with its value. Prefixes such as numbers__ identify the route that produced a column.

handle_unknown="ignore" means an unseen category produces zeros for that field’s known indicators. For example, an unrecognized port does not create a fourth port column. That avoids a shape error, but does not mean the model understands that port. Invalid input should be checked, and genuinely new categories need review. See the encoder’s unknown-category behavior.

Ask what accuracy a model can get without using the inputs

The training set contains more non-survivors than survivors. A majority-class baseline learns only which label is most common, then predicts that label for every row. It is deliberately simple: it does not use age, fare or any passenger information.

Predicting “did not survive” for all 179 test records gets 110 correct, giving accuracy 110 / 179 = 0.615 after rounding. The forest gets 142 / 179 = 0.793. This comparison shows that the forest beats that particular constant strategy on these held-out records. It does not establish that the forest is the best possible model or that every kind of mistake has improved.

We fit the baseline on training labels only. The test set evaluates the already-specified baseline and forest; do not now choose a new model or depth by trying settings until this test score improves. Use development-fold tuning for those choices and reserve untouched data for the final report.

Count errors instead of stopping at accuracy

Prepare and run

Save the Seaborn Titanic CSV as titanic.csv in your script or notebook’s working directory. Use Python with pandas and scikit-learn in the course environment. Each program below runs independently.

Python · complete runnable example

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

# Download the Seaborn example CSV as titanic.csv before running.
data = pd.read_csv("titanic.csv").dropna(subset=["survived"])
X = data[["age", "fare", "pclass", "sex", "embarked"]]
y = data["survived"].astype(int)
train_X, test_X, train_y, test_y = train_test_split(X, y, stratify=y, test_size=0.2, random_state=7)
numeric = make_pipeline(SimpleImputer(strategy="median"))
categorical = make_pipeline(SimpleImputer(strategy="most_frequent"), OneHotEncoder(handle_unknown="ignore"))
prepare = ColumnTransformer([("numbers", numeric, ["age", "fare", "pclass"]), ("categories", categorical, ["sex", "embarked"])])
model = make_pipeline(prepare, RandomForestClassifier(n_estimators=80, max_depth=5, random_state=7))
model.fit(train_X, train_y)

from sklearn.dummy import DummyClassifier
from sklearn.metrics import confusion_matrix, precision_score, recall_score

# Fit the constant baseline to the training labels only.
baseline = DummyClassifier(strategy="most_frequent").fit(train_X, train_y)
predicted = model.predict(test_X)
print("Majority baseline accuracy:",
      round(accuracy_score(test_y, baseline.predict(test_X)), 3))
print("Forest accuracy:", round(accuracy_score(test_y, predicted), 3))
print("Rows: actual 0/1; columns: predicted 0/1")
print(confusion_matrix(test_y, predicted, labels=[0, 1]))
print("Survival precision:", round(precision_score(test_y, predicted), 3))
print("Survival recall:", round(recall_score(test_y, predicted), 3))

Expected output

Majority baseline accuracy: 0.615
Forest accuracy: 0.793
Rows: actual 0/1; columns: predicted 0/1
[[98 12]
 [25 44]]
Survival precision: 0.786
Survival recall: 0.638

The matrix uses actual labels as rows and predictions as columns. Its first row says 98 non-survivors were classified correctly and 12 were classified as survivors. Its second row says 25 survivors were missed and 44 were classified correctly. The diagonal adds to 142 correct decisions; the off-diagonal adds to 37 errors.

Survival precision asks: among the 56 predictions of survival, how many actually survived? 44 / (44 + 12) = 0.786. Survival recall asks: among the 69 actual survivors, how many did we identify? 44 / (44 + 25) = 0.638. The positive class is label 1, so “false positive” means predicted survival for a recorded non-survivor.

The baseline’s survivor recall is zero because it never predicts survival. Its 61.5% accuracy conceals that limitation. The forest still misses 25 survivors, so its 79.3% accuracy should not be read as “79.3% of survivors identified”. Review metric denominators and confusion matrices when reporting this project.

Practice: interpret two predictions and spot leakage

Your task: A test record has actual label 1 but predicted label 0. Another has actual label 0 but predicted label 1. Name the two errors, and say which one reduces survivor recall. Then consider a classmate who fills missing ages with the median of all 891 records before splitting: what should change?

Worked answer

The first is a false negative: an actual survivor is classified as a non-survivor. It adds to the denominator of recall without adding to its numerator. The second is a false positive: a non-survivor is classified as a survivor. It increases the number of positive predictions without increasing the correctly predicted positives, reducing precision.

The classmate’s full-file median lets held-out input values influence preparation. It need not always increase the score, but it violates the intended separation. Split first and fit the imputer within the training pipeline. When comparing approaches through cross-validation, fit that complete pipeline separately inside each fold; do not reuse a preprocessor fitted on all development rows.

Explain what the result does and does not establish

This exercise estimates associations in one historical sample. It does not establish that changing a passenger attribute would change survival, and feature importance is not proof of causation. The recorded sex categories and passenger circumstances also do not justify present-day safety decisions or prescriptions about people.

The random split may place relatives or travel companions in both sets. If the question is performance for entirely new families or groups, define a defensible group identifier and evaluate with groups kept apart. This CSV does not provide a reliable family identifier simply by exposing sibsp or parch; those are counts of relatives aboard, not identities. Do not pretend grouping by those counts solves family overlap.

Further error analysis could examine missing-age records or passenger classes, reporting each subgroup’s size and uncertainty. Treat the displayed test result as final for this fixed experiment. If that analysis drives a revised design, evaluate the revision with a fresh, appropriate protocol rather than presenting the reused score as untouched evidence.

A useful report records the CSV source/version, selected inputs and excluded outcome fields, split seed and sizes, preparation rules, library versions, model settings, baseline and error counts. It should state that this is a teaching classifier on historical records.

Check which input gives away the answer

Which proposed input must be excluded because it restates the survival label?
Choose one answer. You can try again.

Answer and explanation

The alive column containing yes or no.

In this table alive directly restates survived, handing the model the answer.

Check a missing test age

A test passenger has no recorded age. The training median is 28; the median of the complete file is different. What does this fitted pipeline use?
Choose one answer. You can try again.

Answer and explanation

28, learned from training ages.

The trained imputer reuses its learned median without refitting on the test data.

Check the meaning of survivor recall

The forest identifies 44 of 69 actual survivors. Which statement describes its survivor recall?
Choose one answer. You can try again.

Answer and explanation

About 63.8% of recorded survivors are identified in this test set.

Recall divides true positives by all actual positives: 44 / 69.

Keep the experiment and its limits together

You have built a complete historical classification experiment: inspect records, choose five inputs, exclude the repeated outcome, reserve test passengers, fit two preparation routes and train a forest. The fitted transformer turns each passenger into eight numerical inputs without relearning replacements from the test set.

  • Preparation: learn numeric medians and category replacements on training rows, then reuse the same output columns.
  • Comparison: the majority baseline scores 0.615 accuracy; the fixed forest scores 0.793 on the 179 held-out records.
  • Errors: 98 true negatives, 12 false positives, 25 false negatives and 44 true positives. Survivor precision is 0.786 and recall is 0.638.
  • Interpretation: these are associations and predictions for this dataset and protocol, with historical sampling and group-overlap limits. They do not demonstrate causation or safety suitability.

Before reporting a project, make sure you can explain how one row was prepared, why the baseline matters and which denominator each metric uses. Keep the code, data copy and evaluation assumptions with the results.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: House price prediction →