Sign in to run and submit your work
Reading is open to everyone. Running code and saving drafts need an account so your work is yours and comes back on your next visit.
or
CODE WORKSPACE
Marketing spend is allocated by last-touch attribution: a conversion is credited to the most recent channel the user interacted with before converting, within a lookback window. The current implementation keeps the latest touch per user and joins, which credits touches that happened after the conversion — and search has been getting credit for purchases it followed rather than caused.
Write attribute_conversions(touches, conversions, lookback, channel_priority).
Function to write
attribute_conversions(touches: list[dict], conversions: list[dict], lookback: float, channel_priority: list[str]) -> list[dict]One attribution per conversion with the channel, the touch time and the reason it was or was not attributed.
How to approach it
Filter each user's touches to the window before choosing, and rank the ties explicitly.
Sample cases
+ 2 held back until you submit
a week of marketing
A later touch that must be ignored, a tie at one instant, a touch outside the window, and a user with none.
Input
Argument 1
| user | at | channel |
|---|---|---|
| u1 | 100 | search |
| u1 | 500 | |
| u1 | 500 | social |
| u1 | 900 | search |
| u2 | 10 | |
| u3 | 700 | affiliate |
Argument 2
| id | user | at |
|---|---|---|
| cv1 | u1 | 600 |
| cv2 | u2 | 800 |
| cv3 | u4 | 800 |
| cv4 | u3 | 750 |
Argument 3
300Argument 4
[
'search',
'email',
'social'
]Returns
| conversion_id | user | channel | touch_at | reason |
|---|---|---|---|---|
| cv1 | u1 | 500 | attributed | |
| cv2 | u2 | None | None | no_touch_in_window |
| cv3 | u4 | None | None | no_touch_in_window |
| cv4 | u3 | affiliate | 700 | attributed |
no conversions
Nothing converted, so there is nothing to attribute rather than an empty channel.
Input
Argument 1
| user | at | channel |
|---|---|---|
| u1 | 100 | search |
| u1 | 500 | |
| u1 | 500 | social |
| u1 | 900 | search |
| u2 | 10 | |
| u3 | 700 | affiliate |
Argument 2
[] (empty list)
Argument 3
300Argument 4
[
'search'
]Returns
[] (empty list)
a very long lookback
A generous window brings older touches back into scope and changes the answer.
Input
Argument 1
| user | at | channel |
|---|---|---|
| u1 | 100 | search |
| u1 | 500 | |
| u1 | 500 | social |
| u1 | 900 | search |
| u2 | 10 | |
| u3 | 700 | affiliate |
Argument 2
| id | user | at |
|---|---|---|
| cv1 | u1 | 600 |
| cv2 | u2 | 800 |
| cv3 | u4 | 800 |
| cv4 | u3 | 750 |
Argument 3
100000Argument 4
[
'search',
'email',
'social'
]Returns
| conversion_id | user | channel | touch_at | reason |
|---|---|---|---|---|
| cv1 | u1 | 500 | attributed | |
| cv2 | u2 | 10 | attributed | |
| cv3 | u4 | None | None | no_touch_in_window |
| cv4 | u3 | affiliate | 700 | attributed |
Constraints
lookback before it.channel_priority wins. A channel not in the list ranks after all of them, and ties beyond that are broken by channel name.None channel and the reason no_touch_in_window. It still appears in the output.unusable_timestamp and is not attributed to anything.Worked example
Conversion cv1 happens at 600.0 for u1, whose touches are at 100.0, 500.0 twice and 900.0. The touch at 900.0 is after the conversion and cannot have caused it — crediting it is the bug. The touch at 100.0 is 500 seconds earlier, outside a lookback of 300.0. That leaves the two at 500.0, tied.
The tie is not a detail. Email and social both touched the user at the same instant, and whichever one the sort happens to return decides where a real budget goes. With email ranked above social, email wins — and it wins the same way every time the report runs, which is the property that matters more than the choice itself.
Conversion cv4 for u3 has one touch, on the affiliate channel, which is not in the priority list at all. It is still the only qualifying touch, so it takes the credit: the priority list breaks ties, it does not decide eligibility.
What this tests
Point-in-time correctness again, from the marketing side: nothing after the event may influence the event. The tie-break is the second lesson — attribution is a policy, and a policy that is not deterministic is not a policy.
attribute_conversions(touches: list[dict], conversions: list[dict], lookback: float, channel_priority: list[str]) -> list[dict]Submit for review to find out what your query gets right, what it gets wrong, and how it compares with the best working query for this exercise.
This scenario runs a full workspace — editor, canvas and results side by side. It needs a laptop or desktop to be usable. Open this page on a bigger screen to start building.