feat: add MTG 10/13 performance data

This commit is contained in:
王源
2026-10-14 17:23:41 +08:00
parent 62373fde87
commit bb10731ddc
3 changed files with 4096 additions and 93 deletions
+142 -1
View File
@@ -261,19 +261,160 @@ def aggregate(records: list[dict[str, str]], counters: dict[str, int], source: P
}
def parse_mtg1013(source: Path, payload: dict[str, Any]) -> dict[str, Any]:
"""Parse MTG performance rows and relate them to the order coverage keys."""
import csv
from decimal import Decimal, InvalidOperation
if not source.is_file():
raise FileNotFoundError(f"MTG CSV not found: {source}")
required = {
"Adping ID",
"Pkg Name",
"Target Geo",
"Valid Clicks",
"Postback Installs",
"Revenue",
"Cost",
}
coverage_by_key = {
(row["pkg"].casefold(), row["country"]): row for row in payload["records"]
}
def number(value: Any, field: str, line: int) -> Decimal:
try:
return Decimal(clean_text(value) or "0")
except InvalidOperation as exc:
raise ValueError(f"Invalid {field} on CSV line {line}: {value}") from exc
def decimal_value(value: Decimal, places: str = "0.0001") -> float:
return float(value.quantize(Decimal(places)))
records: list[dict[str, Any]] = []
with source.open("r", encoding="utf-8-sig", newline="") as handle:
reader = csv.DictReader(handle)
missing = required.difference(reader.fieldnames or [])
if missing:
raise ValueError(f"Missing MTG CSV columns: {', '.join(sorted(missing))}")
for line, row in enumerate(reader, start=2):
adpingid = clean_text(row["Adping ID"])
pkg = clean_text(row["Pkg Name"])
country = clean_text(row["Target Geo"]).upper()
if not adpingid or not pkg or not country:
raise ValueError(f"Incomplete MTG CSV key on line {line}")
if re.match(r"^https?://", pkg, flags=re.IGNORECASE):
raise ValueError(f"Invalid MTG CSV pkg URL on line {line}")
clicks = int(number(row["Valid Clicks"], "Valid Clicks", line))
installs = int(number(row["Postback Installs"], "Postback Installs", line))
revenue = number(row["Revenue"], "Revenue", line)
cost = number(row["Cost"], "Cost", line)
key = (pkg.casefold(), country)
coverage = coverage_by_key.get(key)
records.append(
{
"adpingid": adpingid,
"pkg": pkg,
"country": country,
"valid_clicks": clicks,
"installs": installs,
"revenue": decimal_value(revenue),
"cost": decimal_value(cost),
"profit": decimal_value(revenue - cost),
"roas": round(float(revenue / cost), 4) if cost else None,
"install_rate": round(installs / clicks, 4) if clicks else None,
"order_match": coverage is not None,
"adpingid_match": bool(coverage and adpingid in coverage["adpingids"]),
"target_present": bool(coverage and coverage["target_present"]),
"missing_target": bool(coverage and coverage["missing_target"]),
}
)
records.sort(key=lambda row: (-row["cost"], -row["valid_clicks"], row["adpingid"]))
total_clicks = sum(row["valid_clicks"] for row in records)
total_installs = sum(row["installs"] for row in records)
total_revenue = sum(Decimal(str(row["revenue"])) for row in records)
total_cost = sum(Decimal(str(row["cost"])) for row in records)
by_key: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
by_country: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in records:
by_key[(row["pkg"].casefold(), row["country"])].append(row)
by_country[row["country"]].append(row)
for key, rows in by_key.items():
coverage = coverage_by_key.get(key)
if not coverage:
continue
coverage["mtg1013"] = {
"adpingids": sort_adpingids({row["adpingid"] for row in rows}),
"valid_clicks": sum(row["valid_clicks"] for row in rows),
"installs": sum(row["installs"] for row in rows),
"revenue": round(sum(row["revenue"] for row in rows), 4),
"cost": round(sum(row["cost"] for row in rows), 4),
}
country_summary = []
for country, rows in by_country.items():
revenue = sum(row["revenue"] for row in rows)
cost = sum(row["cost"] for row in rows)
country_summary.append(
{
"country": country,
"rows": len(rows),
"valid_clicks": sum(row["valid_clicks"] for row in rows),
"installs": sum(row["installs"] for row in rows),
"revenue": round(revenue, 4),
"cost": round(cost, 4),
"profit": round(revenue - cost, 4),
"roas": round(revenue / cost, 4) if cost else None,
}
)
country_summary.sort(key=lambda row: (-row["cost"], row["country"]))
return {
"meta": {
"source": source.name,
"rows": len(records),
"unique_keys": len(by_key),
"unique_adpingids": len({row["adpingid"] for row in records}),
"matched_order_rows": sum(row["order_match"] for row in records),
"matched_adpingid_rows": sum(row["adpingid_match"] for row in records),
"unmatched_order_rows": sum(not row["order_match"] for row in records),
"valid_clicks": total_clicks,
"installs": total_installs,
"revenue": decimal_value(total_revenue),
"cost": decimal_value(total_cost),
"profit": decimal_value(total_revenue - total_cost),
"roas": round(float(total_revenue / total_cost), 4) if total_cost else None,
"install_rate": round(total_installs / total_clicks, 4) if total_clicks else None,
},
"country_summary": country_summary,
"records": records,
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE, help="Source XLSX workbook")
parser.add_argument(
"--mtg-source",
type=Path,
default=DEFAULT_SOURCE.with_name("mtg1013.csv"),
help="MTG 10/13 performance CSV",
)
parser.add_argument("--output", type=Path, default=Path(__file__).with_name("data.json"), help="Output JSON")
args = parser.parse_args()
records, counters = parse_workbook(args.source)
payload = aggregate(records, counters, args.source)
payload["mtg1013"] = parse_mtg1013(args.mtg_source, payload)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(
f"Wrote {args.output}: {payload['meta']['pkg_country_count']} pkg-country keys, "
f"{payload['meta']['missing_count']} missing {TARGET_PLATFORM}"
f"{payload['meta']['missing_count']} missing {TARGET_PLATFORM}, "
f"{payload['mtg1013']['meta']['rows']} MTG rows"
)