Life Table Analysis in R: The Actuarial Method, Step by Step
Build an actuarial (cohort) life table by hand from grouped follow-up data — at risk, deaths, withdrawals, the withdrawal/2 adjustment, and cumulative survival — and see how it differs from, and agrees with, Kaplan-Meier
Biostatistics
Learn the actuarial life table method in R: estimate survival when follow-up data come in grouped time intervals rather than exact event times. Build the table by hand with base R — number at risk, deaths, withdrawals, the effective sample size (n minus half the withdrawals), the conditional probability of death and survival per interval, and cumulative survival — read it in plain language, and overlay it on the Kaplan-Meier curve to confirm the two methods agree when intervals are fine. Worked on the NCCTG lung-cancer data.
Published
June 26, 2026
Modified
July 7, 2026
TipKey takeaways
A life table (the actuarial or cohort method) estimates survival when follow-up data come in grouped time intervals — you know how many died and how many dropped out in each interval, but not the exact event times Kaplan-Meier needs.
Each interval contributes a conditional probability of survival\(p_j = 1 - q_j\), where \(q_j = d_j / (n_j - w_j/2)\): deaths divided by the effective number at risk — the count at the start of the interval minus half the withdrawals (the actuarial adjustment, because censored patients are at risk for only part of the interval). Multiplying the \(p_j\) across intervals gives cumulative survival.
Life table vs Kaplan-Meier: use a life table for grouped / interval-recorded data (annual checkups, large registries); use Kaplan-Meier when you have exact event times. They are the same idea at two resolutions — and they agree closely when intervals are fine.
You can build the whole table with base R (seq() for the breaks, a loop counting at-risk / deaths / withdrawals per interval) — no special package needed.
The single number to remember is the withdrawal/2 adjustment: forget it and your survival is biased.
Introduction
Imagine a long-term registry where patients come in for an annual checkup. At each visit you record who is still alive, who has died since the last visit, and who has dropped out — but you do not know the exact day each death occurred, only the year it fell in. Or picture a large cohort where the data arrive already binned into intervals. In both cases the survival times are grouped, not exact.
Kaplan-Meier needs an exact event time to place each step in the curve, so it does not apply directly here. The method that does is the life table — the actuarial (or cohort) method, the oldest estimator in survival analysis. It divides time into intervals and estimates survival one interval at a time, asking a single question per interval: of those still at risk when the interval began, what fraction survived it?
This lesson builds an actuarial life table by hand in base R so you can see exactly where every number comes from, reads it in plain language, and overlays it on the Kaplan-Meier curve to prove the two methods tell the same story. We work on the NCCTG lung-cancer data so you can reproduce every figure — binning its exact times into intervals to simulate the grouped situation a life table is built for.
If censoring, the survival function, and the Surv() object are new to you, start with what is survival analysis?; for the exact-time estimator this method approximates, see Kaplan-Meier estimation.
The data: grouped follow-up
We use lung, the North Central Cancer Treatment Group dataset that ships with the survival package — survival in 228 advanced lung-cancer patients. Its time is in days and status is coded 1 = censored, 2 = dead. A life table works on grouped data, so we bin the survival times into 90-day intervals (roughly quarterly checkups). Look at the columns and the breaks first:
library(survival)# 90-day intervals from 0 to 1080 days (≈ quarterly follow-up)breaks <-seq(0, 1080, by =90)breaks
# status: 1 = censored (alive at last visit), 2 = dead. Recode to a 0/1 event flag.event <-as.integer(lung$status ==2)c(n_patients =nrow(lung), n_deaths =sum(event), n_censored =sum(event ==0))
n_patients n_deaths n_censored
228 165 63
So 228 patients, 165 deaths, 63 censored — and twelve 90-day intervals to walk through.
The life-table mechanics
For each interval \([t_j, t_{j+1})\) the actuarial method needs four counts and derives three probabilities:
Symbol
Meaning
\(n_j\)
number at risk at the start of the interval (everyone whose follow-up reaches \(t_j\))
\(d_j\)
deaths during the interval
\(w_j\)
withdrawals — censored patients (lost / still alive at last visit) during the interval
\(n_j' = n_j - w_j/2\)
effective number at risk — the actuarial adjustment
\(q_j = d_j / n_j'\)
conditional probability of death in the interval
\(p_j = 1 - q_j\)
conditional probability of surviving the interval
\(S(t_{j+1}) = \prod_{k \le j} p_k\)
cumulative survival to the end of the interval
The one idea that separates a life table from a naive death rate is the withdrawal/2 adjustment. A patient who is censored partway through an interval was at risk for only part of it, not the whole thing. The actuarial method assumes withdrawals are spread evenly across the interval, so on average each contributes half the interval’s exposure. We therefore subtract half the withdrawals from the denominator: \(n_j' = n_j - w_j/2\). Skip this and you over-count the denominator, under-estimate the death rate, and bias survival upward.
Walk through one interval
Take the second interval, 90–180 days, with the lung numbers (computed below): \(n_j = 201\) at risk, \(d_j = 35\) deaths, \(w_j = 6\) withdrawals. The arithmetic is the whole method in miniature:
Effective at risk:\(n_j' = 201 - 6/2 = 198\).
Conditional death prob:\(q_j = 35 / 198 = 0.177\).
Cumulative survival to 180 days: multiply by the previous interval’s survival, \(S(180) = 0.882 \times 0.823 = 0.726\).
That is the recipe; now we run it across all twelve intervals.
Build the life table in base R
No special package is needed — seq() made the breaks, and a plain loop counts the three numbers per interval, then vectorised arithmetic gives the probabilities. Everything is base R, so the block runs copy-pasted in any order:
library(survival)breaks <-seq(0, 1080, by =90)time <- lung$timeevent <-as.integer(lung$status ==2) # 1 = death, 0 = censored# One row per intervallt <-data.frame(start =head(breaks, -1), end = breaks[-1])# Count at-risk, deaths, withdrawals in each intervallt$n_risk <-sapply(lt$start, function(s) sum(time >= s))lt$deaths <-mapply(function(s, e) sum(time >= s & time < e & event ==1), lt$start, lt$end)lt$withdrawn <-mapply(function(s, e) sum(time >= s & time < e & event ==0), lt$start, lt$end)# Actuarial adjustment + conditional and cumulative survivallt$eff_n <- lt$n_risk - lt$withdrawn /2# effective number at risklt$q <- lt$deaths / lt$eff_n # conditional prob of deathlt$p <-1- lt$q # conditional prob of survivallt$surv <-cumprod(lt$p) # cumulative survival# Tidy displayround(lt, 3)
n_risk starts at 228 and falls each interval as patients die or withdraw — it is the cohort still being followed when the interval opens.
deaths / withdrawn are the events and the censored (lost / still-alive) patients in that interval.
eff_n is n_risk − withdrawn/2 — the actuarial denominator. In interval 1 there are no withdrawals so eff_n = n_risk; from interval 2 on the adjustment kicks in.
q rises from about 0.12 to 0.39 — the per-interval death risk climbs as the cohort thins.
surv is the headline: cumulative survival drops from 0.88 at 90 days to about 0.13 at 720 days. Read it as “an estimated 44% of patients survive past 360 days” (surv = 0.437 at the end of the 270–360 interval).
The last intervals show q = 0 and flat survival — only a handful of patients remain, so a tail with no deaths leaves the estimate unchanged. Interpret the tail of any life table with the same caution as a KM tail: few patients, unstable estimate.
NoteThe math behind the actuarial estimator (optional)
The life table estimates the survival function\(S(t)\) as a product of interval-specific survival probabilities — the same product-limit idea as Kaplan-Meier, but over fixed intervals instead of exact event times:
where \(n_k\) is the number at risk at the start of interval \(k\), \(d_k\) the deaths, and \(w_k\) the withdrawals (censored). The effective number at risk\(n_k'\) encodes the actuarial assumption that withdrawals occur uniformly within the interval, so each censored patient contributes on average half the interval’s exposure. As the interval width shrinks toward zero — at most one event per interval — \(n_k'\) approaches the exact risk set and the life table converges to the Kaplan-Meier estimator. Kaplan-Meier is simply the limiting case of the life table when you know the exact event times.
Life table vs Kaplan-Meier: do they agree?
The life table and Kaplan-Meier are the same idea at two resolutions: the life table groups time into intervals; Kaplan-Meier puts a step at every exact event time. So they should agree closely — and the finer the intervals, the closer. Overlay the life-table survival (a point at the end of each interval) on the Kaplan-Meier curve to see it:
library(survival)# Kaplan-Meier on the exact timeskm <-survfit(Surv(time, status) ~1, data = lung)# Life-table survival from the table built above (recomputed self-contained)breaks <-seq(0, 1080, by =90)time <- lung$timeevent <-as.integer(lung$status ==2)starts <-head(breaks, -1); ends <- breaks[-1]n_risk <-sapply(starts, function(s) sum(time >= s))deaths <-mapply(function(s, e) sum(time >= s & time < e & event ==1), starts, ends)withdrawn <-mapply(function(s, e) sum(time >= s & time < e & event ==0), starts, ends)eff_n <- n_risk - withdrawn /2lt_surv <-cumprod(1- deaths / eff_n)# Plot the KM curve, then the life-table survival as points at interval endsplot(km, conf.int =FALSE, mark.time =TRUE,xlab ="Time (days)", ylab ="Survival probability",xlim =c(0, 760), col ="grey20", lwd =2)points(ends[ends <=760], lt_surv[ends <=760],pch =19, col ="#e8731b", cex =1.3)legend("topright", bty ="n",legend =c("Kaplan-Meier (exact times)", "Life table (90-day intervals)"),col =c("grey20", "#e8731b"), lwd =c(2, NA), pch =c(NA, 19))
The orange life-table points sit almost exactly on the Kaplan-Meier curve. Quantify the agreement at a few landmark times:
library(survival)breaks <-seq(0, 1080, by =90)time <- lung$timeevent <-as.integer(lung$status ==2)starts <-head(breaks, -1); ends <- breaks[-1]n_risk <-sapply(starts, function(s) sum(time >= s))deaths <-mapply(function(s, e) sum(time >= s & time < e & event ==1), starts, ends)withdrawn <-mapply(function(s, e) sum(time >= s & time < e & event ==0), starts, ends)lt_surv <-cumprod(1- deaths / (n_risk - withdrawn /2))km <-survfit(Surv(time, status) ~1, data = lung)landmarks <-c(180, 360, 540)data.frame(time = landmarks,life_table =round(lt_surv[match(landmarks, ends)], 3),kaplan_meier =round(summary(km, times = landmarks)$surv, 3))
At 180, 360, and 540 days the two estimates differ by at most ~0.005 — well inside any practical tolerance. Narrow the intervals (say 30 days) and they converge even further; widen them and the life table loses resolution and drifts from KM (see Common issues). The takeaway: when exact times are available, KM and a fine-grained life table give the same answer — so use KM. The life table earns its keep precisely when exact times are not available.
NoteWhich method does my data call for?
Exact event times (the day each event happened) → Kaplan-Meier. It is the standard for clinical-trial follow-up.
Grouped / interval-recorded data (events known only per interval — annual checkups, binned registries) → the life table (this lesson). It is the natural estimator when the resolution of the data is the interval, not the day.
Both estimate the same survival function; the life table is the coarser-resolution sibling of KM.
Try it live
Build the life table yourself — change the interval width (by =) and watch the survival estimates and the agreement with Kaplan-Meier shift. The sandbox boots on first Run.
🟢 With an AI agent
Ask Prova“I have grouped follow-up data with deaths and withdrawals per interval — build an actuarial life table in R, with the withdrawal/2 adjustment and cumulative survival, and check it against a Kaplan-Meier curve” — it answers with base-R code you can run on your own intervals. The runtime is the judge.Ask Prova →
Common issues
You forgot the withdrawal/2 adjustment. Using q = deaths / n_risk (the raw count at the start of the interval) instead of q = deaths / (n_risk - withdrawn/2) over-counts the denominator, under-estimates the death rate, and biases survival upward. The adjustment is the defining feature of the actuarial method — never drop it. (It only matters when there are withdrawals within an interval; with none, the two formulas coincide.)
Intervals too wide — you lose resolution. Very wide intervals lump many deaths into one step, so the curve gets blocky and drifts away from the true (Kaplan-Meier) survival, and the actuarial “uniform withdrawals” assumption gets shakier. Too narrow and most intervals are empty, adding noise. Match the interval to the data’s real resolution (the checkup schedule, the binning) — and aim for a handful of events per interval.
Mixing grouped and exact data. If you actually have exact event times, don’t bin them into a life table — you are throwing away resolution for nothing. Use Kaplan-Meier directly. The life table is for when the data arrive grouped, not for coarsening data you already have at full resolution.
Frequently asked questions
NoteWhat is the difference between a life table and Kaplan-Meier?
A life table (actuarial method) estimates survival over fixed time intervals — it needs only the deaths and withdrawals per interval, not exact event times. Kaplan-Meier places a step at every exact event time. They estimate the same survival function and agree closely; the life table is the coarser-resolution sibling you use when data come grouped (annual checkups, binned registries) and exact times are unavailable.
NoteWhat is the actuarial method in survival analysis?
The actuarial method is the life-table estimator. For each interval it computes the conditional probability of death \(q_j = d_j / (n_j - w_j/2)\) — deaths divided by the effective number at risk, where half the withdrawals are subtracted because censored patients were at risk for only part of the interval — then multiplies the per-interval survival probabilities \(p_j = 1 - q_j\) to get cumulative survival. The name comes from its origins in actuarial (insurance) science.
NoteWhy do you subtract half the withdrawals from the number at risk?
Because a patient censored partway through an interval was at risk for only part of it, not the whole interval. The actuarial method assumes withdrawals are spread uniformly across the interval, so on average each contributes half the interval’s exposure. Subtracting withdrawn/2 gives the effective number at risk; skipping it inflates the denominator and biases survival upward.
NoteWhen should I use a life table instead of Kaplan-Meier?
Use a life table when your data are grouped or interval-recorded — you know how many died and dropped out in each interval but not the exact event times (e.g. periodic checkups, large registries reported per interval). Use Kaplan-Meier whenever you have exact event times, which is the usual clinical-trial case. They agree when intervals are fine, so KM is preferred whenever the exact times exist.
NoteHow do I choose the interval width for a life table?
Match the interval to the resolution of your data — the checkup schedule or the binning the data already have. Aim for a handful of events per interval: too wide and the curve loses resolution and drifts from the true survival; too narrow and most intervals are empty, adding noise. When in doubt, try a couple of widths and confirm the survival estimates are stable.
Test your understanding
ImportantPractice
Build it. In the live cell below, an interval has 100 at risk, 20 deaths, and 10 withdrawals. Fill the blanks to compute the effective number at risk and the conditional probability of survival\(p\) for this interval.
Interpret. If the cumulative survival entering this interval was 0.80, what is the cumulative survival at the end of it?
NoteHint
The effective number at risk subtracts half the withdrawals: n_risk - withdrawn/2. The conditional survival is p = 1 - deaths/eff_n. For the cumulative survival at the end of the interval, multiply the incoming cumulative survival by this interval’s p.
NoteSolution
eff_n <-100-10/2# = 95q <-20/95# = 0.211p <-1- q # = 0.789# Cumulative survival at end of interval:0.80* p # = 0.80 × 0.789 = 0.631
The effective number at risk is 95 (not 100 — half the 10 withdrawals are removed). The conditional probability of surviving the interval is 0.789, so cumulative survival falls from 0.80 to about 0.63 by the end of the interval. Had you forgotten the withdrawal adjustment (q = 20/100 = 0.20, p = 0.80), you would have over-estimated end-of-interval survival as 0.64 — the bias the actuarial correction removes.
TipQuick check
You have a large registry where survival is recorded only at annual visits — you know how many patients died each year but not the exact dates. Which estimator should you use, and why?
NoteShow answer
A life table (actuarial method). The data are grouped into yearly intervals, so you do not have the exact event times that Kaplan-Meier requires — but you do have the deaths and withdrawals per interval, which is exactly what the life table needs. Kaplan-Meier would only be appropriate if you knew the exact day of each death.
Conclusion
You can now run a life table analysis in R: bin grouped follow-up data into intervals, count the number at risk, deaths, and withdrawals per interval, apply the withdrawal/2 actuarial adjustment to get the effective number at risk, and multiply the per-interval survival probabilities into a cumulative survival estimate — all in base R, no special package. The life table is the natural estimator when data come grouped rather than as exact event times; overlaid on the Kaplan-Meier curve it sits almost exactly on it, confirming the two are the same idea at two resolutions. When you do have exact event times, reach for Kaplan-Meier; when your data arrive in intervals, the life table is the right tool.
Every result on this page was produced by the code shown, run at build time against a pinned R environment — edit any block and Run to reproduce it yourself.