Survival Analysis
When to use this skill
Use when the outcome is "how long until X happens?" and some observations haven't experienced X yet (censored). Triggers:
- "Time to churn / cancel / conversion"
- "Survival curve for…"
- "Hazard ratio of feature X"
- "How long do users stay before churning?"
- "Compare retention across segments over time"
If you only care about whether an event happens within a fixed window, use logistic-regression. If you need fixed-time-point retention rates without timing, use cohort-analysis.
Why not just use logistic regression?
| Question |
Right tool |
| Did the user churn within 90 days? |
logistic |
| When did the user churn? Distribution? |
survival |
| How does plan tier affect churn timing? |
survival |
| Users still active — do we throw them out? |
survival (treats as censored, not missing) |
Throwing out censored observations biases logistic regression toward early events.
Required inputs
| Input |
Why it matters |
| Subject ID |
One row per subject |
| Duration |
Time from start to event OR last observation |
| Event indicator (1/0) |
1 = event happened, 0 = censored (still observed without event) |
| Covariates |
Features that may affect timing |
| Start time |
When observation begins (often signup_at) |
Workflow
Define event precisely. "Churn" must have an exact definition:
- Subscription cancellation date
- No-activity threshold (e.g., 30 consecutive inactive days)
- Account deletion
Define censoring clearly. A subject is censored if:
- Still active at the end of the observation window (right censoring — most common)
- Lost to follow-up (e.g., switched accounts, account suspended for unrelated reason)
- Did not start before observation window (left truncation)
Build the duration table: one row per subject with start, duration, event.
df = pd.DataFrame({
"subject_id": ...,
"duration_days": (event_date - start_date).dt.days,
"event": did_event.astype(int),
# ... covariates
})
Fit Kaplan-Meier (KM) curves overall and by segment.
- Plot survival function S(t) = P(T > t)
- 95% CI bands (Greenwood formula)
- Median survival time (if reached)
Compare groups with log-rank test to check if curves differ significantly.
Fit Cox Proportional Hazards model for multivariable analysis.
- Hazard ratios with 95% CIs
- Check the PH assumption (Schoenfeld residuals, proportional hazards plot). If violated for a feature, stratify or include time-interaction term.
Communicate in business language:
- "Median time-to-churn is 142 days. Users on annual plans have HR = 0.42, meaning ~58% lower instantaneous churn risk at any given time."
- Visualize KM curves with confidence bands by segment.
Output format
# Survival Analysis: <event>
## Setup
- Event: <e.g., subscription cancellation>
- Start time: <e.g., signup_at>
- Censoring: subjects still active as of <observation_end_date>
- Sample: N = <total>, events = <X (X%)>
- Duration unit: <days | weeks | months>
- Median follow-up: <D days>
## Kaplan-Meier summary
| Segment | N | Events | Median survival | S(30d) | S(90d) | S(180d) |
|---|---|---|---|---|---|---|
| Overall | 24,800 | 6,420 (26%) | 312 days | 92% | 81% | 71% |
| Monthly plan | 12,400 | 4,810 (39%) | 184 days | 88% | 71% | 58% |
| Annual plan | 12,400 | 1,610 (13%) | not reached | 96% | 91% | 84% |
Log-rank test (monthly vs annual): χ² = 2,143, p < 0.0001 — **significantly different**
## Cox proportional hazards model
| Covariate | HR | 95% CI | p | Notes |
|---|---|---|---|---|
| plan = annual (ref: monthly) | 0.42 | [0.38, 0.46] | < 0.001 | annual reduces hazard by 58% |
| tenure_segment = enterprise | 0.31 | [0.25, 0.39] | < 0.001 | |
| support_tickets_30d | 1.18 | [1.14, 1.22] | < 0.001 | each ticket: +18% hazard |
| has_team_admin | 0.67 | [0.61, 0.73] | < 0.001 | |
| age_days (per 30 days) | 0.94 | [0.93, 0.95] | < 0.001 | older accounts more stable |
Concordance index: 0.74
## Proportional hazards assumption check
- Schoenfeld residuals: <p-value per covariate>
- Covariates violating PH: <list, or "none">
- For violators: <stratified by | time-interacted | accepted with caveat>
## Visualization
- KM curves with 95% CI bands (overall + key segments)
- Hazard ratios with CIs as forest plot
- (Optional) Cumulative incidence functions
## Caveats
- <e.g., observation window 18 months; long-term survival beyond ~500 days extrapolated>
- <e.g., Annual-plan users haven't reached median — estimate uncertain>
- <e.g., Right censoring assumption assumes censoring is independent of churn likelihood; verify>
## Decision implications
- <e.g., shifting customers to annual plans should yield ~50% reduction in long-run churn>
- <e.g., support ticket spike is a strong leading indicator — trigger intervention at 3+ tickets in 30 days>
## Next steps
- <e.g., A/B test annual plan upsell campaign>
- <e.g., build early-warning model using ticket count + tenure>
Validation checks
Edge cases & failure modes
- Informative censoring: if subjects with higher churn risk are more likely to be censored (e.g., paused accounts), Cox model is biased. Hard to detect. Investigate the censoring mechanism.
- Competing risks: a subject can experience multiple mutually exclusive events (churn vs upgrade). Use competing risk models (Fine-Gray), not standard Cox.
- PH violation: a covariate's effect changes over time (e.g., new users have higher early churn). Stratify by that variable or add time-interaction term.
- Time-varying covariates: feature values change during the observation period (e.g., support tickets accumulate). Use time-dependent Cox model with long-format data.
- Left truncation: subjects only enter observation after some time. Use
entry parameter in lifelines.
- Heavy ties (many events at same time): use Efron or exact tie-breaking, not Breslow.
Scripts
scripts/survival_fit.py — Fit KM by segment + Cox PH model, output readout.
python scripts/survival_fit.py \
--input subjects.csv \
--duration tenure_days \
--event churned \
--segment plan_tier \
--covariates plan_tier,support_tickets_30d,has_team_admin
Related skills
cohort-analysis — for fixed-time-point retention without censoring complexity
logistic-regression — for binary outcome within a fixed window
data-quality-audit — verify start/end dates and event indicators before fitting
causal-inference — if you need to estimate intervention effect on survival
1---2name: survival-analysis3description: Runs censoring-aware time-to-event analysis using Kaplan-Meier curves and Cox proportional hazards models. Use when the user mentions survival analysis, time-to-event, time-to-churn, time-to-conversion, hazard ratio, Kaplan-Meier, Cox model, censored data, or duration modeling.4---56# Survival Analysis78## When to use this skill910Use when the outcome is "**how long until X happens?**" and some observations haven't experienced X yet (censored). Triggers:1112- "Time to churn / cancel / conversion"13- "Survival curve for…"14- "Hazard ratio of feature X"15- "How long do users stay before churning?"16- "Compare retention across segments over time"1718If you only care about *whether* an event happens within a fixed window, use `logistic-regression`. If you need fixed-time-point retention rates without timing, use `cohort-analysis`.1920## Why not just use logistic regression?2122| Question | Right tool |23|---|---|24| Did the user churn within 90 days? | logistic |25| When did the user churn? Distribution? | survival |26| How does plan tier affect churn timing? | survival |27| Users still active — do we throw them out? | survival (treats as censored, not missing) |2829Throwing out censored observations biases logistic regression toward early events.3031## Required inputs3233| Input | Why it matters |34|---|---|35| Subject ID | One row per subject |36| Duration | Time from start to event OR last observation |37| Event indicator (1/0) | 1 = event happened, 0 = censored (still observed without event) |38| Covariates | Features that may affect timing |39| Start time | When observation begins (often signup_at) |4041## Workflow42431. **Define event precisely.** "Churn" must have an exact definition:44 - Subscription cancellation date45 - No-activity threshold (e.g., 30 consecutive inactive days)46 - Account deletion47482. **Define censoring clearly.** A subject is censored if:49 - Still active at the end of the observation window (right censoring — most common)50 - Lost to follow-up (e.g., switched accounts, account suspended for unrelated reason)51 - Did not start before observation window (left truncation)52533. **Build the duration table:** one row per subject with `start, duration, event`.54 ```python55 df = pd.DataFrame({56 "subject_id": ...,57 "duration_days": (event_date - start_date).dt.days,58 "event": did_event.astype(int),59 # ... covariates60 })61 ```62634. **Fit Kaplan-Meier (KM) curves overall and by segment.**64 - Plot survival function S(t) = P(T > t)65 - 95% CI bands (Greenwood formula)66 - Median survival time (if reached)67685. **Compare groups with log-rank test** to check if curves differ significantly.69706. **Fit Cox Proportional Hazards model** for multivariable analysis.71 - Hazard ratios with 95% CIs72 - **Check the PH assumption** (Schoenfeld residuals, proportional hazards plot). If violated for a feature, stratify or include time-interaction term.73747. **Communicate in business language:**75 - "Median time-to-churn is 142 days. Users on annual plans have HR = 0.42, meaning ~58% lower instantaneous churn risk at any given time."76 - Visualize KM curves with confidence bands by segment.7778## Output format7980```markdown81# Survival Analysis: <event>8283## Setup84- Event: <e.g., subscription cancellation>85- Start time: <e.g., signup_at>86- Censoring: subjects still active as of <observation_end_date>87- Sample: N = <total>, events = <X (X%)>88- Duration unit: <days | weeks | months>89- Median follow-up: <D days>9091## Kaplan-Meier summary92| Segment | N | Events | Median survival | S(30d) | S(90d) | S(180d) |93|---|---|---|---|---|---|---|94| Overall | 24,800 | 6,420 (26%) | 312 days | 92% | 81% | 71% |95| Monthly plan | 12,400 | 4,810 (39%) | 184 days | 88% | 71% | 58% |96| Annual plan | 12,400 | 1,610 (13%) | not reached | 96% | 91% | 84% |9798Log-rank test (monthly vs annual): χ² = 2,143, p < 0.0001 — **significantly different**99100## Cox proportional hazards model101| Covariate | HR | 95% CI | p | Notes |102|---|---|---|---|---|103| plan = annual (ref: monthly) | 0.42 | [0.38, 0.46] | < 0.001 | annual reduces hazard by 58% |104| tenure_segment = enterprise | 0.31 | [0.25, 0.39] | < 0.001 | |105| support_tickets_30d | 1.18 | [1.14, 1.22] | < 0.001 | each ticket: +18% hazard |106| has_team_admin | 0.67 | [0.61, 0.73] | < 0.001 | |107| age_days (per 30 days) | 0.94 | [0.93, 0.95] | < 0.001 | older accounts more stable |108109Concordance index: 0.74110111## Proportional hazards assumption check112- Schoenfeld residuals: <p-value per covariate>113- Covariates violating PH: <list, or "none">114- For violators: <stratified by | time-interacted | accepted with caveat>115116## Visualization117- KM curves with 95% CI bands (overall + key segments)118- Hazard ratios with CIs as forest plot119- (Optional) Cumulative incidence functions120121## Caveats122- <e.g., observation window 18 months; long-term survival beyond ~500 days extrapolated>123- <e.g., Annual-plan users haven't reached median — estimate uncertain>124- <e.g., Right censoring assumption assumes censoring is independent of churn likelihood; verify>125126## Decision implications127- <e.g., shifting customers to annual plans should yield ~50% reduction in long-run churn>128- <e.g., support ticket spike is a strong leading indicator — trigger intervention at 3+ tickets in 30 days>129130## Next steps131- <e.g., A/B test annual plan upsell campaign>132- <e.g., build early-warning model using ticket count + tenure>133```134135## Validation checks136137- [ ] Event definition is exact (date or threshold)138- [ ] Censoring rule documented (and not correlated with event)139- [ ] Duration is non-negative140- [ ] KM curves include CI bands141- [ ] Log-rank test reported when comparing groups142- [ ] Cox model: PH assumption checked143- [ ] Concordance index reported (Cox's analog of AUC, typically 0.6-0.85)144145## Edge cases & failure modes146147- **Informative censoring**: if subjects with higher churn risk are more likely to be censored (e.g., paused accounts), Cox model is biased. Hard to detect. Investigate the censoring mechanism.148- **Competing risks**: a subject can experience multiple mutually exclusive events (churn vs upgrade). Use competing risk models (Fine-Gray), not standard Cox.149- **PH violation**: a covariate's effect changes over time (e.g., new users have higher early churn). Stratify by that variable or add time-interaction term.150- **Time-varying covariates**: feature values change during the observation period (e.g., support tickets accumulate). Use time-dependent Cox model with long-format data.151- **Left truncation**: subjects only enter observation after some time. Use `entry` parameter in lifelines.152- **Heavy ties** (many events at same time): use Efron or exact tie-breaking, not Breslow.153154## Scripts155156- `scripts/survival_fit.py` — Fit KM by segment + Cox PH model, output readout.157158```bash159python scripts/survival_fit.py \160 --input subjects.csv \161 --duration tenure_days \162 --event churned \163 --segment plan_tier \164 --covariates plan_tier,support_tickets_30d,has_team_admin165```166167## Related skills168169- `cohort-analysis` — for fixed-time-point retention without censoring complexity170- `logistic-regression` — for binary outcome within a fixed window171- `data-quality-audit` — verify start/end dates and event indicators before fitting172- `causal-inference` — if you need to estimate *intervention* effect on survival