test_budget.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. """
  2. 预算模块测试脚本 — 仅测试计算逻辑,不调用腾讯广告 API
  3. 测试内容:
  4. 1. 效率数据查询(ODPS)+ 广告状态查询(ODPS)
  5. 2. 分位数阈值计算(ROI P70/P30 + 消耗 P50)
  6. 3. ROI × 跑量 二维分类 + 5 种动作决策
  7. 4. 缩量/扩量/持平三种场景
  8. 运行方式:
  9. cd /path/to/Agent
  10. python examples/auto_put_ad/test_budget.py --budget 100000 # 缩量
  11. python examples/auto_put_ad/test_budget.py --budget 250000 # 扩量
  12. python examples/auto_put_ad/test_budget.py --budget 175000 # 持平
  13. """
  14. import argparse
  15. import sys
  16. from collections import Counter
  17. from datetime import datetime, timedelta
  18. from pathlib import Path
  19. import pandas as pd
  20. # 项目根目录加入 sys.path
  21. project_root = Path(__file__).parent.parent.parent
  22. sys.path.insert(0, str(project_root))
  23. from examples.auto_put_ad.odps_module import ODPSClient
  24. from examples.auto_put_ad.tools.budget_calc import (
  25. MIN_BID,
  26. MAX_BID,
  27. _build_efficiency_sql,
  28. _classify_ad,
  29. _compute_thresholds,
  30. _decide_action,
  31. _determine_strategy,
  32. _parse_bizdate,
  33. )
  34. def run_budget_test(bizdate: str = "yesterday", total_budget_yuan: float = 100_000):
  35. print("=" * 70)
  36. print(" 预算计算测试(ROI × 跑量 二维矩阵,不执行 API)")
  37. print("=" * 70)
  38. biz, biz_dash = _parse_bizdate(bizdate)
  39. print(f"\n数据日期 : {biz}({biz_dash})")
  40. print(f"今日预算 : {total_budget_yuan:,.0f} 元")
  41. # ── 1. ODPS 查询 ────────────────────────────────────────
  42. print("\n[Step 1] 连接 ODPS + 查询效率数据...")
  43. client = ODPSClient(project="loghubods")
  44. sql_efficiency = _build_efficiency_sql(biz, biz_dash)
  45. df_eff = client.execute_sql(sql_efficiency)
  46. if df_eff.empty:
  47. print(f"❌ 昨日({biz})效率数据为空"); return
  48. print(f" → 效率数据: {len(df_eff)} 个广告")
  49. # ── 2. 广告状态 ─────────────────────────────────────────
  50. print("[Step 2] 查询广告出价/状态...")
  51. ad_ids = [int(x) for x in df_eff["ad_id"].dropna().unique() if str(x) != "nan"]
  52. sql_status = f"""
  53. SELECT ad_id, ad_name, account_id, bid_amount, day_amount, ad_status, optimization_goal
  54. FROM loghubods.ad_put_tencent_ad
  55. WHERE ad_id IN ({",".join(map(str, ad_ids))})
  56. """
  57. df_status = client.execute_sql(sql_status)
  58. print(f" → 广告状态: {len(df_status)} 个")
  59. # ── 3. 合并 + 效率分 ───────────────────────────────────
  60. df_eff["ad_id"] = df_eff["ad_id"].astype(float).astype("Int64")
  61. df_status["ad_id"] = df_status["ad_id"].astype(float).astype("Int64")
  62. df = pd.merge(df_eff, df_status[["ad_id", "bid_amount", "day_amount", "ad_status"]], on="ad_id", how="left")
  63. df["efficiency"] = df.apply(
  64. lambda r: r["fission0_count"] / r["cost"] if r["cost"] and r["cost"] > 0 else None, axis=1,
  65. )
  66. df_valid = df[df["open_count"] >= 100].copy().sort_values("efficiency", ascending=False).reset_index(drop=True)
  67. df_nosample = df[df["open_count"] < 100].copy()
  68. print(f" → 有效广告: {len(df_valid)},样本不足: {len(df_nosample)}")
  69. # ── 4. 分位数阈值 ──────────────────────────────────────
  70. thresholds = _compute_thresholds(df_valid)
  71. print(f"\n[Step 3] 分位数阈值:")
  72. print(f" ROI P70 = {thresholds['roi_p70']:.4f}")
  73. print(f" ROI P30 = {thresholds['roi_p30']:.4f}")
  74. print(f" 消耗 P50 = {thresholds['cost_p50']:.0f} 元")
  75. # ── 5. 策略判断 ─────────────────────────────────────────
  76. yesterday_total = float(df_valid["cost"].sum())
  77. scale_ratio = total_budget_yuan / yesterday_total if yesterday_total > 0 else 1.0
  78. strategy = _determine_strategy(scale_ratio)
  79. direction = "缩量" if scale_ratio < 1 else "扩量" if scale_ratio > 1 else "持平"
  80. print(f"\n[Step 4] 策略判断:")
  81. print(f" 昨日消耗 : {yesterday_total:,.0f} 元")
  82. print(f" scale_ratio : {scale_ratio:.2f}({direction} {abs(1-scale_ratio)*100:.0f}%)")
  83. print(f" 策略 : {strategy}")
  84. # ── 6. 二维矩阵决策 ────────────────────────────────────
  85. print(f"\n[Step 5] 二维矩阵决策...")
  86. results = []
  87. for _, row in df_valid.iterrows():
  88. eff = float(row["efficiency"]) if pd.notna(row["efficiency"]) else 0.0
  89. cost = float(row["cost"])
  90. roi_level, volume_level = _classify_ad(eff, cost, thresholds)
  91. action, adj_ratio = _decide_action(roi_level, volume_level, strategy)
  92. bid = row["bid_amount"] if pd.notna(row["bid_amount"]) else None
  93. new_bid = None
  94. if bid and action in ("increase", "decrease"):
  95. new_bid = max(MIN_BID, min(MAX_BID, int(float(bid) * (1 + adj_ratio))))
  96. elif bid:
  97. new_bid = int(float(bid))
  98. results.append({
  99. "ad_id": int(row["ad_id"]),
  100. "roi_level": roi_level, "volume_level": volume_level,
  101. "efficiency": round(eff, 4), "cost": round(cost, 2),
  102. "current_bid": int(float(bid)) if bid else None,
  103. "new_bid": new_bid,
  104. "adj_ratio": f"{adj_ratio:+.0%}" if adj_ratio != 0 else "—",
  105. "action": action,
  106. "ad_status": str(row["ad_status"]) if pd.notna(row.get("ad_status")) else "",
  107. })
  108. # ── 7. 结果展示 ────────────────────────────────────────
  109. print("\n" + "=" * 70)
  110. print(f" 出价调整方案({direction} {abs(1-scale_ratio)*100:.0f}%)")
  111. print(f" 昨日消耗: {yesterday_total:,.0f} 元 → 今日预算: {total_budget_yuan:,.0f} 元")
  112. print(f" 策略: {strategy}")
  113. print(f" 阈值: ROI P70={thresholds['roi_p70']:.4f}, P30={thresholds['roi_p30']:.4f}, 消耗 P50={thresholds['cost_p50']:.0f}元")
  114. print("=" * 70)
  115. action_labels = [
  116. ("keep", "保持不动"), ("increase", "提价放量"), ("decrease", "降价控量"),
  117. ("close", "建议关停"), ("observe", "观察不动"),
  118. ]
  119. for act, label in action_labels:
  120. sub = [r for r in results if r["action"] == act]
  121. if not sub:
  122. continue
  123. print(f"\n【{label}({act})- {len(sub)}个】")
  124. header = f" {'ad_id':<15} {'ROI':>4} {'量':>4} {'效率分':>8} {'消耗(元)':>10} {'当前出价':>8} {'新出价':>8} {'幅度':>6}"
  125. print(header)
  126. print(" " + "-" * (len(header) - 2))
  127. for item in sub[:8]:
  128. bid_str = str(item["current_bid"]) if item["current_bid"] else "—"
  129. new_str = str(item["new_bid"]) if item["new_bid"] else "—"
  130. print(f" {item['ad_id']:<15} {item['roi_level']:>4} {item['volume_level']:>4} "
  131. f"{item['efficiency']:>8} {item['cost']:>10,.0f} {bid_str:>8} {new_str:>8} {item['adj_ratio']:>6}")
  132. if len(sub) > 8:
  133. print(f" ... 还有 {len(sub)-8} 个")
  134. if len(df_nosample) > 0:
  135. print(f"\n【样本不足 - {len(df_nosample)}个,本次不操作】")
  136. counts = Counter(r["action"] for r in results)
  137. summary = " / ".join(f"{label}:{counts.get(act, 0)}" for act, label in action_labels)
  138. print(f"\n合计:{summary} / 样本不足:{len(df_nosample)}")
  139. print("\n✅ 计算完成(未调用腾讯广告 API)")
  140. print("=" * 70)
  141. if __name__ == "__main__":
  142. parser = argparse.ArgumentParser(description="预算模块计算测试(二维矩阵)")
  143. parser.add_argument("--date", default="yesterday", help="数据日期,YYYYMMDD 或 yesterday")
  144. parser.add_argument("--budget", type=float, default=100_000, help="今日总预算(元)")
  145. args = parser.parse_args()
  146. run_budget_test(bizdate=args.date, total_budget_yuan=args.budget)