175 lines
7.5 KiB
Bash
Executable File
175 lines
7.5 KiB
Bash
Executable File
#!/bin/sh
|
||
set -eu
|
||
|
||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||
export REPORT_DIR="$SCRIPT_DIR"
|
||
|
||
python3 - <<'PY'
|
||
import csv
|
||
import json
|
||
import os
|
||
from collections import defaultdict
|
||
from datetime import date, datetime, timedelta, timezone
|
||
from pathlib import Path
|
||
|
||
root = Path(os.environ["REPORT_DIR"])
|
||
package = "com.amazon.mShop.android.shopping"
|
||
geo = "ZA"
|
||
launch_start = date(2026, 8, 1)
|
||
launch_end = date(2026, 8, 31)
|
||
monthly_budget_zar = 511_500.0
|
||
daily_budget_zar = 16_500.0
|
||
unit_price_zar = 49.5
|
||
fx_zar_per_usd = 16.5
|
||
|
||
spend = defaultdict(float)
|
||
repostback_installs = defaultdict(int)
|
||
with (root / "source-delivery.csv").open(encoding="utf-8-sig", newline="") as f:
|
||
for row in csv.DictReader(f):
|
||
if row.get("广告包") == package and row.get("国家") == geo:
|
||
day = row.get("日期", "")
|
||
if len(day) == 8 and day.isdigit():
|
||
spend[day] += float(row.get("平台消耗") or 0)
|
||
repostback_installs[day] += int(float(row.get("归因安装") or 0))
|
||
|
||
events = defaultdict(lambda: defaultdict(int))
|
||
with (root / "source-events.csv").open(encoding="utf-8-sig", newline="") as f:
|
||
reader = csv.reader(f)
|
||
next(reader, None)
|
||
for row in reader:
|
||
if len(row) < 7 or row[2] != package or row[3] != geo:
|
||
continue
|
||
name, day = row[0], row[1]
|
||
if name not in {"event1", "event2"}:
|
||
continue
|
||
# The source export leaves its last three headers blank. The second
|
||
# numeric column after midas_campaign_id is the repostback count.
|
||
try:
|
||
repostback = int(float(row[6] or 0))
|
||
except (ValueError, TypeError):
|
||
repostback = 0
|
||
events[day][name] += repostback
|
||
|
||
source_days = sorted(set(spend) | set(events))
|
||
data_cutoff = source_days[-1] if source_days else None
|
||
days = []
|
||
cursor = launch_start
|
||
while cursor <= launch_end:
|
||
days.append(cursor.strftime("%Y%m%d"))
|
||
cursor += timedelta(days=1)
|
||
daily = []
|
||
for day in days:
|
||
install = repostback_installs[day]
|
||
event1 = events[day]["event1"]
|
||
event2 = events[day]["event2"]
|
||
conversions = event1 + event2
|
||
payable_zar = conversions * unit_price_zar
|
||
midas_spend_usd = round(spend[day], 2)
|
||
is_actual = bool(data_cutoff and day <= data_cutoff)
|
||
daily.append({
|
||
"date": f"{day[:4]}-{day[4:6]}-{day[6:]}",
|
||
"is_actual": is_actual,
|
||
"install": install,
|
||
"event1": event1,
|
||
"event2": event2,
|
||
"e1e2": conversions,
|
||
"e1e2_rate": round(conversions / install * 100, 2) if install else None,
|
||
"midas_spend": midas_spend_usd,
|
||
"midas_spend_zar": round(midas_spend_usd * fx_zar_per_usd, 2),
|
||
"payable_spend": round(conversions * 3, 2),
|
||
"payable_spend_zar": round(payable_zar, 2),
|
||
"daily_budget_zar": daily_budget_zar,
|
||
"daily_cap": round(daily_budget_zar / unit_price_zar),
|
||
"ffr_rate": round(conversions / (daily_budget_zar / unit_price_zar) * 100, 2),
|
||
"ecpi_usd": round(midas_spend_usd / install, 4) if install else None,
|
||
})
|
||
|
||
actual_daily = [x for x in daily if x["is_actual"]]
|
||
totals = {
|
||
"install": sum(x["install"] for x in actual_daily),
|
||
"event1": sum(x["event1"] for x in actual_daily),
|
||
"event2": sum(x["event2"] for x in actual_daily),
|
||
"e1e2": sum(x["e1e2"] for x in actual_daily),
|
||
"midas_spend": round(sum(x["midas_spend"] for x in actual_daily), 2),
|
||
"midas_spend_zar": round(sum(x["midas_spend_zar"] for x in actual_daily), 2),
|
||
"payable_spend": round(sum(x["payable_spend"] for x in actual_daily), 2),
|
||
"payable_spend_zar": round(sum(x["payable_spend_zar"] for x in actual_daily), 2),
|
||
}
|
||
totals["e1e2_rate"] = round(totals["e1e2"] / totals["install"] * 100, 2) if totals["install"] else None
|
||
|
||
elapsed_days = len(actual_daily)
|
||
remaining_days = max((launch_end - datetime.strptime(data_cutoff, "%Y%m%d").date()).days, 0) if data_cutoff else (launch_end - launch_start).days + 1
|
||
expected_budget_to_date = min(elapsed_days * daily_budget_zar, monthly_budget_zar)
|
||
remaining_budget_zar = monthly_budget_zar - totals["payable_spend_zar"]
|
||
avg_daily_used = totals["payable_spend_zar"] / elapsed_days if elapsed_days else 0
|
||
projected_month_spend = avg_daily_used * ((launch_end - launch_start).days + 1)
|
||
budget = {
|
||
"monthly_budget_zar": monthly_budget_zar,
|
||
"monthly_budget_usd": round(monthly_budget_zar / fx_zar_per_usd, 2),
|
||
"daily_budget_zar": daily_budget_zar,
|
||
"daily_budget_usd": round(daily_budget_zar / fx_zar_per_usd, 2),
|
||
"unit_price_zar": unit_price_zar,
|
||
"unit_price_usd": round(unit_price_zar / fx_zar_per_usd, 2),
|
||
"fx_zar_per_usd": fx_zar_per_usd,
|
||
"daily_cap": round(daily_budget_zar / unit_price_zar),
|
||
"used_budget_zar": totals["payable_spend_zar"],
|
||
"used_rate": round(totals["payable_spend_zar"] / monthly_budget_zar * 100, 2),
|
||
"remaining_budget_zar": round(remaining_budget_zar, 2),
|
||
"remaining_budget_usd": round(remaining_budget_zar / fx_zar_per_usd, 2),
|
||
"expected_budget_to_date_zar": expected_budget_to_date,
|
||
"pacing_rate": round(totals["payable_spend_zar"] / expected_budget_to_date * 100, 2) if expected_budget_to_date else None,
|
||
"elapsed_days": elapsed_days,
|
||
"remaining_days": remaining_days,
|
||
"required_daily_budget_zar": round(remaining_budget_zar / remaining_days, 2) if remaining_days else 0,
|
||
"required_daily_events": round(remaining_budget_zar / remaining_days / unit_price_zar) if remaining_days else 0,
|
||
"projected_month_spend_zar": round(projected_month_spend, 2),
|
||
"projected_month_variance_zar": round(monthly_budget_zar - projected_month_spend, 2),
|
||
"mtd_expected_ffr": round(min(datetime.now().day, 31) / 31 * 100, 2),
|
||
}
|
||
|
||
weeks = []
|
||
for row in actual_daily:
|
||
row_date = datetime.strptime(row["date"], "%Y-%m-%d").date()
|
||
week_start = row_date - timedelta(days=row_date.weekday())
|
||
week_end = week_start + timedelta(days=6)
|
||
label_start = max(week_start, launch_start)
|
||
label_end = min(week_end, launch_end)
|
||
key = week_start.isoformat()
|
||
if not weeks or weeks[-1]["key"] != key:
|
||
weeks.append({"key": key, "label": f"{label_start.strftime('%b %d')}–{label_end.strftime('%b %d')}", "install": 0, "event1": 0, "event2": 0, "e1e2": 0, "payable_spend": 0.0, "midas_spend": 0.0})
|
||
week = weeks[-1]
|
||
for metric in ("install", "event1", "event2", "e1e2"):
|
||
week[metric] += row[metric]
|
||
week["payable_spend"] += row["payable_spend"]
|
||
week["midas_spend"] += row["midas_spend"]
|
||
for week in weeks:
|
||
week["e1e2_rate"] = round(week["e1e2"] / week["install"] * 100, 2) if week["install"] else None
|
||
week["payable_spend"] = round(week["payable_spend"], 2)
|
||
week["midas_spend"] = round(week["midas_spend"], 2)
|
||
week.pop("key", None)
|
||
|
||
payload = {
|
||
"meta": {
|
||
"package": package,
|
||
"geo": geo,
|
||
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||
"date_start": daily[0]["date"] if daily else None,
|
||
"date_end": daily[-1]["date"] if daily else None,
|
||
"data_cutoff": f"{data_cutoff[:4]}-{data_cutoff[4:6]}-{data_cutoff[6:]}" if data_cutoff else None,
|
||
"currency": "USD",
|
||
"install_definition": "delivery repostback (attributed installs); af_app_opened excluded",
|
||
"event_definition": "event1/event2 repostback",
|
||
"rate_definition": "(event1 + event2) / Install",
|
||
"payable_definition": "(event1 + event2) × $3",
|
||
},
|
||
"totals": totals,
|
||
"budget": budget,
|
||
"daily": daily,
|
||
"weekly": weeks,
|
||
}
|
||
|
||
tmp = root / "data.json.tmp"
|
||
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
tmp.replace(root / "data.json")
|
||
PY
|