diff --git a/build_data.py b/build_data.py new file mode 100644 index 0000000..4ad9618 --- /dev/null +++ b/build_data.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# 从 xlsx 重算 data.json。取数规则见 README 注释: +# 平台消耗统计: B=Date, C=Adpingid, D=包名, I=花费 +# 投手: B=Adpingid, L=Media Buyer投放人 +# @某某 == 某某;未填投放人的 Adpingid 丢弃;exclude.json 里的包名永久剔除。 +import openpyxl, collections, json, datetime, os, sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(HERE, '2608每日数据-武汉萌奇熊.xlsx') +EXCL = set(json.load(open(os.path.join(HERE, 'exclude.json')))['packages']) +NON_BUYER = {'GP下架'} +WINDOW_DAYS = 3 + +def key(v): + if isinstance(v, float) and v.is_integer(): + return str(int(v)) + return str(v).strip() + +wb = openpyxl.load_workbook(SRC, data_only=True) + +# --- 投手表:Adpingid -> 投放人;同时收集需按 Adping 剔除的 id --- +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] + if adp is None or str(status).strip() == '示例': + continue + k = key(adp) + if pkg is not None and str(pkg).strip() in EXCL: # 精确匹配,避免误伤 in.swiggy.android + dropped_ids.add(k) + continue + b = str(buyer).strip() if buyer is not None else '' + if b in ('', 'None') or b.lstrip('@').strip() in NON_BUYER: + continue + amap[k] = b.lstrip('@').strip() + +# --- 平台消耗统计 --- +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] + 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)) + +if not rows: + sys.exit('ERROR: 平台消耗统计 无有效数据行') + +end = max(r[0] for r in rows) +start = end - datetime.timedelta(days=WINDOW_DAYS - 1) + +buyers = collections.defaultdict(lambda: collections.defaultdict(float)) +ids = collections.defaultdict(set) +unmatched = collections.defaultdict(float) +excluded_spend, excluded_rows = 0.0, 0 + +for d, adp, pkg, 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: + unmatched[(adp, pkg)] += s + +out = [] +for b, pk in buyers.items(): + out.append({ + "name": b, "@name": b, + "total": round(sum(pk.values()), 4), + "pkgs": [{"pkg": k, "spend": round(v, 4)} + for k, v in sorted(pk.items(), key=lambda x: -x[1])[:5]], + }) +out.sort(key=lambda x: -x['total']) + +lbl = f"{start.month}/{start.day}-{end.month}/{end.day}" +data = { + "meta": { + "week": f"Last {WINDOW_DAYS} Days: {lbl}", + "source": "数据已处理(平台消耗统计 × 投手表匹配,已剔除未填写投放人的 Adpingid 及排除包名)", + "generated_at": datetime.date.today().isoformat(), + "date_range": f"{start:%Y/%m/%d}-{end:%Y/%m/%d}", + "note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;" + "未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。", + }, + "grand_total": round(sum(b['total'] for b in out), 4), + "week34_id_count": sum(len(v) for v in ids.values()), + "buyers": out, + "unmatched": [{"adping": k[0], "pkg": k[1], "spend": round(v, 4)} + for k, v in sorted(unmatched.items(), key=lambda x: -x[1])], +} +json.dump(data, open(os.path.join(HERE, 'data.json'), 'w'), ensure_ascii=False, indent=2) + +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}") diff --git a/data.json b/data.json index ca5c4b7..8682ae5 100644 --- a/data.json +++ b/data.json @@ -1,13 +1,13 @@ { "meta": { "week": "Last 3 Days: 8/23-8/25", - "source": "数据已处理(平台消耗统计 × 投手表匹配,已剔除未填写投放人的 Adpingid)", + "source": "数据已处理(平台消耗统计 × 投手表匹配,已剔除未填写投放人的 Adpingid 及排除包名)", "generated_at": "2026-08-26", "date_range": "2026/08/23-2026/08/25", - "note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;未填写投放人的 Adpingid 不计入。" + "note": "投放人以「投手」表 Media Buyer 列为准,@某某 已与某某合并计算;未填写投放人的 Adpingid 不计入;exclude.json 所列包名已排除。" }, - "grand_total": 11160.5116, - "week34_id_count": 121, + "grand_total": 11063.3116, + "week34_id_count": 119, "buyers": [ { "name": "黄政", @@ -120,7 +120,7 @@ { "name": "詹涵粤", "@name": "詹涵粤", - "total": 1567.5664, + "total": 1470.3664, "pkgs": [ { "pkg": "com.king.candycrushsaga", @@ -139,23 +139,13 @@ "spend": 80.4999 }, { - "pkg": "com.igg.android.lordsmobile", - "spend": 63.0 + "pkg": "com.netease.newspike", + "spend": 30.6 } ] } ], "unmatched": [ - { - "adping": "91534913", - "pkg": "com.qookkagames.xczyw.gp.hk", - "spend": 4.2 - }, - { - "adping": "91533906", - "pkg": "com.sialiagames.xczyw.gp.tw", - "spend": 0.5 - }, { "adping": "91538334", "pkg": "cn.com.vtmarkets", diff --git a/exclude.json b/exclude.json new file mode 100644 index 0000000..f2ad10e --- /dev/null +++ b/exclude.json @@ -0,0 +1,17 @@ +{ + "_comment": "永久排除的包名(精确匹配,不做子串匹配)。用户 2026-08-26 指定,以后每次生成报告都不计入。", + "packages": [ + "com.levelinfinite.sgameGlobal", + "com.igg.android.lordsmobile", + "net.mbc.shahid", + "water.sort.color.puzzle.offline.games", + "com.sialiagames.xczyw.gp.tw", + "com.qookkagames.xczyw.gp.hk", + "com.luizalabs.mlapp", + "br.com.netshoes.app", + "de.autodoc.gmbh", + "id891130066", + "id886443637", + "id1014949597" + ] +}