The Trip Aggregator
A medium Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- medium
- Seniority
- L3
- Asked in
- Booking.com
Problem
The per-user destination counts on our travel dashboard look inflated for anyone who revisits a city, so we're recomputing them from the raw trips records. For every user, report their total hours traveled and how many different destinations they've visited, listed alphabetically by user_id.
Travel records hold patterns waiting to be surfaced.
Example
Input:
trips = [{"origin":"NYC","user_id":"u1","destination":"LA","duration_hours":6},{"origin":"LA","user_id":"u1","destination":"NYC","duration_hours":6},{"origin":"NYC","user_id":"u2","destination":"LA","duration_hours":6},{"origin":"NYC","user_id":"u1","destination":"LA","duration_hours":6}]Output:
[{"user_id":"u1","total_hours":18,"unique_destinations":2},{"user_id":"u2","total_hours":6,"unique_destinations":1}]Worked solution and explanation
Two aggregates that disagree about repeats
This is SUM(duration_hours) and COUNT(DISTINCT destination) under GROUP BY user_id, wearing a Python costume. The whole problem lives in the gap between those two aggregates. Hours are additive: every trip counts, repeats included. Destinations are not: a second 'LA' trip adds six hours and zero destinations. Candidates who keep one counter per user, or who dedupe trips before totaling, get one of the two numbers wrong for exactly the users who revisit a place. That is 'u1' in the example: 3 destinations instead of 2, or 12 hours instead of 18.
Step 1: Key the state by user_id
Each user needs two accumulators that behave differently: an integer for hours and a set for destinations. Create both the first time a user_id shows up. The origin field never enters the state.
Step 2: Add every trip's hours, but only new destinations
Every trip adds its duration_hours, repeats included, because that time was really spent. The destination goes into the set, which quietly ignores a second 'LA'. The set's len() is then the distinct count, with no cleanup pass afterwards.
Step 3: Emit users in sorted key order
Build one dict per user with user_id, total_hours and unique_destinations, walking sorted(users.keys()) rather than the dict's insertion order. Trips arrive in log order, and the first user the log mentions is not necessarily the first alphabetically.
def aggregate_trips(trips: list) -> list:
users = {}
for trip in trips:
uid = trip["user_id"]
if uid not in users:
users[uid] = {"total_hours": 0, "destinations": set()}
users[uid]["total_hours"] += trip["duration_hours"]
users[uid]["destinations"].add(trip["destination"])
result = []
for uid in sorted(users.keys()):
entry = {
"user_id": uid,
"total_hours": users[uid]["total_hours"],
"unique_destinations": len(users[uid]["destinations"])
}
result.append(entry)
return result- One counter per user
- Bumping a count on every trip gives 'u1' a destination count of 3, because the repeat 'LA' trip counts again. Deduping trips first fixes that count and breaks
total_hoursinstead, dropping 'u1' to 12. - A counter and a set per user
- The integer takes every
duration_hours, and the set takes everydestinationbut keeps one copy of each. 'u1' lands at 18 hours and 2 destinations, which matches the example.
Common follow-up questions
- Trips now carry a `cost` field. How do you add total spend per user? (Tests whether another additive accumulator slots into the same per-user state.)
- How would you write this as one SQL query? (Tests `SUM(duration_hours)` and `COUNT(DISTINCT destination)` under `GROUP BY user_id`, with `ORDER BY user_id`.)
- What changes if the trip log is too large to fit in memory? (Tests partitioning by `user_id` so each user's set lives on one worker, or approximate distinct counting.)