policy.py 8.8 KB

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