diff --git a/build_data.py b/build_data.py index 94051eb..24dc016 100644 --- a/build_data.py +++ b/build_data.py @@ -130,6 +130,20 @@ by_buyer = collections.defaultdict(dict) for (b, pkg), v in pkg_spend.items(): by_buyer[b][pkg] = v +# --- 上一周期:紧邻本窗口之前的同样天数,口径完全一致 --- +prev_end = start - datetime.timedelta(days=1) +prev_start = prev_end - datetime.timedelta(days=WINDOW_DAYS - 1) +prev = collections.defaultdict(float) +prev_has_data = any(prev_start <= d <= prev_end for d in by_date) +for d, adp, pkg, geo, s_ in rows: + if not (prev_start <= d <= prev_end): + continue + if pkg in EXCL or adp in dropped_ids: + continue + b = amap.get(adp) + if b is not None: + prev[b] += s_ + out = [] for b, pk in by_buyer.items(): pkgs = [] @@ -150,6 +164,7 @@ for b, pk in by_buyer.items(): out.append({ "name": b, "@name": b, "total": round(sum(pk.values()), 4), + "prev_total": round(prev.get(b, 0.0), 4), "active_ids": len(ids[b]), "owned_ids": owned[b], "pkgs": pkgs, @@ -163,6 +178,7 @@ data = { "source": "萌奇熊日报", "generated_at": datetime.date.today().isoformat(), "date_range": f"{start:%Y/%m/%d}-{end:%Y/%m/%d}", + "prev_range": (f"{prev_start:%Y/%m/%d}-{prev_end:%Y/%m/%d}" if prev_has_data else ""), "note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;" "未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。" "Package 后缀为该 Package 花费最高的投放地区,Adpingid 为该地区内花费最高的 Adping。", @@ -182,6 +198,8 @@ if extra_used: print(f"窗口 {start} -> {end}") print(f"排除包名命中 {excluded_rows} 行,剔除花费 {excluded_spend:.2f}") print(f"合计 {data['grand_total']:.2f} / Adpingid {data['week34_id_count']}") +print(f"上一周期 {prev_start} -> {prev_end} 合计 {sum(prev.values()):.2f}" + + ('' if prev_has_data else ' (无数据)')) for b in out: print(f" {b['name']:6s} {b['total']:10.2f} 在投{b['active_ids']:3d}/负责{b['owned_ids']:3d}") for p in b['pkgs']: diff --git a/data.json b/data.json index 6da8ad3..7085b42 100644 --- a/data.json +++ b/data.json @@ -4,6 +4,7 @@ "source": "萌奇熊日报", "generated_at": "2026-09-07", "date_range": "2026/09/04-2026/09/06", + "prev_range": "2026/09/01-2026/09/03", "note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。Package 后缀为该 Package 花费最高的投放地区,Adpingid 为该地区内花费最高的 Adping。" }, "grand_total": 13199.6784, @@ -13,6 +14,7 @@ "name": "黄政", "@name": "黄政", "total": 3782.5992, + "prev_total": 4004.0344, "active_ids": 36, "owned_ids": 140, "pkgs": [ @@ -57,6 +59,7 @@ "name": "谭超", "@name": "谭超", "total": 3055.013, + "prev_total": 3188.919, "active_ids": 24, "owned_ids": 128, "pkgs": [ @@ -101,6 +104,7 @@ "name": "周梓君", "@name": "周梓君", "total": 2402.8622, + "prev_total": 2022.3678, "active_ids": 29, "owned_ids": 121, "pkgs": [ @@ -145,6 +149,7 @@ "name": "万科", "@name": "万科", "total": 2297.6288, + "prev_total": 2335.8325, "active_ids": 24, "owned_ids": 112, "pkgs": [ @@ -189,6 +194,7 @@ "name": "詹涵粤", "@name": "詹涵粤", "total": 1661.5752, + "prev_total": 1798.7887, "active_ids": 14, "owned_ids": 146, "pkgs": [ diff --git a/index.html b/index.html index 1540e4c..383f6be 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,11 @@ summary{cursor:pointer;color:var(--accent);font-size:12px} .foot{color:var(--sub);font-size:12px;margin-top:30px;border-top:1px solid var(--line);padding-top:14px} .badge{display:inline-block;background:#e6eefc;color:var(--sub);border-radius:6px;padding:2px 8px;font-size:11px;margin-left:8px} .chartbox{position:relative;height:300px} +.up{color:#0F8A5F;font-weight:600}.down{color:#C4506A;font-weight:600}.flat{color:var(--sub)} +#trend th.sub2{font-weight:400;color:var(--sub);font-size:11px} +#trend tr.total td{border-top:2px solid var(--line);font-weight:600} +#trend td:first-child{font-weight:600} +#trend th,#trend td{white-space:nowrap} @@ -32,6 +37,7 @@ summary{cursor:pointer;color:var(--accent);font-size:12px}

武汉自投 · 投手投放花费报告

加载中…
+

各投手本周期内投放花费

各投手主要订单 Package(花费最高)

@@ -69,6 +75,23 @@ function render(d){ const buyers=d.buyers.slice().sort((a,b)=>b.total-a.total); const cards=[['投手数',buyers.length],['投放花费合计',usd(d.grand_total)],['覆盖 Adpingid 数',d.week34_id_count],['花费最高投手',buyers[0].name]]; document.getElementById('cards').innerHTML=cards.map(c=>'
'+c[0]+'
'+c[1]+'
').join(''); + // —— 环比趋势表 —— + if(d.meta.prev_range && buyers.some(b=>b.prev_total!==undefined)){ + document.getElementById('trendPanel').hidden=false; + document.getElementById('trendRange').textContent=d.meta.prev_range+' → '+d.meta.date_range; + const cell=(prev,cur)=>{ + const dv=cur-prev; + const cls=Math.abs(dv)<0.005?'flat':(dv>0?'up':'down'); + const sign=dv>0?'+':(dv<0?'−':''); + const pct=prev>0?(sign+Math.abs(dv/prev*100).toFixed(1)+'%'):'—'; + return ''+sign+'$'+fmt(Math.abs(dv))+''+pct+''; + }; + const rows=buyers.map(b=>''+b.name+''+usd(b.prev_total||0)+''+usd(b.total)+''+cell(b.prev_total||0,b.total)+'').join(''); + const pT=buyers.reduce((s,b)=>s+(b.prev_total||0),0), cT=buyers.reduce((s,b)=>s+b.total,0); + document.getElementById('trend').innerHTML= + '投手上期本期变化幅度' + +rows+'合计'+usd(pT)+''+usd(cT)+''+cell(pT,cT)+''; + } const palette=['#5b8cff','#ff9f5b','#4dd6a8','#c78bff','#ff6b81','#f5c451']; new Chart(document.getElementById('spendChart'),{type:'bar',data:{labels:buyers.map(b=>b.name),datasets:[{label:'投放花费',data:buyers.map(b=>b.total),backgroundColor:buyers.map((b,i)=>palette[i%palette.length])}]},plugins:[barLabels],options:{indexAxis:'y',layout:{padding:{right:76}},plugins:{legend:{display:false},tooltip:{callbacks:{label:(c)=>usd(c.parsed.x)}}},scales:{x:{ticks:{color:'#5b6472',callback:axisUsd},grid:{color:'#d6e2f5'}},y:{ticks:{color:'#111111'},grid:{display:false}}}}}); new Chart(document.getElementById('pkgChart'),{type:'bar',data:{labels:buyers.map(b=>b.name),datasets:[{label:'主要Package花费',data:buyers.map(b=>b.pkgs[0].spend),backgroundColor:'#4dd6a8'}]},options:{plugins:{legend:{display:false},tooltip:{callbacks:{label:(c)=>usd(c.parsed.y),afterLabel:(ctx)=>buyers[ctx.dataIndex].pkgs[0].pkg}}},scales:{x:{ticks:{color:'#111111'},grid:{display:false}},y:{ticks:{color:'#5b6472',callback:axisUsd},grid:{color:'#d6e2f5'}}}}});