Exponential Half-Life Multi-Touch Marketing Attribution Normalized to 100%
1. Analytical Significance & What It Means
Scenario & Frontier TrapCalculates exponential time-decay attribution across previous marketing touches (7-day half-life: w = 2^(-delta_t / 7)), strictly normalizing touchpoint weights so attributed revenue sums to exactly 100% of conversion value.
Naive queries multiply raw exponential decay directly by order revenue without dividing by the sum of weights. In our scenario (raw weights sum to 1.75), naive attribution distributes 175% ($612.50) of the actual $350.00 revenue, severely corrupting marketing ROI calculations.
Attribute conversion revenue ($300.00) across touchpoints occurring within 30 days prior to purchase.
Use exponential time-decay weighting with a 7-day half-life:
raw_weight = POW(0.5, date_diff('second', click_time, conversion_time) / (7.0 * 86400.0))
CRITICAL: Attributed revenue MUST be normalized across touches so the sum of attributed revenue exactly equals the $300.00 conversion revenue.
Required output columns:
- touch_id (INT)
- channel (VARCHAR)
- attributed_revenue (DECIMAL)
Order by touch_id ASC.
2. Underlying Tables, Row Counts & Data Volume
2 Tables (2,000,000 rows • 64.8 MiB)Production-scale fixtures loaded into the isolated DuckDB sandbox with at least 1,000,000 records per table:
order_id (INTEGER) user_id (INTEGER) order_revenue (DECIMAL(10,2)) conversion_time (TIMESTAMP)| # | order_id | user_id | order_revenue | conversion_time |
|---|---|---|---|---|
| 1 | 101 | 42 | 350 | 2026-03-15 12:00:00 |
| 2 | 101 | 42 | 377.27 | 2026-03-01 09:30:00.000004 |
| 3 | 102 | 43 | 266.6 | 2026-03-01 09:30:00.000033 |
| 4 | 103 | 44 | 391.22 | 2026-03-01 09:30:00.000046 |
| 5 | 104 | 45 | 372.02 | 2026-03-01 09:30:00.000063 |
| 6 | 105 | 46 | 317.61 | 2026-03-01 09:30:00.000085 |
| 7 | 106 | 47 | 420.67 | 2026-03-01 09:30:00.000111 |
| 8 | 107 | 48 | 392.91 | 2026-03-01 09:30:00.000127 |
| 9 | 108 | 49 | 443.28 | 2026-03-01 09:30:00.000154 |
| 10 | 109 | 50 | 435.5 | 2026-03-01 09:30:00.000173 |
| 11 | 110 | 51 | 333.04 | 2026-03-01 09:30:00.000183 |
| 12 | 111 | 52 | 339.94 | 2026-03-01 09:30:00.000202 |
| 13 | 112 | 53 | 339.53 | 2026-03-01 09:30:00.000222 |
| 14 | 113 | 54 | 377.5 | 2026-03-01 09:30:00.000244 |
| 15 | 114 | 55 | 307.88 | 2026-03-01 09:30:00.000266 |
touch_id (INTEGER) user_id (INTEGER) channel (VARCHAR) click_time (TIMESTAMP)| # | touch_id | user_id | channel | click_time |
|---|---|---|---|---|
| 1 | 1 | 42 | Google Search | 2026-03-01 12:00:00 |
| 2 | 2 | 42 | Facebook Ad | 2026-03-08 12:00:00 |
| 3 | 3 | 42 | Email Promo | 2026-03-15 12:00:00 |
| 4 | 3 | 42 | Google Search | 2026-03-01 09:30:00.000001 |
| 5 | 4 | 43 | Facebook Ad | 2026-03-01 09:30:00.000027 |
| 6 | 5 | 44 | Email Promo | 2026-03-01 09:30:00.000045 |
| 7 | 6 | 45 | Google Search | 2026-03-01 09:30:00.000064 |
| 8 | 7 | 46 | Facebook Ad | 2026-03-01 09:30:00.000085 |
| 9 | 8 | 47 | Email Promo | 2026-03-01 09:30:00.000111 |
| 10 | 9 | 48 | Google Search | 2026-03-01 09:30:00.000136 |
| 11 | 10 | 49 | Facebook Ad | 2026-03-01 09:30:00.000144 |
| 12 | 11 | 50 | Email Promo | 2026-03-01 09:30:00.000162 |
| 13 | 12 | 51 | Google Search | 2026-03-01 09:30:00.000195 |
| 14 | 13 | 52 | Facebook Ad | 2026-03-01 09:30:00.000219 |
| 15 | 14 | 53 | Email Promo | 2026-03-01 09:30:00.000232 |
3. Graphical Query Execution Plan & Cost Bottlenecks
Physical Operator Tree (DuckDB 2.0 Profiler)Visual flowchart of the physical execution pipeline. Operators with high relative costs (≥ 20% of query runtime or heavy latency) are visually highlighted:
| Operator | Cost % | Latency | Processed Rows | Data Volume | Details / Table | Status |
|---|---|---|---|---|---|---|
| HASH_JOIN | 54.1% | 106.94 ms | 533,542 | 22.39 MB | - | 🔥 BOTTLENECK |
| ORDER_BY | 11.6% | 22.99 ms | 533,542 | 14.25 MB | - | Normal |
| WINDOW | 11.6% | 22.89 ms | 533,542 | 22.39 MB | Projections: sum(raw_weight) OVER () | Normal |
| TABLE_SCAN | 7.0% | 13.82 ms | 1,000,000 | 30.52 MB | memory.main.touchpoints | Normal |
| PROJECTION | 5.1% | 10.03 ms | 533,542 | 14.25 MB | Projections: ['touch_id', 'channel', 'attributed | Normal |
| PROJECTION | 3.7% | 7.26 ms | 533,542 | 18.32 MB | Projections: ['touch_id', 'channel', 'order_reve | Normal |
| PROJECTION | 3.2% | 6.23 ms | 533,542 | 14.25 MB | Projections: ['#0', '__internal_compress_string_ | Normal |
| PROJECTION | 2.8% | 5.58 ms | 533,542 | 14.25 MB | Projections: ['#0', '__internal_decompress_strin | Normal |
| TABLE_SCAN | 0.9% | 1.81 ms | 999,998 | 19.07 MB | memory.main.purchase_conversions | Normal |
| PROJECTION | 0.0% | 0.09 ms | 533,542 | 22.39 MB | Projections: ['#0', '#1', '#2', '#3', '#4'] | Normal |
4. Queries Executed (Candidate vs Golden Ground Truth)
Beautified & Syntax HighlightedWITH touchpoint_weights AS
(SELECT t.touch_id,
t.channel,
c.order_revenue,
pow(0.5, date_diff('second', t.click_time, c.conversion_time) / (7.0 * 86400.0)) AS raw_weight
FROM touchpoints t
JOIN purchase_conversions c ON t.user_id = c.user_id
AND t.click_time <= c.conversion_time
AND t.click_time >= c.conversion_time - INTERVAL 30 DAY)
SELECT touch_id,
channel,
cast(order_revenue * (raw_weight / sum(raw_weight) OVER ()) AS decimal(10, 2)) AS attributed_revenue
FROM touchpoint_weights
ORDER BY touch_id ASC;
WITH weighted_touches AS
(SELECT t.touch_id,
t.channel,
c.order_revenue,
pow(0.5, date_diff('second', t.click_time, c.conversion_time) / (7.0 * 86400.0)) AS raw_weight
FROM purchase_conversions c
JOIN touchpoints t ON c.user_id = t.user_id
AND t.click_time <= c.conversion_time
AND t.click_time >= c.conversion_time - INTERVAL 30 days),
total_weight AS
(SELECT *,
sum(raw_weight) OVER () AS sum_weights
FROM weighted_touches)
SELECT touch_id,
channel,
round(order_revenue * (raw_weight / sum_weights), 2) AS attributed_revenue
FROM total_weight
ORDER BY touch_id ASC;
CREATE TABLE touchpoints (touch_id INT, user_id INT, channel VARCHAR, click_time TIMESTAMP);
CREATE TABLE purchase_conversions (order_id INT, user_id INT, order_revenue decimal(10, 2),
conversion_time TIMESTAMP);
INSERT INTO touchpoints
VALUES (1, 42, 'Google Search', '2026-03-01 12:00:00'),
-- 14 days before (weight = 0.25)
(2, 42, 'Facebook Ad', '2026-03-08 12:00:00'),
-- 7 days before (weight = 0.50)
(3, 42, 'Email Promo', '2026-03-15 12:00:00'); -- 0 days before (weight = 1.00)
INSERT INTO purchase_conversions
VALUES (101, 42, 350.00, '2026-03-15 12:00:00');
5. Query Results & Expected Ground Truth
Tabular Comparison| # | touch_id | channel | attributed_revenue |
|---|---|---|---|
| 1 | 1 | Google Search | 0 |
| 2 | 2 | Facebook Ad | 0 |
| 3 | 3 | Email Promo | 0 |
| 4 | 3 | Google Search | 0 |
| 5 | 3 | Google Search | 0 |
| 6 | 4 | Facebook Ad | 0 |
| 7 | 5 | Email Promo | 0 |
| 8 | 7 | Facebook Ad | 0 |
| 9 | 8 | Email Promo | 0 |
| 10 | 10 | Facebook Ad | 0 |
| 11 | 11 | Email Promo | 0 |
| 12 | 16 | Facebook Ad | 0 |
| 13 | 17 | Email Promo | 0 |
| 14 | 18 | Google Search | 0 |
| 15 | 20 | Email Promo | 0 |
| 16 | 21 | Google Search | 0 |
| 17 | 23 | Email Promo | 0 |
| 18 | 24 | Google Search | 0 |
| 19 | 28 | Facebook Ad | 0 |
| 20 | 29 | Email Promo | 0 |
| 21 | 31 | Facebook Ad | 0 |
| 22 | 32 | Email Promo | 0 |
| 23 | 35 | Email Promo | 0 |
| 24 | 36 | Google Search | 0 |
| 25 | 37 | Facebook Ad | 0 |
| # | touch_id | channel | attributed_revenue |
|---|---|---|---|
| 1 | 1 | Google Search | 0 |
| 2 | 2 | Facebook Ad | 0 |
| 3 | 3 | Email Promo | 0 |
| 4 | 3 | Google Search | 0 |
| 5 | 3 | Google Search | 0 |
| 6 | 4 | Facebook Ad | 0 |
| 7 | 5 | Email Promo | 0 |
| 8 | 7 | Facebook Ad | 0 |
| 9 | 8 | Email Promo | 0 |
| 10 | 10 | Facebook Ad | 0 |
| 11 | 11 | Email Promo | 0 |
| 12 | 16 | Facebook Ad | 0 |
| 13 | 17 | Email Promo | 0 |
| 14 | 18 | Google Search | 0 |
| 15 | 20 | Email Promo | 0 |
| 16 | 21 | Google Search | 0 |
| 17 | 23 | Email Promo | 0 |
| 18 | 24 | Google Search | 0 |
| 19 | 28 | Facebook Ad | 0 |
| 20 | 29 | Email Promo | 0 |
| 21 | 31 | Facebook Ad | 0 |
| 22 | 32 | Email Promo | 0 |
| 23 | 35 | Email Promo | 0 |
| 24 | 36 | Google Search | 0 |
| 25 | 37 | Facebook Ad | 0 |