iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Build on Google AI

AI-Driven MarTech:用 Google Cloud + Vertex AI 打造全自動廣告歸因與多模態素材分析系統系列 第 6 篇

Day 06 | 合成器量產:90 天 50 萬筆灌入 BigQuery 與分佈驗證

  • 分享至 

  • xImage
  •  

1. 前言:資料載入倉儲時的潛在偏差與風險

Day 05 定好了合成器的資料契約與七個植入的訊號,也在 Cloud Shell 跑出第一週的樣本,今天要把合成器放大到完整 90 天,約 52 萬筆事件,灌進 Day 03 建好的 martech_dw 資料集,從這一天起後面每一篇分析都直接在 BigQuery 裡查詢,不再讀本機的 CSV。

搬資料聽起來只是一行上傳指令,但 CSV 本身沒有型別,每一格都只是文字,BigQuery 讀進來時必須替每個欄位決定型別,只要決定錯一次,資料就會在沒有任何錯誤訊息的情況下被改掉:

  • 型別被猜錯:0949247891 這種手機號碼被當成整數,開頭的 0 直接消失
  • 精度被吃掉:GA4 的訪客 ID 長得像 2056953981.1781798586,被當成浮點數後尾數會被捨去,兩位不同的訪客可能從此變成同一人
  • 時區被搞混:訂單時間帶著 +08:00,事件時間是 UTC 微秒,任何一邊解讀錯誤,跨午夜的訂單就會被算到錯誤的日期

這些問題有一個共同點,載入工作會顯示成功,列數也完全正確,只有等到某一篇分析的數字對不上時才會被發現,到時候已經很難回頭追查是哪一步出錯,所以今天的重點不只是把資料搬進去,而是用兩邊對帳的方式證明搬進去的資料和本機的一模一樣。

今日核心目標:

  1. 替五張 raw 表寫好明確的綱要不交給自動偵測
  2. 用免費的批次載入工作把 90 天資料灌進 martech_dw
  3. 在本機與 BigQuery 各算一次 55 項指標,逐項對帳,確認分佈與七個訊號在搬運之後完全沒有走樣

2. 系統架構全景與設計理念

Day 06 合成器量產:從 CSV 到 BigQuery 的四個檢查點

整條流程分成四個步驟,每一步都有一個檢查點,前一步沒過就不會往下走:

  1. 產生:合成器用固定的亂數種子產生 90 天資料,同一個種子在任何一台 Cloud Shell 跑出來的檔案都完全相同
  2. 本機驗證:先跑 Day 05 的 validate.py,32 項檢查全部 PASS 才准許載入,有問題的資料根本不會進倉儲
  3. 批次載入:依照五份綱要檔把 CSV 灌進 BigQuery,載入完立刻比對每張表的列數
  4. 兩邊對帳:本機從 CSV 算一份指標,BigQuery 用 SQL 算同一份,逐項比對後輸出一致或不一致

💡 核心工程理念:

  1. 綱要是契約的一部分:Day 05 的三份設定檔決定資料內容,今天的五份綱要檔則規範資料型別,兩者皆納入儲存庫進行版本控制
  2. 型別跟著 GA4 走:event_date 維持字串、event_timestamp 維持整數、value 維持浮點數,和 GA4 匯出表一致,Day 07 合併真實事件時才不用再轉型
  3. 答案表不進倉儲:S5 的顧客類型只存在本機的 ground_truth/,倉儲裡只放分析會看到的資料,後面各篇查詢就算想偷看答案也找不到
  4. 可以重跑:載入一律用覆寫模式,重跑幾次結果都一樣,不會越跑資料越多

3. 核心技術深度拆解

3.1 綱要先行:為什麼不用自動偵測

BigQuery 提供自動偵測綱要的功能,會讀取檔案開頭的一部分資料來推斷每個欄位的型別,用起來很方便,所以我先用它載入一次作為對照,顧客、訂單與素材表用完整檔案,事件表取前 2,000 列,實際得到的結果如下:

欄位 自動偵測的型別 實際需要的型別 會發生什麼事
phone INTEGER STRING 0949247891 變成 949247891
user_pseudo_id FLOAT STRING 變成 1.3685299351781852E9,尾數被捨去
event_date INTEGER STRING 和 GA4 匯出表的字串型別對不上
value INTEGER FLOAT64 合成資料的金額剛好都是整數,之後和 GA4 的浮點數欄位合併時型別會衝突

這些欄位在 CSV 裡看起來都很正常,偵測的結果以數字格式來說也不算錯,問題在於它不知道這些數字其實是識別碼,訪客 ID 被轉成浮點數最為危險,Day 08 的歸因要靠它把同一個人的多次造訪串成一條路徑,一旦尾數被捨去不同的人就可能被併成同一條路徑,而且表面上完全看不出來。

所以五張表各自寫了一份綱要檔,放在 synthesizer/bigquery/schemas/,每個欄位明確指定型別與說明,幾個關鍵的決定:

  • 金額:廣告花費用 NUMERIC 保留兩位小數,訂單金額是新台幣整數用 INT64,事件的 value 跟 GA4 一樣用 FLOAT64
  • 時間:訂單時間用 TIMESTAMP,CSV 裡帶著 +08:00,BigQuery 會換算成 UTC 存放,查詢時再用台北時區取日期
  • 布林值:素材的 has_person 用 BOOL,文字廣告沒有這個屬性就留空,載入後是 NULL
  • 必填欄位:主鍵與時間欄位設成 REQUIRED,任何一列缺值整個載入工作就會失敗,而不是寫入一列缺值的資料

還有一個容易被忽略的細節,CSV 是依欄位位置載入的,兩個字串欄位如果前後對調,型別檢查也攔不下來,所以載入腳本會先比對 CSV 標題列和綱要檔的欄位順序,不一致就直接停下。

3.2 批次載入:為什麼不用串流

把資料寫進 BigQuery 主要有三種方式,這次選擇批次載入:

方式 適合的情境 費用
批次載入工作 一次搬一整批檔案 不收費,使用共用的運算資源
串流寫入 資料一筆一筆即時進來 依寫入量計費,Storage Write API 每月前 2 TiB 免費
INSERT 語法 少量修正 和查詢一樣依處理量計費,不適合大量寫入

合成資料是一次產生完整 90 天的歷史資料,沒有即時性的需求,串流寫入的免費額度雖然夠用但多了逐筆寫入的程式與重試處理,批次載入正好符合這個情境,而且 Google 官方的定價頁面寫得很清楚,從 Cloud Storage 或本機檔案批次載入預設不收費,約 70 MB 的事件檔也不需要先放到 Cloud Storage,直接從 Cloud Shell 上傳即可。

載入腳本 bigquery/load.sh 做的事情很單純:確認 gcloud 已登入、確認資料集存在、跑本機驗證、比對欄位順序、逐表載入,最後比對列數,實測整段約兩分鐘,其中大部分時間花在上傳 68 MB 的事件檔:

原始表 CSV 列數 BigQuery 列數 儲存量
raw_creatives 30 30 不到 0.01 MiB
raw_ad_daily 1,700 1,700 0.16 MiB
raw_events 524,020 524,020 62.71 MiB
raw_orders 3,304 3,304 0.50 MiB
raw_customers 2,461 2,461 0.17 MiB

分區與叢集這次刻意沒有設定,raw 表的角色是原封不動保留來源資料,怎麼切分區、依哪些欄位建立叢集,要看查詢怎麼使用資料,這是 Day 07 星狀綱要要處理的主題。

3.3 兩邊對帳:列數對了不代表資料對了

列數一致只能證明沒有漏掉任何一列,前言提到的三種問題都不會讓列數改變,所以需要更細的對帳,我的做法是讓本機和 BigQuery 各自獨立計算同一份指標,本機用 Python 讀 CSV,BigQuery 用 SQL 查剛載入的表,兩邊的程式碼互不共用,最後逐項比對:

類別 項目數 檢查什麼 代表性的結果
列數與總量 23 各表列數、曝光點擊花費總和、各事件筆數、訪客與工作階段數 花費 945,974.84 元,兩邊到小數第二位都相同
期間與型別 9 日期範圍、時區換算、手機開頭的 0、訪客 ID 格式 2,461 支手機全部保留開頭的 0
統計分佈 5 各通路點擊率、轉換率、客單價 轉換率 2.29%、客單價 622 元
七個訊號 18 用 SQL 把 S1 到 S7 重新算一次 S1 的 CPC 從 7.57 元升到 15.24 元

型別檢查這一組專門對應前言的三種風險,例如 check.order_date_mismatch 會用台北時區把每筆訂單的時間換回日期,再和 order_date 比對,只要 TIMESTAMP 的時區解讀錯誤就會出現不一致,實測結果是 0 筆。

七個訊號的部分,S4 的素材屬性效果在 SQL 裡要先算出每則素材的點擊率,再依通路與受眾分組比較幾何平均,邏輯和 Day 05 的 validate.py 相同,S3 的每週衰退率則直接用 BigQuery 的共變異數與變異數函式算出迴歸斜率,重算的結果如下:

代號 BigQuery 算出的結果 Day 05 的設定
S1 CPC 7.57 → 15.24 元 ×2.0
S2 8/27 purchase 事件 0 筆、訂單 37 筆 整天沒送出
S3 每週衰退 8.2% 8%
S4 人物 ×1.28、CTA 右下 ×1.13、暖色 ×1.08 ×1.25、×1.10、×1.10
S5 1 筆訂單 1,908 人、2 筆 348 人、3 筆以上 205 人 回購集中在部分顧客
S6 meta 當第一觸點 647 次、最後觸點 241 次 meta 集中在路徑開頭
S7 專案商品占比 53.8% → 66.6% 專案期間上升

S5 在倉儲這一側只檢查訂單數的分佈,顧客屬於哪一種類型只有本機的答案表知道,這正是前面說的答案表不進倉儲,最後 55 項指標全部一致,整數與字串完全相同,浮點數的誤差也在十億分之一以內,這份資料可以放心交給後面的分析篇使用,完整的指標定義與 SQL 放在儲存庫的 synthesizer/bigquery/。


4. FinOps 成本防護實踐:三道防線體系

  1. 第一道防線:善用 Google Cloud 每月免費額度:五張表合計約 63.5 MiB,只占每月 10 GiB 免費儲存額度的 0.6%,對帳查詢實際處理 42.9 MB、計費 52.4 MB,連每月 1 TiB 免費查詢額度的萬分之一都不到
  2. 第二道防線:架構層被動成本防護:批次載入工作本身不收費,也不需要先把檔案放到 Cloud Storage,對帳 SQL 只讀取需要的欄位,今天沒有呼叫 Gemini,沒有任何 Token 成本
  3. 第三道防線:Cloud Billing 預算警報:沿用 Day 03 由 Terraform 建立的預算警報(新台幣帳戶 NT$ 300/美元帳戶 US$ 10),50%、80%、100% 三段通知,今天的操作不會觸發

5. Cloud Shell 實戰演練:兩分鐘灌完 90 天資料

5.1 事前準備

  • 已完成 Day 03 的環境建置,martech_dw 資料集已經存在
  • 已在 Cloud Shell 裡 clone 過本系列的儲存庫
  • 確認 gcloud 有登入中的帳號,輸入 gcloud auth list,帳號前面要有星號

5.2 路線 A|懶人包:一行指令載入並對帳

cd ~/ai-driven-martech-pipeline && git pull && cd synthesizer && bash bigquery/load.sh && python3 bigquery/reconcile.py ./out

最後看到「55 項一致、0 項不一致」就完成了。

5.3 路線 B|逐步教學:理解每一個指令

步驟 1:取得最新程式碼

cd ~/ai-driven-martech-pipeline
git pull
cd synthesizer
ls bigquery bigquery/schemas

會看到 load.sh、reconcile.py、verify.sql 與五份綱要檔。

步驟 2:產生 90 天資料並在本機驗證

python3 synthetic_pipeline.py --out ./out
python3 validate.py ./out

合成器約 10 秒跑完,validate.py 的每一項都應該是 PASS。

步驟 3:批次載入五張表

bash bigquery/load.sh

腳本會逐表載入,最後列出 CSV 與 BigQuery 兩邊的列數,五張表都要是 ✅。

步驟 4:兩邊對帳

python3 bigquery/reconcile.py ./out

每一列會印出指標名稱、本機的值與 BigQuery 的值,約 15 秒跑完。

步驟 5:在 BigQuery 主控台看看資料

打開 BigQuery 主控台,在左側展開 martech_dw,點選 raw_events 的「結構定義」分頁,可以看到每個欄位的型別與說明,和綱要檔完全相同。

5.4 驗證成果

  • load.sh 的列數比對五張表都是 ✅
  • reconcile.py 印出「55 項一致、0 項不一致」
  • raw_customers 的 phone 欄位是 STRING,資料開頭的 0 都還在

5.5 不用了?指令全部清除

for t in raw_creatives raw_ad_daily raw_events raw_orders raw_customers; do bq rm -f -t martech_dw.$t; done

這五張表明天 Day 07 還會用到,建議保留,約 63.5 MiB 的儲存量在免費額度內不會產生費用。


6. 工程實務避坑指南

  1. 不要讓自動偵測決定識別碼欄位的型別:手機號碼與訪客 ID 看起來都像數字,自動偵測會把它們當成整數與浮點數,前者弄丟開頭的 0,後者捨去尾數,載入工作卻照樣顯示成功
  2. 列數一致不代表資料正確:型別與時區錯誤都不會讓列數改變,至少要再比對總和、日期範圍與幾個關鍵欄位的格式
  3. CSV 依位置載入:綱要檔的欄位順序只要和 CSV 標題列不同,型別相同的欄位就會對調而不會報錯,載入前先比對標題列
  4. gcloud 沒有登入中的帳號時 bq 會安靜地回傳空結果:Cloud Shell 閒置一段時間後,gcloud auth list 有時看得到帳號卻沒有星號,這時 bq ls 不會報錯,只會回傳空清單,很容易誤以為資料集不存在,腳本開頭先檢查登入狀態可以避免誤判
  5. 載入要能重跑:用覆寫模式載入,重跑之後的結果和第一次完全相同,改用附加模式的話,每重跑一次資料就會多一份

7. 總結與明日預告

今天把 90 天、52 萬筆合成資料灌進了 BigQuery:五份綱要檔決定每個欄位的型別,批次載入工作免費完成搬運,本機與 BigQuery 各算一次 55 項指標全部一致。

回頭看前言的三種風險,型別被猜錯靠明確的綱要解決,精度被吃掉靠訪客 ID 一律用字串,時區被搞混靠對帳時逐筆換算日期檢查,這三件事的結果都寫在對帳報表裡,不必靠肉眼確認。

明日預告:Day 07《倉儲建模:廣告成效星狀綱要設計》,我們將把今天的五張 raw 表整理成事實表與維度表,並把 GA4 的真實事件攤平後和合成資料合併,開始設計分區與叢集!


上一篇
Day 05 | 雙軌資料工程:電商大數據合成器
下一篇
Day 07 | 倉儲建模:廣告成效星狀綱要設計
系列文
AI-Driven MarTech:用 Google Cloud + Vertex AI 打造全自動廣告歸因與多模態素材分析系統 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言