data_query_tools.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  1. import hashlib
  2. import os
  3. from zoneinfo import ZoneInfo
  4. from datetime import date, datetime, timedelta
  5. import json
  6. from pathlib import Path
  7. def _odps_config():
  8. return {
  9. "access_id": os.getenv("ODPS_ACCESS_ID", "").strip(),
  10. "access_key": os.getenv("ODPS_ACCESS_KEY", "").strip(),
  11. "project": os.getenv("ODPS_PROJECT", "loghubods").strip(),
  12. "endpoint": os.getenv("ODPS_ENDPOINT", "http://service.odps.aliyun.com/api").strip(),
  13. }
  14. def get_odps_data(sql):
  15. from odps import ODPS
  16. from odps.errors import ODPSError
  17. cfg = _odps_config()
  18. if not cfg["access_id"] or not cfg["access_key"]:
  19. raise RuntimeError("ODPS config missing: set ODPS_ACCESS_ID and ODPS_ACCESS_KEY")
  20. # 1. 初始化 ODPS 入口
  21. o = ODPS(cfg["access_id"], cfg["access_key"], cfg["project"], endpoint=cfg["endpoint"])
  22. try:
  23. # 2. 执行 SQL 并获取结果
  24. # execute_sql 会等待任务完成,使用 open_reader 读取数据
  25. with o.execute_sql(sql).open_reader() as reader:
  26. # reader 类似于 Java 中的 List<Record>
  27. # 我们可以直接将其转换为 Python 的 list
  28. records = [record for record in reader]
  29. return records
  30. except ODPSError as e:
  31. print(f"ODPS 错误: {e}")
  32. return None
  33. def execute_odps_sql(sql) -> bool:
  34. from odps import ODPS
  35. from odps.errors import ODPSError
  36. cfg = _odps_config()
  37. if not cfg["access_id"] or not cfg["access_key"]:
  38. raise RuntimeError("ODPS config missing: set ODPS_ACCESS_ID and ODPS_ACCESS_KEY")
  39. o = ODPS(cfg["access_id"], cfg["access_key"], cfg["project"], endpoint=cfg["endpoint"])
  40. try:
  41. instance = o.execute_sql(sql)
  42. instance.wait_for_success()
  43. return True
  44. except ODPSError as e:
  45. print(f"ODPS 错误: {e}")
  46. return False
  47. _STRATEGY_GAP = "当下供需gap"
  48. _STRATEGY_GAP_FENCI = "当下供需gap-分词"
  49. _HIVE_TABLE = "loghubods.dwd_multi_demand_pool_di"
  50. _HIVE_DT_FMT = "%Y%m%d" # 分区格式:yyyymmdd,如 20260519
  51. _CHINA_TZ = ZoneInfo("Asia/Shanghai")
  52. def _hive_partition_dt() -> str:
  53. """中国时区(Asia/Shanghai)当天日期,格式 yyyymmdd。"""
  54. return datetime.now(_CHINA_TZ).date().strftime(_HIVE_DT_FMT)
  55. def _escape_odps_string(value: object) -> str:
  56. return str(value).replace("'", "''")
  57. def _format_odps_string_array(values: list) -> str:
  58. if not values:
  59. return "ARRAY()"
  60. parts = [f"'{_escape_odps_string(v)}'" for v in values]
  61. return f"ARRAY({','.join(parts)})"
  62. def _parse_ext_data(ext_data_raw: object) -> dict:
  63. if isinstance(ext_data_raw, dict):
  64. return ext_data_raw
  65. if isinstance(ext_data_raw, str) and ext_data_raw.strip():
  66. try:
  67. return json.loads(ext_data_raw)
  68. except json.JSONDecodeError:
  69. return {}
  70. return {}
  71. def _get_mysql_db():
  72. from examples.demand.mysql import mysql_db
  73. return mysql_db
  74. def _build_hive_select_part(
  75. row: dict,
  76. ) -> str:
  77. video_ids = row.get("video_list") or []
  78. if not isinstance(video_ids, list):
  79. video_ids = []
  80. return (
  81. "SELECT "
  82. f"'{_escape_odps_string(row.get('strategy', ''))}' AS strategy, "
  83. f"'{_escape_odps_string(row.get('demand_id', ''))}' AS demand_id, "
  84. f"'{_escape_odps_string(row.get('demand_name', ''))}' AS demand_name, "
  85. f"{float(row.get('weight') or 0.0)} AS weight, "
  86. f"'{_escape_odps_string(row.get('type', ''))}' AS `type`, "
  87. f"{int(row.get('video_count') or 0)} AS video_count, "
  88. f"{_format_odps_string_array(video_ids)} AS video_list, "
  89. f"'{_escape_odps_string(row.get('extend', ''))}' AS extend"
  90. )
  91. def _insert_hive_select_parts(select_parts: list[str], partition_dt: str) -> bool:
  92. if not select_parts:
  93. return True
  94. union_sql = "\nUNION ALL\n".join(select_parts)
  95. insert_sql = f"""
  96. INSERT INTO TABLE {_HIVE_TABLE}
  97. PARTITION (dt='{partition_dt}')
  98. (strategy, demand_id, demand_name, weight, `type`, video_count, video_list, extend)
  99. {union_sql}
  100. """
  101. return execute_odps_sql(insert_sql)
  102. def build_dwd_multi_demand_pool_di_rows(rows: list[dict], partition_dt: str) -> list[dict]:
  103. """
  104. 将 demand_content 形态的行纯映射为 dwd_multi_demand_pool_di 输出行。
  105. 不执行 ODPS/Hive 写入;partition_dt 同时用于 demand_id 哈希和本地镜像 dt 字段。
  106. """
  107. if not rows:
  108. return []
  109. gap_rows: list[dict] = []
  110. fenci_rows: list[dict] = []
  111. for row in rows:
  112. merge_leve2 = str(row.get("merge_leve2") or "").strip()
  113. name = str(row.get("name") or "").strip()
  114. if not merge_leve2 or not name:
  115. continue
  116. weight = round(float(row.get("score") or 0.0), 6)
  117. ext_data = _parse_ext_data(row.get("ext_data"))
  118. type_str = str(ext_data.get("type") or "").strip()
  119. video_ids = ext_data.get("video_ids") or []
  120. if not isinstance(video_ids, list):
  121. video_ids = []
  122. video_ids = [str(v).strip() for v in video_ids if v is not None and str(v).strip()]
  123. video_count = len(video_ids)
  124. extend_json = json.dumps({"品类": merge_leve2}, ensure_ascii=False)
  125. demand_name_gap = f"{merge_leve2} {name}"
  126. demand_id_gap = hashlib.md5(
  127. f"{_STRATEGY_GAP}{demand_name_gap}{type_str}{partition_dt}".encode("utf-8")
  128. ).hexdigest()
  129. gap_rows.append(
  130. {
  131. "strategy": _STRATEGY_GAP,
  132. "demand_id": demand_id_gap,
  133. "demand_name": demand_name_gap,
  134. "weight": weight,
  135. "type": type_str,
  136. "video_count": video_count,
  137. "video_list": video_ids,
  138. "extend": extend_json,
  139. "dt": partition_dt,
  140. }
  141. )
  142. demand_id_fenci = hashlib.md5(
  143. f"{_STRATEGY_GAP_FENCI}{name}{merge_leve2}{type_str}{partition_dt}".encode("utf-8")
  144. ).hexdigest()
  145. fenci_rows.append(
  146. {
  147. "strategy": _STRATEGY_GAP_FENCI,
  148. "demand_id": demand_id_fenci,
  149. "demand_name": name,
  150. "weight": weight,
  151. "type": type_str,
  152. "video_count": video_count,
  153. "video_list": video_ids,
  154. "extend": extend_json,
  155. "dt": partition_dt,
  156. }
  157. )
  158. return gap_rows + fenci_rows
  159. def write_dwd_multi_demand_pool_di_to_hive(rows: list[dict]) -> int:
  160. """
  161. 将行数据映射并写入 loghubods.dwd_multi_demand_pool_di(尽力插入,不校验结果)。
  162. 分区与 demand_id 的日期均为中国时区当天(yyyymmdd),不使用行内 dt 字段。
  163. 执行两次 INSERT(同表、同分区),策略不同:
  164. 1) 当下供需gap: demand_name=merge_leve2+' '+name, demand_id=md5(strategy+demand_name+type+dt)
  165. 2) 当下供需gap-分词: demand_name=name, demand_id=md5(strategy+name+品类+type+dt)
  166. """
  167. if not rows:
  168. return 0
  169. china_today = _hive_partition_dt()
  170. output_rows = build_dwd_multi_demand_pool_di_rows(rows=rows, partition_dt=china_today)
  171. gap_parts = [
  172. _build_hive_select_part(row)
  173. for row in output_rows
  174. if row.get("strategy") == _STRATEGY_GAP
  175. ]
  176. fenci_parts = [
  177. _build_hive_select_part(row)
  178. for row in output_rows
  179. if row.get("strategy") == _STRATEGY_GAP_FENCI
  180. ]
  181. if not gap_parts:
  182. return 0
  183. _insert_hive_select_parts(gap_parts, china_today)
  184. _insert_hive_select_parts(fenci_parts, china_today)
  185. return len(output_rows)
  186. def build_feature_point_data_rows(names: list[str], dt: str) -> list[dict]:
  187. """
  188. 将需求名称纯映射为 feature_point_data 输出行。
  189. 不执行 Hive 写入;字段保持当前 Hive 表写入逻辑。
  190. """
  191. normalized_names = [str(name).strip() for name in names if name is not None and str(name).strip()]
  192. return [
  193. {
  194. "特征点": name,
  195. "总分发曝光pv": 5000,
  196. "质bn_rovn": 0.1,
  197. "dt": dt,
  198. }
  199. for name in normalized_names
  200. ]
  201. def write_feature_point_data_to_hive(names: list[str]) -> int:
  202. """
  203. 将需求名称写入 Hive 表 feature_point_data(按北京时间当天分区)。
  204. 仅写入以下字段:
  205. - 特征点
  206. - 总分发曝光pv(固定 5000)
  207. - 质bn_rovn(固定 0.1)
  208. """
  209. dt = _hive_partition_dt()
  210. output_rows = build_feature_point_data_rows(names=names, dt=dt)
  211. if not output_rows:
  212. return 0
  213. select_parts = []
  214. for row in output_rows:
  215. safe_name = _escape_odps_string(row.get("特征点", ""))
  216. select_parts.append(
  217. "SELECT "
  218. f"'{safe_name}' AS `特征点`, "
  219. "5000 AS `总分发曝光pv`, "
  220. "0.1 AS `质bn_rovn`"
  221. )
  222. union_sql = "\nUNION ALL\n".join(select_parts)
  223. insert_sql = f"""
  224. INSERT INTO TABLE feature_point_data
  225. PARTITION (dt='{dt}')
  226. (`特征点`, `总分发曝光pv`, `质bn_rovn`)
  227. {union_sql}
  228. """
  229. ok = execute_odps_sql(insert_sql)
  230. if not ok:
  231. return 0
  232. return len(output_rows)
  233. def _default_gap_partition_dt() -> str:
  234. date_time = datetime.now(ZoneInfo("Asia/Shanghai")).date() - timedelta(days=1)
  235. return date_time.strftime("%Y%m%d")
  236. def _requested_count_from_lack_count(lack_count: float) -> int:
  237. if lack_count > 1000:
  238. return 7
  239. if 500 < lack_count <= 1000:
  240. return 6
  241. if 100 < lack_count <= 500:
  242. return 4
  243. if 50 < lack_count <= 100:
  244. return 2
  245. return 1
  246. def get_demand_gap_source_partition_status(day: str | None = None) -> dict:
  247. day = day or _default_gap_partition_dt()
  248. sql_query = f"""
  249. select
  250. count(1) as row_count,
  251. count(distinct merge二级品类) as category_count
  252. from loghubods.video_dimension_detail_add_column
  253. where dt = '{_escape_odps_string(day)}'
  254. """
  255. data = get_odps_data(sql_query)
  256. if not data:
  257. return {
  258. "dt": day,
  259. "row_count": 0,
  260. "category_count": 0,
  261. "ready": False,
  262. }
  263. row = data[0]
  264. row_count = int(row[0] or 0)
  265. category_count = int(row[1] or 0)
  266. return {
  267. "dt": day,
  268. "row_count": row_count,
  269. "category_count": category_count,
  270. "ready": row_count > 0 and category_count > 0,
  271. }
  272. def get_demand_merge_level2_names(day: str | None = None):
  273. day = day or _default_gap_partition_dt()
  274. min_lack_count = 50
  275. sql_query = f'''
  276. select *
  277. from (
  278. select
  279. dt,
  280. merge二级品类,
  281. sum(当日分发曝光pv) as 分发曝光pv,
  282. sum(累计分享回流uv) AS bn_总回流,
  283. sum(当日分发回流uv)/(sum(当日分发曝光pv)+100) as 质bn_rovn,
  284. case when sum(当日分发曝光pv)>=10000 then
  285. case when sum(当日分发回流uv)/(sum(当日分发曝光pv)+100)<0.035
  286. then -1*(count(DISTINCT 视频id)/avg(总日分发视频数))/((sum(累计分享回流uv)/avg(总日回流uv)))
  287. else 10*(sum(累计分享回流uv)/avg(总日回流uv)*sum(当日分发回流uv)/(sum(当日分发曝光pv)+100))/(count(DISTINCT 视频id)/avg(总日分发视频数))
  288. end
  289. else 0 end AS 总供需分,
  290. case when sum(当日分发曝光pv)>=10000 then
  291. case when sum(当日分发回流uv)/(sum(当日分发曝光pv)+100)<0.035
  292. then -1*(COUNT(DISTINCT CASE WHEN 推荐天数间隔<3 THEN 视频id END ) /avg(总日分发视频数))/(sum(累计分享回流uv)/avg(总日回流uv))
  293. else 10*(sum(累计分享回流uv)/avg(总日回流uv)*sum(当日分发回流uv)/(sum(当日分发曝光pv)+1000))/(COUNT(DISTINCT CASE WHEN 推荐天数间隔<3 THEN 视频id END ) /avg(总日分发视频数))
  294. end
  295. else 0 end AS 新供需分,
  296. count(DISTINCT 视频id) as 分发视频量,
  297. count(DISTINCT if(推荐天数间隔<3,视频id,null)) as 3日新推荐视频量,
  298. case when sum(当日分发曝光pv)>=10000 and sum(当日分发回流uv)/(sum(当日分发曝光pv)+100)>0.035
  299. then (avg(总日分发视频数)*(10*(sum(当日分发回流uv)/(sum(当日分发曝光pv)+100))*(sum(累计分享回流uv)/avg(总日回流uv) ))/0.5-count(DISTINCT 视频id))/3
  300. end as 缺量,
  301. case when sum(当日分发曝光pv)>=10000 and sum(当日分发回流uv)/(sum(当日分发曝光pv)+100)<=0.035
  302. then (avg(总日分发视频数)*(10*(sum(当日分发回流uv)/(sum(当日分发曝光pv)+100))*(sum(累计分享回流uv)/avg(总日回流uv) ))/(2)-count(DISTINCT 视频id))/3
  303. end as 控量,
  304. avg(总日回流uv) AS 总日回流uv,
  305. avg(总日分发视频数) AS 总日分发视频数,
  306. avg(总日推荐视频数) AS 总日推荐视频数,
  307. COUNT(DISTINCT CASE WHEN 总回流uv>0 THEN 视频id END )/avg(总日分发视频数) AS 回流视频个数占比,
  308. sum(当日分发回流uv) AS bn_当日分发回流,
  309. sum(当日分发回流uv)/avg(总日回流uv) AS 分发拉回回流uv占比,
  310. sum(累计分享回流uv)/avg(总日回流uv) AS 回流uv占比,
  311. count(DISTINCT 视频id)/avg(总日分发视频数) AS 分发视频量占比,
  312. COUNT(DISTINCT CASE WHEN 是否当日新推荐=1 THEN 视频id END ) /avg(总日分发视频数) AS 新推荐视频量占比
  313. from loghubods.video_dimension_detail_add_column
  314. where dt = '{day}'
  315. group by dt, merge二级品类
  316. ) t1
  317. where t1.缺量>= {min_lack_count}
  318. '''
  319. data = get_odps_data(sql_query)
  320. result_list = []
  321. if data:
  322. for r in data:
  323. lack_count = r[9]
  324. requested_count = _requested_count_from_lack_count(float(lack_count or 0))
  325. if requested_count == 0:
  326. continue
  327. result_list.append({
  328. "cluster_name": r[1],
  329. "platform_type": "piaoquan",
  330. "count": requested_count,
  331. "gap_dt": str(r[0]),
  332. "lack_count": float(lack_count or 0),
  333. "requested_count": requested_count,
  334. })
  335. return result_list
  336. def get_rov_by_merge_leve2_and_video_ids(merge_leve2, video_ids):
  337. merge_level_in_clause = f"'{merge_leve2}'"
  338. video_ids_in_clause = ", ".join([f"'{video_id}'" for video_id in video_ids])
  339. end_date = (date.today() - timedelta(days=1)).strftime("%Y%m%d")
  340. start_date = (date.today() - timedelta(days=14)).strftime("%Y%m%d")
  341. # sql_query = f'''
  342. # SELECT
  343. # v.videoid,
  344. # CASE
  345. # WHEN COALESCE(SUM(COALESCE(t3.`当日分发曝光pv`, 0)), 0) < 1000 THEN 0
  346. # ELSE COALESCE(AVG(NULLIF(t3.rov_t0, 0)), 0)
  347. # END AS avg_rov_t0
  348. # FROM
  349. # (
  350. # SELECT
  351. # t2.videoid,
  352. # t2.merge_leve2
  353. # FROM videoods.content_profile t1
  354. # JOIN loghubods.video_merge_tag t2
  355. # ON t1.content_id = t2.videoid
  356. # WHERE
  357. # t1.status = 3
  358. # AND t1.is_deleted = 0
  359. # AND t2.merge_leve2 IN ({merge_level_in_clause})
  360. # ) v
  361. # LEFT JOIN loghubods.video_dimension_detail_add_column t3
  362. # ON v.videoid = t3.视频id
  363. # AND t3.dt >= '{start_date}'
  364. # AND t3.dt <= '{end_date}'
  365. # WHERE v.videoid in ({video_ids_in_clause})
  366. # GROUP BY
  367. # v.videoid
  368. # ;
  369. # '''
  370. sql_query = f'''
  371. SELECT
  372. CAST(t3.视频id AS STRING) AS 视频id_str,
  373. CASE
  374. WHEN COALESCE(SUM(COALESCE(t3.`当日分发曝光pv`, 0)), 0) < 1000 THEN 0
  375. ELSE COALESCE(AVG(NULLIF(t3.rov_t0, 0)), 0)
  376. END AS avg_rov_t0
  377. FROM
  378. loghubods.video_dimension_detail_add_column t3
  379. WHERE t3.视频id in ({video_ids_in_clause})
  380. AND t3.dt >= '{start_date}'
  381. AND t3.dt <= '{end_date}'
  382. GROUP BY
  383. t3.视频id
  384. ;
  385. '''
  386. data = get_odps_data(sql_query)
  387. result_dict = {}
  388. if data:
  389. result_dict = {r[0]: r[1] for r in data}
  390. return result_dict
  391. def get_rov_by_tree_and_video_ids(video_ids):
  392. video_ids_in_clause = ", ".join([f"'{video_id}'" for video_id in video_ids])
  393. last_year_today = date.today() - timedelta(days=365)
  394. start_date = last_year_today.strftime("%Y%m%d")
  395. end_date = (last_year_today + timedelta(days=7)).strftime("%Y%m%d")
  396. sql_query = f'''
  397. SELECT
  398. CAST(t3.视频id AS STRING) AS 视频id_str,
  399. CASE
  400. WHEN COALESCE(SUM(COALESCE(t3.`当日分发曝光pv`, 0)), 0) < 1000 THEN 0
  401. ELSE COALESCE(AVG(NULLIF(t3.rov_t0, 0)), 0)
  402. END AS avg_rov_t0
  403. FROM
  404. loghubods.video_dimension_detail_add_column t3
  405. WHERE t3.视频id in ({video_ids_in_clause})
  406. AND t3.dt >= '{start_date}'
  407. AND t3.dt <= '{end_date}'
  408. GROUP BY
  409. t3.视频id
  410. ;
  411. '''
  412. data = get_odps_data(sql_query)
  413. result_dict = {}
  414. if data:
  415. result_dict = {r[0]: r[1] for r in data}
  416. return result_dict
  417. def get_changwen_weight(account_name):
  418. bizdatemax_date = date.today() - timedelta(days=1)
  419. bizdatemin_date = bizdatemax_date - timedelta(days=30)
  420. bizdatemax = bizdatemax_date.strftime("%Y%m%d")
  421. bizdatemin = bizdatemin_date.strftime("%Y%m%d")
  422. sql_query = f'''
  423. SELECT
  424. 公众号名
  425. ,videoid
  426. ,一级品类
  427. ,二级品类
  428. ,头部曝光
  429. ,头部曝光uv
  430. ,头部realplay
  431. ,头部realplay_uv
  432. ,头部分享
  433. ,头部分享uv
  434. ,头部回流人数 AS 头部回流数
  435. ,推荐曝光数
  436. ,当日分发曝光uv
  437. ,推荐realplay
  438. ,分发realplay_uv
  439. ,推荐分享数
  440. ,当日分发分享uv
  441. ,推荐回流数
  442. ,当日回流进入分发曝光次数 AS vov分子
  443. FROM (
  444. SELECT DISTINCT a.公众号名
  445. ,a.videoid
  446. ,e.merge_leve1 AS 一级品类
  447. ,e.merge_leve2 AS 二级品类
  448. ,a.title
  449. ,a.进入分发人数
  450. ,头部曝光pv AS 头部曝光
  451. ,头部realplay_pv AS 头部realplay
  452. ,头部分享pv AS 头部分享
  453. ,a.当日分发曝光pv AS 推荐曝光数
  454. ,a.当日分发播放pv
  455. ,分发realplay_pv AS 推荐realplay
  456. ,分发realplay_pv / a.当日分发播放pv AS 真实播放率pv
  457. ,当日分发播放uv
  458. ,c.realplay_uv AS 分发真实播uv
  459. ,c.realplay_uv / a.当日分发播放uv AS 真实播放率uv
  460. ,a.当日分发分享pv AS 推荐分享数
  461. ,a.当日分发分享pv / a.当日分发曝光pv AS str
  462. ,NVL(b.当日分发回流人数,0) AS 推荐回流数
  463. ,NVL(b.当日回流进入分发人数,0) AS 当日回流进入分发人数
  464. ,NVL(b.当日回流进入分发曝光次数,0) AS 当日回流进入分发曝光次数
  465. ,NVL(b.当日回流进入分发曝光次数,0) / a.当日分发曝光pv AS vov分子
  466. ,d.头部回流人数
  467. ,当日分发曝光uv
  468. ,头部曝光uv
  469. ,当日分发分享uv
  470. ,头部分享uv
  471. ,分发realplay_uv
  472. ,头部realplay_uv
  473. FROM (
  474. SELECT account_name AS 公众号名
  475. ,videoid
  476. ,title
  477. ,COUNT(DISTINCT mid) AS 进入分发人数
  478. ,COUNT(
  479. CASE WHEN pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$' AND businesstype = 'videoView' THEN mid END
  480. ) AS 当日分发曝光pv
  481. ,COUNT(DISTINCT
  482. CASE WHEN pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$' AND businesstype = 'videoView' THEN mid END
  483. ) AS 当日分发曝光uv
  484. ,COUNT(
  485. CASE WHEN pagesource REGEXP 'pages/user-videos-share$' AND businesstype = 'videoView' THEN mid END
  486. ) AS 头部曝光pv
  487. ,COUNT(DISTINCT
  488. CASE WHEN pagesource REGEXP 'pages/user-videos-share$' AND businesstype = 'videoView' THEN mid END
  489. ) AS 头部曝光uv
  490. ,COUNT(
  491. CASE WHEN pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$' AND businesstype = 'videoPlay' THEN mid END
  492. ) AS 当日分发播放pv
  493. ,COUNT(DISTINCT
  494. CASE WHEN pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$' AND businesstype = 'videoPlay' THEN mid END
  495. ) AS 当日分发播放uv
  496. ,COUNT(
  497. CASE WHEN pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$' AND businesstype = 'videoShareFriend' THEN mid END
  498. ) AS 当日分发分享pv
  499. ,COUNT(DISTINCT
  500. CASE WHEN pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$' AND businesstype = 'videoShareFriend' THEN mid END
  501. ) AS 当日分发分享uv
  502. ,COUNT(
  503. CASE WHEN pagesource REGEXP 'pages/user-videos-share$' AND businesstype = 'videoShareFriend' THEN mid END
  504. ) AS 头部分享pv
  505. ,COUNT(DISTINCT
  506. CASE WHEN pagesource REGEXP 'pages/user-videos-share$' AND businesstype = 'videoShareFriend' THEN mid END
  507. ) AS 头部分享uv
  508. FROM (
  509. SELECT DISTINCT a.mid
  510. ,a.videoid
  511. ,a.businesstype
  512. ,a.pagesource
  513. ,a.subsessionid
  514. ,account_name
  515. ,e.title
  516. FROM loghubods.video_action_log_rp a
  517. LEFT JOIN loghubods.user_wechat_identity_info_ha b
  518. ON a.mid = CONCAT('weixin_openid_',b.open_id)
  519. AND b.dt = MAX_PT("loghubods.user_wechat_identity_info_ha")
  520. LEFT JOIN loghubods.gzh_fans_info d
  521. ON b.union_id = d.union_id
  522. AND d.dt = MAX_PT("loghubods.gzh_fans_info")
  523. LEFT JOIN videoods.wx_video e
  524. ON a.videoid = e.id
  525. WHERE a.dt >= '{bizdatemin}'
  526. AND a.dt <= '{bizdatemax}'
  527. AND businesstype IN ('videoView','videoPlay','videoShareFriend')
  528. AND d.user_create_time IS NOT NULL
  529. AND account_name = '{account_name}'
  530. AND a.videoid IN (
  531. SELECT
  532. DISTINCT content_id AS videoid
  533. FROM
  534. videoods.content_profile
  535. WHERE status=3
  536. AND is_deleted = 0
  537. )
  538. ) t
  539. GROUP BY 公众号名
  540. ,videoid
  541. ,title
  542. ) a
  543. LEFT JOIN (
  544. SELECT t.account_name AS 公众号名
  545. ,t.videoid
  546. ,COUNT(DISTINCT s.machinecode) AS 当日分发回流人数
  547. ,COUNT(DISTINCT v.mid) AS 当日回流进入分发人数
  548. ,COUNT(v.mid) AS 当日回流进入分发曝光次数
  549. FROM (
  550. SELECT DISTINCT a.subsessionid
  551. ,a.videoid
  552. ,a.mid
  553. ,d.account_name
  554. ,GET_JSON_OBJECT(extparams,'$.recomTraceId') AS recomtraceid
  555. FROM loghubods.video_action_log_rp a
  556. LEFT JOIN loghubods.user_wechat_identity_info_ha b
  557. ON a.mid = CONCAT('weixin_openid_',b.open_id)
  558. AND b.dt = MAX_PT("loghubods.user_wechat_identity_info_ha")
  559. LEFT JOIN loghubods.gzh_fans_info d
  560. ON b.union_id = d.union_id
  561. AND d.dt = MAX_PT("loghubods.gzh_fans_info")
  562. WHERE a.dt >= '{bizdatemin}'
  563. AND a.dt <= '{bizdatemax}'
  564. AND a.businesstype = 'videoShareFriend'
  565. AND a.pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$'
  566. AND d.user_create_time IS NOT NULL
  567. AND d.account_name = '{account_name}'
  568. ) t
  569. LEFT JOIN (
  570. SELECT DISTINCT subsessionid
  571. ,machinecode
  572. ,recomtraceid
  573. ,clickobjectid
  574. FROM loghubods.user_share_log
  575. WHERE dt >= '{bizdatemin}'
  576. AND dt <= '{bizdatemax}'
  577. AND topic = 'click'
  578. ) s
  579. ON t.recomtraceid = s.recomtraceid
  580. AND t.videoid = s.clickobjectid
  581. LEFT JOIN (
  582. SELECT subsessionid
  583. ,mid
  584. ,videoid
  585. FROM loghubods.video_action_log_rp
  586. WHERE dt >= '{bizdatemin}'
  587. AND dt <= '{bizdatemax}'
  588. AND pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$'
  589. AND businesstype = 'videoView'
  590. ) v
  591. ON s.subsessionid = v.subsessionid
  592. AND s.machinecode = v.mid
  593. GROUP BY account_name
  594. ,t.videoid
  595. ) b
  596. ON a.公众号名 = b.公众号名
  597. AND a.videoid = b.videoid
  598. LEFT JOIN (
  599. SELECT d.account_name AS 公众号名
  600. ,a.videoid
  601. ,COUNT(DISTINCT a.mid) AS realplay_uv
  602. ,COUNT(
  603. CASE WHEN a.pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$' THEN a.mid END
  604. ) AS 分发realplay_pv
  605. ,COUNT(CASE WHEN a.pagesource REGEXP 'pages/user-videos-share$' THEN a.mid END) AS 头部realplay_pv
  606. ,COUNT(DISTINCT
  607. CASE WHEN a.pagesource REGEXP 'category$|recommend$|-pages/user-videos-detail$' THEN a.mid END
  608. ) AS 分发realplay_uv
  609. ,COUNT(DISTINCT CASE WHEN a.pagesource REGEXP 'pages/user-videos-share$' THEN a.mid END) AS 头部realplay_uv
  610. FROM loghubods.ods_video_play_log_day a
  611. LEFT JOIN (
  612. SELECT DISTINCT open_id
  613. ,union_id
  614. FROM loghubods.user_wechat_identity_info_ha
  615. WHERE dt = MAX_PT("loghubods.user_wechat_identity_info_ha")
  616. ) b
  617. ON a.mid = CONCAT('weixin_openid_',b.open_id)
  618. LEFT JOIN loghubods.gzh_fans_info d
  619. ON b.union_id = d.union_id
  620. AND d.dt = MAX_PT("loghubods.gzh_fans_info")
  621. WHERE a.dt >= '{bizdatemin}'
  622. AND a.dt <= '{bizdatemax}'
  623. AND a.businesstype = 'videoRealPlay'
  624. AND d.user_create_time IS NOT NULL
  625. AND d.account_name = '{account_name}'
  626. GROUP BY d.account_name
  627. ,a.videoid
  628. ORDER BY 分发realplay_pv DESC
  629. ) c
  630. ON a.公众号名 = c.公众号名
  631. AND a.videoid = c.videoid
  632. LEFT JOIN (
  633. SELECT t.account_name AS 公众号名
  634. ,t.videoid
  635. ,COUNT(DISTINCT s.machinecode) AS 头部回流人数
  636. FROM (
  637. SELECT DISTINCT a.shareobjectid AS videoid
  638. ,a.shareid
  639. ,a.machinecode
  640. ,d.account_name
  641. FROM loghubods.user_share_log a
  642. LEFT JOIN loghubods.user_wechat_identity_info_ha b
  643. ON a.machinecode = CONCAT('weixin_openid_',b.open_id)
  644. AND b.dt = MAX_PT("loghubods.user_wechat_identity_info_ha")
  645. LEFT JOIN loghubods.gzh_fans_info d
  646. ON b.union_id = d.union_id
  647. AND d.dt = MAX_PT("loghubods.gzh_fans_info")
  648. WHERE a.dt >= '{bizdatemin}'
  649. AND a.dt <= '{bizdatemax}'
  650. AND a.topic = 'share'
  651. AND a.pagesource REGEXP 'pages/user-videos-share$'
  652. AND d.user_create_time IS NOT NULL
  653. AND d.account_name = '{account_name}'
  654. ) t
  655. LEFT JOIN (
  656. SELECT DISTINCT shareid
  657. ,machinecode
  658. ,clickobjectid
  659. FROM loghubods.user_share_log
  660. WHERE dt >= '{bizdatemin}'
  661. AND dt <= '{bizdatemax}'
  662. AND topic = 'click'
  663. ) s
  664. ON t.shareid = s.shareid
  665. GROUP BY account_name
  666. ,t.videoid
  667. ) d
  668. ON a.公众号名 = d.公众号名
  669. AND a.videoid = d.videoid
  670. LEFT JOIN loghubods.video_merge_tag e
  671. ON a.videoid = e.videoid
  672. )
  673. ORDER BY 推荐曝光数 DESC
  674. '''
  675. result_list = []
  676. data = get_odps_data(sql_query)
  677. if data:
  678. for r in data:
  679. result_list.append(
  680. {
  681. "account_name": r[0],
  682. "videoid": r[1],
  683. "一级品类": r[2],
  684. "二级品类": r[3],
  685. "ext_data": {
  686. "头部曝光": r[4],
  687. "头部曝光uv": r[5],
  688. "头部realplay": r[6],
  689. "头部realplay_uv": r[7],
  690. "头部分享": r[8],
  691. "头部分享uv": r[9],
  692. "头部回流数": r[10],
  693. "推荐曝光数": r[11],
  694. "当日分发曝光uv": r[12],
  695. "推荐realplay": r[13],
  696. "分发realplay_uv": r[14],
  697. "推荐分享数": r[15],
  698. "当日分发分享uv": r[16],
  699. "推荐回流数": r[17],
  700. "vov分子": r[18],
  701. },
  702. }
  703. )
  704. # 输出到 examples/demand/data/changwen_data/
  705. output_dir = Path(__file__).parent / "data" / "changwen_data"
  706. output_dir.mkdir(parents=True, exist_ok=True)
  707. output_file = output_dir / f"{account_name}.json"
  708. with output_file.open("w", encoding="utf-8") as f:
  709. json.dump(result_list, f, ensure_ascii=False, indent=2)
  710. return result_list
  711. def get_zengzhang_weight(account_name):
  712. bizdatemax_date = date.today() - timedelta(days=1)
  713. bizdatemin_date = bizdatemax_date - timedelta(days=30)
  714. bizdatemax = bizdatemax_date.strftime("%Y%m%d")
  715. bizdatemin = bizdatemin_date.strftime("%Y%m%d")
  716. sql_query = f'''
  717. SELECT 合作方名
  718. ,合作方简称
  719. ,videoid
  720. ,一级品类
  721. ,二级品类
  722. ,SUM(头部曝光) as 头部曝光
  723. ,SUM(头部曝光uv) as 头部曝光uv
  724. ,SUM(头部realplay) as 头部realplay
  725. ,SUM(头部realplay_uv) as 头部realplay_uv
  726. ,SUM(头部分享) as 头部分享
  727. ,SUM(头部分享uv) as 头部分享uv
  728. ,SUM(头部回流数) as 头部回流数
  729. ,SUM(推荐曝光数) as 推荐曝光数
  730. ,SUM(当日分发曝光uv) as 当日分发曝光uv
  731. ,SUM(推荐realplay) as 推荐realplay
  732. ,SUM(分发realplay_uv) as 分发realplay_uv
  733. ,SUM(推荐分享数) as 推荐分享数
  734. ,SUM(当日分发分享uv) as 当日分发分享uv
  735. ,SUM(推荐回流数) as 推荐回流数
  736. ,SUM(vov分子) as vov分子
  737. FROM loghubods.dws_growth_partner_vid_data
  738. WHERE dt BETWEEN '{bizdatemin}' AND '{bizdatemax}'
  739. AND 合作方名 = '{account_name}'
  740. GROUP BY 合作方名
  741. ,合作方简称
  742. ,videoid
  743. ,一级品类
  744. ,二级品类
  745. ORDER BY SUM(推荐曝光数)
  746. ;
  747. '''
  748. result_list = []
  749. data = get_odps_data(sql_query)
  750. if data:
  751. for r in data:
  752. result_list.append(
  753. {
  754. "account_name": r[0],
  755. "合作方简称": r[1],
  756. "videoid": r[2],
  757. "一级品类": r[3],
  758. "二级品类": r[4],
  759. "ext_data": {
  760. "头部曝光": r[5],
  761. "头部曝光uv": r[6],
  762. "头部realplay": r[7],
  763. "头部realplay_uv": r[8],
  764. "头部分享": r[9],
  765. "头部分享uv": r[10],
  766. "头部回流数": r[11],
  767. "推荐曝光数": r[12],
  768. "当日分发曝光uv": r[13],
  769. "推荐realplay": r[14],
  770. "分发realplay_uv": r[15],
  771. "推荐分享数": r[16],
  772. "当日分发分享uv": r[17],
  773. "推荐回流数": r[18],
  774. "vov分子": r[19],
  775. },
  776. }
  777. )
  778. # 输出到 examples/demand/data/zengzhang_data/
  779. output_dir = Path(__file__).parent / "data" / "zengzhang_data"
  780. output_dir.mkdir(parents=True, exist_ok=True)
  781. output_file = output_dir / f"{account_name}.json"
  782. with output_file.open("w", encoding="utf-8") as f:
  783. json.dump(result_list, f, ensure_ascii=False, indent=2)
  784. return result_list
  785. def get_merge_leve2_by_video_ids(video_ids, batch_size=2000):
  786. result = {}
  787. if not video_ids:
  788. return result
  789. normalized_ids = [str(video_id) for video_id in video_ids if video_id is not None]
  790. for i in range(0, len(normalized_ids), batch_size):
  791. batch_ids = normalized_ids[i:i + batch_size]
  792. escaped_ids = [video_id.replace("'", "''") for video_id in batch_ids]
  793. video_ids_in_clause = ", ".join([f"'{video_id}'" for video_id in escaped_ids])
  794. sql_query = f'''
  795. SELECT videoid, merge_leve2
  796. FROM loghubods.video_merge_tag
  797. WHERE videoid IN ({video_ids_in_clause})
  798. '''
  799. data = get_odps_data(sql_query)
  800. if not data:
  801. continue
  802. for row in data:
  803. result[str(row[0])] = row[1]
  804. return result
  805. def get_all_decode_task_result_rows():
  806. mysql_db = _get_mysql_db()
  807. return mysql_db.select(
  808. "workflow_decode_task_result",
  809. columns="id, channel_content_id, merge_leve2",
  810. )
  811. def update_decode_task_result_merge_leve2(channel_content_id, merge_leve2):
  812. mysql_db = _get_mysql_db()
  813. return mysql_db.update(
  814. "workflow_decode_task_result",
  815. {"merge_leve2": str(merge_leve2)},
  816. "channel_content_id = %s",
  817. (str(channel_content_id),),
  818. )
  819. def backfill_merge_leve2_for_decode_task_result():
  820. rows = get_all_decode_task_result_rows()
  821. updated_count = 0
  822. skipped_count = 0
  823. valid_content_ids = []
  824. for row in rows:
  825. channel_content_id = row.get("channel_content_id")
  826. if channel_content_id is None:
  827. skipped_count += 1
  828. continue
  829. channel_content_id = str(channel_content_id)
  830. if len(channel_content_id) > 8:
  831. skipped_count += 1
  832. continue
  833. valid_content_ids.append(channel_content_id)
  834. merge_leve2_map = get_merge_leve2_by_video_ids(valid_content_ids, batch_size=2000)
  835. for channel_content_id in valid_content_ids:
  836. merge_leve2 = merge_leve2_map.get(channel_content_id)
  837. if not merge_leve2:
  838. continue
  839. affected = update_decode_task_result_merge_leve2(channel_content_id, merge_leve2)
  840. if affected > 0:
  841. updated_count += affected
  842. return {
  843. "total": len(rows),
  844. "updated": updated_count,
  845. "skipped": skipped_count,
  846. }
  847. #
  848. # if __name__ == '__main__':
  849. # backfill_merge_leve2_for_decode_task_result()