Estimate Page-Visit Duration from the Next User Event
AirbnbData Engineer · OnsiteLast updated
Implement logic to compute user page-visit durations based on adjacent event timestamps, deterministically resolving ties and handling boundary conditions for terminal events.
Problem Statement
In event tracking pipelines, the time a user spends on a page is commonly inferred from the arrival time of their subsequent event. Given a list of event records from user browsing sessions, calculate the duration in seconds that each user spent on each visited page.
Each event record contains:
event_id(integer): A unique identifier for the event.user_id(string): The identifier for the user.page_name(string): The name of the page visited.timestamp(integer): Unix timestamp in seconds when the event occurred.
Requirements
- Group events by
user_id. - Sort each user's events chronologically by
timestampin ascending order. If two events have the identicaltimestamp, break ties deterministically byevent_idin ascending order. - The
duration_secondsfor an event is calculated as thetimestampof the user's immediately following event minus the current event'stimestamp. - If an event is the user's final event in the dataset, its
duration_secondsshould beNone. - The function should return the list of records with an added key
duration_seconds, sorted byuser_idascending, thentimestampascending, thenevent_idascending.
Function Signature
def calculate_page_durations(events: list[dict]) -> list[dict]:
pass
Input & Output Format
- Input: A list of dictionaries, where each dictionary represents an event with keys
event_id,user_id,page_name, andtimestamp. - Output: A list of dictionaries containing the original fields plus
duration_seconds(an integer orNone).
Constraints
0 <= len(events) <= 100,000timestampis a non-negative integer (Unix epoch in seconds).event_idis a unique positive integer across all events.user_idandpage_nameare non-empty strings.
Examples
Example 1
events = [
{"event_id": 2, "user_id": "user_A", "page_name": "search", "timestamp": 1600000030},
{"event_id": 1, "user_id": "user_A", "page_name": "home", "timestamp": 1600000000},
{"event_id": 3, "user_id": "user_A", "page_name": "listing", "timestamp": 1600000100}
]
# Output:
[
{"event_id": 1, "user_id": "user_A", "page_name": "home", "timestamp": 1600000000, "duration_seconds": 30},
{"event_id": 2, "user_id": "user_A", "page_name": "search", "timestamp": 1600000030, "duration_seconds": 70},
{"event_id": 3, "user_id": "user_A", "page_name": "listing", "timestamp": 1600000100, "duration_seconds": None}
]
Example 2 (Timestamp Tie)
events = [
{"event_id": 12, "user_id": "user_B", "page_name": "checkout", "timestamp": 1600000500},
{"event_id": 10, "user_id": "user_B", "page_name": "cart", "timestamp": 1600000500}
]
# Output:
[
{"event_id": 10, "user_id": "user_B", "page_name": "cart", "timestamp": 1600000500, "duration_seconds": 0},
{"event_id": 12, "user_id": "user_B", "page_name": "checkout", "timestamp": 1600000500, "duration_seconds": None}
]
Edge Cases to Consider
- Empty input list
[]should return[]. - A user having only a single event must have
duration_seconds: None. - Simultaneous events (identical timestamps) resulting in
duration_seconds: 0.
Hints
Hint 1
Sort the entire list or group by user using a tuple key `(user_id, timestamp, event_id)` to handle ordering and tie-breaking in a single pass.
Hint 2
When iterating over sorted events, check if the next event belongs to the same user before taking the difference in timestamps.
Hint 3
Be careful to avoid mutating the input dictionaries directly if the caller expects idempotency.
Follow-up questions
- How would you implement this same calculation in SQL using window functions like `LEAD()`?
- If a gap between events exceeds 30 minutes, it likely represents a session boundary rather than active reading time. How would you adjust your logic to cap or nullify duration in that case?
- How would you scale this calculation if the input dataset is too large to fit in memory on a single machine (e.g., using PySpark or MapReduce)?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.