feat: add MTG 10/13 performance data
This commit is contained in:
+142
-1
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user