diff --git a/build_data.py b/build_data.py index 227fce0..35f4e89 100644 --- a/build_data.py +++ b/build_data.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -# 从 xlsx 重算 data.json。取数规则见 README 注释: -# 平台消耗统计: B=Date, C=Adpingid, D=包名, I=花费 -# 投手: B=Adpingid, L=Media Buyer投放人 -# @某某 == 某某;未填投放人的 Adpingid 丢弃;exclude.json 里的包名永久剔除。 +# 从 xlsx 重算 data.json。 +# 平台消耗统计: B=Date, C=Adpingid, D=包名, E=地区, I=花费 +# 投手: B=Adpingid, D=pkg, L=Media Buyer投放人 +# 规则:@某某 == 某某;未填投放人的 Adpingid 丢弃;exclude.json 里的包名永久剔除(精确匹配)。 +# 每个 package 明细:Geo 后缀 = 该 package 花费最高的地区;Adpingid = 该地区内花费最高的 Adping。 import openpyxl, collections, json, datetime, os, sys HERE = os.path.dirname(os.path.abspath(__file__)) @@ -18,7 +19,7 @@ def key(v): wb = openpyxl.load_workbook(SRC, data_only=True) -# --- 投手表:Adpingid -> 投放人;同时收集需按 Adping 剔除的 id --- +# --- 投手表:Adpingid -> 投放人 --- amap, dropped_ids = {}, set() for r in wb['投手'].iter_rows(min_row=2, values_only=True): adp, pkg, buyer, status = r[1], r[3], r[11], r[0] @@ -38,14 +39,15 @@ owned = collections.Counter(amap.values()) # 每位投手名下的 Adpingid # --- 平台消耗统计 --- rows = [] for r in wb['平台消耗统计'].iter_rows(min_row=2, values_only=True): - d, adp, pkg, spend = r[1], r[2], r[3], r[8] + d, adp, pkg, geo, spend = r[1], r[2], r[3], r[4], r[8] if d is None or adp is None or not isinstance(d, datetime.datetime): continue try: s = float(spend) if spend is not None else 0.0 except (TypeError, ValueError): s = 0.0 - rows.append((d.date(), key(adp), (str(pkg).strip() if pkg else '(未知)'), s)) + rows.append((d.date(), key(adp), (str(pkg).strip() if pkg else '(未知)'), + (str(geo).strip() if geo else ''), s)) if not rows: sys.exit('ERROR: 平台消耗统计 无有效数据行') @@ -53,32 +55,55 @@ if not rows: end = max(r[0] for r in rows) start = end - datetime.timedelta(days=WINDOW_DAYS - 1) -buyers = collections.defaultdict(lambda: collections.defaultdict(float)) +pkg_spend = collections.defaultdict(float) # (buyer, pkg) -> spend +geo_spend = collections.defaultdict(float) # (buyer, pkg, geo) -> spend +ga_spend = collections.defaultdict(float) # (buyer, pkg, geo, adping) -> spend ids = collections.defaultdict(set) unmatched = collections.defaultdict(float) excluded_spend, excluded_rows = 0.0, 0 -for d, adp, pkg, s in rows: +for d, adp, pkg, geo, s in rows: if not (start <= d <= end): continue if pkg in EXCL or adp in dropped_ids: excluded_spend += s; excluded_rows += 1 continue - if adp in amap: - buyers[amap[adp]][pkg] += s - ids[amap[adp]].add(adp) - else: + b = amap.get(adp) + if b is None: unmatched[(adp, pkg)] += s + continue + pkg_spend[(b, pkg)] += s + geo_spend[(b, pkg, geo)] += s + ga_spend[(b, pkg, geo, adp)] += s + ids[b].add(adp) + +by_buyer = collections.defaultdict(dict) +for (b, pkg), v in pkg_spend.items(): + by_buyer[b][pkg] = v out = [] -for b, pk in buyers.items(): +for b, pk in by_buyer.items(): + pkgs = [] + for pkg, v in sorted(pk.items(), key=lambda x: -x[1])[:5]: + # 花费最高的地区,再取该地区内花费最高的 Adpingid + top_geo = max((g for (bb, pp, g) in geo_spend if bb == b and pp == pkg), + key=lambda g: geo_spend[(b, pkg, g)]) + top_adp = max((a for (bb, pp, g, a) in ga_spend + if bb == b and pp == pkg and g == top_geo), + key=lambda a: ga_spend[(b, pkg, top_geo, a)]) + pkgs.append({ + "pkg": pkg, + "geo": top_geo, + "label": f"{pkg}_{top_geo}" if top_geo else pkg, + "adping": top_adp, + "spend": round(v, 4), + }) out.append({ "name": b, "@name": b, "total": round(sum(pk.values()), 4), - "active_ids": len(ids[b]), # 本周期内有消耗的 Adpingid 数 - "owned_ids": owned[b], # 名下负责的 Adpingid 总数 - "pkgs": [{"pkg": k, "spend": round(v, 4)} - for k, v in sorted(pk.items(), key=lambda x: -x[1])[:5]], + "active_ids": len(ids[b]), + "owned_ids": owned[b], + "pkgs": pkgs, }) out.sort(key=lambda x: -x['total']) @@ -90,7 +115,8 @@ data = { "generated_at": datetime.date.today().isoformat(), "date_range": f"{start:%Y/%m/%d}-{end:%Y/%m/%d}", "note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;" - "未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。", + "未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。" + "Package 后缀为该 Package 花费最高的投放地区,Adpingid 为该地区内花费最高的 Adping。", }, "grand_total": round(sum(b['total'] for b in out), 4), "week34_id_count": sum(len(v) for v in ids.values()), @@ -104,4 +130,6 @@ print(f"窗口 {start} -> {end}") print(f"排除包名命中 {excluded_rows} 行,剔除花费 {excluded_spend:.2f}") print(f"合计 {data['grand_total']:.2f} / Adpingid {data['week34_id_count']}") for b in out: - print(f" {b['name']:6s} {b['total']:10.2f}") + print(f" {b['name']:6s} {b['total']:10.2f} 在投{b['active_ids']:3d}/负责{b['owned_ids']:3d}") + for p in b['pkgs']: + print(f" {p['label']:52s} {p['adping']:>10s} {p['spend']:9.2f}") diff --git a/data.json b/data.json index ef294d3..371fc16 100644 --- a/data.json +++ b/data.json @@ -4,7 +4,7 @@ "source": "数据已处理(平台消耗统计 × 投手表匹配,已剔除未填写投放人的 Adpingid 及排除包名)", "generated_at": "2026-08-27", "date_range": "2026/08/24-2026/08/26", - "note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。" + "note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。Package 后缀为该 Package 花费最高的投放地区,Adpingid 为该地区内花费最高的 Adping。" }, "grand_total": 11377.758, "week34_id_count": 127, @@ -18,22 +18,37 @@ "pkgs": [ { "pkg": "com.amazon.mShop.android.shopping", + "geo": "BR", + "label": "com.amazon.mShop.android.shopping_BR", + "adping": "10912510", "spend": 1418.4064 }, { "pkg": "com.king.candycrushsodasaga", + "geo": "BR", + "label": "com.king.candycrushsodasaga_BR", + "adping": "8460883", "spend": 994.619 }, { "pkg": "com.yandex.lavka", + "geo": "RU", + "label": "com.yandex.lavka_RU", + "adping": "8156506", "spend": 365.8305 }, { "pkg": "asrtum.ent.domovyata", + "geo": "RU", + "label": "asrtum.ent.domovyata_RU", + "adping": "7872826", "spend": 354.475 }, { "pkg": "com.fxpro.direct.application", + "geo": "PK", + "label": "com.fxpro.direct.application_PK", + "adping": "7202934", "spend": 237.2 } ] @@ -47,22 +62,37 @@ "pkgs": [ { "pkg": "com.kwai.bulldog", + "geo": "ID", + "label": "com.kwai.bulldog_ID", + "adping": "7815836", "spend": 1225.92 }, { "pkg": "asrtum.ent.domovyata", + "geo": "RU", + "label": "asrtum.ent.domovyata_RU", + "adping": "8628042", "spend": 387.38 }, { "pkg": "com.vitastudio.mahjong", + "geo": "RU", + "label": "com.vitastudio.mahjong_RU", + "adping": "91496148", "spend": 347.66 }, { "pkg": "astrum.ent.m2alyona", + "geo": "RU", + "label": "astrum.ent.m2alyona_RU", + "adping": "11690182", "spend": 276.14 }, { "pkg": "tech.jm", + "geo": "RU", + "label": "tech.jm_RU", + "adping": "9297014", "spend": 86.65 } ] @@ -76,22 +106,37 @@ "pkgs": [ { "pkg": "ru.aliexpress.buyer", + "geo": "RU", + "label": "ru.aliexpress.buyer_RU", + "adping": "6607117", "spend": 942.263 }, { "pkg": "com.king.candycrushsaga", + "geo": "TH", + "label": "com.king.candycrushsaga_TH", + "adping": "5497580", "spend": 928.304 }, { "pkg": "ru.kinopoisk", + "geo": "RU", + "label": "ru.kinopoisk_RU", + "adping": "10599681", "spend": 91.0 }, { "pkg": "com.edadeal.android", + "geo": "RU", + "label": "com.edadeal.android_RU", + "adping": "10599157", "spend": 84.5 }, { "pkg": "ru.mobstudio.andgalaxy", + "geo": "RU", + "label": "ru.mobstudio.andgalaxy_RU", + "adping": "7953512", "spend": 30.62 } ] @@ -105,22 +150,37 @@ "pkgs": [ { "pkg": "ru.kinopoisk", + "geo": "RU", + "label": "ru.kinopoisk_RU", + "adping": "8291713", "spend": 609.84 }, { "pkg": "ru.ozon.app.android", + "geo": "KZ", + "label": "ru.ozon.app.android_KZ", + "adping": "5963354", "spend": 590.7586 }, { "pkg": "com.edadeal.android", + "geo": "RU", + "label": "com.edadeal.android_RU", + "adping": "6395488", "spend": 180.0 }, { "pkg": "com.yandex.bank", + "geo": "RU", + "label": "com.yandex.bank_RU", + "adping": "2937658", "spend": 93.281 }, { "pkg": "ru.yandex.music", + "geo": "RU", + "label": "ru.yandex.music_RU", + "adping": "8566541", "spend": 70.57 } ] @@ -134,22 +194,37 @@ "pkgs": [ { "pkg": "com.king.candycrushsaga", + "geo": "MY", + "label": "com.king.candycrushsaga_MY", + "adping": "5464039", "spend": 1000.69 }, { "pkg": "com.esprit.rpg.lttlefirykng", + "geo": "RU", + "label": "com.esprit.rpg.lttlefirykng_RU", + "adping": "10514860", "spend": 120.8 }, { "pkg": "ru.kinopoisk", + "geo": "RU", + "label": "ru.kinopoisk_RU", + "adping": "91399821", "spend": 101.76 }, { "pkg": "com.reefgames.fabletown", + "geo": "RU", + "label": "com.reefgames.fabletown_RU", + "adping": "91532857", "spend": 72.909 }, { "pkg": "com.netease.newspike", + "geo": "US", + "label": "com.netease.newspike_US", + "adping": "91544364", "spend": 37.714 } ] diff --git a/index.html b/index.html index 4acfd95..1540e4c 100644 --- a/index.html +++ b/index.html @@ -73,8 +73,8 @@ function render(d){ 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'}}}}}); document.getElementById('tables').innerHTML=buyers.map(b=>{ - const rows=b.pkgs.map(p=>''+p.pkg+''+usd(p.spend)+'').join(''); - return '
'+b.name+' 合计 '+usd(b.total)+' · 在投 Adping '+(b.active_ids??'-')+' 个(共负责 '+(b.owned_ids??'-')+' 个)'+' · 主要订单 '+b.pkgs[0].pkg+'
展开 '+b.pkgs.length+' 个 Package 明细'+rows+'
Package花费
'; + const rows=b.pkgs.map(p=>''+(p.label||p.pkg)+''+(p.adping||'-')+''+usd(p.spend)+'').join(''); + return '
'+b.name+' 合计 '+usd(b.total)+' · 在投 Adping '+(b.active_ids??'-')+' 个(共负责 '+(b.owned_ids??'-')+' 个)'+' · 主要订单 '+(b.pkgs[0].label||b.pkgs[0].pkg)+'
展开 '+b.pkgs.length+' 个 Package 明细'+rows+'
PackageAdpingid花费
'; }).join(''); document.getElementById('foot').textContent=d.meta.note; }