A funnel counts units that complete declared steps in order within a stated observation window.
Ordered funnels: step order, entry rules and conversion windows
Decide who may enter
A closed checkout funnel begins only with an eligible checkout-start event. An open funnel can begin at a later step, which answers a different question. Choose one before computing stage rates. If users may restart checkout, decide whether to analyze first attempt, every attempt or one attempt per session. Event identity sets the unit; the funnel rule sets the sequence.
Enforce order and time
Suppose the steps are checkout start, payment attempt and order confirmation. A payment attempt before the selected checkout start does not qualify, even if it belongs to the same user. A confirmation 73 hours later should not count for a 48-hour funnel. Process each actor’s accepted events in occurrence-time order, with event ID as a stable tie-break. Use a half-open window if the boundary matters, and write down whether exactly 48 hours qualifies.
Protect the observation cutoff
A checkout that began an hour before today’s data cutoff has not had 48 hours to convert. Either exclude immature entries from the fully observed 48-hour rate or report them as still pending. Treating pending entries as failures biases recent cohorts downward. Censored outcomes provide another analysis route when the full conversion-time distribution matters.
Report both conditional and entry rates
If 83 users entered, 57 attempted payment and 41 confirmed an order, payment-to-confirmation is 41/57 while entry-to-confirmation is 41/83. These are not interchangeable. Publish counts, conditional stage rates and the total entry rate. A stage can improve while the full funnel worsens if the incoming mix changes. Composition checks belong beside the chart.
Inspect missing paths
A user may complete a purchase without a recorded payment-attempt event because of an alternate wallet flow or instrumentation gap. Do not force that path into a failed step. Count and inspect skipped-step sequences. A changed payment provider can alter event emission without changing orders, so reconcile confirmations against the transaction ledger before assigning blame to customer behavior.
Implementation
def completed_checkout(accepted_events, actor_id, window_minutes=2880):
actor_events = sorted(
(event for event in accepted_events if event["actor_id"] == actor_id),
key=lambda event: (event["minute"], event["event_id"]),
)
entry_minute = None
payment_seen = False
for event in actor_events:
if entry_minute is None:
if event["kind"] == "checkout_start":
entry_minute = event["minute"]
continue
if event["minute"] - entry_minute > window_minutes:
return False
if event["kind"] == "checkout_start":
return False # This report measures the first attempt only.
if event["kind"] == "payment_attempt":
payment_seen = True
elif payment_seen and event["kind"] == "order_confirmed":
return True
return False
assert completed_checkout([
{"actor_id": "buyer-23", "event_id": "e1", "minute": 5, "kind": "checkout_start"},
{"actor_id": "buyer-23", "event_id": "e2", "minute": 8, "kind": "payment_attempt"},
{"actor_id": "buyer-23", "event_id": "e3", "minute": 11, "kind": "order_confirmed"},
], "buyer-23")Performance and operating cost
Sorting E events for one actor costs O(E log E) time and O(E) memory; the state-machine scan is O(E). The report deliberately measures the first attempt and ends it at a restart. A session-based or every-attempt funnel needs a different attribution rule, stated before counting.
Common Mistakes
- Do not count later-step events that occurred before funnel entry.
- Do not mark recent entries as failures before their conversion window closes.
- Do not report a stage-to-stage rate as if it were the rate from initial entry.
