Build an event ledger, verify tied-time risk sets, estimate day-six resolution and bounded open time, and separate cancellations from censoring.
Project: audit support resolution time and competing exits
Freeze a case-level cohort
Start with cases created during a documented enrollment window and follow each from creation to first resolution, cancellation, or extraction cutoff. Keep one case ID, queue, created timestamp, final event timestamp, terminal status and source-record version per row. Reject impossible negative intervals and duplicate IDs before analysis. If the extract includes only cases reaching a specialist queue, change the time origin or explicitly model delayed entry. Cohort entry is part of the estimand.
Make the event table reviewable
Use an eight-case single-outcome fixture first: durations and resolution flags (2,1), (3,0), (3,1), (5,1), (5,0), (8,1), (10,0), (10,1). The risk sets at distinct days are eight, seven, five, three and two. Check that each day removes exactly its resolutions and censors before the next day. The day-three event and censor share the same seven-case denominator. The event-table lesson supplies the accounting rule.
Reproduce the fixed-horizon metrics
Calculate unresolved shares of 0.875, 0.75 and 0.60 after days two, three and five. Day-six estimated resolution is 0.40, and restricted mean unresolved time through day six is 4.975 case-days. These are training-fixture values, not a production service claim. Capture the calculation code, denominator table, selected horizon and extraction cutoff in the project output. The bounded-time measure avoids inventing a distant tail.
Introduce cancellations deliberately
Create a second six-case ledger with one resolution at day two; one cancellation and one censor at day three; one resolution at day five; one resolution and one cancellation at day seven. Verify final cumulative resolution incidence 11/18 and cancellation incidence 7/18. Do not merge this fixture into the first one: it tests a different outcome policy. Explain why censoring cancellations in a resolution-only curve would answer a different question. Competing risks provides the accounting.
Write the release decision
The packet should contain cohort SQL or extraction logic, a status-code map, one-row-per-case checks, event table, unresolved curve, cumulative incidence by terminal outcome, common-horizon restricted mean, risk counts and a caveat on censoring. Compare queue groups only after checking their entry dates and follow-up maturity. If cancellation meanings shifted during collection or the day-six risk set is too thin, withhold the cross-queue comparison and document the repair needed.
Implementation
def support_resolution_audit(case_records, horizon_days):
if len({case_id for case_id, _, _ in case_records}) != len(case_records):
raise ValueError("duplicate case ID")
day_counts = {}
for case_id, day, outcome in case_records:
if day < 0 or outcome not in {"resolved", "cancelled", "open"}:
raise ValueError("invalid case record")
day_counts.setdefault(day, {"resolved": 0, "cancelled": 0, "open": 0})[outcome] += 1
at_risk = len(case_records)
event_free = 1.0
resolution_incidence = 0.0
cancellation_incidence = 0.0
open_days = 0.0
previous_day = 0
for day, counts in sorted(day_counts.items()):
if day > horizon_days:
break
open_days += (day - previous_day) * event_free
resolution_incidence += event_free * counts["resolved"] / at_risk
cancellation_incidence += event_free * counts["cancelled"] / at_risk
event_free *= 1 - (counts["resolved"] + counts["cancelled"]) / at_risk
at_risk -= sum(counts.values())
previous_day = day
if previous_day < horizon_days and at_risk == 0:
raise ValueError("no observed support through horizon")
open_days += (horizon_days - previous_day) * event_free
return resolution_incidence, cancellation_incidence, open_days
cases = [("R31", 2, "resolved"), ("R32", 3, "cancelled"),
("R33", 3, "open"), ("R34", 5, "resolved"),
("R35", 7, "cancelled"), ("R36", 7, "resolved")]
resolution, cancellation, open_days = support_resolution_audit(cases, 7)
assert round(resolution, 6) == round(11 / 18, 6)
assert round(cancellation, 6) == round(7 / 18, 6)
assert open_days > 0Performance and operating cost
The ledger grouping pass is O(N) expected time and O(U) space; sorting U distinct days costs O(U log U), followed by an O(U) scan. A release build should also test timestamp ordering, status-code provenance and group-specific follow-up. These checks catch errors faster hardware cannot.
Common Mistakes
- Do not reuse the single-event fixture as if its censors were known cancellations.
- Do not publish a day-six queue comparison without sufficient follow-up in both queues.
- Do not turn an unresolved status into a resolution event to make totals reconcile.
Read next
- Event-time risk sets and tied outcomes
- Kaplan–Meier curves for support resolution
- Competing risks and resolution incidence
- Restricted mean unresolved time at a business horizon
Continue the workflow: Project: audit specialist-queue resolution comparisons.
