policy.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. """Translate versioned ROI metrics into auditable recommendations and actions."""
  2. from __future__ import annotations
  3. import hashlib
  4. import math
  5. from typing import Any
  6. import pandas as pd
  7. from .fission_multiplier import (
  8. DISPLAY_MULTIPLIER_COLUMN,
  9. DISPLAY_TOTAL_TO_FIRST_COLUMN,
  10. )
  11. from .metrics import ENTITY_KEYS, ENTITY_SELF, ENTITY_SELF_AD
  12. ACTION_PAUSE_CREATIVE = "PAUSE_CREATIVE"
  13. ACTION_PAUSE_AD = "PAUSE_AD"
  14. ACTION_SCALE_BID = "SCALE_BID"
  15. MODE_ACTIONABLE = "ACTIONABLE"
  16. MODE_NOTIFY_ONLY = "NOTIFY_ONLY"
  17. def safe_int(value: Any) -> int | None:
  18. if value is None or value == "":
  19. return None
  20. try:
  21. number = int(float(value))
  22. except (TypeError, ValueError):
  23. return None
  24. return number if number > 0 else None
  25. def _entity_hash(row: pd.Series) -> str:
  26. keys = ENTITY_KEYS[str(row["entity_type"])]
  27. raw = "\x1f".join(str(row.get(key, "")) for key in keys)
  28. return hashlib.sha256(raw.encode("utf-8")).hexdigest()
  29. def _finite(value: Any) -> float | None:
  30. try:
  31. number = float(value)
  32. except (TypeError, ValueError):
  33. return None
  34. return number if math.isfinite(number) else None
  35. def annotate_execution(
  36. summary: pd.DataFrame,
  37. managed_account_ids: set[int],
  38. run_id: str,
  39. ) -> tuple[pd.DataFrame, list[dict[str, Any]], list[dict[str, Any]]]:
  40. """Keep policy recommendations separate from Tencent eligibility.
  41. Returns annotated summary, complete metric snapshots, and deduplicated
  42. executable action items.
  43. """
  44. annotated = summary.copy()
  45. annotated["执行模式"] = MODE_NOTIFY_ONLY
  46. annotated["执行说明"] = "无调控建议"
  47. annotated["审批选择"] = "不可执行"
  48. annotated["执行状态"] = ""
  49. annotated["执行结果"] = ""
  50. annotated["动作幂等键"] = ""
  51. actions: list[dict[str, Any]] = []
  52. snapshots: list[dict[str, Any]] = []
  53. seen_actions: set[str] = set()
  54. for index, row in annotated.iterrows():
  55. entity_type = str(row.get("entity_type") or "")
  56. recommendation = str(row.get("动作") or "")
  57. account_id = safe_int(row.get("账号id"))
  58. adgroup_id = safe_int(row.get("广告id"))
  59. creative_id = safe_int(row.get("创意id"))
  60. execution_mode = MODE_NOTIFY_ONLY
  61. execution_reason = "无调控建议"
  62. action_type: str | None = None
  63. if recommendation:
  64. if recommendation == "观察":
  65. execution_reason = "观察行仅展示,不执行腾讯写操作"
  66. elif entity_type not in (ENTITY_SELF, ENTITY_SELF_AD):
  67. execution_reason = "合作渠道当前仅通知,不执行腾讯写操作"
  68. elif account_id not in managed_account_ids:
  69. execution_reason = "账户不在自动化管理范围,仅通知"
  70. elif not adgroup_id:
  71. execution_reason = "缺少有效广告ID,仅通知"
  72. elif (
  73. recommendation == "关停"
  74. and entity_type == ENTITY_SELF
  75. and not creative_id
  76. ):
  77. execution_reason = "缺少有效动态创意ID,仅通知"
  78. elif recommendation == "关停" and entity_type == ENTITY_SELF_AD:
  79. action_type = ACTION_PAUSE_AD
  80. elif recommendation == "关停" and entity_type == ENTITY_SELF:
  81. action_type = ACTION_PAUSE_CREATIVE
  82. elif recommendation == "扩量" and entity_type == ENTITY_SELF:
  83. action_type = ACTION_SCALE_BID
  84. else:
  85. execution_reason = "该建议当前没有自动执行器,仅通知"
  86. if action_type:
  87. suffix = creative_id if action_type == ACTION_PAUSE_CREATIVE else adgroup_id
  88. idempotency_key = f"{run_id}:{action_type}:{account_id}:{suffix}"
  89. execution_mode = MODE_ACTIONABLE
  90. if action_type == ACTION_PAUSE_CREATIVE:
  91. execution_reason = "审批后暂停该动态创意"
  92. elif action_type == ACTION_PAUSE_AD:
  93. execution_reason = "审批后暂停整个广告"
  94. else:
  95. execution_reason = "审批后按ROI策略提高广告永久基础出价"
  96. if idempotency_key not in seen_actions:
  97. actions.append(
  98. {
  99. "idempotency_key": idempotency_key,
  100. "action_type": action_type,
  101. "account_id": account_id,
  102. "adgroup_id": adgroup_id,
  103. "dynamic_creative_id": creative_id,
  104. "execution_status": "PENDING",
  105. }
  106. )
  107. seen_actions.add(idempotency_key)
  108. annotated.at[index, "执行模式"] = execution_mode
  109. annotated.at[index, "执行说明"] = execution_reason
  110. if execution_mode == MODE_ACTIONABLE:
  111. annotated.at[index, "审批选择"] = ""
  112. annotated.at[index, "执行状态"] = "待审批"
  113. annotated.at[index, "动作幂等键"] = idempotency_key
  114. daily_metrics = {
  115. column: _finite(row.get(column))
  116. for column in annotated.columns
  117. if column.startswith(
  118. (
  119. "首层UV_20",
  120. "T0裂变数_20",
  121. "成本_20",
  122. "效率收入_20",
  123. "裂变效率收入_20",
  124. "实际ROI_20",
  125. "ROI_20",
  126. )
  127. )
  128. }
  129. daily_metrics.update(
  130. {
  131. "fission_parameter_version": str(
  132. row.get("传播裂变参数版本") or ""
  133. ),
  134. "fission_cohort_date": str(
  135. row.get("传播裂变参数cohort日期") or ""
  136. ),
  137. "fission_match_level": str(
  138. row.get("传播裂变系数匹配层级") or ""
  139. ),
  140. "fission_source": str(row.get("传播裂变系数来源") or ""),
  141. "fission_parameter_status": str(
  142. row.get("传播裂变参数状态") or ""
  143. ),
  144. "control_participation_status": str(
  145. row.get("调控参与状态") or ""
  146. ),
  147. "roi_method": str(row.get("ROI计算口径") or ""),
  148. }
  149. )
  150. snapshots.append(
  151. {
  152. "entity_hash": _entity_hash(row),
  153. "entity_type": entity_type,
  154. "channel": str(row.get("channel") or ""),
  155. "account_id": account_id,
  156. "account_name": str(row.get("账号名称") or ""),
  157. "adgroup_id": adgroup_id,
  158. "adgroup_name": str(row.get("广告名称") or ""),
  159. "dynamic_creative_id": creative_id,
  160. "audience_name": str(row.get("包名") or ""),
  161. "conversion_goal": str(row.get("广告优化目标") or ""),
  162. "partner_name": str(row.get("合作方名") or ""),
  163. "official_account_name": str(row.get("公众号名") or ""),
  164. "fission_parameter_version": str(
  165. row.get("传播裂变参数版本") or ""
  166. ),
  167. "fission_cohort_date": str(
  168. row.get("传播裂变参数cohort日期") or ""
  169. ),
  170. "fission_multiplier_vs_t0": _finite(
  171. row.get(DISPLAY_MULTIPLIER_COLUMN)
  172. ),
  173. "fission_multiplier_vs_first": _finite(
  174. row.get(DISPLAY_TOTAL_TO_FIRST_COLUMN)
  175. ),
  176. "fission_match_level": str(
  177. row.get("传播裂变系数匹配层级") or ""
  178. ),
  179. "fission_source": str(row.get("传播裂变系数来源") or ""),
  180. "ad_age": safe_int(row.get("广告age")),
  181. "avg_first_uv": _finite(row.get("日均首层UV")),
  182. "first_uv": _finite(row.get("窗口首层UV")),
  183. "t0_fission_count": _finite(row.get("窗口T0裂变数")),
  184. "t0_fission_rate": _finite(row.get("T0裂变率")),
  185. "cost": _finite(row.get("成本")),
  186. "efficiency_revenue": _finite(row.get("效率收入")),
  187. "fission_revenue": _finite(row.get("T0实际裂变收入")),
  188. "actual_total_revenue": _finite(
  189. row.get("实际全链路效率收入")
  190. ),
  191. "actual_roi": _finite(row.get("实际ROI")),
  192. "predicted_tail_revenue": _finite(
  193. row.get("预测T1-T15裂变收入")
  194. ),
  195. "predicted_fission_revenue": _finite(
  196. row.get("预测T0-T15裂变收入")
  197. ),
  198. "total_revenue": _finite(row.get("全链路效率收入")),
  199. "roi": _finite(row.get("ROI")),
  200. "stop_threshold": _finite(row.get("t_stop")),
  201. "scale_threshold": _finite(row.get("t_up")),
  202. "recommended_action": recommendation or None,
  203. "action_reason": str(row.get("动作原因") or ""),
  204. "execution_mode": execution_mode,
  205. "ineligible_reason": (
  206. execution_reason if execution_mode == MODE_NOTIFY_ONLY else None
  207. ),
  208. "daily_metrics_json": daily_metrics,
  209. }
  210. )
  211. return annotated, snapshots, actions