From 3c525a96725f19557c6febbae07d2b1697787098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=BA=90?= Date: Thu, 10 Sep 2026 15:26:54 +0800 Subject: [PATCH] init: voypace ad report --- adscenter_daily_sync.py | 129 ++++++++ campaigns.json | 162 ++++++++++ db_fetch.py | 92 ++++++ excel_fetch.py | 105 ++++++ index.html | 694 ++++++++++++++++++++++++++++++++++++++++ matrix_overrides.json | 11 + mtg_data.json | 434 +++++++++++++++++++++++++ orders.json | 129 ++++++++ seed_from_sheet4.py | 77 +++++ 9 files changed, 1833 insertions(+) create mode 100644 adscenter_daily_sync.py create mode 100644 campaigns.json create mode 100644 db_fetch.py create mode 100644 excel_fetch.py create mode 100644 index.html create mode 100644 matrix_overrides.json create mode 100644 mtg_data.json create mode 100644 orders.json create mode 100644 seed_from_sheet4.py diff --git a/adscenter_daily_sync.py b/adscenter_daily_sync.py new file mode 100644 index 0000000..a015b8d --- /dev/null +++ b/adscenter_daily_sync.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# 在 ads-center 上每天运行:取前一天3渠道数据 -> 合并 mtg_data.json -> 生成矩阵 -> git push +# 缺表渠道(Adsterra/TrafficStars/Eagllwin)由 excel_fetch.py 从运营 Excel 补数(可选,找不到就跳过) +# 由 launchd 定时触发。Python: /Users/ads-center/py314env/bin/python +import sqlite3, json, subprocess, sys, os +from collections import defaultdict +from datetime import date, timedelta + +DB = "/Users/ads-center/Documents/database/report_api.db" +REPO = "/Users/ads-center/voypace-report-sync/voypace-ad-report" + +def log(m): print(f"[sync] {m}", flush=True) + +# 目标日期:前一天(可用参数覆盖,便于补数) +TARGET = sys.argv[1] if len(sys.argv) > 1 else (date.today() - timedelta(days=1)).isoformat() + +os.chdir(REPO) +# 先 pull 最新,避免冲突 +subprocess.run(["git", "pull", "--rebase", "-q"], cwd=REPO) + +camps = json.load(open("campaigns.json")) +CAMPS = {r["campid"]: {"product": r["product"], "pkg": r["pkg"]} for r in camps} +IDS = list(CAMPS.keys()) + +conn = sqlite3.connect(DB); conn.row_factory = sqlite3.Row +agg = defaultdict(lambda: {"impression":0,"clicks":0,"conversions":0,"spend":0.0}) +meta = {} + +def cid_of(t): + t = str(t or "") + for c in IDS: + if c in t: return c + return None + +# Mintegral(广告单元名可能落在 Offer_Name 或 Campaign_Id,两字段都匹配) +like = " OR ".join([f"Offer_Name LIKE '%{c}%' OR Campaign_Id LIKE '%{c}%'" for c in IDS]) +for r in conn.execute(f"SELECT Date d,Offer_Name nm,Campaign_Id cmp,Location loc,Impression imp,Click clk,Conversion cv,Spend sp FROM mintegral WHERE Date=? AND ({like})", (TARGET,)): + c = cid_of(r["nm"]) or cid_of(r["cmp"]) + if not c: continue + k = (r["d"],"Mintegral",c,(r["loc"] or "").upper()); a=agg[k] + a["impression"]+=int(r["imp"] or 0); a["clicks"]+=int(r["clk"] or 0) + a["conversions"]+=int(r["cv"] or 0); a["spend"]+=float(r["sp"] or 0) + meta[k]=(r["nm"] or r["cmp"],CAMPS[c]) + +# Bigo +like = " OR ".join([f"campaignName LIKE '%{c}%'" for c in IDS]) +for r in conn.execute(f"SELECT aggregateTime d,campaignName nm,country loc,impression imp,click clk,real_result_conversion cv,totalCost cost FROM bigo WHERE aggregateTime=? AND ({like})", (TARGET,)): + c = cid_of(r["nm"]) + if not c: continue + k = (r["d"],"Bigo",c,(r["loc"] or "").upper()); a=agg[k] + a["impression"]+=int(r["imp"] or 0); a["clicks"]+=int(r["clk"] or 0) + a["conversions"]+=int(r["cv"] or 0); a["spend"]+=float(r["cost"] or 0)/100.0 + meta[k]=(r["nm"],CAMPS[c]) + +# MediaGo (spend = cpc*click) +like = " OR ".join([f"campaign_name LIKE '%{c}%'" for c in IDS]) +for r in conn.execute(f"SELECT date d,campaign_name nm,adset_country loc,vimp imp,click clk,cv cvn,cpc FROM mediago WHERE date=? AND ({like})", (TARGET,)): + c = cid_of(r["nm"]) + if not c: continue + k = (r["d"],"MediaGo",c,(r["loc"] or "").upper()); a=agg[k] + clk=int(r["clk"] or 0) + a["impression"]+=int(r["imp"] or 0); a["clicks"]+=clk + a["conversions"]+=int(r["cvn"] or 0); a["spend"]+=float(r["cpc"] or 0)*clk + meta[k]=(r["nm"],CAMPS[c]) +conn.close() + +new_rows=[] +for k,a in agg.items(): + dt,ch,c,loc=k; nm,info=meta[k] + imp,clk,cv=a["impression"],a["clicks"],a["conversions"] + new_rows.append({"date":dt,"channel":ch,"campid":c,"offer_name":nm,"product":info["product"],"pkg":info["pkg"], + "location":loc,"bid_type":"","impression":imp,"clicks":clk,"conversions":cv, + "ctr":round(clk/imp,5) if imp else 0.0,"cvr":round(cv/clk,5) if clk else 0.0,"spend":round(a["spend"],2)}) +log(f"{TARGET} DB三渠道新增匹配 {len(new_rows)} 条") + +# 可选:从运营 Excel 补齐缺表渠道(Adsterra/TrafficStars/Eagllwin 等) +excel_rows = [] +try: + ef = os.path.join(REPO, "excel_fetch.py") + if os.path.exists(ef): + p = subprocess.run([sys.executable, ef], cwd=REPO, capture_output=True, text=True) + if p.returncode == 0 and p.stdout.strip(): + excel_rows = json.loads(p.stdout) + # 只保留目标日期,保持“每天增量”语义(补数全量跑时可去掉此过滤) + excel_rows = [r for r in excel_rows if r.get("date") == TARGET] + else: + log(f"Excel 补数无输出/失败:{p.stderr[:120]}") +except Exception as e: + log(f"Excel 补数跳过:{e}") +if excel_rows: + log(f"Excel 补数新增 {len(excel_rows)} 条") + new_rows += excel_rows + +# 合并(按 date+channel+campid+location 去重) +existing = json.load(open("mtg_data.json")) if os.path.exists("mtg_data.json") else [] +merged = {(r["date"],r.get("channel",""),r["campid"],r["location"]): r for r in existing} +for r in new_rows: + merged[(r["date"],r["channel"],r["campid"],r["location"])] = r +data = sorted(merged.values(), key=lambda x:(x["date"],x.get("channel",""),-x["impression"])) +json.dump(data, open("mtg_data.json","w"), ensure_ascii=False, indent=2) +log(f"合并后共 {len(data)} 条") + +# 重新生成矩阵状态(近3天单日最高展示>=100 且产品订单为Active -> Active) +orders = json.load(open("orders.json")) +pkg_status={} +for o in orders: + for p in str(o["pkg"]).replace(" ","").split("/"): + if p: pkg_status[p]=o.get("status","Pause") +g=defaultdict(lambda: defaultdict(int)) +for r in data: g[(r["channel"],r["pkg"])][r["date"]]+=r["impression"] +alldates=sorted(set(r["date"] for r in data)); last3=alldates[-3:] +ov={} +for (ch,pkg),days in g.items(): + ps=pkg_status.get(pkg,"Pause") + if ps!="Active": ov[f"{ch}|{pkg}"]="Pause"; continue + total=sum(days.values()); rmax=max([days.get(dt,0) for dt in last3]) if last3 else 0 + ov[f"{ch}|{pkg}"]= "none" if total==0 else ("Active" if rmax>=100 else "Pause") +json.dump(ov, open("matrix_overrides.json","w"), ensure_ascii=False, indent=2) +log("矩阵状态已刷新") + +# git push +subprocess.run(["git","add","mtg_data.json","matrix_overrides.json"], cwd=REPO) +r=subprocess.run(["git","commit","-m",f"data: daily sync {TARGET}"], cwd=REPO, capture_output=True, text=True) +if "nothing to commit" in (r.stdout+r.stderr): + log("无变化,跳过 push") +else: + p=subprocess.run(["git","push","-q","origin","main"], cwd=REPO, capture_output=True, text=True) + log("已 push" if p.returncode==0 else f"push失败: {p.stderr[:200]}") +log("完成") diff --git a/campaigns.json b/campaigns.json new file mode 100644 index 0000000..156e67a --- /dev/null +++ b/campaigns.json @@ -0,0 +1,162 @@ +[ + { + "campid": "91504360", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "geo": "RU", + "channel": "Mintegral", + "mode": "自投" + }, + { + "campid": "91508373", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "geo": "RU", + "channel": "Bigo", + "mode": "自投" + }, + { + "campid": "91508374", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "geo": "RU", + "channel": "MediaGo", + "mode": "自投" + }, + { + "campid": "91508376", + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "geo": "RU", + "channel": "Bigo", + "mode": "自投" + }, + { + "campid": "91508375", + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "geo": "RU", + "channel": "MediaGo", + "mode": "自投" + }, + { + "campid": "91505669", + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "geo": "RU", + "channel": "Mintegral", + "mode": "自投" + }, + { + "campid": "91513412", + "product": "Melbet", + "pkg": "org.melbet.client", + "geo": "RU", + "channel": "Bigo", + "mode": "自投" + }, + { + "campid": "91513413", + "product": "Melbet", + "pkg": "org.melbet.client", + "geo": "RU", + "channel": "Adsterra", + "mode": "自投" + }, + { + "campid": "91513414", + "product": "Melbet", + "pkg": "org.melbet.client", + "geo": "RU", + "channel": "TrafficStars", + "mode": "自投" + }, + { + "campid": "91511819", + "product": "Avito", + "pkg": "se.scmv.morocco", + "geo": "RU", + "channel": "Bigo", + "mode": "自投" + }, + { + "campid": "91513417", + "product": "Avito", + "pkg": "se.scmv.morocco", + "geo": "RU", + "channel": "Mintegral", + "mode": "自投" + }, + { + "campid": "91513418", + "product": "Avito", + "pkg": "se.scmv.morocco", + "geo": "RU", + "channel": "MediaGo", + "mode": "自投" + }, + { + "campid": "91513416", + "product": "Megapari", + "pkg": "org.megapari.client", + "geo": "BR", + "channel": "Bigo", + "mode": "自投" + }, + { + "campid": "91513572", + "product": "Megapari", + "pkg": "org.megapari.client", + "geo": "BR", + "channel": "Adsterra", + "mode": "自投" + }, + { + "campid": "91545519", + "product": "Viu", + "pkg": "com.vuclip.viu", + "geo": "MY", + "channel": "Mintegral", + "mode": "自投" + }, + { + "campid": "91545518", + "product": "Vdrf", + "pkg": "com.cathayholdings.vdrf", + "geo": "VN", + "channel": "Mintegral", + "mode": "自投" + }, + { + "campid": "91554851", + "product": "Viu", + "pkg": "com.vuclip.viu", + "geo": "ID", + "channel": "MediaGo", + "mode": "自投" + }, + { + "campid": "91554852", + "product": "Viu", + "pkg": "com.vuclip.viu", + "geo": "ID", + "channel": "Bigo", + "mode": "自投" + }, + { + "campid": "91554853", + "product": "Viu", + "pkg": "com.vuclip.viu", + "geo": "ID", + "channel": "Eagllwin", + "mode": "自投" + }, + { + "campid": "91555309", + "product": "Viu", + "pkg": "com.vuclip.viu", + "geo": "ID", + "channel": "Mintegral", + "mode": "自投" + } +] diff --git a/db_fetch.py b/db_fetch.py new file mode 100644 index 0000000..fd3e821 --- /dev/null +++ b/db_fetch.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# 在 ads-center 上运行:从 report_api.db 取 mintegral/bigo/mediago 三渠道 +# 按 campid 匹配 Voypace 产品,聚合(日期+campid+国家),输出 JSON 到 stdout。 +# 参数:start_date(含) end_date(含),格式 yyyy-MM-dd +import sqlite3, json, sys, os +from collections import defaultdict + +DB = "/Users/ads-center/Documents/database/report_api.db" +START = sys.argv[1] if len(sys.argv) > 1 else "2026-07-15" +END = sys.argv[2] if len(sys.argv) > 2 else "2026-12-31" + +# campid -> 产品信息(从同目录 campaigns.json 读取) +HERE = os.path.dirname(os.path.abspath(__file__)) +_camps = json.load(open(os.path.join(HERE, "campaigns.json"))) +CAMPS = {r["campid"]: {"product": r["product"], "pkg": r["pkg"]} for r in _camps} + +conn = sqlite3.connect(DB) +conn.row_factory = sqlite3.Row + +# 聚合容器:key=(date,channel,campid,location) -> 累加指标 +agg = defaultdict(lambda: {"impression":0,"clicks":0,"conversions":0,"spend":0.0}) +meta = {} + +def campid_of(text): + t = str(text or "") + for cid in CAMPS: + if cid in t: + return cid + return None + +# ---- Mintegral ---- +# 广告单元名(如 91555309_Viu_ID)可能落在 Offer_Name 或 Campaign_Id,两字段都匹配 +like = " OR ".join([f"Offer_Name LIKE '%{cid}%' OR Campaign_Id LIKE '%{cid}%'" for cid in CAMPS]) +sql = f"""SELECT Date d, Offer_Name nm, Campaign_Id cmp, Location loc, Impression imp, Click clk, + Conversion cv, Spend sp + FROM mintegral WHERE Date>=? AND Date<=? AND ({like})""" +for r in conn.execute(sql, (START, END)): + cid = campid_of(r["nm"]) or campid_of(r["cmp"]) + if not cid: continue + k = (r["d"], "Mintegral", cid, (r["loc"] or "").upper()) + a = agg[k] + a["impression"] += int(r["imp"] or 0); a["clicks"] += int(r["clk"] or 0) + a["conversions"] += int(r["cv"] or 0); a["spend"] += float(r["sp"] or 0) + meta[k] = (r["nm"] or r["cmp"], CAMPS[cid]) + +# ---- Bigo ---- +like = " OR ".join([f"campaignName LIKE '%{cid}%'" for cid in CAMPS]) +sql = f"""SELECT aggregateTime d, campaignName nm, country loc, tgtPkgName pkg, + impression imp, click clk, real_result_conversion cv, totalCost cost + FROM bigo WHERE aggregateTime>=? AND aggregateTime<=? AND ({like})""" +for r in conn.execute(sql, (START, END)): + cid = campid_of(r["nm"]) + if not cid: continue + k = (r["d"], "Bigo", cid, (r["loc"] or "").upper()) + a = agg[k] + a["impression"] += int(r["imp"] or 0); a["clicks"] += int(r["clk"] or 0) + a["conversions"] += int(r["cv"] or 0); a["spend"] += float(r["cost"] or 0)/100.0 + meta[k] = (r["nm"], CAMPS[cid]) + +# ---- MediaGo ---- (ad级明细,需聚合;spend = cpc*click 反算) +like = " OR ".join([f"campaign_name LIKE '%{cid}%'" for cid in CAMPS]) +sql = f"""SELECT date d, campaign_name nm, adset_country loc, vimp imp, click clk, + cv cvn, cpc + FROM mediago WHERE date>=? AND date<=? AND ({like})""" +for r in conn.execute(sql, (START, END)): + cid = campid_of(r["nm"]) + if not cid: continue + k = (r["d"], "MediaGo", cid, (r["loc"] or "").upper()) + a = agg[k] + clk = int(r["clk"] or 0) + a["impression"] += int(r["imp"] or 0); a["clicks"] += clk + a["conversions"] += int(r["cvn"] or 0); a["spend"] += float(r["cpc"] or 0) * clk + meta[k] = (r["nm"], CAMPS[cid]) + +conn.close() + +out = [] +for k, a in agg.items(): + date, channel, cid, loc = k + nm, info = meta[k] + imp, clk, cv = a["impression"], a["clicks"], a["conversions"] + out.append({ + "date": date, "channel": channel, "campid": cid, + "offer_name": nm, "product": info["product"], "pkg": info["pkg"], + "location": loc, "bid_type": "", + "impression": imp, "clicks": clk, "conversions": cv, + "ctr": round(clk/imp, 5) if imp else 0.0, + "cvr": round(cv/clk, 5) if clk else 0.0, + "spend": round(a["spend"], 2), + }) +out.sort(key=lambda x: (x["date"], x["channel"], -x["impression"])) +print(json.dumps(out, ensure_ascii=False)) diff --git a/excel_fetch.py b/excel_fetch.py new file mode 100644 index 0000000..0d10ecc --- /dev/null +++ b/excel_fetch.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# 从本机 Excel 补齐「数据库没有表」的渠道数据(Adsterra / TrafficStars / Eagllwin 等)。 +# 数据库只有 mintegral/bigo/mediago,其余渠道由运营手工维护到 Excel,本脚本读取后 +# 输出成与 db_fetch.py 相同结构的 JSON(stdout),供 daily_sync 合并进 mtg_data.json。 +# +# ── Excel 规范 ────────────────────────────────────────────── +# 路径:<运营>/Voypace/Voypace补充数据.xlsx (可用环境变量 VOYPACE_XLSX 覆盖) +# 第一个 sheet,首行为表头,需包含以下列(列名大小写/顺序不限): +# date 日期,yyyy-MM-dd(或 Excel 日期) +# campid 对应 campaigns.json 里的 campid +# impression 展示 +# clicks 点击 +# conversions 转化/安装 +# spend 花费(USD) +# 可选列:location(国家,缺省用 campaigns.json 里的 geo) +# 渠道/产品/包名 一律由 campid 反查 campaigns.json,Excel 无需再填。 +# 只读取 campid 命中 campaigns.json 的行;其它行忽略。 +# ──────────────────────────────────────────────────────────── +import json, sys, os, datetime + +HERE = os.path.dirname(os.path.abspath(__file__)) +_camps = json.load(open(os.path.join(HERE, "campaigns.json"))) +CAMPS = {str(r["campid"]): r for r in _camps} + +DEFAULT_XLSX = os.environ.get( + "VOYPACE_XLSX", + "/Users/wangyuan/Documents/SHAREit/运营/Voypace/Voypace补充数据.xlsx", +) + +def norm_date(v): + if v is None: return "" + if isinstance(v, (datetime.datetime, datetime.date)): + return v.strftime("%Y-%m-%d") + s = str(v).strip() + return s[:10] if s else "" + +def to_int(v): + try: return int(float(str(v).replace(",", "").strip() or 0)) + except: return 0 + +def to_float(v): + try: return float(str(v).replace(",", "").replace("$", "").strip() or 0) + except: return 0.0 + +def main(path): + if not os.path.exists(path): + # 没有补充表就输出空数组,daily_sync 照常跑 + print("[]"); return + try: + import openpyxl + except ImportError: + sys.stderr.write("openpyxl 未安装,跳过 Excel 补数\n") + print("[]"); return + + wb = openpyxl.load_workbook(path, data_only=True) + ws = wb.worksheets[0] + rows = list(ws.iter_rows(values_only=True)) + if not rows: + print("[]"); return + + header = [str(c).strip().lower() if c is not None else "" for c in rows[0]] + def col(*names): + for n in names: + if n in header: return header.index(n) + return None + ci = { + "date": col("date", "时间", "日期"), + "campid": col("campid", "adping id", "adping_id"), + "loc": col("location", "地区", "国家"), + "imp": col("impression", "impressions", "展示"), + "clk": col("clicks", "click", "点击"), + "cv": col("conversions", "install", "installs", "转化", "安装"), + "spend": col("spend", "花费", "cost"), + } + + out = [] + for r in rows[1:]: + if ci["campid"] is None: break + cid = str(r[ci["campid"]]).strip() if r[ci["campid"]] is not None else "" + cid = cid.split(".")[0] # 去掉可能的 .0 + if cid not in CAMPS: continue + info = CAMPS[cid] + loc = "" + if ci["loc"] is not None and r[ci["loc"]] is not None: + loc = str(r[ci["loc"]]).strip().upper() + if not loc: loc = str(info.get("geo", "")).upper() + imp = to_int(r[ci["imp"]]) if ci["imp"] is not None else 0 + clk = to_int(r[ci["clk"]]) if ci["clk"] is not None else 0 + cv = to_int(r[ci["cv"]]) if ci["cv"] is not None else 0 + sp = to_float(r[ci["spend"]]) if ci["spend"] is not None else 0.0 + dt = norm_date(r[ci["date"]]) if ci["date"] is not None else "" + if not dt: continue + out.append({ + "date": dt, "channel": info["channel"], "campid": cid, + "offer_name": f'{cid}_{info["product"]}', "product": info["product"], + "pkg": info["pkg"], "location": loc, "bid_type": "", + "impression": imp, "clicks": clk, "conversions": cv, + "ctr": round(clk/imp, 5) if imp else 0.0, + "cvr": round(cv/clk, 5) if clk else 0.0, + "spend": round(sp, 2), + }) + print(json.dumps(out, ensure_ascii=False)) + +if __name__ == "__main__": + main(sys.argv[1] if len(sys.argv) > 1 else DEFAULT_XLSX) diff --git a/index.html b/index.html new file mode 100644 index 0000000..0c9e72f --- /dev/null +++ b/index.html @@ -0,0 +1,694 @@ + + + + + + Voypace 广告投放报告 + + + + + +
+
+

Voypace 广告投放报告

+

投放数据 · 订单信息 · Campaign ID 明细

+
+ +
+ + + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + ~ + + + +
+
+ + +
+ + +
+

投放趋势(展示 / 花费 / 转化)

+ +
+ + +
+
+

渠道对比(花费 / 转化)

+ +
+
+

花费占比

+ +
+
+ + +
+

渠道效率对比(产品_国家 × 渠道)

+

每个产品_国家下列出其投放过的各渠道及效率指标,同组内可横向对比不同渠道表现。

+
+ + + + + + + + +
产品_国家渠道展示点击转化花费CTRCVReCPMCPCCPA
+
+
+ + +
+

产品表现排行

+
+ + + + + + + + +
产品渠道数展示点击转化花费CTRCVRCPA
+
+
+ + +
+
+

国家展示分布 Top

+ +
+
+

国家维度明细

+
+ + + +
国家展示点击转化花费CTR
+
+
+
+ + +
+

明细数据(按 Campaign×日期)

+
+ + + + + + + + +
日期渠道产品Offer Name国家展示点击转化CTRCVRSpend
+
+
+
+ + +
+
+ +
+

产品订单基础信息

+
+ + + + + + + + + + + + + + + +
ProductPkg / 包名平台地区付费类型单价(USD)日预算状态MMP
+
+
+ +
+

渠道投放矩阵(渠道 × 产品)

+

+ Active 投放中  + Pause 投过已停  + 空白 = 暂无数据(未接入或未投放)
+ 状态依据实际投放数据判定:产品订单为 Pause 直接判停;产品在投则看该渠道近期展示趋势(掉到个位数/断更视为已停)。仅显示已接入数据的渠道。 +

+
+
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + + + + + + + + + + +
Campaign IDProductPkg / 包名投放国家渠道
+
+
+
+ +
+
+ + + + diff --git a/matrix_overrides.json b/matrix_overrides.json new file mode 100644 index 0000000..7d46c80 --- /dev/null +++ b/matrix_overrides.json @@ -0,0 +1,11 @@ +{ + "Bigo|org.melbet.client": "Pause", + "Bigo|ru.bistrodengi.my": "Pause", + "Bigo|robot.zaimer.ru": "Pause", + "Bigo|org.megapari.client": "Pause", + "MediaGo|ru.bistrodengi.my": "Pause", + "MediaGo|robot.zaimer.ru": "Pause", + "Mintegral|robot.zaimer.ru": "Pause", + "Mintegral|ru.bistrodengi.my": "Pause", + "Bigo|se.scmv.morocco": "Pause" +} \ No newline at end of file diff --git a/mtg_data.json b/mtg_data.json new file mode 100644 index 0000000..3592335 --- /dev/null +++ b/mtg_data.json @@ -0,0 +1,434 @@ +[ + { + "date": "2026-07-27", + "channel": "Bigo", + "campid": "91513412", + "offer_name": "91513412_Melbet", + "product": "Melbet", + "pkg": "org.melbet.client", + "location": "RU", + "bid_type": "", + "impression": 24597, + "clicks": 15776, + "conversions": 0, + "spend": 12.62, + "ctr": 0.64138, + "cvr": 0.0 + }, + { + "date": "2026-07-27", + "channel": "Bigo", + "campid": "91508373", + "offer_name": "91508373_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 14366, + "clicks": 1479, + "conversions": 2, + "spend": 5.32, + "ctr": 0.10295, + "cvr": 0.00135 + }, + { + "date": "2026-07-27", + "channel": "Bigo", + "campid": "91508376", + "offer_name": "91508376_Robotzaimer", + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "location": "RU", + "bid_type": "", + "impression": 6326, + "clicks": 1064, + "conversions": 0, + "spend": 5.72, + "ctr": 0.16819, + "cvr": 0.0 + }, + { + "date": "2026-07-27", + "channel": "Bigo", + "campid": "91513416", + "offer_name": "91513416_Megapari", + "product": "Megapari", + "pkg": "org.megapari.client", + "location": "BR", + "bid_type": "", + "impression": 0, + "clicks": 0, + "conversions": 0, + "spend": 0.0, + "ctr": 0.0, + "cvr": 0.0 + }, + { + "date": "2026-07-27", + "channel": "MediaGo", + "campid": "91508374", + "offer_name": "91508374_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 3693, + "clicks": 40, + "conversions": 1, + "spend": 0.12, + "ctr": 0.01083, + "cvr": 0.025 + }, + { + "date": "2026-07-27", + "channel": "MediaGo", + "campid": "91508375", + "offer_name": "91508375_Robotzaimer", + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "location": "RU", + "bid_type": "", + "impression": 906, + "clicks": 27, + "conversions": 0, + "spend": 0.06, + "ctr": 0.0298, + "cvr": 0.0 + }, + { + "date": "2026-07-27", + "channel": "Mintegral", + "campid": "91505669", + "offer_name": "91505669_Robotzaimer", + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "location": "RU", + "bid_type": "", + "impression": 29251, + "clicks": 36, + "conversions": 0, + "spend": 0.0, + "ctr": 0.00123, + "cvr": 0.0 + }, + { + "date": "2026-07-27", + "channel": "Mintegral", + "campid": "91504360", + "offer_name": "91504360_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 24835, + "clicks": 56, + "conversions": 1, + "spend": 0.3, + "ctr": 0.00225, + "cvr": 0.01786 + }, + { + "date": "2026-07-28", + "channel": "Bigo", + "campid": "91513416", + "offer_name": "91513416_Megapari", + "product": "Megapari", + "pkg": "org.megapari.client", + "location": "BR", + "bid_type": "", + "impression": 49584, + "clicks": 32132, + "conversions": 0, + "spend": 12.85, + "ctr": 0.64803, + "cvr": 0.0 + }, + { + "date": "2026-07-28", + "channel": "Bigo", + "campid": "91513412", + "offer_name": "91513412_Melbet", + "product": "Melbet", + "pkg": "org.melbet.client", + "location": "RU", + "bid_type": "", + "impression": 35860, + "clicks": 21776, + "conversions": 0, + "spend": 17.42, + "ctr": 0.60725, + "cvr": 0.0 + }, + { + "date": "2026-07-28", + "channel": "Bigo", + "campid": "91508376", + "offer_name": "91508376_Robotzaimer", + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "location": "RU", + "bid_type": "", + "impression": 7941, + "clicks": 573, + "conversions": 1, + "spend": 1.27, + "ctr": 0.07216, + "cvr": 0.00175 + }, + { + "date": "2026-07-28", + "channel": "Bigo", + "campid": "91508373", + "offer_name": "91508373_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 461, + "clicks": 1, + "conversions": 0, + "spend": 0.0, + "ctr": 0.00217, + "cvr": 0.0 + }, + { + "date": "2026-07-28", + "channel": "MediaGo", + "campid": "91508375", + "offer_name": "91508375_Robotzaimer", + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "location": "RU", + "bid_type": "", + "impression": 20173, + "clicks": 79, + "conversions": 3, + "spend": 0.26, + "ctr": 0.00392, + "cvr": 0.03797 + }, + { + "date": "2026-07-28", + "channel": "MediaGo", + "campid": "91508374", + "offer_name": "91508374_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 5147, + "clicks": 56, + "conversions": 1, + "spend": 0.17, + "ctr": 0.01088, + "cvr": 0.01786 + }, + { + "date": "2026-07-28", + "channel": "Mintegral", + "campid": "91504360", + "offer_name": "91504360_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 40376, + "clicks": 172, + "conversions": 5, + "spend": 1.5, + "ctr": 0.00426, + "cvr": 0.02907 + }, + { + "date": "2026-07-28", + "channel": "Mintegral", + "campid": "91505669", + "offer_name": "91505669_Robotzaimer", + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "location": "RU", + "bid_type": "", + "impression": 20173, + "clicks": 79, + "conversions": 3, + "spend": 0.26, + "ctr": 0.00392, + "cvr": 0.03797 + }, + { + "date": "2026-07-29", + "channel": "Bigo", + "campid": "91513416", + "offer_name": "91513416_Megapari", + "product": "Megapari", + "pkg": "org.megapari.client", + "location": "BR", + "bid_type": "", + "impression": 172007, + "clicks": 28416, + "conversions": 0, + "spend": 11.37, + "ctr": 0.1652, + "cvr": 0.0 + }, + { + "date": "2026-07-29", + "channel": "Bigo", + "campid": "91513412", + "offer_name": "91513412_Melbet", + "product": "Melbet", + "pkg": "org.melbet.client", + "location": "RU", + "bid_type": "", + "impression": 78368, + "clicks": 44318, + "conversions": 0, + "spend": 17.73, + "ctr": 0.56551, + "cvr": 0.0 + }, + { + "date": "2026-07-29", + "channel": "Bigo", + "campid": "91508373", + "offer_name": "91508373_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 1136, + "clicks": 5, + "conversions": 0, + "spend": 0.0, + "ctr": 0.0044, + "cvr": 0.0 + }, + { + "date": "2026-07-29", + "channel": "MediaGo", + "campid": "91508374", + "offer_name": "91508374_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 33777, + "clicks": 29, + "conversions": 0, + "spend": 0.53, + "ctr": 0.00086, + "cvr": 0.0 + }, + { + "date": "2026-07-29", + "channel": "Mintegral", + "campid": "91504360", + "offer_name": "91504360_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 58734, + "clicks": 689, + "conversions": 5, + "spend": 3.0, + "ctr": 0.01173, + "cvr": 0.00726 + }, + { + "date": "2026-07-30", + "channel": "Bigo", + "campid": "91511819", + "offer_name": "91511819_Avito", + "product": "Avito", + "pkg": "se.scmv.morocco", + "location": "RU", + "bid_type": "", + "impression": 170946, + "clicks": 2887, + "conversions": 0, + "spend": 10.12, + "ctr": 0.01689, + "cvr": 0.0 + }, + { + "date": "2026-07-30", + "channel": "Bigo", + "campid": "91513416", + "offer_name": "91513416_Megapari", + "product": "Megapari", + "pkg": "org.megapari.client", + "location": "BR", + "bid_type": "", + "impression": 42450, + "clicks": 2445, + "conversions": 0, + "spend": 0.98, + "ctr": 0.0576, + "cvr": 0.0 + }, + { + "date": "2026-07-30", + "channel": "Bigo", + "campid": "91508373", + "offer_name": "91508373_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 27031, + "clicks": 3103, + "conversions": 1, + "spend": 12.93, + "ctr": 0.11479, + "cvr": 0.00032 + }, + { + "date": "2026-07-30", + "channel": "Bigo", + "campid": "91513412", + "offer_name": "91513412_Melbet", + "product": "Melbet", + "pkg": "org.melbet.client", + "location": "RU", + "bid_type": "", + "impression": 2872, + "clicks": 1691, + "conversions": 0, + "spend": 0.68, + "ctr": 0.58879, + "cvr": 0.0 + }, + { + "date": "2026-07-30", + "channel": "MediaGo", + "campid": "91508374", + "offer_name": "91508374_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 5430, + "clicks": 15, + "conversions": 0, + "spend": 0.57, + "ctr": 0.00276, + "cvr": 0.0 + }, + { + "date": "2026-07-30", + "channel": "Mintegral", + "campid": "91504360", + "offer_name": "91504360_Bistro", + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "location": "RU", + "bid_type": "", + "impression": 19561, + "clicks": 548, + "conversions": 9, + "spend": 6.75, + "ctr": 0.02801, + "cvr": 0.01642 + } +] \ No newline at end of file diff --git a/orders.json b/orders.json new file mode 100644 index 0000000..509aaff --- /dev/null +++ b/orders.json @@ -0,0 +1,129 @@ +[ + { + "product": "Bistro", + "pkg": "ru.bistrodengi.my", + "platform": "Android", + "geo": "RU", + "pay_mode": "CPI", + "price": "0.6", + "budget": "-", + "status": "Pause", + "mmp": "AppsFlyer", + "agency_channels": [], + "self_channels": [ + "Mintegral", + "Bigo", + "MediaGo" + ], + "source": "Voypace投放计划 SO10020030" + }, + { + "product": "Robotzaimer", + "pkg": "robot.zaimer.ru", + "platform": "Android", + "geo": "RU", + "pay_mode": "CPI", + "price": "0.3", + "budget": "-", + "status": "Pause", + "mmp": "AppsFlyer", + "agency_channels": [], + "self_channels": [ + "Mintegral", + "Bigo", + "MediaGo" + ], + "source": "Voypace投放计划 SO10020029" + }, + { + "product": "Melbet", + "pkg": "org.melbet.client", + "platform": "Android", + "geo": "RU", + "pay_mode": "CPR", + "price": "1", + "budget": "-", + "status": "Pause", + "mmp": "AppsFlyer", + "agency_channels": [ + "Adsterra", + "TrafficStars" + ], + "self_channels": [ + "Bigo" + ], + "source": "Voypace投放计划 SO10020057" + }, + { + "product": "Avito", + "pkg": "se.scmv.morocco", + "platform": "Android", + "geo": "RU", + "pay_mode": "CPI", + "price": "0.5", + "budget": "-", + "status": "Pause", + "mmp": "AppsFlyer", + "agency_channels": [], + "self_channels": [ + "Mintegral", + "Bigo", + "MediaGo" + ], + "source": "Voypace投放计划 SO10020058" + }, + { + "product": "Megapari", + "pkg": "org.megapari.client", + "platform": "Android", + "geo": "BR", + "pay_mode": "CPI", + "price": "0.4", + "budget": "-", + "status": "Pause", + "mmp": "AppsFlyer", + "agency_channels": [ + "Adsterra" + ], + "self_channels": [ + "Bigo" + ], + "source": "Voypace投放计划 SO10020059" + }, + { + "product": "Viu", + "pkg": "com.vuclip.viu", + "platform": "Android", + "geo": "MY / ID", + "pay_mode": "CPI / CPA", + "price": "0.15 (MY) / 1.5 (ID)", + "budget": "-", + "status": "Active", + "mmp": "AppsFlyer", + "agency_channels": [ + "Eagllwin" + ], + "self_channels": [ + "Mintegral", + "Bigo", + "MediaGo" + ], + "source": "Voypace投放计划 SO10020141 / SO10020180" + }, + { + "product": "Vdrf", + "pkg": "com.cathayholdings.vdrf", + "platform": "Android", + "geo": "VN", + "pay_mode": "CPI", + "price": "0.3", + "budget": "-", + "status": "Pause", + "mmp": "AppsFlyer", + "agency_channels": [], + "self_channels": [ + "Mintegral" + ], + "source": "Voypace投放计划 SO10020140" + } +] diff --git a/seed_from_sheet4.py b/seed_from_sheet4.py new file mode 100644 index 0000000..adca3df --- /dev/null +++ b/seed_from_sheet4.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# 一次性:从 运营/Voypace/Voypace投放计划.xlsx 的 Sheet4 生成初始 mtg_data.json +# 之后的每日增量由 adscenter_daily_sync.py 负责。 +import openpyxl, json, os, datetime + +XLSX = "/Users/wangyuan/Documents/SHAREit/运营/Voypace/Voypace投放计划.xlsx" +HERE = os.path.dirname(os.path.abspath(__file__)) +camps = json.load(open(os.path.join(HERE, "campaigns.json"))) +CAMPS = {r["campid"]: r for r in camps} +# 渠道名归一:Sheet4 用 Mediago,campaigns 用 MediaGo +CH_NORM = {"mediago": "MediaGo", "mintegral": "Mintegral", "bigo": "Bigo"} +# 无 campid 行的兜底:按 (包名, 渠道) 反查 campid +BY_PKG_CH = {(r["pkg"], r["channel"]): r["campid"] for r in camps} + +base = datetime.date(1899, 12, 30) +def to_date(v): + if isinstance(v, (int, float)): + return (base + datetime.timedelta(days=int(v))).isoformat() + if isinstance(v, (datetime.datetime, datetime.date)): + return v.strftime("%Y-%m-%d") + return str(v)[:10] + +def num(v): + try: return float(str(v).replace(",", "").strip() or 0) + except: return 0.0 + +wb = openpyxl.load_workbook(XLSX, data_only=True) +ws = wb["Sheet4"] +rows = list(ws.iter_rows(values_only=True)) + +cur_date = None +out = {} +for r in rows[1:]: + # 列: 0时间 1单价 2地区 3包名 4OS 5AdpingID 6投放平台 7Imp 8Clk 9CTR 10Install 11CVR 12Spend 13oCPI + if r[0] not in (None, ""): + cur_date = to_date(r[0]) + pkg = (str(r[3]).strip() if r[3] else "") + loc = (str(r[2]).strip().upper() if r[2] else "") + adping = (str(r[5]).strip() if r[5] else "") + ch_raw = (str(r[6]).strip() if r[6] else "") + ch = CH_NORM.get(ch_raw.lower(), ch_raw) + if not ch or not cur_date: + continue + # 定位 campid + cid = adping.split(".")[0] if adping else "" + if cid not in CAMPS: + # 兜底:靠 包名+渠道(处理 Avito 那种 campid 为 campaign_name 的行) + # 包名可能带 (APK)等后缀,取主体 + pkg_main = pkg.split("(")[0].split("(")[0].strip() + cid = BY_PKG_CH.get((pkg_main, ch), "") + if cid not in CAMPS: + continue + info = CAMPS[cid] + imp = int(num(r[7])); clk = int(num(r[8])); cv = int(num(r[10])); sp = round(num(r[12]), 2) + if not loc: + loc = str(info.get("geo", "")).upper() + key = (cur_date, info["channel"], cid, loc) + a = out.get(key) + if a: + a["impression"] += imp; a["clicks"] += clk; a["conversions"] += cv; a["spend"] = round(a["spend"] + sp, 2) + else: + out[key] = { + "date": cur_date, "channel": info["channel"], "campid": cid, + "offer_name": f'{cid}_{info["product"]}', "product": info["product"], + "pkg": info["pkg"], "location": loc, "bid_type": "", + "impression": imp, "clicks": clk, "conversions": cv, "spend": sp, + } + +data = [] +for a in out.values(): + imp, clk, cv = a["impression"], a["clicks"], a["conversions"] + a["ctr"] = round(clk/imp, 5) if imp else 0.0 + a["cvr"] = round(cv/clk, 5) if clk else 0.0 + data.append(a) +data.sort(key=lambda x: (x["date"], x["channel"], -x["impression"])) +json.dump(data, open(os.path.join(HERE, "mtg_data.json"), "w"), ensure_ascii=False, indent=2) +print(f"生成 {len(data)} 条 -> mtg_data.json")