Stepcode · any analysis, any toolbrowse ↗
Preview. This page currently shows the analysis skeleton. The full worked walkthrough — sample dataset, connected steps, runnable code, interpretation — is being built.

HR / People Analytics · how-to

Flight Risk — predict which employees will resign — in R

A turnover-prediction analysis. Every step below is the same analysis rendered for R — grounded in the source, honest where R can't do a step cleanly.

Same analysis, your tool

The problem

Every time a valued employee resigns, your organization absorbs a hidden bill: the cost of recruiting a replacement, the weeks of lost productivity while the seat sits empty, and the slow drain on morale as remaining team members pick up the slack. HR leaders and line managers feel this most sharply, because they are the ones who learn about a departure only after the resignation letter lands — far too late to do anything about it.

The question on the table is simple to ask and hard to answer: which of the people you have right now are most likely to walk out the door next? If you could see that coming, you could spend your limited retention budget and management attention on the handful of people who actually need it, instead of reacting one exit at a time.

The analysis — and why it fits

You are trying to sort each current employee into one of two outcomes — likely to leave, or likely to stay — and to attach a number to that judgment so you can rank people by how much attention they need. A method built for this shape takes a set of measurable employee attributes and estimates the probability of the leave-or-stay outcome, producing a score between 0 and 1 for every person. That probability is exactly what a manager needs: not just a yes-or-no label, but a sense of how urgent each case is.

The main alternative is a rule-based tree that splits people into groups with a series of if-then questions. That approach is easy to read, but it hands you buckets rather than a smooth, comparable likelihood for each individual, which makes ranking people against one another awkward — so here you want the probability estimate instead.

Why R

R earns its place in analytics because it was built by statisticians for statistical work, and logistic regression is a first-class citizen of the language rather than an add-on. The `glm` function fits the model, reports coefficients, standard errors, and P-values in one call, and the whole ecosystem of diagnostics, correlation, and prediction is available without leaving the console. For this flight-risk problem specifically, that means you can encode a category, screen predictors by correlation, fit the model, inspect significance, and score every employee using the same data frame and a handful of functions that all speak the same object model. R is also free and reproducible, so the exact steps below run identically on anyone's machine.

Where R is weaker is polish and hand-holding: it will happily fit a model on a tiny sample and give you confident-looking numbers with no warning that the sample is too small to trust, and its error messages assume you already understand what went wrong. With only a few dozen rows, the P-values and coefficients you see here should be read as a teaching illustration of the mechanics, not a production-grade retention model.

Who this is for. You work in HR, people analytics, or a management role where you own retention outcomes and want to move from reacting to resignations to anticipating them. You are comfortable with basic spreadsheets and summary statistics, but you do not need any prior modeling experience; the accompanying dataset download gives you every column you need to follow along. Plan on about 45 minutes.

Where we’re going

By the end, you will have a ranked flight-risk table: one row per employee, each carrying a resignation probability between 0 and 1 and a clear flag marking anyone above the halfway mark as a likely leaver. Sorted from highest probability to lowest, this table tells a manager exactly where to start — the person at the top is the one to schedule a stay conversation with this week, review for a pay adjustment, or simply check in on before a quiet frustration becomes a resignation letter. Instead of a vague worry that turnover is high, you hand leadership a short, prioritized list of names and the confidence behind each one.

The plan

We'll build the flight-risk table in five steps, moving from raw employee records to a ranked, actionable list.

  1. Convert the categorical commute distance into a numeric TravelTime indicator so the model can use it.
  2. Screen each predictor against the resignation outcome and keep only the ones that genuinely track turnover.
  3. Fit a model that links CompaRatio, Age, and TravelTime to the probability of resigning.
  4. Confirm each predictor and the overall model clear the significance threshold, dropping and refitting anything that fails.
  5. Score every employee into a resignation probability and flag those above the halfway mark as flight risks.

The worked dataset

Each row is one employee pulled from the HRIS, combining a compensation snapshot with demographic and commute attributes and a resignation outcome flag. CompaRatio comes from the compensation module (actual pay divided by the pay-grade midpoint), Age from the personnel master, and TravelTime from the commute or work-location field. Resign is set from the offboarding records for anyone who voluntarily left in the review window. Rows are generated with a seeded statistical model (effects are real but noisy — no perfect separation), so a fitted model shows plausible, finite estimates.

Resign · outcomeCompaRatio · predictorAge · predictorTravelTimeRaw · categoryTravelTime · predictor
11.148Far1
01.228Far1
00.8328Far1
01.1830Near0
0156Far1
01.0646Near0
10.8956Far1
01.1343Far1

30 rows total · download employee_flight_risk_records.csv

The workflow

travel_time_codedpredictor_correlationslogit_coefficientssignificance_gateresignation_probabilities

Step 1 · DERIVEtravel_time_coded

You convert the categorical travel distance into a numeric dummy variable, assigning 1 for a far commute and 0 for a near commute, stored in the TravelTime column. Logistic regression can only work with numeric inputs, so this encoding step makes the categorical predictor usable in the model. Without it, the fitting routine has no way to weigh commute distance against resignation.

You start in a plain R session by reading the CSV into a data frame. The file already carries a numeric `TravelTime` column, but you rebuild it explicitly from `TravelTimeRaw` so the encoding rule is visible and auditable in your code rather than trusted blindly. You use `ifelse` to assign 1 for a far commute and 0 for a near one, writing the result back to `TravelTime`.

emp <- read.csv("employee_flight_risk_records.csv", stringsAsFactors = FALSE)

emp$TravelTime <- ifelse(emp$TravelTimeRaw == "Far", 1, 0)

travel_time_coded <- emp$TravelTime
table(emp$TravelTimeRaw, travel_time_coded)

You should see: The cross-tab confirms every "Far" row maps to 1 and every "Near" row maps to 0, with no stray values in between. In the first five rows you'll see four Far commutes coded 1 and one Near coded 0, matching the raw labels exactly.

Common mistakes. A frequent slip is coding the category as a text factor and assuming `glm` will handle it. R can accept factors, but if you leave `TravelTimeRaw` as a string with unexpected whitespace or capitalization, the dummy it builds may not be the one you think. Encoding to an explicit 0/1 numeric column removes that ambiguity.
Step 2 · CORRELATEpredictor_correlations

Now that `TravelTime` is a clean numeric column, all four variables are numeric and we can compare them on equal footing. Before committing predictors to a model, you screen how strongly each one tracks the resignation outcome.

You measure how strongly each predictor tracks the resignation outcome, keeping only those with a correlation magnitude above 0.75. This screening tells you which variables genuinely move with turnover before you commit them to a model. It saves you from building an equation around noise that has no real relationship to who leaves.

You build a correlation matrix on the outcome and the three predictors using `cor`, then read the column for `Resign` to see which predictors move with turnover. You keep the screening threshold at a correlation magnitude above 0.75.

vars <- emp[, c("Resign", "CompaRatio", "Age", "TravelTime")]

predictor_correlations <- cor(vars)
print(round(predictor_correlations[, "Resign"], 3))

keep <- names(which(abs(predictor_correlations[, "Resign"]) > 0.75))
keep <- setdiff(keep, "Resign")
keep

You should see: You'll see correlations with `Resign` such as CompaRatio around -0.30, Age around -0.20, and TravelTime around 0.25 — all modest and none clearing the strict 0.75 bar. That is a warning sign: on this sample no single predictor tracks resignation strongly on its own.

Limitations of the sample dataset. A 0.75 cutoff is aggressive, and with only a handful of rows correlations are unstable — a couple of employees can swing them noticeably. If you dropped every predictor that failed, you'd have nothing left to model, so treat this screen as a directional check here and proceed to the full fit to see how the variables behave jointly.
Step 3 · FIT-CLASSIFIERlogit_coefficients

The correlation screen told us each predictor has only a mild individual link to resigning, which is exactly why we let the model weigh them together. You now fit a logistic regression so the coefficients account for all three predictors at once.

You fit a logistic regression that estimates the intercept and coefficients linking CompaRatio, Age, and TravelTime to the probability of resigning, chosen to maximize the log-likelihood. These coefficients form the equation that turns employee attributes into a resignation likelihood. This is the core model that everything downstream depends on.

You call `glm` with `family = binomial`, which tells R to fit a logistic model by maximizing the log-likelihood. The formula puts `Resign` on the left and the three predictors on the right, and `summary` exposes the estimated intercept and coefficients.

logit_model <- glm(Resign ~ CompaRatio + Age + TravelTime,
                    data = emp, family = binomial)

logit_coefficients <- summary(logit_model)$coefficients
print(logit_coefficients)

You should see: The coefficient table shows an intercept plus one estimate per predictor — for example a negative CompaRatio coefficient near -2.5 (higher relative pay lowers resignation odds), a small negative Age coefficient, and a positive TravelTime coefficient meaning a far commute raises the odds of leaving. These four numbers are the equation everything downstream uses.

How the math works

Logistic regression models the log-odds of resigning as a linear combination: log(p/(1-p)) = b0 + b1·CompaRatio + b2·Age + b3·TravelTime. The fitting routine searches for the coefficient values that make the observed 0/1 outcomes most likely, rather than minimizing squared error the way ordinary regression does. Converting log-odds back to a probability happens in the prediction step.

Step 4 · VALIDITYsignificance_gate

With coefficients in hand, we have an equation — but an equation is only worth trusting if each term is doing reliable work. You now gate the model on statistical significance before scoring anyone.

You check that each predictor's P-value and the overall model significance fall below 0.05, dropping any predictor that fails and refitting. This confirms each variable contributes reliably rather than by chance. Passing this gate is what lets you trust the coefficients before you score anyone with them.

You read the P-value column from the coefficient table and compare each to 0.05, and you check the overall model against its null via a likelihood-ratio test. Predictors that fail get dropped and the model refit. Everything lives in the same `logit_model` object.

pvals <- logit_coefficients[, "Pr(>|z|)"]
print(round(pvals, 4))

null_dev <- logit_model$null.deviance
resid_dev <- logit_model$deviance
df_diff  <- logit_model$df.null - logit_model$df.residual
model_p <- pchisq(null_dev - resid_dev, df = df_diff, lower.tail = FALSE)

significance_gate <- list(predictor_p = pvals, model_p = model_p)
significance_gate

You should see: On this small sample most P-values land well above 0.05 — CompaRatio might be the closest at around 0.18 — and the overall model P-value likely sits near 0.30. Strictly read, no predictor passes the gate, which honestly reflects that a few dozen rows cannot establish significance.

Limitations of the example. With so few observations the standard errors are wide, so real effects can hide behind large P-values. In a genuine retention study you'd want hundreds of employees before dropping predictors on significance alone; here we note the failure, keep the predictors for illustration, and continue to prediction so you can see the full workflow end to end.
Step 5 · PREDICTresignation_probabilities

Having fit and examined the model, the final job is to turn it into action. You apply the fitted equation to every employee to get a resignation probability and flag the high-risk ones.

You apply the logistic formula to each employee, computing the linear score from the coefficients and converting it into a probability between 0 and 1, then flag anyone above 0.5 as a flight risk. This produces a per-employee resignation likelihood that ranks who most needs attention. It turns the model into an actionable list for prioritizing retention effort.

You use `predict` with `type = "response"`, which runs each employee's attributes through the coefficients and applies the logistic function to return a probability between 0 and 1. You then flag anyone above 0.5 as a flight risk and attach both columns back to the data frame.

emp$resignation_probabilities <- predict(logit_model, type = "response")
emp$flight_risk <- ifelse(emp$resignation_probabilities > 0.5, 1, 0)

resignation_probabilities <- emp$resignation_probabilities
head(emp[, c("CompaRatio", "Age", "TravelTime",
             "resignation_probabilities", "flight_risk")])

emp[order(-emp$resignation_probabilities),
    c("resignation_probabilities", "flight_risk")]

You should see: Each employee gets a probability such as 0.62 or 0.14, and those above 0.5 carry `flight_risk = 1`. Sorting descending gives you a ranked worklist — the highest-probability employees, typically those with below-midpoint CompaRatio and a far commute, appear at the top for retention attention.

In the real world. A 0.5 cutoff is a default, not a law. If retention outreach is cheap you might flag everyone above 0.3 to cast a wider net; if it's expensive you'd raise the threshold to focus on the most likely leavers. Tune it to the cost of acting versus the cost of losing someone.

What it means

Working end to end, you encoded commute distance, screened the predictors, fit a logistic model, and turned it into a per-employee resignation probability with a ranked flight-risk list — a complete pipeline from raw records to an actionable retention worklist. The direction of the effects is sensible: below-midpoint pay and a far commute push resignation probability up, which matches what you'd expect drives turnover. The honest limitation is sample size: with only a few dozen rows, no predictor cleared the significance gate and the correlations were unstable, so these coefficients illustrate the mechanics rather than justify real personnel decisions. Read the ranked probabilities as a demonstration of how the model prioritizes attention, not as a verdict on any individual. From here, gather several hundred more employee records to stabilize the estimates and let the significance test actually mean something, add predictors like tenure and manager span, and once the model validates on a held-out set, wrap the `predict` call in a scheduled script so HR gets a refreshed flight-risk list each month.

References & further reading

On the analysis

  • Predictive HR AnalyticsCedric Ng Mong Shen (2020). Walks you through building a resignation-prediction model step by step in a spreadsheet, so you can see every calculation instead of trusting a black box.
  • Applied Logistic RegressionDavid W. Hosmer, Stanley Lemeshow, and Rodney X. Sturdivant (2013). The definitive treatment of fitting and interpreting logistic models, invaluable when you need to explain odds ratios and check model fit for a flight-risk classifier.
  • An Introduction to Statistical LearningGareth James, Daniela Witten, Trevor Hastie, and Robert Tibshirani (2013). A gentle, example-driven grounding in classification and validation that helps you avoid overfitting when predicting who might leave.
  • Competing on Analytics: The New Science of WinningThomas H. Davenport and Jeanne G. Harris (2007). Frames why data-driven people decisions matter to an organization, giving useful context before you turn attrition modeling into action.

On the tool

  • The Definitive Guide to DAXMarco Russo and Alberto Ferrari (2019). The clearest path to writing measures and calculated columns correctly in Power BI, with the evaluation-context detail you need for reliable results.
  • R for Data ScienceHadley Wickham and Garrett Grolemund (2017). A friendly, hands-on tour of importing, tidying, and modeling data in R that gets you productive fast.
  • An Introduction to Statistical Learning with Applications in RGareth James, Daniela Witten, Trevor Hastie, and Robert Tibshirani (2013). Pairs statistical concepts with concrete R code, making it a practical companion when you fit classification models yourself.

Doing HR / People Analytics work?

The full HR / People Analytics guide covers this and the whole workflow around it — reconciled from the field’s best books.

Want to run it, not read it?

Run this analysis live on your data, in R or any tool — powered by the Stepcode engine.