monthly_demands.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. """逐月策略 ODPS 查询。"""
  2. import re
  3. from datetime import datetime
  4. from zoneinfo import ZoneInfo
  5. from app.odps.client import get_odps_client
  6. from app.strategies.odps._utils import normalize_scalar, parse_video_list
  7. SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
  8. _DATE_PARTITION_RE = re.compile(r"^\d{8}$")
  9. _EXCLUDED_ELEMENTS = (
  10. "元旦", "腊八节", "小年", "除夕", "春节", "正月初一", "正月初二", "正月初三",
  11. "正月初四", "正月初五", "情人节", "元宵节", "龙抬头", "妇女节", "植树节", "劳动节",
  12. "母亲节", "儿童节", "端午节", "父亲节", "建党节", "建军节", "七夕节", "中元节",
  13. "中秋节", "国庆节", "重阳节", "感恩节", "公祭日", "平安夜", "圣诞节", "小寒",
  14. "大寒", "立春", "雨水", "惊蛰", "春分", "清明", "谷雨", "立夏", "小满", "芒种",
  15. "夏至", "小暑", "大暑", "立秋", "处暑", "白露", "秋分", "寒露", "霜降", "立冬",
  16. "小雪", "大雪", "冬至", "早上好", "中午好", "下午好", "晚上好", "晚安", "祝福",
  17. "祝愿", "祝你", "祝贺", "祝大家", "祝您", "祝好运", "祝群主", "祝朋友",
  18. )
  19. def _validate_bizdate(bizdate: str) -> str:
  20. value = bizdate.strip()
  21. if not _DATE_PARTITION_RE.match(value):
  22. raise ValueError(f"bizdate 须为 YYYYMMDD 格式,当前为 {value!r}")
  23. return value
  24. def _sql_string_list(values: tuple[str, ...]) -> str:
  25. return ", ".join(f"'{item}'" for item in values)
  26. def build_monthly_demands_sql(
  27. *,
  28. bizdate: str,
  29. strategy_label: str,
  30. view_pv_count: int,
  31. month_total_pv_threshold: float,
  32. min_contribution_score: float,
  33. rov_avg: float,
  34. min_frequency: int,
  35. ) -> str:
  36. bizdate_value = _validate_bizdate(bizdate)
  37. label = strategy_label.strip()
  38. if not label:
  39. raise ValueError("strategy_label cannot be empty")
  40. excluded_sql = _sql_string_list(_EXCLUDED_ELEMENTS)
  41. return f"""
  42. WITH biz_day AS (
  43. SELECT TO_DATE(
  44. CONCAT(
  45. SUBSTR('{bizdate_value}', 1, 4), '-',
  46. SUBSTR('{bizdate_value}', 5, 2), '-',
  47. SUBSTR('{bizdate_value}', 7, 2)
  48. )
  49. ) AS biz_dt
  50. ),
  51. yesterday AS (
  52. SELECT DATE_SUB((SELECT biz_dt FROM biz_day), 1) AS yest
  53. ),
  54. window_bounds AS (
  55. SELECT
  56. CAST((SELECT yest FROM yesterday) AS DATETIME) AS end_dt,
  57. CAST(DATE_SUB((SELECT yest FROM yesterday), 359) AS DATETIME) AS start_dt
  58. ),
  59. cleaned_video_metrics AS (
  60. SELECT
  61. CAST(视频id AS STRING) AS vid,
  62. CAST(FLOOR(DATEDIFF(
  63. (SELECT yest FROM yesterday),
  64. TO_DATE(REGEXP_REPLACE(CAST(dt AS STRING), '-', ''), 'yyyyMMdd')
  65. ) / 30) AS STRING) AS ym,
  66. rov_t0,
  67. COALESCE(`当日分发曝光pv`, 0) AS day_dist_pv
  68. FROM loghubods.video_dimension_detail_add_column
  69. WHERE TO_DATE(REGEXP_REPLACE(CAST(dt AS STRING), '-', ''), 'yyyyMMdd')
  70. BETWEEN (SELECT start_dt FROM window_bounds) AND (SELECT end_dt FROM window_bounds)
  71. AND COALESCE(`当日分发曝光pv`, 0) >= {int(view_pv_count)}
  72. ),
  73. video_monthly_avg_metrics AS (
  74. SELECT
  75. ym,
  76. vid,
  77. AVG(CASE WHEN rov_t0 = 0 THEN NULL ELSE rov_t0 END) AS vid_avg_rov,
  78. SUM(day_dist_pv) AS month_total_pv
  79. FROM cleaned_video_metrics
  80. GROUP BY ym, vid
  81. HAVING SUM(day_dist_pv) > {float(month_total_pv_threshold)}
  82. ),
  83. tag_vid_dedup AS (
  84. SELECT DISTINCT
  85. CAST(vid AS STRING) AS vid,
  86. 原始元素
  87. FROM loghubods.dwd_topic_decode_result_detail_di
  88. WHERE dt = MAX_PT('loghubods.dwd_topic_decode_result_detail_di')
  89. AND 元素维度 = '实质'
  90. AND 贡献分 >= {float(min_contribution_score)}
  91. ),
  92. element_monthly_metrics AS (
  93. SELECT
  94. t1.原始元素,
  95. t2.ym,
  96. COALESCE(ROUND(AVG(t2.vid_avg_rov), 6), 0) AS month_avg_rov
  97. FROM tag_vid_dedup t1
  98. JOIN video_monthly_avg_metrics t2
  99. ON t1.vid = t2.vid
  100. GROUP BY t1.原始元素, t2.ym
  101. HAVING COALESCE(ROUND(AVG(t2.vid_avg_rov), 6), 0) >= {float(rov_avg)}
  102. ),
  103. element_total_rov AS (
  104. SELECT
  105. 原始元素,
  106. ROUND(SUM(month_avg_rov), 6) AS avg_rov
  107. FROM element_monthly_metrics
  108. GROUP BY 原始元素
  109. ),
  110. element_vid_dedup AS (
  111. SELECT DISTINCT
  112. em.原始元素,
  113. vm.vid
  114. FROM element_monthly_metrics em
  115. JOIN tag_vid_dedup tv
  116. ON em.原始元素 = tv.原始元素
  117. JOIN video_monthly_avg_metrics vm
  118. ON tv.vid = vm.vid
  119. AND em.ym = vm.ym
  120. ),
  121. element_vid_stats AS (
  122. SELECT
  123. 原始元素,
  124. COUNT(DISTINCT vid) AS vid_count,
  125. COLLECT_SET(vid) AS vid_list
  126. FROM element_vid_dedup
  127. GROUP BY 原始元素
  128. ),
  129. element_freq AS (
  130. SELECT
  131. 原始元素,
  132. COUNT(1) AS 频次
  133. FROM element_monthly_metrics
  134. GROUP BY 原始元素
  135. )
  136. SELECT
  137. '{label}' AS strategy,
  138. md5(CONCAT('{label}', r.原始元素, '{bizdate_value}')) AS demand_id,
  139. r.原始元素 AS demand_name,
  140. r.avg_rov AS weight,
  141. '特征点' AS type,
  142. COALESCE(v.vid_count, 0) AS video_count,
  143. v.vid_list AS video_list,
  144. '{{}}' AS extend
  145. FROM element_total_rov r
  146. LEFT JOIN element_vid_stats v
  147. ON r.原始元素 = v.原始元素
  148. LEFT JOIN element_freq f
  149. ON r.原始元素 = f.原始元素
  150. WHERE r.原始元素 NOT IN ({excluded_sql})
  151. AND COALESCE(f.频次, 0) >= {int(min_frequency)}
  152. ORDER BY weight DESC
  153. """
  154. def query_monthly_demands(
  155. *,
  156. bizdate: str | None = None,
  157. strategy_label: str = "逐月",
  158. view_pv_count: int,
  159. month_total_pv_threshold: float,
  160. min_contribution_score: float,
  161. rov_avg: float,
  162. min_frequency: int,
  163. ) -> list[dict[str, object]]:
  164. if bizdate is None:
  165. bizdate = datetime.now(SHANGHAI_TZ).strftime("%Y%m%d")
  166. sql = build_monthly_demands_sql(
  167. bizdate=bizdate,
  168. strategy_label=strategy_label,
  169. view_pv_count=view_pv_count,
  170. month_total_pv_threshold=month_total_pv_threshold,
  171. min_contribution_score=min_contribution_score,
  172. rov_avg=rov_avg,
  173. min_frequency=min_frequency,
  174. )
  175. odps_client = get_odps_client()
  176. instance = odps_client.execute_sql(
  177. sql,
  178. hints={
  179. "odps.sql.submit.mode": "script",
  180. "odps.sql.decimal.odps2": "true",
  181. },
  182. )
  183. rows: list[dict[str, object]] = []
  184. with instance.open_reader(tunnel=True) as reader:
  185. for record in reader:
  186. rows.append(
  187. {
  188. "strategy": record["strategy"],
  189. "demand_id": record["demand_id"],
  190. "demand_name": record["demand_name"],
  191. "weight": normalize_scalar(record["weight"]),
  192. "type": record["type"],
  193. "video_count": record["video_count"],
  194. "video_list": parse_video_list(record["video_list"]),
  195. "extend": record["extend"],
  196. }
  197. )
  198. return rows