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

One-pass accumulate, then reshape
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 result

Common follow-up questions