打開 GA4 的流量來源報表或購物車後台的訂單來源統計,很常看到這樣的畫面:Google 搜尋廣告帶來一大堆成交,Meta 的轉換數少得可憐,換算下來 ROAS 也難看,直覺反應是把 Meta 的預算挪去搜尋,但這份報表用的多半是最後點擊歸因,也就是整筆訂單的功勞全部算給顧客下單前最後一次點進來的那個通路。
問題在於顧客很少只看一次廣告就下單,常見的路徑是先在 Meta 滑到廣告點進來逛逛,隔幾個小時又被 LINE 的廣告喚回,最後想買的時候直接去 Google 搜尋品牌名稱結帳,最後點擊只看得到最後一步,前面兩支廣告做的事在報表上完全不存在,這時候砍掉 Meta,最後一步的搜尋量也可能跟著慢慢變少卻沒有人發現。
Day 05 植入合成器的訊號 S6 正是這個現象:meta 多出現在購買路徑的開頭、google 搜尋廣告多出現在最後一步,今天要用 Day 07 整理好的 fct_events 把它找回來。
今日核心目標:
mart_attribution 表歸因要回答的問題只有一個:一筆訂單的功勞要怎麼分給路徑上的每一個通路,整個流程分成三步:
| 步驟 | 輸入 | 輸出 |
|---|---|---|
| 串路徑 | fct_orders 的每一筆訂單、fct_events 的每一次造訪 |
每筆訂單下單前的造訪清單,依時間排序 |
| 分功勞 | 購買路徑 | 路徑上每個觸點分到的功勞,三種規則各一個數字 |
| 看結果 | mart_attribution |
各通路在三種規則下分到的訂單數與營收 |
這裡的觸點指的是一次造訪,也就是一個 session_start 事件,來源取它身上的 utm_source 與 utm_medium,例如 meta / paid_social,同一次造訪裡看了幾個商品都只算一個接觸點。
💡 核心工程理念:
mart_attribution 一列是一筆訂單的一個觸點,三種規則再各分 direct 算與不算兩個版本,六個功勞欄位並排放在同一列,要比較時就不用重算build.sql 開頭的兩個變數,想換設定改一行就能重跑串路徑的做法是拿每一筆訂單,去 fct_events 找同一位訪客在下單前的 session_start,照時間排好就是這筆訂單的路徑,但有三條界線要先畫清楚:
| 界線 | 設定 | 沒畫清楚會怎樣 |
|---|---|---|
| 下單之後 | 只取下單時間之前的造訪 | 付款完成後回來查訂單的造訪會被當成促成成交的觸點 |
| 回溯窗口 | 下單前 30 天 | 三個月前來過一次的造訪也分到功勞 |
| 回購 | 從上一筆訂單之後重新起算 | 第二筆訂單會把第一筆的路徑再算一次功勞 |
實際跑完 3,304 筆訂單全部串得到路徑,總共 9,043 個觸點,平均一筆訂單約 2.7 個,路徑長度的分佈如下:
| 路徑長度 | 訂單數 | 其中回購 |
|---|---|---|
| 1 個觸點 | 1,042 | 448 |
| 2 個觸點 | 884 | 277 |
| 3 個觸點 | 535 | 84 |
| 4 個觸點 | 344 | 29 |
| 5 個以上 | 499 | 5 |
只有一個觸點的訂單占 31.5%,這些訂單不管用哪一種規則,功勞都是同一個通路拿走,三種規則可能給出不同答案的是另外 68.5% 的多觸點訂單。
另外有 666 筆訂單下單日在 7/19 之前,往前推 30 天會超過合成資料的起點 6/19,路徑可能被截斷,表裡用 window_truncated 標記起來,後面的主表會把它們排除。
以一筆實際跑出來的首購訂單為例,這位訪客的路徑是 meta → line → google 搜尋廣告,下單金額 540 元:
| 觸點 | 距離下單 | 第一次接觸 | 最後接觸 | 時間衰減 |
|---|---|---|---|---|
| meta / paid_social | 約 7 小時 | 1 | 0 | 0.328 |
| line / display | 約 2 小時 | 0 | 0 | 0.335 |
| google / cpc | 約 4 分鐘 | 0 | 1 | 0.338 |
時間衰減的三個數字幾乎一樣,這不是算錯而是這條路徑從第一次點進來到下單只花了 7 個小時,對半衰期 7 天來說三個觸點都算「剛剛發生」,這件事在 3.3 最後會再回來談。
SQL 的寫法不複雜,先用 ROW_NUMBER() 替路徑上的觸點編號,第一次接觸是編號等於 1 的那一列拿 1,最後接觸是編號等於路徑長度的那一列拿 1,時間衰減則是先算每個觸點的權重,再用視窗函式除以同一筆訂單的權重總和,完整的 SQL 放在儲存庫的 attribution/build.sql,欄位定義放在同一個目錄的 README.md。
主表只看首購、而且回溯窗口完整的 1,833 筆訂單,回購訂單的路徑主要是電子報和舊客回訪,混進來會蓋掉廣告帶新客的樣子,這張表 direct 不算觸點,原因放在避坑指南第 1 點:
| 通路 | 第一次接觸 | 最後接觸 | 時間衰減 |
|---|---|---|---|
| meta / paid_social | 610 | 385 | 507.2 |
| line / display | 545 | 324 | 454.5 |
| google / organic | 333 | 404 | 355.2 |
| google / cpc | 295 | 670 | 466.1 |
| (direct) / (none) | 50 | 50 | 50 |
每一欄加總都是 1,833,功勞守恆成立,差別只在同一批訂單分給誰:
這就是 S6 被找回來的樣子,同樣的 1,833 筆訂單,只因為換了一種分功勞的規則,meta 從第一名掉到第三名,google 搜尋廣告從第四名升到第一名,如果只看最後點擊報表,就會得出前言那個「Meta 沒用」的結論。
時間衰減的數字落在兩者之間,但這份合成資料有一個要交代清楚的地方:主表這批首購訂單裡,多觸點路徑從第一個觸點到下單的中位數只有約 4.5 小時,90% 在約 13 小時內走完,換個角度看,2,461 位買家裡只有 41 位的首購距離第一次造訪超過一天。
原因出在合成器挑回訪者的方式,Day 05 設定回訪名單保留近 14 天來過、還沒下單的訪客,但每次挑人時只從名單最後的 400 筆裡挑,合成資料一天有一千多次造訪,最後 400 筆大約就是最近幾個小時的訪客,回訪自然集中在同一天,半衰期 7 天放在幾個小時的路徑上,權重幾乎平均分配,時間衰減在這裡的效果接近把功勞平分給每個觸點,真實電商的考慮期通常以天計,半衰期應該先查自家從第一次造訪到下單的天數分佈再決定。
S7 秋日專案的專案商品占比要和專案開始前比較才看得出變化,這部分留給 Day 09 和 ROAS 異常一起對照。
三種規則沒有對錯,是在回答不同的問題,選哪一種要看手上的決策:
| 要做的決策 | 建議看 | 原因 |
|---|---|---|
| 開發新客的預算要不要加 | 第一次接觸 | 它衡量的是誰把陌生人帶進來 |
| 關鍵字出價與收割型廣告的成效 | 最後接觸 | 它衡量的是誰促成最後一步 |
| 跨通路的整體預算分配 | 時間衰減,並和另外兩種並看 | 每個通路都有分到功勞,不會有通路被完全抹掉 |
實務上最穩的做法是三種一起看,某個通路在三種規則下排名差很多,就代表它在路徑上有固定的位置,砍預算前要先想清楚它負責的是哪一段。
GA4 在 2023 年移除了第一次點擊、線性、時間衰減與根據位置四種規則式模型,目前只剩數據驅動與最後點擊兩類,數據驅動歸因由 Google 依自家模型分配功勞,看不到計算過程,自己在 BigQuery 用規則式模型算一次,至少每一個數字都知道是怎麼來的。
mart_attribution 整張表不到 10 MiB,五段報表每段都只會以最低的 10 MiB 計費,連同建表與檢查的計費量合計約 150 MB,大約是每月 1 TiB 免費查詢額度的萬分之一點四,這張表只有 9,043 列,儲存量可以忽略fct_events 開啟了必須指定分區條件,build.sql 先從訂單表算出最早與最晚的下單日,再往前多讀一個回溯窗口,只讀需要的日期,另外每個陳述式讀到的每一張表都至少以 10 MiB 計費,建表腳本裡有好幾個陳述式,所以計費量會比處理量多,今天沒有呼叫 Gemini,沒有任何 Token 成本martech_dw 裡有 fct_events 與 fct_orders
gcloud auth list,帳號前面要有星號cd ~/ai-driven-martech-pipeline && git pull && bash attribution/run.sh
看到「14 項通過、0 項不通過」與五段報表就完成了,報表的第二段就是 3.3 的主表。
cd ~/ai-driven-martech-pipeline
git pull
ls attribution
會看到 build.sql、check.sql、report.sql、run.sh 與說明文件 README.md 五個檔案。
bq query --nouse_legacy_sql < attribution/build.sql
想換回溯天數或半衰期,改 build.sql 開頭的 lookback_days 與 half_life_days 再執行一次,表會整張重建,改了回溯天數的話 check.sql 裡檢查天數上限的 30 也要一起改。
bq query --nouse_legacy_sql --format=pretty --max_rows=100 < attribution/check.sql
這份檢查逐筆確認六個功勞欄位的加總都是 1、訂單數與營收和 fct_orders 一致、沒有下單之後的觸點、沒有超過 30 天的觸點,另外檢查第一次與最後接觸的功勞落在正確的位置、回購路徑沒有跨過上一筆訂單,ok 欄位全部是 OK 或 INFO 就代表沒有問題。
bq query --nouse_legacy_sql --format=pretty "
SELECT touch_seq, channel, days_before_order, credit_first, credit_last, ROUND(credit_decay, 3) AS credit_decay
FROM martech_dw.mart_attribution
WHERE order_date = '2026-07-19' AND transaction_id = 'SY2026071910141634F7'
ORDER BY touch_seq"
這就是 3.2 的那筆訂單,三列分別是三個觸點,可以直接看到同一條路徑在三種規則下各自怎麼分。
sed -n '/-- ②/,/;/p' attribution/report.sql | bq query --nouse_legacy_sql --format=pretty
report.sql 裡有五段查詢,這行只取出第二段執行,也就是 3.3 的主表,路線 A 的 run.sh 則會把五段逐一印出。
fct_orders 相同bq rm -f -t martech_dw.mart_attribution
這張表 Day 21 讓 AI 查資料庫時會用到,建議保留,想重建隨時再跑一次 run.sh 即可。
今天把 fct_events 的造訪串成 3,304 筆訂單的購買路徑,用第一次接觸、最後接觸與時間衰減三種規則分配功勞,結果存進 mart_attribution,14 項檢查全部通過,每種規則的功勞加總都剛好等於訂單數。
回頭看前言的問題,同樣 1,833 筆首購訂單,meta 在第一次接觸分到 610 筆,在最後接觸只剩 385 筆,google 搜尋廣告則從 295 筆升到 670 筆,Day 05 植入的 S6 完整找回來了,最後點擊報表裡的 Meta 不是沒用而是它負責的那一段剛好不在報表看得到的地方。
明日預告:Day 09《廣告成效突然跳水?直接在 SQL 裡叫 AI 找出原因》,我們將在 BigQuery 裡建立連到 Gemini 的遠端模型,直接用 SQL 把 ROAS 異常的資料交給 AI 判讀,看它能不能分辨 8/12 起競價變貴和 8/27 追蹤碼失效這兩種完全不同的狀況!