Repair event flags, choose a supported service horizon and compare unresolved time without hiding curve crossings.
Project: compare specialist queues with censored resolution times
Lock the episode contract
A support organization compares two specialist queues on first completed ticket resolution after assignment. Define each ticket’s start, event, censoring time and group at assignment. A ticket still open at export is right-censored. Transfers to a different team need their own disposition because transfer may be related to difficulty; they cannot silently become resolutions. Risk-set rules depend on this event ledger.
Find the export error
The first feed marks transferred tickets as completed when a transfer message was sent. That creates an artificial early resolution advantage for the queue with more transfers. Reconcile transfer and close events using immutable ticket IDs, then rebuild curves and at-risk counts. One queue now resolves more quickly early, while the other catches up later. A single median hides the crossing pattern, and a global test is not a substitute for the service question. Censoring definitions keep unresolved follow-up in the dataset.
Compare a fixed horizon
The service commitment is 12 hours. Check that both queues have enough observed follow-up and at-risk tickets at 12 hours, then calculate restricted mean unresolved time through that horizon. Report the difference with uncertainty based on independent assigned units. If a group has almost no support near 12 hours, hold the comparison rather than extrapolating. The area measure summarizes time within the fixed window, not every part of the curve.
Publish limits and action
Deliver group sizes, event and censor counts, transfer audit, curves, at-risk table, horizon support, area difference and sensitivity to transfer handling. The queues were not randomized, so describe the contrast as observational and examine priority mix. A rollout decision should also check late unresolved tail and staffing cost. Tail support and the target frame close the packet.
Implementation
def queue_comparison_gate(report, limits):
if report["transfers_mislabeled_as_resolved"]:
return "hold:event-ledger"
if min(report["at_risk_at_horizon"].values()) < limits["minimum_at_risk"]:
return "hold:horizon-support"
if report["censoring_difference"] > limits["maximum_censor_gap"]:
return "review:censoring-pattern"
if not report["assignment_randomized"]:
return "publish:observational-contrast"
return "review:randomized-contrast"
limits = {"minimum_at_risk": 17, "maximum_censor_gap": 0.12}
report = {"transfers_mislabeled_as_resolved": 4,
"at_risk_at_horizon": {"queue-a": 24, "queue-b": 19},
"censoring_difference": 0.06, "assignment_randomized": False}
assert queue_comparison_gate(report, limits) == "hold:event-ledger"
assert queue_comparison_gate({**report, "transfers_mislabeled_as_resolved": 0},
limits) == "publish:observational-contrast"
Performance and operating cost
The gate is O(g) time for g groups and O(g) temporary space for group counts. Rebuilding reliable event histories and checking censoring mechanisms are the costly steps. A quick curve from the flawed transfer flag would answer a different question with false precision.
Common Mistakes
- Treating transfer as completed resolution.
- Discarding tickets still open at extraction.
- Choosing the comparison horizon after inspecting crossing curves.
- Describing nonrandom queue assignment as a causal treatment effect.
Read next
- Survival risk sets: keep censored cases until they leave observation
- Restricted mean time: compare curves at a supported horizon
- Population, estimand and sampling frame: name the quantity before calculating
- Right censoring and time-to-event analysis for open cases
- Survival-curve uncertainty and thin-tail support
Continue the workflow: Project: audit support resolution when customers can withdraw.
