結論:合併 bug,CK 上午已修。Excel 已驗證修復有效。
修復後 P36M 數字:
已不再是 162 萬。修法在
3_資料整合.py:42-46(stream_filter chunk 邊界 bug)。
不是 bug,是定義門檻太鬆。整套標籤體系都有「一年買過一次就上低標」的共通設計問題,詳見 §三 D2。
位置:2_標籤專案資料撈取加速版.py:181-186
註解定義:白天 06:00–18:00 / 晚上 18:00–06:00
when dayofweek in (0, 6) and (hour>=18 or hour<6) ... then '假日白天' -- 18-06 標白天 ❌
when dayofweek in (0, 6) and hour>=6 and hour<18 then '假日晚上' -- 06-18 標晚上 ❌
when dayofweek not in (0, 6) and (hour>=18 or hour<6) ... then '平日白天' -- 同上 ❌
when dayofweek not in (0, 6) and hour>=6 and hour<18 then '平日晚上' -- 同上 ❌Excel 佐證:百貨「假日白天」16% 違反直覺——假日午後是百貨黃金時段不可能最少。 客戶回饋佐證:和泰驗證的同事亦表示「與百貨消費時段認知不符合」。 修法:把四個分支「白天」「晚上」字串對調。
位置:2_*.py:20-25 +
2_*.py:62-68 + 3_*.py:14-19
明碼包含使用者帳號與密碼(Redshift production)。zip 已外傳
Andy/Xians,請 CK 知會 IT 立刻換密碼,並把交付版改成
os.getenv() 或 placeholder。
位置:3_資料整合.py:58-72
Excel 鐵證:西裝 P36M = 5,324 < 2024 P12M = 7,703(ratio 0.69x)。三年合併後比任一年都少——邏輯上不可能,除非合併把資料丟掉。
機制:目前是 per-member fall-through。2024 買過西裝的會員 A,只要在 2026 有任何發票(即使一杯咖啡),他的所有標籤就只從 2026 取。2026 沒買西裝 → 標籤消失。
受害範圍(P36M / max(P12M) < 1.15 的低頻 tag):
| Tag | P36/max | 三年趨勢 |
|---|---|---|
| 西裝 | 0.69x | 7,703→5,033→3,883 |
| 共享汽車 | 1.03x | 41,067→34,876→31,809 |
| 旅宿愛好者 | 1.04x | 48,293→45,911→43,100 |
| 品酒族 | 1.05x | 58,759→57,295→51,537 |
| 新生兒家庭 | 1.11x | 138,750→134,361→131,607 |
對照:高頻黏著類標籤(消費時間/出沒地帶/工作時段/折扣敏感)P36/max 都在 1.27-1.28 — 這才是合理區間。
修法:per-member fall-through → per-(member, tag_type) fall-through。對每個 tag_type 獨立做年度回補。
位置:2_*.py:2814-2833 —
and vax_no = '83118125'
| 年度 | 人數 |
|---|---|
| 2024 | 6 |
| 2025 | 1 |
| 2026 | 107,009 |
2024/2025 只撈到個位數——不是低消費,是這個 vax_no 在那兩年沒在
dw.r_invoice 出現。現行 P36M 搭乘計程車實際上等於
2026 single year,不是三年合併。
請 CK 確認:Uber 是否在某時點才換成此統編?或還有其他統編需要加入?
位置:2_*.py:2814-2833
現行:tags 是「1, 2, 3, ...,
100+」的計次字串,下游難用,且大量 (Tag_type, Tags) 組合人數 <
200(見 P0-6)。
客戶指定新規格:
年度發票 < 4 張 → 低
年度發票 4–10 張 → 中
年度發票 10–25 張 → 高
年度發票 >= 25 張 → 特高
修改 SQL:
select
member_id,
'搭乘計程車' as tag_type,
case
when inv_count >= 25 then '特高'
when inv_count >= 10 and inv_count < 25 then '高'
when inv_count >= 4 and inv_count < 10 then '中'
when inv_count < 4 then '低'
end as tags
from (
select member_id, count(distinct inv_id) as inv_count
from dw.r_invoice r
where r.inv_date >= '{start_time}' and r.inv_date < '{end_time}'
and r.create_time < '{create_time}'
and vax_no = '83118125' -- 連同 P0-4 一起補擴大名單
group by member_id
);規則:單一 (Tag_type, Tags) 組合人數 < 200 的不交付給和泰。
P36M 影響範圍:281 個組合需排除,分佈:
| Tag_type | < 200 的組合數 | 範例 |
|---|---|---|
| 出沒地帶 | 73 | 屏東縣霧台鄉 (6)、台東縣金峰鄉 (4)、新北市烏來區 (74) … |
| 工作時段消費區域 | 73 | 同上偏鄉小區 |
| 非工作時段消費區域 | 71 | 同上偏鄉小區 |
| 搭乘計程車 | 61 | 「97」(27 人)、「84」(29)…改為 P0-5 分級後自動消化 |
| 汽機車品牌 | 3 | PEUGEOT (55)、AUDI (90)、BMW (195) |
實作建議:在 4_資料彙整.py /
4_資料彙整_年度.py 加入:
report = report[report['Tags人數'] >= 200]注意:BMW 195 距 200 僅 5 人差距,若重跑後仍 < 200,需告知客戶;目前 BMW 不交付。
位置:2_*.py:182, 185
when dayofweek in (0, 6) and (hour>=18 or hour<6) or hour is null then '假日白天'SQL AND 優先於 OR,實際解析為:
(dayofweek in (0,6) AND (hour>=18 OR hour<6)) OR (hour IS NULL)→ 只要 hour IS NULL
就一律落入「假日白天」分支(即使是平日)。
影響取決於 invhdr.inv_time 是否可為
null。請 CK 跟 DBA 確認。
| Tag | 註解 | 程式碼 |
|---|---|---|
顧客消費力 (2_*.py:1164) |
>=130k 高 / 45k~130k 中 / <45k 低 | 4 級:>=330k 特高 / >=200k 高 / >=100k 中 / <100k 低 |
品酒族 (2_*.py:1222) |
年度發票 >=4 高 / <4 低(計次) | 4 級(計金額:8100/3770/1900) |
車輛充電 (2_*.py:465) |
年度金額 >=6000 高 / <6000 低 | >=250 高 / <250 低 |
車輛充電的 250 元門檻是極度偏低(一張充電發票就過),請 CK 確認 250 是不是寫錯。
請 CK 全面校對所有 tag 的註解 vs code,並更新交付給和泰的標籤說明書。
LIKE '%優惠%' 關鍵字過寬位置:2_*.py:1198
product_name like '%買一送一%' or product_name like '%優惠%' or product_name like '%折扣%'「優惠」會中「優惠價」「會員優惠」等非真正打折商品 → 86.4% of total 被標折扣敏感,幾乎沒有區辨力。客戶驗證亦質疑此標數量過多。
建議:改用更精準詞(「買X送X」「N折」「特價」),或加 amount 比對基準價。
LIKE '%環保%' 同樣過寬位置:2_*.py:1217
「環保袋」「環保餐具」「環保標章」都中 → 48.5% of
total。建議改用 category_id 或更嚴格詞表。
位置:2_*.py:726-746
客戶疑問:外食族 88,463 < 連鎖餐廳 487,839,「外食族照理應該 > 連鎖餐廳」。
SQL 對比:
2_*.py:895):
having inv_count >= 48(年 ≥48 張才出現)2_*.py:746): 沒有
having,「低」門檻是「年 ≥1 張」兩者門檻不對等 → 連鎖餐廳數字遠遠膨脹。
建議:連鎖餐廳加
having inv_count >= 27(與「<27 低」的最低 bucket
對齊),讓兩個 tag 的「低」閥值一致。預估改後連鎖餐廳人數會從 89.3%
大幅下降,外食族 > 連鎖餐廳 自然成立。
位置:3_資料整合.py:91-97 若 cell_no 在
public.member 對應到多個 member_id,可能出現「同 phone 同
Tag_type 有兩個不同 Tags 值」。目前 dedup key
(Phone, Tag_type, Tags)
不會丟資料,但下游若假設「一支 phone × 一個 Tag_type → 一個
Tags 值」會踩坑。
位置:2_*.py:1156 其他 tag 的 tags 都是
string,這個是 integer 1-50。pandas concat 會 upcast 為
object,現況不會炸,但下游若用
.str.contains 處理會踩坑。
整體設計選擇,造成多個 tag 覆蓋率異常高,「低」這個 bucket 訊號強度接近 0:
| Tag | P36M % | 「低」實際意涵 |
|---|---|---|
| 自炊者 | 91.5% | 一年買過一次柴米油鹽 |
| 連鎖餐廳 | 89.3% | 一年踏進過一次餐廳(見 P1-5) |
| 折扣敏感度 | 86.4% | 一年買過一次「優惠」品名商品 |
| 機車族 | 65% | 一年加過一次 ≤300 元油 |
| 環保綠能族 | 48.5% | 一年買過一次「環保」品名商品 |
請 Andy/Xians 評估:
客戶手上的測試批次母體為 76 萬人。注意:以下回應假設客戶手上的是修復前版本;若是修復後,數據結構應接近我們的 P36M 226 萬版本。請 CK 先確認客戶用的是哪個版本,必要時重新交付。
| 客戶反映 | 我們的判斷 |
|---|---|
| 消費時間 731,499 / 76 萬母體 = 96%,不是每人都有? | 待確認版本。修復後應為 100%。差距可能來自 (a) 母體含非 09 開頭手機被
3_*.py:84-85 過濾,(b) 母體含無發票的會員 |
| 顧客消費力 507,103 < 消費時間 731,499 | 這在邏輯上不該發生——兩者都來自
public.invhdr 同一張表,母體應該相同。極有可能是修復前的
stream_filter bug 殘留。重跑後應與消費時間一致 |
| 自炊者 499,569 過多、非自炊者也被貼 | D2 共通問題。一年買過一次食材就上「低」標。需拉高門檻或重新定義 |
| 折扣敏感度 495,955 過多 | P1-3。LIKE '%優惠%'
抓到「優惠價」這類非折扣商品 |
| 百貨公司消費時間 與認知不符 | P0-1 已確認。白天/晚上 label 寫反 |
| 外食族 88,463 < 連鎖餐廳 487,839(反直覺) | P1-5 新發現。連鎖餐廳沒 HAVING,「低」門檻是 ≥1 張,外食族 ≥48 張,兩個 tag 的低標不對等 |
| 車輛充電 67,626 ≈ 汽機車品牌 69,067 | 不是邏輯關聯,是巧合。兩個 query 完全獨立(充電通路 vs 原廠保養廠)。我們 P36M 看到差距 14k,較合理。但 P1-2 已發現車輛充電門檻為 250 元(註解寫 6000),請 CK 確認門檻是否寫錯 |
| 新生兒家庭:同事買奶粉沒被貼 | category_id 範圍 (239-247 共 9 個) 可能不涵蓋 Costco/Momo/大樹的細項。請 CK 用同事 member_id 抽樣:(a) 查發票是否被歸到這 9 個 category_id,(b) 查 dim.category 看 239-247 涵蓋哪些品項,(c) 視情況加入「奶粉/尿布/嬰兒副食品」LIKE 規則或擴大 category_id 名單 |
今天必做:
本週內:
待和泰回覆:
Review 方法:靜態 code review + Excel 數字交叉驗證 + 客戶回饋逐項對照 + 與團隊聊天記錄交叉驗證。所有結論已二次自檢,未經驗證的推論已標明「請 CK/DBA 確認」字樣。