|
@@ -43,10 +43,37 @@ from config import (
|
|
|
BID_CEILING_YUAN,
|
|
BID_CEILING_YUAN,
|
|
|
COLD_START_DAYS,
|
|
COLD_START_DAYS,
|
|
|
CAUTIOUS_DAYS,
|
|
CAUTIOUS_DAYS,
|
|
|
|
|
+ ROI_LOW_FACTOR,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
+# ═══════════════════════════════════════════
|
|
|
|
|
+# 策略参数动态加载(阈值不写死在代码中)
|
|
|
|
|
+# ═══════════════════════════════════════════
|
|
|
|
|
+
|
|
|
|
|
+STRATEGY_PARAMS_FILE = _MINI_DIR / "strategy_params.json"
|
|
|
|
|
+
|
|
|
|
|
+def _load_strategy_params():
|
|
|
|
|
+ """从json文件加载策略参数,如不存在则使用config.py默认值"""
|
|
|
|
|
+ import json
|
|
|
|
|
+
|
|
|
|
|
+ if STRATEGY_PARAMS_FILE.exists():
|
|
|
|
|
+ try:
|
|
|
|
|
+ with open(STRATEGY_PARAMS_FILE) as f:
|
|
|
|
|
+ data = json.load(f)
|
|
|
|
|
+ return data.get("params", {})
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning(f"加载strategy_params.json失败,使用config.py默认值: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ # 使用config.py默认值
|
|
|
|
|
+ return {
|
|
|
|
|
+ "ROI_LOW_FACTOR": ROI_LOW_FACTOR,
|
|
|
|
|
+ "BID_DOWN_ROI_FACTOR": BID_DOWN_ROI_FACTOR,
|
|
|
|
|
+ "BID_UP_ROI_FACTOR": BID_UP_ROI_FACTOR,
|
|
|
|
|
+ "BID_UP_LOW_SPEND_FACTOR": BID_UP_LOW_SPEND_FACTOR,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════
|
|
# ═══════════════════════════════════════════
|
|
|
# 辅助函数
|
|
# 辅助函数
|
|
@@ -816,6 +843,9 @@ async def get_ads_for_review(
|
|
|
min_spend_for_class_a: 7日均消耗低于此值(元)→ A 类(默认 1.0)
|
|
min_spend_for_class_a: 7日均消耗低于此值(元)→ A 类(默认 1.0)
|
|
|
"""
|
|
"""
|
|
|
try:
|
|
try:
|
|
|
|
|
+ # 加载策略参数(动态阈值,不写死在代码中)
|
|
|
|
|
+ params = _load_strategy_params()
|
|
|
|
|
+
|
|
|
if not metrics_csv:
|
|
if not metrics_csv:
|
|
|
metrics_csv = str(_MINI_DIR / "outputs" / "metrics_temp.csv")
|
|
metrics_csv = str(_MINI_DIR / "outputs" / "metrics_temp.csv")
|
|
|
|
|
|
|
@@ -855,10 +885,10 @@ async def get_ads_for_review(
|
|
|
cost_series = df["cost_7d_avg"].dropna()
|
|
cost_series = df["cost_7d_avg"].dropna()
|
|
|
cost_median = float(cost_series.median()) if len(cost_series) > 0 else 0.0
|
|
cost_median = float(cost_series.median()) if len(cost_series) > 0 else 0.0
|
|
|
|
|
|
|
|
- # 分类
|
|
|
|
|
- class_a = []
|
|
|
|
|
- class_b = []
|
|
|
|
|
- class_c_count = 0
|
|
|
|
|
|
|
+ # 分类(业务语言)
|
|
|
|
|
+ zero_spend_ads = [] # 零消耗待关停
|
|
|
|
|
+ need_review_ads = [] # 待优化评估
|
|
|
|
|
+ normal_ads_count = 0 # 正常运行
|
|
|
|
|
|
|
|
for _, row in df.iterrows():
|
|
for _, row in df.iterrows():
|
|
|
cost_7d_avg = float(row.get("cost_7d_avg", 0) or 0)
|
|
cost_7d_avg = float(row.get("cost_7d_avg", 0) or 0)
|
|
@@ -869,16 +899,22 @@ async def get_ads_for_review(
|
|
|
stable_days = float(row.get("stable_spend_days_30d", 0) or 0)
|
|
stable_days = float(row.get("stable_spend_days_30d", 0) or 0)
|
|
|
bid_amount = float(row.get("bid_amount", 0) or 0)
|
|
bid_amount = float(row.get("bid_amount", 0) or 0)
|
|
|
|
|
|
|
|
- # A 类:几乎零消耗
|
|
|
|
|
|
|
+ # 零消耗待关停:7日均消耗 < 1元,几乎无活动
|
|
|
if cost_7d_avg < min_spend_for_class_a:
|
|
if cost_7d_avg < min_spend_for_class_a:
|
|
|
- class_a.append({
|
|
|
|
|
|
|
+ zero_spend_ads.append({
|
|
|
"ad_id": int(row["ad_id"]),
|
|
"ad_id": int(row["ad_id"]),
|
|
|
"ad_name": str(row.get("ad_name", "")),
|
|
"ad_name": str(row.get("ad_name", "")),
|
|
|
"cost_7d_avg": round(cost_7d_avg, 2),
|
|
"cost_7d_avg": round(cost_7d_avg, 2),
|
|
|
})
|
|
})
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- # B 类:ROI 偏低 或 衰退信号 或 出价调整候选
|
|
|
|
|
|
|
+ # 冷启动保护(前置判断):广告年龄 ≤ 4天,直接归类为正常运行(hold)
|
|
|
|
|
+ # 不进入LLM推理,节省token,避免护栏拦截
|
|
|
|
|
+ if ad_age is not None and ad_age <= COLD_START_DAYS:
|
|
|
|
|
+ normal_ads_count += 1
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 待优化评估:ROI 偏低 或 衰退信号 或 出价调整候选(需要智能判断)
|
|
|
roi_low = (not pd.isna(f_roi)) and (f_roi < roi_mean * roi_review_factor)
|
|
roi_low = (not pd.isna(f_roi)) and (f_roi < roi_mean * roi_review_factor)
|
|
|
decay_signal = (
|
|
decay_signal = (
|
|
|
stable_days >= 7
|
|
stable_days >= 7
|
|
@@ -888,20 +924,20 @@ async def get_ads_for_review(
|
|
|
# 出价调整候选:高ROI低量(提价)或 ROI偏低(降价)
|
|
# 出价调整候选:高ROI低量(提价)或 ROI偏低(降价)
|
|
|
bid_up_candidate = (
|
|
bid_up_candidate = (
|
|
|
(not pd.isna(f_roi))
|
|
(not pd.isna(f_roi))
|
|
|
- and f_roi > roi_mean * BID_UP_ROI_FACTOR
|
|
|
|
|
- and cost_7d_avg < cost_median * BID_UP_LOW_SPEND_FACTOR
|
|
|
|
|
|
|
+ and f_roi > roi_mean * params["BID_UP_ROI_FACTOR"]
|
|
|
|
|
+ and cost_7d_avg < cost_median * params["BID_UP_LOW_SPEND_FACTOR"]
|
|
|
and bid_amount > 0
|
|
and bid_amount > 0
|
|
|
) if BID_ADJUSTMENT_ENABLED else False
|
|
) if BID_ADJUSTMENT_ENABLED else False
|
|
|
bid_down_candidate = (
|
|
bid_down_candidate = (
|
|
|
(not pd.isna(f_roi))
|
|
(not pd.isna(f_roi))
|
|
|
- and f_roi < roi_mean * BID_DOWN_ROI_FACTOR
|
|
|
|
|
- and f_roi >= roi_mean * 0.5
|
|
|
|
|
|
|
+ and f_roi < roi_mean * params["BID_DOWN_ROI_FACTOR"]
|
|
|
|
|
+ and f_roi >= roi_mean * params["ROI_LOW_FACTOR"]
|
|
|
and cost_7d_avg >= 100
|
|
and cost_7d_avg >= 100
|
|
|
and bid_amount > 0
|
|
and bid_amount > 0
|
|
|
) if BID_ADJUSTMENT_ENABLED else False
|
|
) if BID_ADJUSTMENT_ENABLED else False
|
|
|
|
|
|
|
|
if roi_low or decay_signal or bid_up_candidate or bid_down_candidate:
|
|
if roi_low or decay_signal or bid_up_candidate or bid_down_candidate:
|
|
|
- class_b.append({
|
|
|
|
|
|
|
+ need_review_ads.append({
|
|
|
"ad_id": int(row["ad_id"]),
|
|
"ad_id": int(row["ad_id"]),
|
|
|
"ad_name": str(row.get("ad_name", "")),
|
|
"ad_name": str(row.get("ad_name", "")),
|
|
|
"动态ROI_7日均值": round(float(f_roi), 4) if not pd.isna(f_roi) else None,
|
|
"动态ROI_7日均值": round(float(f_roi), 4) if not pd.isna(f_roi) else None,
|
|
@@ -916,15 +952,16 @@ async def get_ads_for_review(
|
|
|
})
|
|
})
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- class_c_count += 1
|
|
|
|
|
|
|
+ # 正常运行:ROI 正常且无异常信号
|
|
|
|
|
+ normal_ads_count += 1
|
|
|
|
|
|
|
|
import json
|
|
import json
|
|
|
result = {
|
|
result = {
|
|
|
"summary": {
|
|
"summary": {
|
|
|
"total": len(df),
|
|
"total": len(df),
|
|
|
- "class_a": len(class_a),
|
|
|
|
|
- "class_b": len(class_b),
|
|
|
|
|
- "class_c": class_c_count,
|
|
|
|
|
|
|
+ "zero_spend_ads": len(zero_spend_ads),
|
|
|
|
|
+ "need_review_ads": len(need_review_ads),
|
|
|
|
|
+ "normal_ads": normal_ads_count,
|
|
|
},
|
|
},
|
|
|
"distribution": {
|
|
"distribution": {
|
|
|
"roi_mean": round(roi_mean, 4),
|
|
"roi_mean": round(roi_mean, 4),
|
|
@@ -936,24 +973,34 @@ async def get_ads_for_review(
|
|
|
},
|
|
},
|
|
|
"bid_adjustment": {
|
|
"bid_adjustment": {
|
|
|
"enabled": BID_ADJUSTMENT_ENABLED,
|
|
"enabled": BID_ADJUSTMENT_ENABLED,
|
|
|
- "bid_down_line": round(roi_mean * BID_DOWN_ROI_FACTOR, 4),
|
|
|
|
|
- "bid_up_line": round(roi_mean * BID_UP_ROI_FACTOR, 4),
|
|
|
|
|
- "low_spend_line": round(cost_median * BID_UP_LOW_SPEND_FACTOR, 2),
|
|
|
|
|
|
|
+ "bid_down_line": round(roi_mean * params["BID_DOWN_ROI_FACTOR"], 4),
|
|
|
|
|
+ "bid_up_line": round(roi_mean * params["BID_UP_ROI_FACTOR"], 4),
|
|
|
|
|
+ "low_spend_line": round(cost_median * params["BID_UP_LOW_SPEND_FACTOR"], 2),
|
|
|
},
|
|
},
|
|
|
- "class_a": class_a,
|
|
|
|
|
- "class_b": class_b,
|
|
|
|
|
|
|
+ "thresholds_used": {
|
|
|
|
|
+ "ROI_LOW_FACTOR": params["ROI_LOW_FACTOR"],
|
|
|
|
|
+ "BID_DOWN_ROI_FACTOR": params["BID_DOWN_ROI_FACTOR"],
|
|
|
|
|
+ "BID_UP_ROI_FACTOR": params["BID_UP_ROI_FACTOR"],
|
|
|
|
|
+ "BID_UP_LOW_SPEND_FACTOR": params["BID_UP_LOW_SPEND_FACTOR"],
|
|
|
|
|
+ "roi_mean": round(roi_mean, 4),
|
|
|
|
|
+ "pause_line": round(roi_mean * params["ROI_LOW_FACTOR"], 4),
|
|
|
|
|
+ "bid_down_line": round(roi_mean * params["BID_DOWN_ROI_FACTOR"], 4),
|
|
|
|
|
+ "bid_up_line": round(roi_mean * params["BID_UP_ROI_FACTOR"], 4),
|
|
|
|
|
+ },
|
|
|
|
|
+ "zero_spend_ads": zero_spend_ads,
|
|
|
|
|
+ "need_review_ads": need_review_ads,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
output_json = json.dumps(result, ensure_ascii=False, indent=2)
|
|
output_json = json.dumps(result, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
|
return ToolResult(
|
|
return ToolResult(
|
|
|
- title=f"待评估广告(A类:{len(class_a)} B类:{len(class_b)} C类:{class_c_count})",
|
|
|
|
|
|
|
+ title=f"广告分类(零消耗:{len(zero_spend_ads)} 待评估:{len(need_review_ads)} 正常:{normal_ads_count})",
|
|
|
output=output_json,
|
|
output=output_json,
|
|
|
metadata={
|
|
metadata={
|
|
|
"total": len(df),
|
|
"total": len(df),
|
|
|
- "class_a": len(class_a),
|
|
|
|
|
- "class_b": len(class_b),
|
|
|
|
|
- "class_c": class_c_count,
|
|
|
|
|
|
|
+ "zero_spend_ads": len(zero_spend_ads),
|
|
|
|
|
+ "need_review_ads": len(need_review_ads),
|
|
|
|
|
+ "normal_ads": normal_ads_count,
|
|
|
"roi_mean": roi_mean,
|
|
"roi_mean": roi_mean,
|
|
|
"end_date": end_date,
|
|
"end_date": end_date,
|
|
|
},
|
|
},
|
|
@@ -969,7 +1016,7 @@ async def get_ads_for_review(
|
|
|
# ═══════════════════════════════════════════
|
|
# ═══════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
|
-@tool(description="智能引擎:接收LLM的决策列表,保存为结构化结果")
|
|
|
|
|
|
|
+@tool(description="智能引擎:接收LLM的决策列表,合并A/C类自动决策,保存为结构化结果")
|
|
|
async def apply_decisions(
|
|
async def apply_decisions(
|
|
|
ctx: ToolContext,
|
|
ctx: ToolContext,
|
|
|
decisions: str,
|
|
decisions: str,
|
|
@@ -977,14 +1024,19 @@ async def apply_decisions(
|
|
|
metrics_csv: str = "",
|
|
metrics_csv: str = "",
|
|
|
) -> ToolResult:
|
|
) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
- 接收 LLM 的决策,合并 A 类广告(自动关停),保存到 llm_decisions_{date}.csv。
|
|
|
|
|
|
|
+ 接收 LLM 的决策,合并 A 类广告(自动关停)和 C 类广告(自动保持),保存到 llm_decisions_{date}.csv。
|
|
|
|
|
+
|
|
|
|
|
+ 决策分类:
|
|
|
|
|
+ - 零消耗待关停:7日均消耗 < 1元,几乎无活动 → 规则判断自动关停
|
|
|
|
|
+ - 待优化评估:ROI 偏低、衰退信号、出价调整候选 → 智能判断
|
|
|
|
|
+ - 正常运行:ROI 正常且无异常信号 → 规则判断自动保持
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
- decisions: JSON 字符串,LLM 输出的决策列表
|
|
|
|
|
- 格式:[{"ad_id": 123, "action": "pause"/"hold",
|
|
|
|
|
|
|
+ decisions: JSON 字符串,LLM 输出的"待优化评估"类广告决策列表
|
|
|
|
|
+ 格式:[{"ad_id": 123, "action": "pause"/"hold"/"bid_up"/"bid_down",
|
|
|
"dimension": "...", "reason": "...", "confidence": "high"/"medium"/"low"}]
|
|
"dimension": "...", "reason": "...", "confidence": "high"/"medium"/"low"}]
|
|
|
end_date: 结束日期
|
|
end_date: 结束日期
|
|
|
- metrics_csv: ROI 指标 CSV 路径(用于获取 A 类广告)
|
|
|
|
|
|
|
+ metrics_csv: ROI 指标 CSV 路径(用于获取 A/C 类广告)
|
|
|
"""
|
|
"""
|
|
|
import json
|
|
import json
|
|
|
|
|
|
|
@@ -1001,8 +1053,8 @@ async def apply_decisions(
|
|
|
if not isinstance(llm_list, list):
|
|
if not isinstance(llm_list, list):
|
|
|
return ToolResult(title="apply_decisions 失败", output="decisions 必须是 JSON 数组")
|
|
return ToolResult(title="apply_decisions 失败", output="decisions 必须是 JSON 数组")
|
|
|
|
|
|
|
|
- # 加载 A 类广告(自动关停)
|
|
|
|
|
- a_class_rows = []
|
|
|
|
|
|
|
+ # 加载零消耗待关停广告(规则判断)
|
|
|
|
|
+ zero_spend_rows = []
|
|
|
if not metrics_csv:
|
|
if not metrics_csv:
|
|
|
metrics_csv = str(_MINI_DIR / "outputs" / "metrics_temp.csv")
|
|
metrics_csv = str(_MINI_DIR / "outputs" / "metrics_temp.csv")
|
|
|
|
|
|
|
@@ -1011,29 +1063,87 @@ async def apply_decisions(
|
|
|
for _, row in df_metrics.iterrows():
|
|
for _, row in df_metrics.iterrows():
|
|
|
cost_7d_avg = float(row.get("cost_7d_avg", 0) or 0)
|
|
cost_7d_avg = float(row.get("cost_7d_avg", 0) or 0)
|
|
|
if cost_7d_avg < 1.0:
|
|
if cost_7d_avg < 1.0:
|
|
|
- a_class_rows.append({
|
|
|
|
|
|
|
+ # 优化reason表达:避免"0.00元"显示,改用"几乎无消耗"
|
|
|
|
|
+ if cost_7d_avg == 0:
|
|
|
|
|
+ reason_text = "7日几乎无消耗,长期无活动"
|
|
|
|
|
+ else:
|
|
|
|
|
+ reason_text = f"7日均消耗={cost_7d_avg:.2f}元,长期低消耗"
|
|
|
|
|
+ zero_spend_rows.append({
|
|
|
"ad_id": int(row["ad_id"]),
|
|
"ad_id": int(row["ad_id"]),
|
|
|
"action": "pause",
|
|
"action": "pause",
|
|
|
"dimension": "长期零消耗",
|
|
"dimension": "长期零消耗",
|
|
|
- "reason": f"7日均消耗={cost_7d_avg:.2f}元,几乎无活动",
|
|
|
|
|
|
|
+ "reason": reason_text,
|
|
|
"confidence": "high",
|
|
"confidence": "high",
|
|
|
- "source": "class_a_auto",
|
|
|
|
|
|
|
+ "source": "规则判断",
|
|
|
|
|
+ "cost_7d_avg": cost_7d_avg, # 用于排序
|
|
|
})
|
|
})
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.warning("加载 A 类广告失败(跳过): %s", e)
|
|
|
|
|
|
|
+ logger.warning("加载零消耗待关停广告失败(跳过): %s", e)
|
|
|
|
|
|
|
|
- # 合并 LLM 决策(标注来源)
|
|
|
|
|
|
|
+ # 合并 LLM 决策(标注来源 + 添加cost_7d_avg用于排序)
|
|
|
for item in llm_list:
|
|
for item in llm_list:
|
|
|
- item["source"] = "llm"
|
|
|
|
|
|
|
+ item["source"] = "智能判断"
|
|
|
|
|
+ ad_id = item.get("ad_id")
|
|
|
|
|
+ # 从metrics中获取cost_7d_avg
|
|
|
|
|
+ try:
|
|
|
|
|
+ cost_row = df_metrics[df_metrics["ad_id"] == ad_id]
|
|
|
|
|
+ if not cost_row.empty:
|
|
|
|
|
+ item["cost_7d_avg"] = float(cost_row.iloc[0].get("cost_7d_avg", 0) or 0)
|
|
|
|
|
+ else:
|
|
|
|
|
+ item["cost_7d_avg"] = 0.0
|
|
|
|
|
+ except:
|
|
|
|
|
+ item["cost_7d_avg"] = 0.0
|
|
|
|
|
+
|
|
|
|
|
+ # 加载正常运行广告(规则判断)
|
|
|
|
|
+ normal_running_rows = []
|
|
|
|
|
+ try:
|
|
|
|
|
+ # 收集零消耗和待评估的 ad_id
|
|
|
|
|
+ zero_spend_ad_ids = {row["ad_id"] for row in zero_spend_rows}
|
|
|
|
|
+ need_review_ad_ids = {item["ad_id"] for item in llm_list}
|
|
|
|
|
+
|
|
|
|
|
+ # 正常运行 = 所有广告 - 零消耗 - 待评估
|
|
|
|
|
+ for _, row in df_metrics.iterrows():
|
|
|
|
|
+ ad_id = int(row["ad_id"])
|
|
|
|
|
+ if ad_id not in zero_spend_ad_ids and ad_id not in need_review_ad_ids:
|
|
|
|
|
+ cost_7d_avg = float(row.get("cost_7d_avg", 0) or 0)
|
|
|
|
|
+ f_roi = row.get("动态ROI_7日均值")
|
|
|
|
|
+ ad_age_days = row.get("ad_age_days")
|
|
|
|
|
+
|
|
|
|
|
+ # 冷启动保护:广告年龄 ≤ 4天
|
|
|
|
|
+ if ad_age_days is not None and ad_age_days <= COLD_START_DAYS:
|
|
|
|
|
+ roi_str = f"{f_roi:.2f}" if not pd.isna(f_roi) else "数据不足"
|
|
|
|
|
+ normal_running_rows.append({
|
|
|
|
|
+ "ad_id": ad_id,
|
|
|
|
|
+ "action": "hold",
|
|
|
|
|
+ "dimension": "冷启动保护",
|
|
|
|
|
+ "reason": f"广告年龄{ad_age_days}天 ≤ {COLD_START_DAYS}天(冷启动期),ROI={roi_str},消耗{cost_7d_avg:.2f}元/天,保持观察",
|
|
|
|
|
+ "confidence": "high",
|
|
|
|
|
+ "source": "规则判断",
|
|
|
|
|
+ "cost_7d_avg": cost_7d_avg, # 用于排序
|
|
|
|
|
+ })
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 正常运行
|
|
|
|
|
+ roi_str = f"{f_roi:.2f}" if not pd.isna(f_roi) else "数据不足"
|
|
|
|
|
+ normal_running_rows.append({
|
|
|
|
|
+ "ad_id": ad_id,
|
|
|
|
|
+ "action": "hold",
|
|
|
|
|
+ "dimension": "正常运行",
|
|
|
|
|
+ "reason": f"ROI={roi_str},消耗正常({cost_7d_avg:.2f}元/天),无异常信号",
|
|
|
|
|
+ "confidence": "high",
|
|
|
|
|
+ "source": "规则判断",
|
|
|
|
|
+ "cost_7d_avg": cost_7d_avg, # 用于排序
|
|
|
|
|
+ })
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning("加载正常运行广告失败(跳过): %s", e)
|
|
|
|
|
|
|
|
- all_decisions = a_class_rows + llm_list
|
|
|
|
|
|
|
+ all_decisions = zero_spend_rows + llm_list + normal_running_rows
|
|
|
|
|
|
|
|
if not all_decisions:
|
|
if not all_decisions:
|
|
|
return ToolResult(title="apply_decisions", output="无决策数据")
|
|
return ToolResult(title="apply_decisions", output="无决策数据")
|
|
|
|
|
|
|
|
df_out = pd.DataFrame(all_decisions)
|
|
df_out = pd.DataFrame(all_decisions)
|
|
|
|
|
|
|
|
- # 过滤:如果决策是 pause,但广告已经是 AD_STATUS_SUSPEND,则排除
|
|
|
|
|
|
|
+ # 过滤:已经是 AD_STATUS_SUSPEND 的广告不应出现在决策表中(已暂停无需再决策)
|
|
|
ad_status_path = _MINI_DIR / "outputs" / "ad_status" / f"ad_status_{end_date}.csv"
|
|
ad_status_path = _MINI_DIR / "outputs" / "ad_status" / f"ad_status_{end_date}.csv"
|
|
|
if ad_status_path.exists():
|
|
if ad_status_path.exists():
|
|
|
try:
|
|
try:
|
|
@@ -1042,11 +1152,9 @@ async def apply_decisions(
|
|
|
df_status[df_status["ad_status"] == "AD_STATUS_SUSPEND"]["ad_id"].tolist()
|
|
df_status[df_status["ad_status"] == "AD_STATUS_SUSPEND"]["ad_id"].tolist()
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- # 过滤掉已经暂停的广告(仅针对 action=pause 的决策)
|
|
|
|
|
|
|
+ # 过滤掉所有已暂停的广告(不论决策是什么,已暂停的广告不应出现在决策表中)
|
|
|
before_count = len(df_out)
|
|
before_count = len(df_out)
|
|
|
- df_out = df_out[
|
|
|
|
|
- ~((df_out["action"] == "pause") & (df_out["ad_id"].isin(suspended_ads)))
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ df_out = df_out[~df_out["ad_id"].isin(suspended_ads)]
|
|
|
filtered_count = before_count - len(df_out)
|
|
filtered_count = before_count - len(df_out)
|
|
|
if filtered_count > 0:
|
|
if filtered_count > 0:
|
|
|
logger.info(f"过滤掉 {filtered_count} 个已暂停广告(AD_STATUS_SUSPEND)")
|
|
logger.info(f"过滤掉 {filtered_count} 个已暂停广告(AD_STATUS_SUSPEND)")
|
|
@@ -1058,10 +1166,18 @@ async def apply_decisions(
|
|
|
if col not in df_out.columns:
|
|
if col not in df_out.columns:
|
|
|
df_out[col] = ""
|
|
df_out[col] = ""
|
|
|
# 数值列用 None 而非空字符串,避免 float("") 异常
|
|
# 数值列用 None 而非空字符串,避免 float("") 异常
|
|
|
- for col in ["recommended_change_pct", "current_bid", "recommended_bid"]:
|
|
|
|
|
|
|
+ for col in ["recommended_change_pct", "current_bid", "recommended_bid", "cost_7d_avg"]:
|
|
|
if col not in df_out.columns:
|
|
if col not in df_out.columns:
|
|
|
df_out[col] = None
|
|
df_out[col] = None
|
|
|
|
|
|
|
|
|
|
+ # 按7日均消耗降序排列(消耗高的广告排在前面,更需要关注)
|
|
|
|
|
+ if "cost_7d_avg" in df_out.columns:
|
|
|
|
|
+ df_out["cost_7d_avg"] = pd.to_numeric(df_out["cost_7d_avg"], errors="coerce").fillna(0)
|
|
|
|
|
+ df_out = df_out.sort_values("cost_7d_avg", ascending=False).reset_index(drop=True)
|
|
|
|
|
+
|
|
|
|
|
+ # 删除cost_7d_avg列(仅用于排序,不保存到最终文件)
|
|
|
|
|
+ df_out = df_out.drop(columns=["cost_7d_avg"], errors="ignore")
|
|
|
|
|
+
|
|
|
# 保存
|
|
# 保存
|
|
|
reports_dir = _MINI_DIR / "outputs" / "reports"
|
|
reports_dir = _MINI_DIR / "outputs" / "reports"
|
|
|
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1075,8 +1191,8 @@ async def apply_decisions(
|
|
|
|
|
|
|
|
output_parts = [
|
|
output_parts = [
|
|
|
f"智能引擎决策已保存: {out_path}",
|
|
f"智能引擎决策已保存: {out_path}",
|
|
|
- f" 关停: {pause_count} 个(含A类自动关停: {len(a_class_rows)} 个)",
|
|
|
|
|
- f" 保持: {hold_count} 个",
|
|
|
|
|
|
|
+ f" 关停: {pause_count} 个(含零消耗待关停: {len(zero_spend_rows)} 个)",
|
|
|
|
|
+ f" 保持: {hold_count} 个(含正常运行: {len(normal_running_rows)} 个)",
|
|
|
]
|
|
]
|
|
|
if bid_up_count > 0:
|
|
if bid_up_count > 0:
|
|
|
output_parts.append(f" 提价: {bid_up_count} 个")
|
|
output_parts.append(f" 提价: {bid_up_count} 个")
|
|
@@ -1093,7 +1209,8 @@ async def apply_decisions(
|
|
|
"hold": int(hold_count),
|
|
"hold": int(hold_count),
|
|
|
"bid_up": int(bid_up_count),
|
|
"bid_up": int(bid_up_count),
|
|
|
"bid_down": int(bid_down_count),
|
|
"bid_down": int(bid_down_count),
|
|
|
- "class_a_auto": len(a_class_rows),
|
|
|
|
|
|
|
+ "zero_spend_ads": len(zero_spend_rows),
|
|
|
|
|
+ "normal_running_ads": len(normal_running_rows),
|
|
|
"end_date": end_date,
|
|
"end_date": end_date,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|