Executive Summary
Executive summary
Among 1,307 Titanic passengers, 26.0% survived. The strongest independent predictor of survival is Sex: 1 with an odds ratio of 6.60. Logistic regression identified multiple demographic and socioeconomic factors that independently predict survival outcomes.
Analysis Overview
Analysis overview and framework setup
Statistical analysis of 1,307 Titanic passengers to identify demographic and socioeconomic factors predicting survival. Overall, 26.0% of passengers survived. Logistic regression quantifies the independent contribution of predictor terms derived from six variables—passenger class, sex, age, family size, fare, and embarkation port—producing 8 total model coefficients while controlling for correlated factors. The model achieves strong statistical significance.
Data Quality
Row accounting and preprocessing steps applied before analysis.
Loaded 1,309 passenger records. Removed 2 rows with missing age values; 1,307 rows (99.8%) used for analysis. Sex is encoded as an integer (0 = Male, 1 = Female). Passenger class is encoded as an integer (1 = First Class, 2 = Second Class, 3 = Third Class). Survival is a binary integer (0 = Did Not Survive, 1 = Survived).
Survival Rate by Passenger Group
Observed survival rates by sex and passenger class
Female — 2nd Class showed the highest survival rate at 66%, while Male — 3rd Class had the lowest at 9.5%. The overall survival rate was 26%. Significant disparities in survival outcomes are evident across demographic groups.
Top Survival Drivers — Logistic Regression
Odds ratios and 95% confidence intervals from logistic regression model
| Predictor | Odds Ratio | CI Lower | CI Upper |
|---|---|---|---|
| Sex: 1 | 6.599 | 4.929 | 8.895 |
| Class: 3 | 0.2067 | 0.1303 | 0.326 |
| Class: 2 | 0.5113 | 0.3226 | 0.8075 |
| Family Size | 0.8954 | 0.8007 | 0.9938 |
| Embarked: 2 | 0.8973 | 0.6262 | 1.294 |
| Age | 0.9697 | 0.9577 | 0.9815 |
| Embarked: 1 | 1.002 | 0.5553 | 1.79 |
| Fare | 0.9997 | 0.9965 | 1.003 |
Logistic regression quantifies the independent association between each passenger attribute and survival, controlling for all other predictors in the model. Odds ratios (OR) measure the multiplicative change in survival odds for a unit increase (or factor-level change) in the predictor. An OR > 1 indicates increased survival odds; OR < 1 indicates decreased odds. Of the 8 predictors modeled, 2 are associated with higher survival odds and 6 with lower odds. The strongest driver is Sex: 1 (OR = 6.599, 95% CI [4.93, 8.89]), which increases the odds of survival. Narrow confidence intervals indicate precise estimates; wide intervals reflect uncertainty.
Survival Rate by Passenger Class
Observed survival rates stratified by passenger class, showing the gradient between economic tiers.
Survival rates varied substantially across the three passenger classes. Among the 1,307 passengers analyzed, 1st Class passengers had the highest survival rate at 41.7% (n=321), while 3rd class passengers had the lowest at 16.8% (n=709). The gradient between the highest and lowest tiers was 25.0 percentage points. This steep differential—far exceeding the 26.0% overall survival rate—indicates that passenger class was a dominant determinant of survival probability, with first-class passengers dramatically more likely to survive than third-class passengers.
Age Distribution by Survival Status
Passenger age distribution stratified by survival status.
Among 1,307 passengers analyzed, those who survived (n=340) had a median age of 28 years (mean: 28.2; range: 0-80), compared to 28 years for non-survivors (n=967; mean: 29.9; range: 0-76). The median age difference of 0 years between groups indicates subtle variation in age distribution. However, both groups share the same median age of 28 years, suggesting overall age composition differed between survival groups.
Methodology
Statistical methodology and diagnostics for Titanic Survival — Passenger Drivers
Statistical Method
Analyses Titanic passenger records to surface the demographic and socioeconomic factors that differentiate survivors from non-survivors. Logistic regression quantifies each predictor's independent contribution as odds ratios with 95% confidence intervals, while targeted visualisations compare survival rates across passenger class and sex and contrast age distributions between outcome groups.
Analysis Code
Complete R source code for this analysis
Titanic Survival — Passenger Drivers
Analyses Titanic passenger records to surface the demographic and socioeconomic factors that differentiate survivors from non-survivors. Logistic regression quantifies each predictor's independent contribution as odds ratios with 95% confidence intervals, while targeted visualisations compare survival rates across passenger class and sex and contrast age distributions between outcome groups.
Why This Method?
Logistic regression models the binary survival outcome directly and produces odds ratios that quantify each predictor's independent effect while adjusting for other factors. This approach isolates the contribution of passenger class, sex, age, and family composition to survival risk.
What This Analysis Covers
- Executive Summary: overall survival rate and strongest independent drivers
- Survival Rate by Group: sex × class breakdown of observed rates
- Top Survival Drivers: logistic regression odds ratios with 95% CIs
- Class Survival Curve: gradient across passenger classes
- Age Distribution: age spread by survival status
suppressPackageStartupMessages(library(htmltools))
suppressPackageStartupMessages(library(jsonlite))
suppressPackageStartupMessages(library(plotly))
suppressPackageStartupMessages(library(DT))
suppressPackageStartupMessages(library(htmlwidgets))
suppressPackageStartupMessages(library(arrow))
suppressPackageStartupMessages(library(knitr))
suppressPackageStartupMessages(library(rmarkdown))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(tidyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(stringr))
suppressPackageStartupMessages(library(lubridate))
suppressPackageStartupMessages(library(broom))
suppressPackageStartupMessages(library(survival))
suppressPackageStartupMessages(library(Matrix))
suppressPackageStartupMessages(library(cluster))
suppressPackageStartupMessages(library(data.table))Step 1: Data Preparation and Row Accounting
Capture initial size, handle missing values, prepare factors.
initial_rows <- nrow(df)
# Remove rows with missing age (needed for logistic regression)
df_clean <- na.omit(df[, c("survived", "pclass", "sex", "age", "sibsp", "parch", "embarked", "fare")])
final_rows <- nrow(df_clean)
rows_removed <- initial_rows - final_rowsStep 2: Encode Target and Predictors as Factors
Classification requires factor encoding before glm().
df_clean$survived <- as.factor(df_clean$survived)
df_clean$pclass <- as.factor(df_clean$pclass)
df_clean$sex <- as.factor(df_clean$sex)
df_clean$embarked <- as.factor(df_clean$embarked)
# Derive family size (family_size = sibsp + parch + 1)
df_clean$family_size <- df_clean$sibsp + df_clean$parch + 1LStep 3: Logistic Regression Model
Fit glm with tryCatch to handle potential issues (quasi-separation, singular matrix).
model <- tryCatch(
glm(survived ~ pclass + sex + age + family_size + fare + embarked,
family = binomial(link = "logit"),
data = df_clean),
error = function(e) NULL
)Step 4: Extract Odds Ratios and Confidence Intervals
Use broom::tidy() to compute exponentiated coefficients and CIs.
logistic_df <- NULL
if (!is.null(model)) {
coef_tidy <- tryCatch(
broom::tidy(model, exponentiate = TRUE, conf.int = TRUE),
error = function(e) NULL
)
if (!is.null(coef_tidy) && nrow(coef_tidy) > 0) {
# Remove intercept, humanize predictor names
coef_tidy <- coef_tidy[coef_tidy$term != "(Intercept)", ]
# Humanize predictor names: convert dummy-coded factor levels
# sex0 -> "Sex: 0", pclass2 -> "Class: 2", etc.
predictor_names <- sapply(coef_tidy$term, function(term) {
if (startsWith(term, "pclass")) {
level <- substring(term, nchar("pclass") + 1)
paste0("Class: ", level)
} else if (startsWith(term, "sex")) {
level <- substring(term, nchar("sex") + 1)
paste0("Sex: ", level)
} else if (startsWith(term, "embarked")) {
level <- substring(term, nchar("embarked") + 1)
paste0("Embarked: ", level)
} else if (term == "age") {
"Age"
} else if (term == "family_size") {
"Family Size"
} else if (term == "fare") {
"Fare"
} else {
gsub("_", " ", tools::toTitleCase(term))
}
}, USE.NAMES = FALSE)
logistic_df <- data.frame(
predictor = predictor_names,
odds_ratio = coef_tidy$estimate,
ci_lower = coef_tidy$conf.low,
ci_upper = coef_tidy$conf.high,
stringsAsFactors = FALSE
)
# Clamp extreme odds ratios to prevent chart scale issues
logistic_df$odds_ratio <- pmin(pmax(logistic_df$odds_ratio, 0.1), 100)
logistic_df$ci_lower <- pmin(pmax(logistic_df$ci_lower, 0.1), 100)
logistic_df$ci_upper <- pmin(pmax(logistic_df$ci_upper, 0.1), 100)
# Sort by absolute distance from 1 (most impactful first)
logistic_df$distance_from_1 <- abs(log(logistic_df$odds_ratio))
logistic_df <- logistic_df[order(logistic_df$distance_from_1, decreasing = TRUE), ]
logistic_df$distance_from_1 <- NULL
rownames(logistic_df) <- NULL
# Count actual predictor terms in model (accounts for dummy-coded factors)
n_pred_actual <- nrow(logistic_df)
} else {
n_pred_actual <- 0
}
} else {
n_pred_actual <- 0
}Step 5: Survival Rate by Sex × Class Groups
Aggregate survival rate with sparse-bucket filter (n >= 5).
sex_labels <- c("0" = "Male", "1" = "Female")
class_labels <- c("1" = "1st Class", "2" = "2nd Class", "3" = "3rd Class")
survival_by_group <- df_clean %>%
dplyr::group_by(sex, pclass) %>%
dplyr::summarise(
n = dplyr::n(),
n_survived = sum(as.numeric(as.character(survived))),
survival_rate = mean(as.numeric(as.character(survived))),
.groups = "drop"
) %>%
dplyr::filter(n >= 5) %>%
dplyr::mutate(
risk_group = paste0(sex_labels[as.character(sex)], " — ", class_labels[as.character(pclass)])
) %>%
dplyr::select(risk_group, survival_rate, n, n_survived) %>%
dplyr::arrange(dplyr::desc(survival_rate))
survival_rate_by_group <- survival_by_group[, c("risk_group", "survival_rate")]
# Validate survival_rate range (0-1)
stopifnot(all(is.na(survival_rate_by_group$survival_rate) |
(survival_rate_by_group$survival_rate >= 0 &
survival_rate_by_group$survival_rate <= 1)))Step 6: Survival Rate by Passenger Class
Group by class only, apply sparse-bucket filter.
survival_by_class_df <- df_clean %>%
dplyr::group_by(pclass) %>%
dplyr::summarise(
n = dplyr::n(),
n_survived = sum(as.numeric(as.character(survived))),
survival_rate = mean(as.numeric(as.character(survived))),
.groups = "drop"
) %>%
dplyr::filter(n >= 5) %>%
dplyr::mutate(
pclass_label = class_labels[as.character(pclass)]
) %>%
dplyr::select(pclass_label, survival_rate, n, n_survived) %>%
dplyr::arrange(factor(pclass_label, levels = c("1st Class", "2nd Class", "3rd Class")))
survival_by_class <- survival_by_class_df[, c("pclass_label", "survival_rate")]
# Validate range
stopifnot(all(is.na(survival_by_class$survival_rate) |
(survival_by_class$survival_rate >= 0 &
survival_by_class$survival_rate <= 1)))Step 7: Age Distribution by Survival Status
Return raw age + survival status for box plot.
age_by_survival_df <- df_clean %>%
dplyr::mutate(
survival_status = ifelse(as.numeric(as.character(survived)) == 1, "Survived", "Did Not Survive")
) %>%
dplyr::select(age, survival_status)
age_by_survival <- age_by_survival_dfStep 8: Compute Summary Metrics
Overall statistics for tldr and overview.
overall_survival_rate <- mean(as.numeric(as.character(df_clean$survived)))
overall_survival_pct <- overall_survival_rate * 100
# Count passengers by key groups for tldr context
n_female_1st <- nrow(df_clean[df_clean$sex == "1" & df_clean$pclass == "1", ])
n_male_3rd <- nrow(df_clean[df_clean$sex == "0" & df_clean$pclass == "3", ])
# Template variables for row counts (Rule T)
tpl_vars <- list(
n_customers = format(final_rows, big.mark = ","),
n_rows_initial = format(initial_rows, big.mark = ","),
n_rows_final = format(final_rows, big.mark = ","),
pct_retention = sprintf("%.1f%%", 100 * final_rows / initial_rows)
)Step 9: Expose Results
Return named list with metrics, data frames, and template variables.
list(
# LWS-74: row accounting
initial_rows = initial_rows,
final_rows = final_rows,
rows_removed = rows_removed,
filters_applied = "Removed rows with missing age values",
# Metrics for tldr + overview
metrics = list(
n_observations = final_rows,
overall_survival_rate = overall_survival_rate,
overall_survival_pct = overall_survival_pct,
n_predictors = n_pred_actual,
model_p_value = if (!is.null(model)) {
# Approximate p-value from model deviance
1 - pchisq(model$null.deviance - model$deviance,
model$df.null - model$df.residual)
} else NA_real_
),
# Derived data frames for card plotting
survival_rate_by_group = survival_rate_by_group,
survival_by_class = survival_by_class,
age_by_survival = age_by_survival,
# Logistic regression results
logistic_drivers = logistic_df,
model = model,
model_summary = list(
formula = "survived ~ pclass + sex + age + family_size + fare + embarked",
type = "Logistic Regression",
n_predictors = n_pred_actual,
n_observations = final_rows,
overall_survival_rate = overall_survival_rate
),
# LAT-349: datasets with primary dataset info + target distribution (classification)
datasets = list(
primary = list(
n_rows = final_rows,
target_col = "survived",
class_distribution = as.list(table(df_clean$survived))
)
),
# Template variables for Rule T (size strings)
tpl_vars = tpl_vars,
# Raw data for cards needing detailed inspection
df_clean = df_clean,
survival_by_group_details = survival_by_group,
survival_by_class_details = survival_by_class_df
)
}Compute shared resources
shared <- compute_shared(df, params)