Day 05 定好了合成器的資料契約與七個植入的訊號,也在 Cloud Shell 跑出第一週的樣本,今天要把合成器放大到完整 90 天,約 52 萬筆事件,灌進 Day 03 建好的 martech_dw 資料集,從這一天起後面每一篇分析都直接在 BigQuery 裡查詢,不再讀本機的 CSV。
搬資料聽起來只是一行上傳指令,但 CSV 本身沒有型別,每一格都只是文字,BigQuery 讀進來時必須替每個欄位決定型別,只要決定錯一次,資料就會在沒有任何錯誤訊息的情況下被改掉:
0949247891 這種手機號碼被當成整數,開頭的 0 直接消失2056953981.1781798586,被當成浮點數後尾數會被捨去,兩位不同的訪客可能從此變成同一人+08:00,事件時間是 UTC 微秒,任何一邊解讀錯誤,跨午夜的訂單就會被算到錯誤的日期這些問題有一個共同點,載入工作會顯示成功,列數也完全正確,只有等到某一篇分析的數字對不上時才會被發現,到時候已經很難回頭追查是哪一步出錯,所以今天的重點不只是把資料搬進去,而是用兩邊對帳的方式證明搬進去的資料和本機的一模一樣。
今日核心目標:
martech_dw
整條流程分成四個步驟,每一步都有一個檢查點,前一步沒過就不會往下走:
validate.py,32 項檢查全部 PASS 才准許載入,有問題的資料根本不會進倉儲💡 核心工程理念:
event_date 維持字串、event_timestamp 維持整數、value 維持浮點數,和 GA4 匯出表一致,Day 07 合併真實事件時才不用再轉型ground_truth/,倉儲裡只放分析會看到的資料,後面各篇查詢就算想偷看答案也找不到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/,每個欄位明確指定型別與說明,幾個關鍵的決定:
value 跟 GA4 一樣用 FLOAT64+08:00,BigQuery 會換算成 UTC 存放,查詢時再用台北時區取日期has_person 用 BOOL,文字廣告沒有這個屬性就留空,載入後是 NULL還有一個容易被忽略的細節,CSV 是依欄位位置載入的,兩個字串欄位如果前後對調,型別檢查也攔不下來,所以載入腳本會先比對 CSV 標題列和綱要檔的欄位順序,不一致就直接停下。
把資料寫進 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 星狀綱要要處理的主題。
列數一致只能證明沒有漏掉任何一列,前言提到的三種問題都不會讓列數改變,所以需要更細的對帳,我的做法是讓本機和 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/。
martech_dw 資料集已經存在gcloud auth list,帳號前面要有星號cd ~/ai-driven-martech-pipeline && git pull && cd synthesizer && bash bigquery/load.sh && python3 bigquery/reconcile.py ./out
最後看到「55 項一致、0 項不一致」就完成了。
cd ~/ai-driven-martech-pipeline
git pull
cd synthesizer
ls bigquery bigquery/schemas
會看到 load.sh、reconcile.py、verify.sql 與五份綱要檔。
python3 synthetic_pipeline.py --out ./out
python3 validate.py ./out
合成器約 10 秒跑完,validate.py 的每一項都應該是 PASS。
bash bigquery/load.sh
腳本會逐表載入,最後列出 CSV 與 BigQuery 兩邊的列數,五張表都要是 ✅。
python3 bigquery/reconcile.py ./out
每一列會印出指標名稱、本機的值與 BigQuery 的值,約 15 秒跑完。
打開 BigQuery 主控台,在左側展開 martech_dw,點選 raw_events 的「結構定義」分頁,可以看到每個欄位的型別與說明,和綱要檔完全相同。
load.sh 的列數比對五張表都是 ✅reconcile.py 印出「55 項一致、0 項不一致」raw_customers 的 phone 欄位是 STRING,資料開頭的 0 都還在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 的儲存量在免費額度內不會產生費用。
gcloud auth list 有時看得到帳號卻沒有星號,這時 bq ls 不會報錯,只會回傳空清單,很容易誤以為資料集不存在,腳本開頭先檢查登入狀態可以避免誤判今天把 90 天、52 萬筆合成資料灌進了 BigQuery:五份綱要檔決定每個欄位的型別,批次載入工作免費完成搬運,本機與 BigQuery 各算一次 55 項指標全部一致。
回頭看前言的三種風險,型別被猜錯靠明確的綱要解決,精度被吃掉靠訪客 ID 一律用字串,時區被搞混靠對帳時逐筆換算日期檢查,這三件事的結果都寫在對帳報表裡,不必靠肉眼確認。
明日預告:Day 07《倉儲建模:廣告成效星狀綱要設計》,我們將把今天的五張 raw 表整理成事實表與維度表,並把 GA4 的真實事件攤平後和合成資料合併,開始設計分區與叢集!