• Home
  • About

Employee Retention Analysis using Survival Modeling in R (2/2)

Programming
Programming/R
Published

October 19, 2025

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

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 themes


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")])
NoteShow survival summary table for BusinessTravel
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
NoteShow survival summary table for OverTime
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
NoteShow survival summary table for BusinessTravel + OverTime
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.



Built with Quarto and Netlify