rm(list = ls()) # clear the objects from work environment
library(tidyverse)
library(ggplot2)
library(knitr)
library(skimr)
library(survival)
library(survminer)
library(data.table)
library(htmltools)
df <- fread("WA_Fn-UseC_-HR-Employee-Attrition.csv")
source("custom_themes.R", encoding = "UTF-8") # import pre-built custom themesEmployee Retention Analysis using Survival Modeling in R (2/2)
In the previous post, the fundamentals of survival modeling were introduced, covering how to create a survival object, fit a Kaplan-Meier model, and visualize survival curves.
This follow-up post explains how distinct values of a single variable, as well as combinations of multiple variables, can help uncover differences in retention ratios across employee cohorts.
Loading the required packages and data
Survival object creation
df[, Attrition := ifelse(Attrition == "Yes", 1, 0)]
s_obj <- Surv(time = df$YearsAtCompany, event = df$Attrition)
head(s_obj)[1] 6 10+ 0 8+ 2+ 7+
Exploring the BusinessTravel and OverTime variables
For this exercise, BusinessTravel and OverTime variables are examined to understand how they relate to employee retention.
table(df$BusinessTravel, df$OverTime)
No Yes
Non-Travel 115 35
Travel_Frequently 191 86
Travel_Rarely 748 295
Recoding the OverTime variable to make it easier to interpret.
df[, OverTime := ifelse(OverTime=="No", "No Over Time", "Over Time")]
table(df$OverTime)
No Over Time Over Time
1054 416
Fitting Kaplan-Meier models for single variables and their combinations
km_fit_BusinessTravel <- survfit(s_obj ~ df$BusinessTravel)
km_fit_OverTime <- survfit(s_obj ~ df$OverTime)
km_fit_BusinessTravel_OverTime <- survfit(s_obj ~ BusinessTravel + OverTime, data = df)Simplifying the strata names to make interpretation easier.
names(km_fit_BusinessTravel$strata) <- sub(".*=", "", names(km_fit_BusinessTravel$strata))
names(km_fit_OverTime$strata) <- sub(".*=", "", names(km_fit_OverTime$strata))
names(km_fit_BusinessTravel_OverTime$strata) <- gsub("df\\$[^=]+=|BusinessTravel=|OverTime=", "", names(km_fit_BusinessTravel_OverTime$strata))The model summaries are converted into a clean table to review the survival statistics for each group.
km_fit_summary_BusinessTravel <- as.data.table(summary(km_fit_BusinessTravel)[c("strata","time", "n.risk", "n.event", "n.censor", "surv", "std.err")])
km_fit_summary_OverTime <- as.data.table(summary(km_fit_OverTime)[c("strata","time", "n.risk", "n.event", "n.censor", "surv", "std.err")])
km_fit_summary_BusinessTravel_OverTime <- as.data.table(summary(km_fit_BusinessTravel_OverTime)[c("strata","time", "n.risk", "n.event", "n.censor", "surv", "std.err")])kable(km_fit_summary_BusinessTravel)| strata | time | n.risk | n.event | n.censor | surv | std.err |
|---|---|---|---|---|---|---|
| Non-Travel | 0 | 150 | 1 | 6 | 0.9933333 | 0.0066444 |
| Non-Travel | 1 | 143 | 4 | 14 | 0.9655478 | 0.0151434 |
| Non-Travel | 2 | 125 | 1 | 5 | 0.9578234 | 0.0168777 |
| Non-Travel | 3 | 119 | 1 | 15 | 0.9497745 | 0.0185562 |
| Non-Travel | 4 | 103 | 1 | 8 | 0.9405534 | 0.0205397 |
| Non-Travel | 5 | 94 | 2 | 19 | 0.9205416 | 0.0244968 |
| Non-Travel | 10 | 38 | 2 | 42 | 0.8720920 | 0.0406263 |
| Travel_Frequently | 0 | 277 | 7 | 4 | 0.9747292 | 0.0094300 |
| Travel_Frequently | 1 | 266 | 17 | 8 | 0.9124345 | 0.0170765 |
| Travel_Frequently | 2 | 241 | 7 | 23 | 0.8859323 | 0.0192960 |
| Travel_Frequently | 3 | 211 | 6 | 14 | 0.8607399 | 0.0213127 |
| Travel_Frequently | 4 | 191 | 6 | 17 | 0.8337009 | 0.0233273 |
| Travel_Frequently | 5 | 168 | 4 | 22 | 0.8138509 | 0.0247935 |
| Travel_Frequently | 6 | 142 | 4 | 15 | 0.7909255 | 0.0266133 |
| Travel_Frequently | 7 | 123 | 3 | 13 | 0.7716347 | 0.0281986 |
| Travel_Frequently | 8 | 107 | 3 | 14 | 0.7500000 | 0.0300473 |
| Travel_Frequently | 9 | 90 | 2 | 17 | 0.7333334 | 0.0316064 |
| Travel_Frequently | 10 | 71 | 5 | 20 | 0.6816902 | 0.0368654 |
| Travel_Frequently | 11 | 46 | 1 | 4 | 0.6668708 | 0.0389288 |
| Travel_Frequently | 13 | 39 | 1 | 7 | 0.6497716 | 0.0415165 |
| Travel_Frequently | 18 | 26 | 1 | 11 | 0.6247804 | 0.0468414 |
| Travel_Frequently | 21 | 15 | 1 | 9 | 0.5831283 | 0.0594185 |
| Travel_Frequently | 23 | 8 | 1 | 3 | 0.5102373 | 0.0857441 |
| Travel_Rarely | 0 | 1043 | 8 | 18 | 0.9923298 | 0.0027014 |
| Travel_Rarely | 1 | 1017 | 38 | 90 | 0.9552516 | 0.0064490 |
| Travel_Rarely | 2 | 889 | 19 | 72 | 0.9348357 | 0.0078294 |
| Travel_Rarely | 3 | 798 | 13 | 79 | 0.9196065 | 0.0087674 |
| Travel_Rarely | 4 | 706 | 12 | 66 | 0.9039758 | 0.0097104 |
| Travel_Rarely | 5 | 628 | 15 | 134 | 0.8823840 | 0.0109626 |
| Travel_Rarely | 6 | 479 | 5 | 45 | 0.8731733 | 0.0115962 |
| Travel_Rarely | 7 | 429 | 8 | 60 | 0.8568904 | 0.0127290 |
| Travel_Rarely | 8 | 361 | 6 | 49 | 0.8426484 | 0.0137815 |
| Travel_Rarely | 9 | 306 | 6 | 43 | 0.8261259 | 0.0150719 |
| Travel_Rarely | 10 | 257 | 11 | 75 | 0.7907664 | 0.0178025 |
| Travel_Rarely | 11 | 171 | 1 | 20 | 0.7861421 | 0.0182892 |
| Travel_Rarely | 13 | 139 | 1 | 23 | 0.7804864 | 0.0190120 |
| Travel_Rarely | 14 | 126 | 2 | 12 | 0.7680977 | 0.0206299 |
| Travel_Rarely | 15 | 112 | 1 | 11 | 0.7612397 | 0.0215555 |
| Travel_Rarely | 16 | 100 | 1 | 10 | 0.7536273 | 0.0226443 |
| Travel_Rarely | 17 | 89 | 1 | 8 | 0.7451596 | 0.0239207 |
| Travel_Rarely | 19 | 73 | 1 | 13 | 0.7349519 | 0.0256788 |
| Travel_Rarely | 20 | 66 | 1 | 19 | 0.7238163 | 0.0275988 |
| Travel_Rarely | 22 | 37 | 1 | 20 | 0.7042537 | 0.0330671 |
| Travel_Rarely | 24 | 25 | 1 | 3 | 0.6760835 | 0.0420657 |
| Travel_Rarely | 31 | 11 | 1 | 11 | 0.6146214 | 0.0699756 |
| Travel_Rarely | 32 | 9 | 1 | 0 | 0.5463301 | 0.0895233 |
| Travel_Rarely | 33 | 8 | 1 | 2 | 0.4780389 | 0.1010781 |
| Travel_Rarely | 40 | 1 | 1 | 4 | 0.0000000 | NaN |
kable(km_fit_summary_OverTime)| strata | time | n.risk | n.event | n.censor | surv | std.err |
|---|---|---|---|---|---|---|
| No Over Time | 0 | 1054 | 6 | 24 | 0.9943074 | 0.0023174 |
| No Over Time | 1 | 1024 | 31 | 85 | 0.9642063 | 0.0057787 |
| No Over Time | 2 | 908 | 12 | 80 | 0.9514635 | 0.0067727 |
| No Over Time | 3 | 816 | 6 | 82 | 0.9444674 | 0.0073003 |
| No Over Time | 4 | 728 | 11 | 69 | 0.9301966 | 0.0083625 |
| No Over Time | 5 | 648 | 7 | 132 | 0.9201482 | 0.0090938 |
| No Over Time | 6 | 509 | 4 | 48 | 0.9129172 | 0.0097145 |
| No Over Time | 7 | 457 | 3 | 66 | 0.9069243 | 0.0102484 |
| No Over Time | 8 | 388 | 3 | 53 | 0.8999120 | 0.0109396 |
| No Over Time | 9 | 332 | 6 | 57 | 0.8836485 | 0.0125967 |
| No Over Time | 10 | 269 | 8 | 80 | 0.8573690 | 0.0152688 |
| No Over Time | 11 | 181 | 1 | 23 | 0.8526322 | 0.0159023 |
| No Over Time | 13 | 146 | 2 | 29 | 0.8409523 | 0.0176996 |
| No Over Time | 14 | 126 | 2 | 16 | 0.8276038 | 0.0197759 |
| No Over Time | 17 | 89 | 1 | 25 | 0.8183049 | 0.0216297 |
| No Over Time | 18 | 82 | 1 | 9 | 0.8083256 | 0.0235558 |
| No Over Time | 20 | 65 | 1 | 27 | 0.7958898 | 0.0262717 |
| No Over Time | 22 | 35 | 1 | 19 | 0.7731501 | 0.0339654 |
| No Over Time | 23 | 24 | 1 | 1 | 0.7409355 | 0.0453217 |
| No Over Time | 32 | 8 | 1 | 15 | 0.6483186 | 0.0952801 |
| No Over Time | 33 | 6 | 1 | 1 | 0.5402655 | 0.1266252 |
| No Over Time | 40 | 1 | 1 | 3 | 0.0000000 | NaN |
| Over Time | 0 | 416 | 10 | 4 | 0.9759615 | 0.0075097 |
| Over Time | 1 | 402 | 28 | 27 | 0.9079841 | 0.0142250 |
| Over Time | 2 | 347 | 15 | 20 | 0.8687341 | 0.0168375 |
| Over Time | 3 | 312 | 14 | 26 | 0.8297524 | 0.0190341 |
| Over Time | 4 | 272 | 8 | 22 | 0.8053479 | 0.0203361 |
| Over Time | 5 | 242 | 14 | 43 | 0.7587576 | 0.0226533 |
| Over Time | 6 | 185 | 5 | 19 | 0.7382506 | 0.0238252 |
| Over Time | 7 | 161 | 8 | 13 | 0.7015673 | 0.0259322 |
| Over Time | 8 | 140 | 6 | 18 | 0.6715002 | 0.0275733 |
| Over Time | 9 | 116 | 2 | 17 | 0.6599226 | 0.0282871 |
| Over Time | 10 | 97 | 10 | 22 | 0.5918893 | 0.0325395 |
| Over Time | 11 | 65 | 1 | 7 | 0.5827833 | 0.0332887 |
| Over Time | 15 | 50 | 1 | 13 | 0.5711277 | 0.0346033 |
| Over Time | 16 | 43 | 1 | 5 | 0.5578456 | 0.0362582 |
| Over Time | 19 | 32 | 1 | 8 | 0.5404130 | 0.0390919 |
| Over Time | 21 | 22 | 1 | 10 | 0.5158487 | 0.0443664 |
| Over Time | 24 | 13 | 1 | 4 | 0.4761681 | 0.0559521 |
| Over Time | 31 | 7 | 1 | 6 | 0.4081441 | 0.0791599 |
kable(km_fit_summary_BusinessTravel_OverTime)| strata | time | n.risk | n.event | n.censor | surv | std.err |
|---|---|---|---|---|---|---|
| Non-Travel, No Over Time | 0 | 115 | 1 | 6 | 0.9913043 | 0.0086578 |
| Non-Travel, No Over Time | 1 | 108 | 1 | 12 | 0.9821256 | 0.0125317 |
| Non-Travel, No Over Time | 2 | 95 | 1 | 3 | 0.9717874 | 0.0161093 |
| Non-Travel, No Over Time | 4 | 77 | 1 | 19 | 0.9591668 | 0.0202490 |
| Non-Travel, No Over Time | 10 | 28 | 1 | 48 | 0.9249109 | 0.0388950 |
| Non-Travel, Over Time | 1 | 35 | 3 | 2 | 0.9142857 | 0.0473188 |
| Non-Travel, Over Time | 3 | 28 | 1 | 3 | 0.8816327 | 0.0557686 |
| Non-Travel, Over Time | 5 | 23 | 2 | 8 | 0.8049689 | 0.0726353 |
| Non-Travel, Over Time | 10 | 10 | 1 | 8 | 0.7244720 | 0.1005248 |
| Travel_Frequently, No Over Time | 0 | 191 | 2 | 4 | 0.9895288 | 0.0073654 |
| Travel_Frequently, No Over Time | 1 | 185 | 10 | 4 | 0.9360408 | 0.0178655 |
| Travel_Frequently, No Over Time | 2 | 171 | 4 | 19 | 0.9141451 | 0.0205297 |
| Travel_Frequently, No Over Time | 3 | 148 | 2 | 10 | 0.9017918 | 0.0220324 |
| Travel_Frequently, No Over Time | 4 | 136 | 3 | 15 | 0.8818993 | 0.0243565 |
| Travel_Frequently, No Over Time | 5 | 118 | 2 | 17 | 0.8669518 | 0.0261366 |
| Travel_Frequently, No Over Time | 6 | 99 | 3 | 10 | 0.8406806 | 0.0294183 |
| Travel_Frequently, No Over Time | 7 | 86 | 2 | 12 | 0.8211299 | 0.0318170 |
| Travel_Frequently, No Over Time | 8 | 72 | 1 | 9 | 0.8097253 | 0.0333565 |
| Travel_Frequently, No Over Time | 10 | 47 | 1 | 31 | 0.7924971 | 0.0368281 |
| Travel_Frequently, No Over Time | 13 | 24 | 1 | 11 | 0.7594764 | 0.0478599 |
| Travel_Frequently, No Over Time | 18 | 14 | 1 | 7 | 0.7052281 | 0.0686127 |
| Travel_Frequently, No Over Time | 23 | 4 | 1 | 6 | 0.5289210 | 0.1611248 |
| Travel_Frequently, Over Time | 0 | 86 | 5 | 0 | 0.9418605 | 0.0252336 |
| Travel_Frequently, Over Time | 1 | 81 | 7 | 4 | 0.8604651 | 0.0373645 |
| Travel_Frequently, Over Time | 2 | 70 | 3 | 4 | 0.8235880 | 0.0413870 |
| Travel_Frequently, Over Time | 3 | 63 | 4 | 4 | 0.7712967 | 0.0462868 |
| Travel_Frequently, Over Time | 4 | 55 | 3 | 2 | 0.7292260 | 0.0497285 |
| Travel_Frequently, Over Time | 5 | 50 | 2 | 5 | 0.7000570 | 0.0518406 |
| Travel_Frequently, Over Time | 6 | 43 | 1 | 5 | 0.6837766 | 0.0531299 |
| Travel_Frequently, Over Time | 7 | 37 | 1 | 1 | 0.6652961 | 0.0548139 |
| Travel_Frequently, Over Time | 8 | 35 | 2 | 5 | 0.6272792 | 0.0578994 |
| Travel_Frequently, Over Time | 9 | 28 | 2 | 2 | 0.5824735 | 0.0618273 |
| Travel_Frequently, Over Time | 10 | 24 | 4 | 4 | 0.4853946 | 0.0679558 |
| Travel_Frequently, Over Time | 11 | 16 | 1 | 0 | 0.4550575 | 0.0701542 |
| Travel_Frequently, Over Time | 21 | 8 | 1 | 9 | 0.3981753 | 0.0812357 |
| Travel_Rarely, No Over Time | 0 | 748 | 3 | 14 | 0.9959893 | 0.0023109 |
| Travel_Rarely, No Over Time | 1 | 731 | 20 | 69 | 0.9687393 | 0.0064160 |
| Travel_Rarely, No Over Time | 2 | 642 | 7 | 58 | 0.9581767 | 0.0074857 |
| Travel_Rarely, No Over Time | 3 | 577 | 4 | 58 | 0.9515342 | 0.0081373 |
| Travel_Rarely, No Over Time | 4 | 515 | 7 | 49 | 0.9386007 | 0.0093808 |
| Travel_Rarely, No Over Time | 5 | 459 | 5 | 101 | 0.9283763 | 0.0103331 |
| Travel_Rarely, No Over Time | 6 | 353 | 1 | 32 | 0.9257464 | 0.0106333 |
| Travel_Rarely, No Over Time | 7 | 320 | 1 | 49 | 0.9228534 | 0.0109865 |
| Travel_Rarely, No Over Time | 8 | 270 | 2 | 38 | 0.9160175 | 0.0119212 |
| Travel_Rarely, No Over Time | 9 | 230 | 6 | 30 | 0.8921213 | 0.0150826 |
| Travel_Rarely, No Over Time | 10 | 194 | 6 | 59 | 0.8645300 | 0.0183463 |
| Travel_Rarely, No Over Time | 11 | 129 | 1 | 13 | 0.8578282 | 0.0193896 |
| Travel_Rarely, No Over Time | 13 | 107 | 1 | 19 | 0.8498111 | 0.0207998 |
| Travel_Rarely, No Over Time | 14 | 95 | 2 | 12 | 0.8319203 | 0.0239014 |
| Travel_Rarely, No Over Time | 17 | 67 | 1 | 20 | 0.8195036 | 0.0265749 |
| Travel_Rarely, No Over Time | 20 | 51 | 1 | 25 | 0.8034349 | 0.0305278 |
| Travel_Rarely, No Over Time | 22 | 27 | 1 | 15 | 0.7736781 | 0.0414351 |
| Travel_Rarely, No Over Time | 32 | 7 | 1 | 11 | 0.6631526 | 0.1083149 |
| Travel_Rarely, No Over Time | 33 | 6 | 1 | 1 | 0.5526272 | 0.1353780 |
| Travel_Rarely, No Over Time | 40 | 1 | 1 | 3 | 0.0000000 | NaN |
| Travel_Rarely, Over Time | 0 | 295 | 5 | 4 | 0.9830508 | 0.0075154 |
| Travel_Rarely, Over Time | 1 | 286 | 18 | 21 | 0.9211805 | 0.0157757 |
| Travel_Rarely, Over Time | 2 | 247 | 12 | 14 | 0.8764268 | 0.0195979 |
| Travel_Rarely, Over Time | 3 | 221 | 9 | 21 | 0.8407352 | 0.0221181 |
| Travel_Rarely, Over Time | 4 | 191 | 5 | 17 | 0.8187264 | 0.0236278 |
| Travel_Rarely, Over Time | 5 | 169 | 10 | 33 | 0.7702811 | 0.0267389 |
| Travel_Rarely, Over Time | 6 | 126 | 4 | 13 | 0.7458277 | 0.0285489 |
| Travel_Rarely, Over Time | 7 | 109 | 7 | 11 | 0.6979305 | 0.0319438 |
| Travel_Rarely, Over Time | 8 | 91 | 4 | 11 | 0.6672523 | 0.0340238 |
| Travel_Rarely, Over Time | 10 | 63 | 5 | 29 | 0.6142957 | 0.0386979 |
| Travel_Rarely, Over Time | 15 | 31 | 1 | 13 | 0.5944797 | 0.0422194 |
| Travel_Rarely, Over Time | 16 | 28 | 1 | 5 | 0.5732483 | 0.0457395 |
| Travel_Rarely, Over Time | 19 | 18 | 1 | 6 | 0.5414012 | 0.0531413 |
| Travel_Rarely, Over Time | 24 | 7 | 1 | 8 | 0.4640582 | 0.0848655 |
| Travel_Rarely, Over Time | 31 | 4 | 1 | 3 | 0.3480436 | 0.1189359 |
Visualizing the Kaplan-Meier survival curves
Show the code
ggsurvplot(fit = km_fit_BusinessTravel,
data = df,
title = "\n\nSurvival Probabilities by Business Travel Status",
xlab = "\nYears At Company",
ylab = "Probability of Continued Employment\n",
break.x.by=1,
break.y.by=.1,
conf.int = FALSE,
censor = FALSE,
xlim = c(0, max(df$YearsAtCompany)),
surv.scale = "percent",
fontsize = 4,
risk.table.title=element_blank(),
legend.title = element_blank(),
risk.table.col = "#808080",
ggtheme = theme_survival,
legend = c(0.2, 0.2),
palette = c("#1F497D", "#9BBB59", "#8064A2", "#C0504D", "#4F81BD", "#F79646")
)
Employees who travel frequently show the lowest retention, with survival probability dropping to around 70% after 10 years. Those who do not travel maintain the highest retention, staying above 85% even after a decade. The separation between the curves can be seen from the early years, suggesting that travel intensity affects retention early in the employment period.
Show the code
ggsurvplot(fit = km_fit_OverTime,
data = df,
title = "\n\nSurvival Probabilities by Over Time Status",
xlab = "\nYears At Company",
ylab = "Probability of Continued Employment\n",
break.x.by=1,
break.y.by=.1,
conf.int = FALSE,
censor = FALSE,
xlim = c(0, max(df$YearsAtCompany)),
surv.scale = "percent",
fontsize = 4,
risk.table.title=element_blank(),
legend.title = element_blank(),
risk.table.col = "#808080",
ggtheme = theme_survival,
legend = c(0.2, 0.2),
palette = c("#1F497D", "#9BBB59", "#8064A2", "#C0504D", "#4F81BD", "#F79646")
)
Employees who work overtime have a noticeably steeper decline in survival probability, falling to roughly 60% after 10 years, compared to about 85% for those who don’t. The curves diverge within the first few years, indicating that overtime starts influencing attrition risk early in an employee’s tenure.
Show the code
ggsurvplot(fit = km_fit_BusinessTravel_OverTime,
data = df,
title = "\n\nSurvival Probabilities by Business Travel and Over Time Status",
xlab = "\nYears At Company",
ylab = "Probability of Continued Employment\n",
break.x.by=1,
break.y.by=.1,
conf.int = FALSE,
censor = FALSE,
xlim = c(0, max(df$YearsAtCompany)),
surv.scale = "percent",
fontsize = 4,
risk.table.title=element_blank(),
legend.title = element_blank(),
risk.table.col = "#808080",
ggtheme = theme_survival,
legend = c(0.2, 0.2),
palette = c("#1F497D", "#9BBB59", "#8064A2", "#C0504D", "#4F81BD", "#F79646")
)
The combined model shows a clear interaction: Frequent travelers who also work overtime have the lowest retention, with survival dropping below 50% by year 10. Meanwhile, non-traveling employees without overtime keep the highest retention rate, remaining above 90% through the same period. The difference between the groups becomes apparent almost immediately, with the gap widening over time.
Summary and further directions
These two posts provided a simple and practical introduction to survival analysis, showing how different factors relate to the timing of events. Although the examples focused on employee retention, the same approach is widely used in areas such as healthcare, customer analytics, and risk prediction. This was only a starting point; survival analysis can be explored in much greater depth. For instance, models like the Cox proportional hazards model can predict the likelihood and timing of events at the individual level, offering even richer insights. It is also important to consider the number of observations in each subgroup when interpreting results, as this directly affects the reliability of the conclusions drawn. A more detailed investigation that takes these counts into account would provide a clearer understanding, but that is beyond the scope of these posts.