A product-limit survival curve updates at events using the number still at risk immediately before each event time.
Survival risk sets: keep censored cases until they leave observation
Define the clock and event
A specialist queue measures time from ticket assignment to first completed resolution. Some tickets remain open when the reporting window ends; those observations are right-censored rather than resolved at the censoring time. Specify start time, resolution rule, administrative end, timezone and whether reopened tickets count as a new episode. The censoring contract prevents unresolved tickets from being discarded as inconvenient missing outcomes.
Build each risk set
At a distinct event time, divide the number of resolutions by the number of tickets still observed and unresolved immediately beforehand, then multiply survival by one minus that fraction. Censored tickets leave the risk set after contributing observed follow-up. When an event and censoring share a recorded time, state the tie convention; the small program processes events before removals at that time. The curve estimates the probability of remaining unresolved beyond time t under its censoring assumptions. The foundational curve lesson describes the same estimator at a broader level.
Expose tail support
A late curve step can be based on only two tickets. Report the number at risk at decision horizons and avoid extrapolating after follow-up ends. Censoring must be reasonably independent of the event time conditional on the design; tickets transferred to another team because they are difficult violate a casual administrative-censoring story. Compare transfer reasons and censoring frequency by queue. Tail uncertainty needs this support table.
Compare a decision horizon
For a service commitment such as 12 hours, report the unresolved probability at 12 hours with interval and at-risk count, if follow-up supports that horizon. If curves cross, one global test can obscure early and late differences; restricted mean time gives a horizon-based quantity. The project catches an export that records transferred tickets as if they resolved immediately.
Implementation
from collections import defaultdict
def unresolved_survival_curve(cases):
if not cases or any(hours < 0 or event not in (0, 1)
for hours, event in cases):
raise ValueError("valid observed durations required")
by_time = defaultdict(lambda: [0, 0])
for hours, event in cases:
by_time[hours][0 if event else 1] += 1
at_risk = len(cases)
survival = 1.0
steps = []
for hours in sorted(by_time):
events, censored = by_time[hours]
if events:
survival *= 1 - events / at_risk
steps.append((hours, survival, at_risk))
at_risk -= events + censored
return steps
curve = unresolved_survival_curve([(4, 1), (6, 0), (8, 1), (8, 0)])
assert curve == [(4, 0.75, 4), (8, 0.375, 2)]
Performance and operating cost
Grouping n cases by time is O(n) expected time and O(k) space for k distinct times; sorting times costs O(k log k). The bigger cost is correct episode and censoring records. A mathematically exact curve from misclassified transfers can systematically understate unresolved work.
Common Mistakes
- Dropping open tickets instead of retaining their observed follow-up.
- Counting censored tickets as resolved at their last contact.
- Ignoring how event and censor ties are ordered.
- Reporting a late survival estimate without the number still at risk.
Read next
- Restricted mean time: compare curves at a supported horizon
- Project: compare specialist queues with censored resolution times
- Right censoring and time-to-event analysis for open cases
- Kaplan–Meier curves for support resolution
- Survival-curve uncertainty and thin-tail support
Continue the workflow: Competing events: calculate the probability of each first outcome.
