learn-marketing-attribution-with-phoebe / Builder session 7 of 10
Learn Marketing Attribution with Phoebe · Builder session 7 of 10

GA4 DDA in practice: and reconciling the tools that all claim the sale

GA4's Data-Driven Attribution is now the default, and it is Shapley with a time-decay twist - which means you already understand its engine. What you have to learn is its limits: it is a black box that only sees Google-observable touches, and it lives in a world where Meta and TikTok each self-attribute the very same conversion. Sum those platform numbers and your blended ROAS beats reality. This session is the builder's reconciliation job: pull GA4 DDA from the BigQuery export, line it up against your own warehouse model, de-duplicate, and treat every platform's self-reported number as a ceiling, not a truth.

🟠 Advanced Builders · analytics engineers GA4 + BigQuery 45 min
0-4 · Set-up 4-18 · What DDA does 18-30 · Double-counting 30-45 · Reconcile
Part 0

The tool that grades its own homework

Every ad platform in your stack shares one convenient habit: it reports the conversions it thinks it caused, and it is generous with itself. GA4, Meta, and TikTok will each raise a hand for the same Lumen order. Individually the numbers look great. Added up, they describe a business selling more than it actually sold. Your job as the builder is not to trust any single platform's self-attribution - it is to pull them into one warehouse, reconcile them against ground truth, and hand your leaders a de-duplicated number they can defend. GA4 DDA is a useful input to that. It is not the answer.

Live - coded in session Self-study - read after class ★ Build-along - everyone runs it The data: Lumen Skincare
★ What you ship today A query that pulls Lumen's conversions and channels from the GA4 BigQuery export, a reconciliation join that finds the orders GA4 and your warehouse both claim, and a single de-duplicated cross-platform ROAS view that survives contact with a CFO who asks "why do these three dashboards add up to more than our revenue?"
Part 1 · under the hood

What GA4 DDA actually does 6 min live

Google removed the rules-based models from GA4 reporting; Data-Driven Attribution is now the default. Under the hood it is close to what you have already built. DDA trains a model that predicts conversion probability from the journey, then runs the Shapley algorithm - feeding every combination of touchpoints (present or absent) through the trained probabilistic model to measure each touch's marginal effect - and layers in a time-decay element so more recent interactions carry more weight. Its defaults: the last 50 interactions per journey, a 90-day lookback. So far, so familiar. The catch is everything Google does not hand you.

Journey last 50 touches, 90-day lookback Trained model predicts P(convert) weights not exposed Shapley + decay every touch combo recent weighted more Credit per channel ▲ black box - the model and per-channel weights are Google's, not yours DDA = Shapley + time-decay. Same engine you built in B5 - but you cannot inspect, audit, or reproduce its weights. Worse: it only sees Google-observable, consented, web/app touches. Your Meta, TikTok, CTV, and offline touches are invisible to it. So DDA is a partial view computed by a model you cannot open. Useful - but never mistake it for the full cross-channel journey.
🔍 Click to zoom - GA4 DDA is Shapley + time-decay inside a black box that only sees Google-observable touches
LiveThree limits that decide how you use DDA3 min

DDA's engine is sound. Its blind spots are structural, and each one changes how much you can lean on it:

  • It is a black box. Google does not expose the model or the per-channel weights. You get the output credit, never the machinery. You cannot reproduce it, unit-test it, or explain a swing to a stakeholder from first principles - which is exactly why you also keep your own warehouse model.
  • It only sees what Google can see. Consented, web/app, Google-observable touches. A journey that ran through Meta, TikTok, CTV, an influencer link, or an offline touch is - to DDA - simply not there. It attributes the credit it can see to the channels it can see.
  • Its defaults are choices. Last 50 interactions, 90-day lookback. Fine for most, wrong for a long-consideration purchase. Know the window before you compare its numbers to anything.
Real world

A brand saw GA4 hand 55% of credit to organic and direct and concluded paid social "didn't work". What actually happened: the paid-social touches lived on Meta, which GA4 could not observe, so the credit collapsed onto the Google-visible tail of each journey. The black box wasn't lying - it was answering honestly about a journey it could only half see.

Self-studyWhy the DDA weights aren't in the export2 min read

Builders reach for the GA4 BigQuery export expecting to find the per-channel DDA weights sitting in a column. They are not there. The export gives you the raw events - traffic source, session, conversion value, timestamps - which is everything you need to build your own attribution. It does not give you Google's model output per touch, because the model is proprietary. That is not an oversight; it is the black box, made concrete. The practical consequence: from the export you reconstruct credit yourself, and you use the DDA numbers you see in the GA4 UI as a cross-check, not as a source of truth you can decompose.

Part 2 · the core problem

The double-counting problem 5 min live

Here is the failure that quietly inflates half the marketing reports in the world. GA4, Meta, and TikTok each run their own attribution and each self-report conversions. When a single Lumen customer sees a TikTok ad, clicks a Meta ad, and converts after a Google search, all three platforms claim that one $92 order. Add their conversion counts together and you get three sales from one. Divide spend by that inflated total and your blended ROAS looks heroic. It is arithmetic fiction. View-through versus click-through conventions and direct or branded-search inflation make it worse.

GA4 Meta TikTok 1 order · $92 Each platform claims it: GA4 says "+1 conversion" Meta says "+1 conversion" TikTok says "+1 conversion" Summed = 3 sales from 1 → blended ROAS exceeds reality One conversion, three claimants. Platform self-attribution is a ceiling, never a sum. De-dupe to one truthful row per order.
🔍 Click to zoom - one conversion claimed by GA4, Meta, and TikTok; add them up and the business looks 3x bigger than it is
LiveWhy platform numbers are a ceiling, not a truth3 min

Every self-attributing platform is optimizing to justify its own spend. That is not villainy; it is incentive. But it means their conversion counts share three inflation habits you have to strip out:

  • Overlapping claims. The same order appears in multiple platforms' totals. Summed, they overstate. The fix is de-duplication against a single order key from your warehouse.
  • View-through generosity. Some platforms count a conversion when the user merely saw an ad (view-through), not clicked. Compare view-through and click-through numbers separately - they are not the same currency.
  • Direct and branded-search inflation. A user who already decided to buy types "lumen serum" - branded search or direct traffic grabs a last-click it did not earn. Ground truth is your order table, not the platform tag.
The reconciliation mindset Treat every platform's self-reported conversions as the most it could possibly have driven - a ceiling. Your job is to work down from those ceilings to one de-duplicated number tied to real orders, not up from zero by trusting each tag.
Part 3 · the builder's job

The reconciliation workflow 4 min live

Reconciliation is a pipeline, not an opinion. You pull each source into the warehouse, key everything to a real order, de-duplicate the overlapping claims, and compute ROAS against actual revenue. GA4 DDA becomes one input in that pipeline - valuable for the Google-observable slice, but reconciled against your own touchpoint model and the raw platform exports rather than trusted on its own.

Land everything in one place. GA4 DDA from the BigQuery export, Meta and TikTok conversion exports, and your own warehouse touchpoint model (B1-B5) - all into the warehouse, all keyed to order_id.

De-duplicate against ground truth. Your order table is the single source of what actually sold. Every platform claim gets matched to a real order; unmatched claims are flagged, not summed.

Treat self-attribution as a ceiling. Where platforms overlap on one order, keep the order once. The platform totals become upper bounds you reconcile down from, never numbers you add together.

Report one de-duplicated ROAS. Total spend over de-duplicated real revenue - a blended number that ties back to the P&L. Then, and only then, break it down by your own attribution model for channel-level decisions.

Build-along 1 of 3

Pull GA4 DDA credit from the BigQuery export ★ 11 min · everyone

GA4 exports one row per event to BigQuery. We pull Lumen's purchase conversions with each session's channel and order value. Remember: the DDA per-channel weights are not in the export - so we pull the raw events and reconstruct, using the GA4 UI's DDA numbers only as a cross-check.

SQL · GA4 events export → conversions + channel -- GA4 BigQuery export: analytics_<property>.events_* (one row per event) -- The DDA per-channel *weights* are NOT here - Google keeps the model. -- We pull the raw conversions + channel so we can reconcile ourselves. SELECT user_pseudo_id, event_timestamp, traffic_source.medium AS channel_medium, traffic_source.source AS channel_source, (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS order_id, (SELECT value.double_value FROM UNNEST(event_params) WHERE key = 'value') AS order_value FROM `analytics_318290.events_*` WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260630' AND event_name = 'purchase' ORDER BY event_timestamp;

UNNEST the event params. GA4 stores conversion value and transaction id inside a repeated event_params field. The correlated sub-selects pull each key out into a flat column.

Key on transaction_id. That is your join to the warehouse order table - the anchor for the whole reconciliation. Without a shared order key you cannot de-duplicate.

Accept the limit up front. This gives you Google-observable, consented touches only. It is the Google slice of the truth, not the truth - which is exactly why Build-along 2 exists.

Real world

Teams burn a day looking for a "dda_credit" column in the export before someone points out it does not exist. The export is raw events by design. Read the DDA output in the GA4 UI, pull the raw events here, and reconcile the two - that is the whole workflow, and knowing it saves the wasted day.

Build-along 2 of 3

Reconcile GA4 vs your warehouse model ★ 11 min · everyone

Now we join GA4's conversions to our own warehouse touchpoint model on order_id and find the overlap: the orders both systems claim. An outer join with an indicator makes the three buckets - both, GA4-only, warehouse-only - fall out cleanly.

Python · outer join + dedupe on order_id import pandas as pd # ga4: conversions pulled from the export (Build-along 1) # wh: our own warehouse touchpoint-model credit (B1-B5) merged = ga4.merge( wh, on="order_id", how="outer", suffixes=("_ga4", "_wh"), indicator=True) both = merged[merged["_merge"] == "both"] # same sale, counted ONCE ga4_only = merged[merged["_merge"] == "left_only"] # GA4 sees, we don't wh_only = merged[merged["_merge"] == "right_only"] # we see, GA4 can't print(f"orders both claim : {len(both):,} -> keep once") print(f"GA4-only orders : {len(ga4_only):,}") print(f"warehouse-only : {len(wh_only):,} (often non-Google journeys)")

The indicator=True column is the whole trick. It labels every row both, left_only, or right_only, so de-duplication becomes a filter instead of a guess.

Warehouse-only orders are the signal. These are conversions GA4 could not see - Meta, TikTok, CTV, offline. Their count is a direct measure of how much of Lumen's journey lives outside Google's view.

"Both" gets counted once. That is the de-duplication in action: an order GA4 and your model both claim is still one order. Never add the two systems' totals.

Watch the join keys GA4's transaction_id and your warehouse order_id must be the same string - trailing whitespace, casing, or a currency prefix will silently drop matches into "GA4-only" and "warehouse-only", inventing a discrepancy that is really a data-cleaning bug. Validate the key before you trust the buckets.
Build-along 3 of 3

Build a de-duplicated cross-platform ROAS view ★ 8 min · everyone

Finally we fold GA4, Meta, and TikTok claims into one truthful row per order and compute a blended ROAS against actual revenue - the number that ties back to the P&L instead of triple-counting the same sale.

SQL · one truthful row per order → blended ROAS -- each platform self-reports the SAME orders. Summing = double-counting. -- De-dupe to one row per order, then ROAS against ACTUAL revenue. WITH claims AS ( SELECT order_id, 'ga4' AS platform, revenue FROM ga4_conversions UNION ALL SELECT order_id, 'meta' AS platform, revenue FROM meta_conversions UNION ALL SELECT order_id, 'tiktok' AS platform, revenue FROM tiktok_conversions ), deduped AS ( -- one truthful row per order SELECT order_id, MAX(revenue) AS order_value FROM claims GROUP BY order_id ) SELECT SUM(order_value) AS real_revenue, (SELECT SUM(spend) FROM ad_spend) AS total_spend, SUM(order_value) / (SELECT SUM(spend) FROM ad_spend) AS blended_roas FROM deduped;

UNION ALL then GROUP BY collapses the claims. Three platforms claiming one order become three rows, then GROUP BY order_id collapses them to one. The double-count is gone.

ROAS uses de-duplicated revenue. Sum the real per-order value, divide by real spend. Compare this honest blended number to the sum of the three platform dashboards and watch the gap - that gap was the fiction.

Blend first, attribute second. This de-duplicated total is the trustworthy top-line. Only after it is locked do you split channel credit with your own model - never with the platforms' self-attribution.

Real world

One team's three platform dashboards summed to a 6.2x ROAS; the de-duplicated warehouse view came in at 3.4x. Nothing was broken - the platforms were each honestly claiming shared conversions. The de-duplicated number was the one the CFO could act on, and presenting both side by side is what finally ended the "which dashboard is right?" argument.

Before Builder Session 8

This week ◐ 45 min total

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · GA4 Data-Driven Attribution is best described as...

DDA is Shapley with a time-decay element, trained on conversion probability. Google does not expose the model or per-channel weights, and it only observes Google-visible, consented web/app touches - so it is a partial view inside a black box.

2 · Why does summing GA4, Meta, and TikTok conversions overstate performance?

One customer, one order, three platforms all raising a hand. Add their counts and one sale becomes three; divide spend by that inflated total and blended ROAS beats reality. De-duplicate to one row per order.

3 · How should you treat a platform's self-reported conversions?

Self-attribution is incentive-driven and overlaps across tools. Treat each platform's count as an upper bound, key everything to real orders, de-duplicate, and report one blended number tied to actual revenue.

Source material

What this session covers

This session turns the GA4 attribution documentation and the Analytics certification material into a builder's reconciliation workflow - DDA's engine and limits, the cross-platform double-counting problem, and the de-duplication that fixes it. Vendor certificates stay official.

Google Analytics attribution + DDA (Skillshop / Analytics Certification)DDA as Shapley + time-decay, defaults - Part 1
GA4 BigQuery export schema (GA4 docs)events export, event_params, traffic source - Build-along 1
Cross-platform double-counting + reconciliationself-attribution as a ceiling, de-dupe - Parts 2-3
Why correlational credit still isn't causalthe case for holdout tests - Builder Session 9
Mix modeling for the un-observable channelsthe aggregate answer - Builder Session 8

Builder Session 7 cheat sheet · pin this

GA4 DDA engineShapley + time-decay over touchpoint combinations, trained on conversion probability. Defaults: last 50 touches, 90-day lookback.
Black boxGoogle does not expose the model or per-channel weights. You get output credit, never the machinery - so keep your own model too.
Google-observable onlyDDA sees consented web/app Google-visible touches. Meta, TikTok, CTV, offline are invisible to it - not the full journey.
Double-countingGA4 + Meta + TikTok each self-attribute the same order. Summed conversions and ROAS exceed reality. De-dupe on order_id.
Self-attribution = ceilingTreat each platform's count as the most it could have driven, not a number to sum. Reconcile down to real orders.
The export has no dda columnPull raw events (transaction_id, value, traffic source), reconstruct credit yourself, cross-check against the GA4 UI's DDA.