Celeus

Survival analysis: Kaplan-Meier, Cox and AFT in R

Survival analysis in R on public lung cancer data: censoring, Kaplan-Meier, log-rank, Cox regression with its proportional hazards check, and AFT.

Updated . Written by Matt Gencer.

On this page

Survival analysis is the family of methods for one kind of outcome: the time until something happens, measured on units that you could not all follow until it did. The “something” can be death, relapse, a machine failing, a seedling flowering or a customer cancelling. What makes these data special is not the subject but the shape of the information. Some units have an exact event time; the rest have only a lower bound, because the study ended or they left it before the event.

This guide walks through a complete survival analysis from start to finish on one public dataset, the North Central Cancer Treatment Group (NCCTG) lung cancer data that ships with R’s survival package. It covers Kaplan-Meier curves, the log-rank test, a Cox model, the proportional hazards check the Cox model rests on (including what a failed check looks like), and when an accelerated failure time (AFT) model describes the data better. The R script for the main model is below the article.

The example uses data from people with advanced cancer because it is a public, standard teaching dataset. It illustrates the statistics. Nothing here is a statement about any person’s prognosis or treatment.

What survival data look like

Every survival dataset needs three things for each unit:

  • A start: a clearly defined time zero. Enrolment in a study, the date of diagnosis, the moment a machine is switched on. Everyone’s clock must start at the same kind of event.
  • A time: how long the unit was followed from that start, in one unit (days, months, hours).
  • A status: whether the follow-up ended in the event of interest, or ended without it.

A unit whose follow-up ended without the event is censored. More precisely it is right-censored: you know the event, if it ever happens, comes after the last time you saw the unit. That is the usual situation, and it is the one this guide handles. Left censoring (the event happened before you started looking) and interval censoring (it happened between two visits) need other methods; values below a detection limit are a related problem that the guide to data below a detection limit covers.

In the NCCTG data, each row is one person. time is the number of days from enrolment to death or to the last contact, and status records which of the two ended the follow-up (coded 2 for died and 1 for censored in the package; the analysis maps 2 to the event). The other columns used here are sex (1 = male, 2 = female), age in years, and two physician-rated performance scores: the ECOG score (ph.ecog, 0 for fully active, higher for more restricted) and the Karnofsky score (ph.karno, 0 to 100, higher for better function).

The dataset has 228 people, of whom 165 died during follow-up. The rest were censored.

Why you cannot simply average the times

Two shortcuts look tempting and both are wrong.

Dropping the censored rows. Censored people are mostly those still alive when the study ended, so they are disproportionately those with the longest times, so removing them pulls every summary towards shorter survival.

Treating the censoring time as an event time. This records deaths that never happened, which also pulls survival downward, and it mixes two different things in one column.

Survival methods do neither. A censored person contributes exactly what is known: they were at risk, and event-free, up to their censoring time, and after that they are no longer counted. Clark and colleagues give a short introduction to why these methods are necessary and how they handle censoring, in the first of a series on the subject.

The assumption no test can check

Every method in this guide assumes non-informative censoring: given the variables in the model, a unit censored at a given time has the same future risk of the event as one that stays under observation. Censoring because a study closed on a fixed date usually satisfies this. Censoring because someone left the study when they became too unwell to attend does not, because the censoring itself signals a higher risk.

No output from any model checks this. It is a question about how the data were collected, and the place to address it is the methods section: say why units were censored and why that is plausibly unrelated to their risk. Two related requirements are follow-up long enough for events to occur and follow-up as complete as possible, because people lost early are censored early, and if their risk differs the censoring is informative. Report how many were lost.

A related trap is a competing event: something that makes the event of interest impossible, such as death from another cause when the outcome is relapse. Treating it as ordinary censoring answers a hypothetical question (what would happen if the competing event could not occur) and usually overstates the risk of the event of interest. Competing risks need their own methods; this guide does not cover them.

Step 1: describe survival with Kaplan-Meier curves

The Kaplan-Meier estimator, published by Kaplan and Meier in 1958, estimates the survival function: the probability of remaining event-free beyond each time. It works one event at a time. At each time where at least one event occurs, the running estimate is multiplied by the fraction of people still at risk who did not have the event at that moment. Censored people leave the risk set between events without causing a drop. The result is a step curve that starts at 1 and falls at each event time.

In symbols, the estimate at time t is the product, over the event times up to t, of (1 minus d/n), where d is the number of events at that time and n the number at risk just before it. A person censored between two event times is counted in n for the earlier one only.

In R, the curve for each sex is one line:

library(survival)
fit_km <- survfit(Surv(time, status) ~ sex, data = lung)
summary(fit_km)$table  # n, events, median and its interval per group
plot(fit_km, xlab = "Days since enrolment", ylab = "Proportion alive")

Surv(time, status) builds the survival outcome: it pairs each time with its status, so every function downstream knows which times are exact and which are censored. In this dataset status is coded 1 and 2, which Surv() reads as censored and event.

Median survival, not mean survival

The usual one-number summary of a Kaplan-Meier curve is the median survival time: the earliest time at which the estimated curve reaches 0.5 or lower. In the companion engine run on these data, the median was 270 days for men (95% confidence interval 212 to 310) and 426 days for women (348 to 550). The interval comes from the curve’s own pointwise 95% confidence band (computed on the log scale, the survfit() default): it covers the times at which that band still contains 0.5.

Two details matter when you report a median. First, it exists only if the curve actually reaches one half; with heavy censoring or a low event rate it may be undefined, and the right summary is then the survival probability at a fixed time. Second, the mean is generally not estimable from a Kaplan-Meier curve when the longest follow-up ends in censoring, because the tail of the curve is unknown. That is the reason survival reports use medians, survival probabilities at stated times, or a restricted mean (discussed below).

Reading the curve honestly

The right-hand end of a Kaplan-Meier curve is estimated from few people, so each drop is large and the confidence band wide. Plot the number at risk under the curve so a reader can see where the estimate stops being informative, and do not read meaning into the last few steps.

Survival, hazard and cumulative hazard

Three functions describe the same distribution. The survival function S(t), which the Kaplan-Meier curve estimates, is the probability of being event-free beyond t. The hazard h(t) is the instantaneous event rate at t among those still at risk; it can rise, fall or stay flat, and the Cox model in Step 3 works with it. The cumulative hazard H(t) adds the hazard up from time zero, and S(t) equals exp(-H(t)).

Step 2: compare groups with the log-rank test

The log-rank test asks whether two or more survival curves differ anywhere over follow-up. At each event time it compares the number of events observed in each group with the number expected if all groups shared one survival curve, given who was at risk. It then sums those differences over all event times and refers the total to a chi-square distribution.

survdiff(Surv(time, status) ~ sex, data = lung)

For men versus women in the NCCTG data, the companion engine run gave a log-rank chi-square of 10.33 on 1 degree of freedom, p = 0.0013. The data are hard to reconcile with identical survival curves for the two sexes.

What the log-rank test does not give you is the size of the difference. It is a test, not an estimate, so report it alongside the medians (or survival at stated times) and, for two groups, a hazard ratio with its interval. In the same run, a Cox model with sex as the only predictor gave a hazard ratio of 0.588 for women relative to men (95% CI 0.424 to 0.816).

Three properties are worth knowing before you rely on the log-rank test:

  • It weights every event time equally. It has the most power when one group’s hazard is a constant multiple of the other’s over the whole follow-up, which is the proportional hazards situation.
  • It can miss crossing curves. If one group does worse early and better later, the early and late differences cancel in the sum, and the test can report little evidence of a difference between curves that are visibly different. Weighted versions (such as the Peto-Peto test) put more weight on early or late differences, but the weighting has to be chosen before you look.
  • It does not adjust for anything. If the groups differ in age or baseline severity, the log-rank comparison mixes that difference with the one you care about. Adjusting needs a model.

Step 3: adjust for covariates with a Cox model

The Cox proportional hazards model, introduced by Cox in 1972, is the standard regression model for survival data. It describes the hazard, the instantaneous event rate among those still at risk at each time, as a baseline hazard that can have any shape, multiplied by a factor that depends on the predictors: For reading the resulting hazard ratios correctly, see how to interpret a hazard ratio; Cox regression in R walks through the coxph() output line by line.

hazard(t) = baseline hazard(t) × exp(b1 × x1 + b2 × x2 + …)

The baseline hazard is never estimated as a formula, which is why the model is called semi-parametric and why it is so widely applicable: you do not need to know how the risk of the event changes over time. What the model does assume is that the ratio of hazards between any two people stays the same over follow-up. That is the proportional hazards assumption, and Step 4 checks it.

Each coefficient, exponentiated, is a hazard ratio: the multiplicative change in the hazard for a one-unit increase in that predictor, with the other predictors held fixed.

Besides proportional hazards, the model assumes non-informative censoring, independent units, and that a continuous predictor acts on the log hazard in a straight line. For the last, plot martingale residuals against the predictor or compare with a flexible (spline) term.

The worked example

The research question for the main example is: adjusting for age and sex, how is the ECOG performance score associated with the hazard of death, and do the hazards stay proportional over follow-up?

fit_cox <- coxph(Surv(time, status) ~ ph.ecog + sex + age, data = lung)
summary(fit_cox)

Some design notes before the numbers. Each person appears once, so no one is measured twice. The people were, however, enrolled at several institutions (the inst column), and people treated at the same institution may be more alike than people at different ones. This model ignores that, so its standard errors assume independence across institutions; cluster(inst) for robust standard errors, or a frailty term, would allow for it (see the section on when survival analysis is the wrong tool). The ECOG score enters as a number, so its hazard ratio is per one-point step and the model assumes each step multiplies the hazard by the same amount; only one person has a score of 3, so the estimate is driven by scores 0 to 2. sex is coded 1 and 2, so its “per unit” hazard ratio is the comparison of women with men. One person has no ECOG score and is dropped, which leaves 227 people and 164 deaths.

The engine fitted this model with the survival package’s coxph(). The results:

Term Hazard ratio 95% CI p
ECOG score (per point) 1.590 1.273 to 1.986 < 0.001
Sex (female vs male) 0.575 0.414 to 0.799 < 0.001
Age (per year) 1.011 0.993 to 1.030 0.232

The intervals are Wald intervals on the log-hazard scale, exponentiated. The likelihood ratio test of the whole model against a model with no predictors was 30.50 on 3 degrees of freedom (p < 0.001).

Reading the hazard ratios

ECOG score. With sex and age held fixed, each one-point higher ECOG score is associated with a hazard of death 1.590 times as high, and the data are compatible with a ratio anywhere from 1.273 to 1.986. The Wald z statistic was 4.083. In the dataset’s terms, people rated as more restricted in daily activity had a higher estimated death rate, and the interval excludes a ratio of 1.

Sex. With ECOG score and age held fixed, women had an estimated hazard 0.575 times that of men (0.414 to 0.799). This is close to the unadjusted 0.588 from the log-rank companion run (which used all 228 people), so in these data adjusting for ECOG score and age changes the estimate for sex very little.

Age. The estimate is a hazard 1.011 times as high per year of age, and the interval runs from 0.993 to 1.030. That interval includes 1, so the data are compatible with no association with age after adjusting for the other two terms, and also with a modest one. A p-value of 0.232 does not show that age is unrelated to survival here; it shows the data cannot tell those possibilities apart.

Test each predictor, not each coefficient

The Wald p-values in the table test one coefficient each. For a predictor that takes several coefficients (a factor with four levels, say), the right test is a likelihood ratio test that drops the whole predictor and compares the fits. The engine reports these term tests for every model. Here each predictor has one coefficient, and the likelihood ratio statistics were 16.62 for ECOG score, 11.44 for sex and 1.45 for age (p = 0.229), each on 1 degree of freedom, which agree with the Wald results.

What a hazard ratio is, and is not

A hazard ratio compares event rates at each moment among those still at risk. It is not a ratio of probabilities of dying, and it is not a ratio of survival times. A hazard ratio of 0.575 does not mean women lived 1/0.575 times as long; for that kind of statement you need either the survival curves themselves or an AFT model (Step 5).

Hernán sets out two further reasons for care when reading a hazard ratio, particularly as a causal effect. The ratio can change over time, so a single number may average quite different early and late effects. And because each moment’s comparison is among those who are still alive, the groups being compared become selected over time, even in a randomised study. Neither problem shows up in the output. Hernán’s suggested remedy is to report (suitably adjusted) survival curves, which show absolute risks over the whole follow-up, alongside the hazard ratio.

Events, not people, set the precision

In a survival analysis, the information comes from the events. A study of 10,000 people with 20 events estimates hazard ratios far less precisely than its size suggests. The engine checks the number of events per estimated parameter against a working floor of 10; here it was 54.7 (164 deaths over 3 parameters), comfortably above it. That check is a floor that flags models at real risk of instability, not an assessment that the model is valid or well specified. The engine does not compute power for a Cox model, and the observed hazard ratios should not be used to plan the size of a new study; a sample size calculation starts from the smallest effect that would matter and the expected event rate.

Step 4: check the proportional hazards assumption

The Cox model’s single hazard ratio per predictor is only a complete description if the ratio is roughly constant over follow-up. This is the step many introductions describe as difficult or leave out, and it is the one that most changes what you are entitled to say.

The test and the plot

The standard check uses scaled Schoenfeld residuals, as developed by Grambsch and Therneau. For each predictor, the residuals at each event time estimate how the coefficient would have to vary over time to fit the data at that moment. If the hazards are proportional, those estimates scatter around a horizontal line. A trend in time suggests the effect is changing. The test fits a line through the residuals against (a transform of) time and asks whether its slope differs from zero; there is one test per term and a global test for the model.

zp <- cox.zph(fit_cox)
zp          # one row per term, plus GLOBAL
plot(zp)    # one panel per term: smoothed coefficient over time

For the main model, the engine reported:

Term Chi-square df p
ECOG score 2.05 1 0.152
Sex 2.31 1 0.129
Age 0.19 1 0.665
Global 4.46 3 0.216

None of the tests detected a departure from proportional hazards. That is the most it says. It is not evidence that the hazards are exactly proportional: the test may lack the power to detect a moderate change, and the residual plot is the better guide to whether any change in the effect over time is large enough to matter. Stensrud and Hernán go further and argue that tests for proportional hazards are unnecessary: hazard ratios are expected to vary over follow-up, so a test that returns a high p-value is probably underpowered, and the Cox hazard ratio should be read as a weighted average over the follow-up period. Their recommendation is to supplement it with survival differences or the restricted mean survival difference at prespecified times.

A second, older check is graphical: for a categorical predictor, plot log(-log(survival)) against log(time) for each group from the Kaplan-Meier fit. Under proportional hazards the curves are roughly parallel; curves that converge, diverge or cross point to a changing effect. It needs groups, so it does not work directly for a continuous predictor such as age, and it does not adjust for the other terms in the model; the residual approach handles both, which is one reason it became the standard.

What a failed check looks like

To show the other outcome on the same data, another engine run replaced the ECOG score with the Karnofsky score (ph.karno), keeping sex and age. The Cox model gave a hazard ratio of 0.987 per Karnofsky point (95% CI 0.975 to 0.998, p = 0.023): higher scores, meaning better physical function, went with a lower hazard.

The proportional hazards check told a different story from the ECOG model. For the Karnofsky term, the Schoenfeld residual test gave a chi-square of 8.02 on 1 degree of freedom, p = 0.0046, and the global test gave p = 0.016. The engine flagged the result with a warning that the assumption may be violated and suggested stratification or a time-varying coefficient.

What this means in practice: the data are hard to reconcile with a Karnofsky effect that is constant over follow-up. The reported hazard ratio of 0.987 is then best read as a kind of weighted average of an effect that changes over time, which is how Stensrud and Hernán describe the hazard ratio when hazards are not proportional. It is not wrong, but it is incomplete, and the residual plot is where to see how the effect changes.

What to do when the check fails

The right response depends on what you need from the model.

  • Look first. plot(cox.zph(fit)) shows the shape of the change. An effect that fades after the first months calls for a different fix from one that reverses.
  • Model the change. Let the coefficient vary with time, either through a time-transform term or by splitting follow-up into periods and estimating a hazard ratio for each. Grambsch and Therneau show how the residual plot itself suggests the form of the time-varying coefficient.
  • Stratify. For a categorical variable you need to adjust for but do not need an estimate of, strata() gives each level its own baseline hazard, so the assumption no longer applies to it. This does not work for the variable you want to estimate.
  • Report a measure that does not need proportional hazards. Survival probability at stated times, or the restricted mean survival time, which Royston and Parmar describe as the area under the survival curve up to a chosen time, are both well defined whatever the hazards do.
  • Consider an AFT model if its assumptions fit better (next step).
# A time-varying effect for the Karnofsky score: its log hazard ratio changes with log(time)
fit_tt <- coxph(Surv(time, status) ~ ph.karno + sex + age + tt(ph.karno),
                data = lung, tt = function(x, t, ...) x * log(t))

# Stratify on a categorical variable you do not need an estimate for
fit_strat <- coxph(Surv(time, status) ~ ph.ecog + age + strata(sex), data = lung)

These code blocks show the R calls; the engine runs in this guide did not fit them, so no numbers from them are quoted.

Step 5: an accelerated failure time model

An accelerated failure time model takes a different view of the same data. Instead of multiplying the hazard, each predictor stretches or shrinks the time scale. Its effect measure is a time ratio: the factor by which a one-unit change in the predictor multiplies survival time. A time ratio above 1 means longer survival, below 1 shorter, which is the opposite direction from a hazard ratio.

Wei describes the AFT model as a useful alternative to the Cox model with an intuitive interpretation, because it regresses the logarithm of the survival time on the covariates. The trade-off is that a parametric AFT model has to assume a distribution for the event times. The Weibull is the common choice: it allows a hazard that rises or falls steadily over time, and it is the one distribution under which the model is both an AFT model and a proportional hazards model.

fit_aft <- survreg(Surv(time, status) ~ ph.ecog + sex + age, data = lung, dist = "weibull")
summary(fit_aft)
exp(coef(fit_aft))  # time ratios

A further engine run fitted this model with the same three predictors and the same 227 people:

Term Time ratio 95% CI
ECOG score (per point) 0.712 0.605 to 0.839
Sex (female vs male) 1.493 1.172 to 1.903
Age (per year) 0.993 0.979 to 1.006

Read in the AFT way: holding sex and age fixed, each one-point higher ECOG score is associated with survival times about 0.712 times as long, with the data compatible with factors from 0.605 to 0.839. Women’s survival times were estimated to be 1.493 times as long as men’s at the same ECOG score and age. The age interval includes 1. The likelihood ratio test of the whole model was 29.98 on 3 degrees of freedom, close to the Cox model’s 30.50, and the term test for ECOG score was 16.64.

The model’s log scale parameter was -0.313, so the scale was about exp(-0.313) = 0.73. In survreg() the scale is 1 divided by the usual Weibull shape, so the fitted shape was about 1 / 0.73 = 1.37. A shape above 1 (a survreg() scale below 1) means a hazard that rises over follow-up; a scale of exactly 1 is the exponential model with a constant hazard. That describes the fitted Weibull, not the data directly, so compare it with the Kaplan-Meier curves before relying on it.

Cox or AFT?

Both models gave the same direction and very similar strength of evidence for each predictor here, which is common. Choose by the question and the assumptions:

  • Use Cox when you want to avoid assuming any shape for the baseline hazard, when proportional hazards is plausible, and when your field reports hazard ratios.
  • Use AFT when a statement about time (how much a predictor shortens or lengthens survival) is what readers need, when the distribution fits the data (compare the fitted and Kaplan-Meier curves), or when proportional hazards fails but a constant time ratio is plausible.
  • Report one primary model, chosen before seeing the results, and treat the other as a sensitivity analysis rather than picking whichever looks better.

The Kaplan-Meier and Cox regression guide goes deeper on this choice with a different, synthetic engineering example.

How to report a survival analysis

A complete report lets a reader judge the follow-up, the events and the assumptions as well as the estimates. Built from the main example:

Of 228 participants, 165 died during follow-up and the remainder were censored at last contact. Median survival was 270 days in men (95% CI 212 to 310) and 426 days in women (348 to 550); log-rank chi-square 10.33, 1 df, p = 0.0013. In a Cox model adjusting for sex and age (227 participants with complete data, 164 deaths; one excluded for a missing ECOG score), each one-point increase in ECOG performance score was associated with a higher hazard of death (hazard ratio 1.590, 95% CI 1.273 to 1.986; p < 0.001). Schoenfeld residual tests did not detect a departure from proportional hazards (global p = 0.216); [state what the residual plots showed]. Participants were enrolled at several institutions; standard errors did not account for clustering by institution.

Alongside that paragraph, a full report includes:

  • The time origin, the time unit and why follow-up ended for censored units.
  • The number of events, overall and by group, not only the number of units.
  • The length of follow-up, usually as the median follow-up estimated with the “reverse” Kaplan-Meier method (censoring treated as the event), so a reader knows how far the curves reach.
  • Kaplan-Meier curves with the number at risk shown below them.
  • Each hazard ratio or time ratio with its confidence interval, and which variables the model adjusted for.
  • How the proportional hazards assumption was checked, and what was done if it failed.
  • How missing data were handled (here, complete-case analysis that dropped one person).

Common mistakes

Analysing time to event as an ordinary continuous outcome. A t-test or linear regression on the times ignores censoring and treats the last contact as if it were the event. The t-tests guide explains what that test assumes; censored times violate it.

Analysing only “dead or alive at the end” with logistic regression. This throws away when the events happened and treats someone followed for a week the same as someone followed for two years. It answers a different question and needs everyone to have the same follow-up. The logistic regression guide covers when a binary outcome is the right choice.

Defining groups by something that happens during follow-up. Comparing people who received a later treatment with those who did not, from a common start, gives the first group guaranteed survival up to the treatment time, which biases the comparison in its favour. A variable that changes during follow-up must enter the model as time-varying.

Reporting a hazard ratio as a ratio of risks or of survival times. It is a ratio of instantaneous rates. If readers need risks or times, report survival at stated times or use an AFT model.

Ignoring competing events or informative censoring. No model output warns you about either. Both are questions about how the data were collected.

When survival analysis is the wrong tool

Survival methods fit one time-to-first-event per unit with right censoring. Other designs need other methods:

  • Competing events that prevent the event of interest: competing-risks methods (cumulative incidence functions, cause-specific or subdistribution hazards).
  • Repeated events on the same unit, such as several hospital admissions or machine faults: recurrent-event models that account for events within a unit being related.
  • Clustered units, such as patients within hospitals: frailty terms or robust standard errors for the clustering.
  • Left-censored or interval-censored values, including values below a detection limit: censored regression or interval-censored methods.
  • Time recorded only in coarse periods, or a share of units that will never have the event: discrete-time models and cure models.
  • Everyone followed to the event with no censoring: ordinary methods for a continuous outcome may then be enough, although the times are often skewed. The guide to choosing a statistical test starts from the design question.

Rerunning the example

Below the article is the R script exactly as the engine generated it, with the seed and the bundled data path, and the output it printed. The script loads the data, maps status from 1 and 2 to 0 and 1, drops the one row with a missing value in the model variables, and fits the Cox model. On the same package versions it reproduces the hazard ratios and their intervals, the likelihood ratio term tests and the cox.zph proportional hazards table quoted in Steps 3 and 4.

The Kaplan-Meier and log-rank numbers, the AFT time ratios and the Karnofsky model come from three further engine runs on the same data, using the same R calls as the code blocks in Steps 1, 2, 4 and 5. They are not part of the script below. The guide to reproducible statistical analysis explains what a reviewer needs in order to rerun a result like this.

For the method details behind each step, see the engine’s reference pages for Cox regression, Kaplan-Meier with the log-rank test and the Weibull AFT model.

Worked example

Data: Loprinzi CL, Laurie JA, Wieand HS, Krook JE, Novotny PJ, Kugler JW, et al. (1994). Prospective evaluation of prognostic variables from patient-completed questionnaires. Journal of Clinical Oncology 12(3):601-607. https://doi.org/10.1200/JCO.1994.12.3.601. Seed: 20261022.

Run on Celeus engine 0.30.0, seed 20261022, public data.

Output

# Results

- Test: Cox proportional-hazards regression
- n: 227
- statistic: 4.083
- p-value: < 0.001
- effect size: hazard ratio = 1.59 (95% CI 1.273 to 1.986)
- 95% CI: [1.273, 1.986]
- rows used: 227 of 228 (1 dropped)
- seed: 20261022

## Assumption checks
- proportional hazards (cox.zph): passed=TRUE (no evidence against proportional hazards)
- events per variable: passed=TRUE (at or above the rule-of-thumb floor (a convention, not a guarantee of a stable model))

## Term tests
| Term | Test | Statistic | df | Residual df | p-value |
|---|---|---|---|---|---|
| ph.ecog | LR | 16.62 | 1 | - | < 0.001 |
| sex | LR | 11.44 | 1 | - | < 0.001 |
| age | LR | 1.447 | 1 | - | 0.229 |

## Power
- Power is not calculated for this test.
R script
# Celeus - reproduce.R  (run from this package folder; bundled data in data/)
# Test: Cox proportional-hazards regression  |  engine 0.30.0  |  catalog c39-d3cd
# Research context: not provided for this run.
# Reproduces the recorded result deterministically.

set.seed(20261022)
.data_path <- "data/survival-analysis-guide.csv"
if (!file.exists(.data_path)) stop("Run reproduce.R from the package folder (the bundled data is in data/).")
data <- readr::read_csv(.data_path, show_col_types = FALSE)  # needs the readr package
clean_cells <- function (df) 
{
    ctrl_collapse <- "[\r\n\t\v\f]+"
    zero_width_chars <- intToUtf8(c(8203L, 65279L), multiple = TRUE)
    uni_space_chars <- intToUtf8(c(160L, 8239L, 8199L), multiple = TRUE)
    clean_text <- function(x) {
        out <- x
        for (ch in zero_width_chars) out <- gsub(ch, "", out, 
            fixed = TRUE)
        out <- gsub(ctrl_collapse, " ", out)
        for (ch in uni_space_chars) out <- gsub(ch, " ", out, 
            fixed = TRUE)
        trimws(gsub(" +", " ", out))
    }
    changed_cols <- character(0)
    cells_cleaned <- 0L
    retyped <- character(0)
    for (j in seq_along(df)) {
        col <- df[[j]]
        if (!is.character(col)) 
            next
        cleaned <- clean_text(col)
        n_changed <- sum(!is.na(col) & !is.na(cleaned) & col != 
            cleaned, na.rm = TRUE)
        if (n_changed > 0L) {
            changed_cols <- c(changed_cols, names(df)[j])
            cells_cleaned <- cells_cleaned + n_changed
        }
        df[[j]] <- cleaned
        non_blank <- cleaned[!is.na(cleaned) & nzchar(cleaned)]
        if (length(non_blank) && all(grepl("^[+-]?(0|[1-9][0-9]*)([.][0-9]+)?$", 
            non_blank))) {
            parsed <- suppressWarnings(as.numeric(cleaned))
            was_blank <- is.na(cleaned) | !nzchar(cleaned)
            lost <- (is.na(parsed) | is.infinite(parsed)) & !was_blank
            if (!any(lost)) {
                finite <- parsed[!is.na(parsed)]
                all_whole <- all(finite == floor(finite))
                in_range <- all(abs(finite) <= .Machine$integer.max)
                df[[j]] <- if (all_whole && in_range) 
                  as.integer(parsed)
                else parsed
                retyped <- c(retyped, names(df)[j])
            }
        }
    }
    names(df) <- clean_text(names(df))
    if (length(changed_cols)) {
        attr(df, "ingest_clean_columns") <- changed_cols
        attr(df, "ingest_clean_cells_cleaned") <- cells_cleaned
    }
    if (length(retyped)) 
        attr(df, "ingest_clean_retyped") <- retyped
    df
}
data <- clean_cells(data)
# Complete-case analysis: drop rows missing any analysis value.
data[["status"]] <- as.integer(factor(data[["status"]], levels = c("1", "2"))) - 1L
.acols <- c("ph.ecog", "sex", "age", "time", "status")
data <- data[stats::complete.cases(data[, .acols, drop = FALSE]), , drop = FALSE]
result <- { .fit <- survival::coxph(survival::Surv(`time`, `status`) ~ `ph.ecog` + `sex` + `age`, data = data); list(summary = summary(.fit), ph = survival::cox.zph(.fit), term_tests = stats::drop1(.fit, test = "Chisq")) }
print(result)

Common questions

What is survival analysis?

Survival analysis is the set of methods for an outcome that is the time until an event, when some units are still event-free at the end of follow-up. Those censored times are kept as 'lasted at least this long' rather than dropped or treated as events. The event can be anything with a clear time: death, relapse, a machine failing, a customer leaving.

Why can't I just drop the censored observations?

Censored units are usually the ones that lasted longest, so dropping them biases every summary towards shorter times, and counting their censoring time as an event invents events that never happened. Survival methods use a censored unit for exactly what is known: it was at risk, and event-free, up to its censoring time.

When do I use Kaplan-Meier and when Cox regression?

Use Kaplan-Meier curves and the log-rank test to describe survival and compare a few groups without adjusting for anything. Use Cox regression when you want a hazard ratio for one variable while adjusting for others, or when a predictor is continuous. Most analyses report both: the curves to show the data, the model for the adjusted estimate.

What does a hazard ratio of 1.59 mean?

It means that, at any moment during follow-up, the instantaneous event rate is estimated to be 1.59 times higher for a one-unit increase in the predictor, holding the other model terms fixed. It is a ratio of rates, not of probabilities or of survival times, and it is a single number only if the ratio stays roughly constant over time.

Do I need survival analysis if no observations are censored?

Not strictly: with every unit followed to the event, the times are fully observed and methods for a continuous outcome can work, although survival times are often skewed and a log scale or a rank method may fit better. Survival methods still apply and give the same kind of output as a censored analysis, which keeps results comparable with other studies.

What should I do if the proportional hazards assumption fails?

First look at the residual plot to see how the effect changes over time. Then either model that change (a time-varying coefficient, or a hazard ratio per time period), stratify on a categorical variable you do not need an estimate for, or report a measure that does not need proportional hazards, such as the difference in survival at a stated time or the restricted mean survival time. An accelerated failure time model is another option when its own assumptions fit.

Do I need to check proportional hazards before using the log-rank test?

Not as a gate. The log-rank test is most powerful when the hazard ratio is roughly constant, but it remains a valid test of whether the curves differ. If the curves cross, it can miss a real difference. If crossing is plausible before the data are seen, prespecify a weighted test or a restricted mean survival comparison; if the curves cross unexpectedly, report that alongside the log-rank result rather than switching tests.

What is the difference between the survival function and the hazard function?

The survival function is the probability of being event-free beyond a given time, and a Kaplan-Meier curve estimates it. The hazard function is the instantaneous event rate at that time among those still at risk. The cumulative hazard adds the hazard up over time, and survival equals the exponential of minus the cumulative hazard.

How many events do I need for a Cox model?

The events, not the number of people, set the precision. A common rule of thumb asks for roughly ten events per estimated parameter as a floor against unstable estimates. It is a screen, not a guarantee, and a sample size calculation should start from the smallest effect that would matter and the expected event rate.

Sources

  1. Kaplan EL, Meier P (1958). Nonparametric estimation from incomplete observations. Journal of the American Statistical Association 53(282):457-481. doi:10.1080/01621459.1958.10501452
  2. Cox DR (1972). Regression models and life-tables. Journal of the Royal Statistical Society: Series B (Methodological) 34(2):187-202. doi:10.1111/j.2517-6161.1972.tb00899.x
  3. Grambsch PM, Therneau TM (1994). Proportional hazards tests and diagnostics based on weighted residuals. Biometrika 81(3):515-526. doi:10.1093/biomet/81.3.515
  4. Wei LJ (1992). The accelerated failure time model: a useful alternative to the Cox regression model in survival analysis. Statistics in Medicine 11(14-15):1871-1879. doi:10.1002/sim.4780111409
  5. Clark TG, Bradburn MJ, Love SB, Altman DG (2003). Survival analysis part I: basic concepts and first analyses. British Journal of Cancer 89(2):232-238. doi:10.1038/sj.bjc.6601118
  6. Hernán MA (2010). The hazards of hazard ratios. Epidemiology 21(1):13-15. doi:10.1097/EDE.0b013e3181c1ea43
  7. Stensrud MJ, Hernán MA (2020). Why test for proportional hazards? JAMA 323(14):1401. doi:10.1001/jama.2020.1267
  8. Royston P, Parmar MKB (2013). Restricted mean survival time: an alternative to the hazard ratio for the design and analysis of randomized trials with a time-to-event outcome. BMC Medical Research Methodology 13:152. doi:10.1186/1471-2288-13-152

Methods discussed

Try it on your own data

Upload a dataset and Celeus suggests a method, checks its assumptions, runs it in R, and gives you the script to rerun it.

Try it in Celeus