Add period-over-period trend table at top of report
build_data.py now also computes each buyer's total for the preceding 3-day window under identical rules (exclusions, attribution), exposed as prev_total plus meta.prev_range. index.html renders it as the first panel, with green/red deltas and a total row; the panel stays hidden if no prior period exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,20 @@ by_buyer = collections.defaultdict(dict)
|
|||||||
for (b, pkg), v in pkg_spend.items():
|
for (b, pkg), v in pkg_spend.items():
|
||||||
by_buyer[b][pkg] = v
|
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 = []
|
out = []
|
||||||
for b, pk in by_buyer.items():
|
for b, pk in by_buyer.items():
|
||||||
pkgs = []
|
pkgs = []
|
||||||
@@ -150,6 +164,7 @@ for b, pk in by_buyer.items():
|
|||||||
out.append({
|
out.append({
|
||||||
"name": b, "@name": b,
|
"name": b, "@name": b,
|
||||||
"total": round(sum(pk.values()), 4),
|
"total": round(sum(pk.values()), 4),
|
||||||
|
"prev_total": round(prev.get(b, 0.0), 4),
|
||||||
"active_ids": len(ids[b]),
|
"active_ids": len(ids[b]),
|
||||||
"owned_ids": owned[b],
|
"owned_ids": owned[b],
|
||||||
"pkgs": pkgs,
|
"pkgs": pkgs,
|
||||||
@@ -163,6 +178,7 @@ data = {
|
|||||||
"source": "萌奇熊日报",
|
"source": "萌奇熊日报",
|
||||||
"generated_at": datetime.date.today().isoformat(),
|
"generated_at": datetime.date.today().isoformat(),
|
||||||
"date_range": f"{start:%Y/%m/%d}-{end:%Y/%m/%d}",
|
"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 列为准,@某某 已与某某合并计算;"
|
"note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;"
|
||||||
"未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。"
|
"未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。"
|
||||||
"Package 后缀为该 Package 花费最高的投放地区,Adpingid 为该地区内花费最高的 Adping。",
|
"Package 后缀为该 Package 花费最高的投放地区,Adpingid 为该地区内花费最高的 Adping。",
|
||||||
@@ -182,6 +198,8 @@ if extra_used:
|
|||||||
print(f"窗口 {start} -> {end}")
|
print(f"窗口 {start} -> {end}")
|
||||||
print(f"排除包名命中 {excluded_rows} 行,剔除花费 {excluded_spend:.2f}")
|
print(f"排除包名命中 {excluded_rows} 行,剔除花费 {excluded_spend:.2f}")
|
||||||
print(f"合计 {data['grand_total']:.2f} / Adpingid {data['week34_id_count']}")
|
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:
|
for b in out:
|
||||||
print(f" {b['name']:6s} {b['total']:10.2f} 在投{b['active_ids']:3d}/负责{b['owned_ids']:3d}")
|
print(f" {b['name']:6s} {b['total']:10.2f} 在投{b['active_ids']:3d}/负责{b['owned_ids']:3d}")
|
||||||
for p in b['pkgs']:
|
for p in b['pkgs']:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"source": "萌奇熊日报",
|
"source": "萌奇熊日报",
|
||||||
"generated_at": "2026-09-07",
|
"generated_at": "2026-09-07",
|
||||||
"date_range": "2026/09/04-2026/09/06",
|
"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。"
|
"note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。Package 后缀为该 Package 花费最高的投放地区,Adpingid 为该地区内花费最高的 Adping。"
|
||||||
},
|
},
|
||||||
"grand_total": 13199.6784,
|
"grand_total": 13199.6784,
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
"name": "黄政",
|
"name": "黄政",
|
||||||
"@name": "黄政",
|
"@name": "黄政",
|
||||||
"total": 3782.5992,
|
"total": 3782.5992,
|
||||||
|
"prev_total": 4004.0344,
|
||||||
"active_ids": 36,
|
"active_ids": 36,
|
||||||
"owned_ids": 140,
|
"owned_ids": 140,
|
||||||
"pkgs": [
|
"pkgs": [
|
||||||
@@ -57,6 +59,7 @@
|
|||||||
"name": "谭超",
|
"name": "谭超",
|
||||||
"@name": "谭超",
|
"@name": "谭超",
|
||||||
"total": 3055.013,
|
"total": 3055.013,
|
||||||
|
"prev_total": 3188.919,
|
||||||
"active_ids": 24,
|
"active_ids": 24,
|
||||||
"owned_ids": 128,
|
"owned_ids": 128,
|
||||||
"pkgs": [
|
"pkgs": [
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
"name": "周梓君",
|
"name": "周梓君",
|
||||||
"@name": "周梓君",
|
"@name": "周梓君",
|
||||||
"total": 2402.8622,
|
"total": 2402.8622,
|
||||||
|
"prev_total": 2022.3678,
|
||||||
"active_ids": 29,
|
"active_ids": 29,
|
||||||
"owned_ids": 121,
|
"owned_ids": 121,
|
||||||
"pkgs": [
|
"pkgs": [
|
||||||
@@ -145,6 +149,7 @@
|
|||||||
"name": "万科",
|
"name": "万科",
|
||||||
"@name": "万科",
|
"@name": "万科",
|
||||||
"total": 2297.6288,
|
"total": 2297.6288,
|
||||||
|
"prev_total": 2335.8325,
|
||||||
"active_ids": 24,
|
"active_ids": 24,
|
||||||
"owned_ids": 112,
|
"owned_ids": 112,
|
||||||
"pkgs": [
|
"pkgs": [
|
||||||
@@ -189,6 +194,7 @@
|
|||||||
"name": "詹涵粤",
|
"name": "詹涵粤",
|
||||||
"@name": "詹涵粤",
|
"@name": "詹涵粤",
|
||||||
"total": 1661.5752,
|
"total": 1661.5752,
|
||||||
|
"prev_total": 1798.7887,
|
||||||
"active_ids": 14,
|
"active_ids": 14,
|
||||||
"owned_ids": 146,
|
"owned_ids": 146,
|
||||||
"pkgs": [
|
"pkgs": [
|
||||||
|
|||||||
+23
@@ -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}
|
.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}
|
.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}
|
.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}
|
||||||
</style>
|
</style>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -32,6 +37,7 @@ summary{cursor:pointer;color:var(--accent);font-size:12px}
|
|||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1>武汉自投 · 投手投放花费报告 <span class="badge" id="weekBadge"></span></h1>
|
<h1>武汉自投 · 投手投放花费报告 <span class="badge" id="weekBadge"></span></h1>
|
||||||
<div class="sub" id="metaLine">加载中…</div>
|
<div class="sub" id="metaLine">加载中…</div>
|
||||||
|
<div class="panel" id="trendPanel" hidden><h2>投手花费环比<span class="badge" id="trendRange"></span></h2><div style="overflow-x:auto"><table id="trend"></table></div></div>
|
||||||
<div class="cards" id="cards"></div>
|
<div class="cards" id="cards"></div>
|
||||||
<div class="panel"><h2>各投手本周期内投放花费</h2><div class="chartbox"><canvas id="spendChart"></canvas></div></div>
|
<div class="panel"><h2>各投手本周期内投放花费</h2><div class="chartbox"><canvas id="spendChart"></canvas></div></div>
|
||||||
<div class="panel"><h2>各投手主要订单 Package(花费最高)</h2><div class="chartbox"><canvas id="pkgChart"></canvas></div></div>
|
<div class="panel"><h2>各投手主要订单 Package(花费最高)</h2><div class="chartbox"><canvas id="pkgChart"></canvas></div></div>
|
||||||
@@ -69,6 +75,23 @@ function render(d){
|
|||||||
const buyers=d.buyers.slice().sort((a,b)=>b.total-a.total);
|
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]];
|
const cards=[['投手数',buyers.length],['投放花费合计',usd(d.grand_total)],['覆盖 Adpingid 数',d.week34_id_count],['花费最高投手',buyers[0].name]];
|
||||||
document.getElementById('cards').innerHTML=cards.map(c=>'<div class="card"><div class="k">'+c[0]+'</div><div class="v">'+c[1]+'</div></div>').join('');
|
document.getElementById('cards').innerHTML=cards.map(c=>'<div class="card"><div class="k">'+c[0]+'</div><div class="v">'+c[1]+'</div></div>').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 '<td class="num '+cls+'">'+sign+'$'+fmt(Math.abs(dv))+'</td><td class="num '+cls+'">'+pct+'</td>';
|
||||||
|
};
|
||||||
|
const rows=buyers.map(b=>'<tr><td>'+b.name+'</td><td class="num">'+usd(b.prev_total||0)+'</td><td class="num">'+usd(b.total)+'</td>'+cell(b.prev_total||0,b.total)+'</tr>').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=
|
||||||
|
'<thead><tr><th>投手</th><th class="num sub2">上期</th><th class="num sub2">本期</th><th class="num">变化</th><th class="num">幅度</th></tr></thead><tbody>'
|
||||||
|
+rows+'<tr class="total"><td>合计</td><td class="num">'+usd(pT)+'</td><td class="num">'+usd(cT)+'</td>'+cell(pT,cT)+'</tr></tbody>';
|
||||||
|
}
|
||||||
const palette=['#5b8cff','#ff9f5b','#4dd6a8','#c78bff','#ff6b81','#f5c451'];
|
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('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'}}}}});
|
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'}}}}});
|
||||||
|
|||||||
Reference in New Issue
Block a user