Sum of Their Parts
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
- L4
- Asked in
- Egen
Problem
Each event in events carries a user_id, an event_type and an amount, and a user can trigger the same type any number of times. Produce one flat record per user, sorted by user_id ascending, with the user_id and one key per event type, named after that type, holding that type's total amount; a record keeps only the user's three biggest types, and a user with fewer keeps just the ones they triggered, never a zero. When totals tie at the cutoff, the type the user triggered first stays.
A stream of raw actions, folded into one portrait per person.
Example
Input:
events = [{"amount":5,"user_id":"u1","event_type":"click"},{"amount":10,"user_id":"u1","event_type":"view"},{"amount":3,"user_id":"u2","event_type":"click"},{"amount":2,"user_id":"u1","event_type":"click"},{"amount":4,"user_id":"u1","event_type":"scroll"},{"amount":1,"user_id":"u1","event_type":"share"}]Output:
[{"view":10,"click":7,"scroll":4,"user_id":"u1"},{"click":3,"user_id":"u2"}]Worked solution and explanation
What this really is
Strip off the 'pivot' costume and this is a two-level accumulation: a sum bucketed by (user, event_type), then a per-user reshape where each record's keys are decided by that record's own totals. Everyone gets the outer grouping right. What separates candidates is the second level: the three types a user keeps are chosen by ordering that user's totals biggest first, ties going to the type triggered first, and a user with fewer types carries only what they produced, never a zero. The quieter trap is assigning instead of adding: a repeated type keeps only its last amount, the totals shrink, and the wrong types make the cut.
How to get there
Step 1: Accumulate into user -> {type: total}
Walk the events once into a nested dict keyed by user_id, whose value is another dict mapping event_type to its running total. One pass, no pre-scan of the type universe. This is what keeps the pivot sparse: a type only appears in a user's inner dict if that user produced it.
Step 2: Add, never overwrite
Use dict.get(etype, 0) + amount so the second click for the same user folds into the first instead of clobbering it. The single most common wrong answer assigns instead of adds and silently keeps only the last amount.
Step 3: Order each user's types by their own total
For each user, sorted(sums, key=lambda etype: -sums[etype]) lines their types up biggest total first, and [:3] keeps the top three. Because sums was filled in trigger order and the sort is stable, equal totals keep first-trigger order with no extra tie-break key. A dict comprehension over that short list builds the record, and user_id goes in last.
Step 4: Emit records in user_id order
Walk sorted(users) for the outer list, because the stream does not arrive sorted and first-appearance order only passes by luck.
The solution
def pivot_events(events):
users = {}
for event in events:
bucket = users.setdefault(event['user_id'], {})
etype = event['event_type']
bucket[etype] = bucket.get(etype, 0) + event['amount']
result = []
for uid in sorted(users):
sums = users[uid]
# stable sort: equal totals keep the order the user first triggered them
biggest = sorted(sums, key=lambda etype: -sums[etype])[:3]
record = {etype: sums[etype] for etype in biggest}
record['user_id'] = uid
result.append(record)
return resultCommon follow-up questions
- Now make it a dense pivot where every user carries every `event_type`, defaulting to 0. (Tests collecting the full type universe first and initializing each user's row with zeros.)
- How would you express this with pandas instead? (Tests knowledge of `pd.pivot_table` with index, columns, values, and aggfunc.)
- What changes if there are billions of events across millions of users? (Tests streaming or chunked accumulation when the event list does not fit in memory.)
- Now the cap is a parameter `k`, and ties at the cutoff break alphabetically instead. What changes? (Tests whether the candidate knows stability is what carried the first-trigger rule, and that a name tie-break needs an explicit secondary key.)