user_timeline.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. """单用户完整行为时间线:视频、广告、播放、用户行为、活跃路径五类离线日志。
  4. 五类日志在同一个 ODPS SQL 中 UNION ALL;Python 端补充中文行为定义并输出 Excel。
  5. 用法:python3 user_timeline.py <machinecode/mid值> <日期yyyyMMdd> [apptype] [--output-dir 目录]
  6. """
  7. import argparse
  8. from datetime import datetime
  9. from pathlib import Path
  10. import re
  11. import pandas as pd
  12. from odps_module import ODPSClient
  13. PAGESTATUS_MEAN = {
  14. "1": "头部未起播(封面/广告覆盖)",
  15. "2": "feed正常态",
  16. "3": "feed_m初始态",
  17. "4": "沉浸式页",
  18. "5": "feed-从沉浸/feed_m返回(粘)",
  19. "7": "首页category曝光/分享",
  20. "8": "feed_m-从沉浸返回(粘)",
  21. }
  22. EXCLUDED_BUSINESSTYPES = {"deviceId", "openGIdSuccess", "buttonView"}
  23. def sql_text(value):
  24. return str(value).replace("'", "''")
  25. def safe_name(value):
  26. return re.sub(r"[^A-Za-z0-9_-]", "_", str(value))
  27. SQL_ALL = """
  28. WITH v AS (
  29. SELECT CAST(clienttimestamp AS BIGINT) AS ts, 'video' AS 来源,
  30. businesstype, pagesource, CAST(NULL AS STRING) AS endRoutePath,
  31. CAST(NULL AS STRING) AS isAdPlaying, CAST(NULL AS STRING) AS creativeCode, videoid AS 视频id,
  32. GET_JSON_OBJECT(extparams, '$.auto_enter') AS auto_enter,
  33. GET_JSON_OBJECT(extparams, '$.newPage') AS newPage,
  34. GET_JSON_OBJECT(extparams, '$.pageStatus') AS pageStatus,
  35. CAST(NULL AS STRING) AS hotsencetype, CAST(NULL AS STRING) AS path, subsessionid, sessionid
  36. FROM loghubods.video_action_log_applet
  37. WHERE dt='{day}' AND apptype='{apptype}' AND mid='{mc}'
  38. AND businesstype <> 'videoPreView'
  39. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  40. ),
  41. a AS (
  42. SELECT CAST(clienttimestamp AS BIGINT) AS ts, 'ad' AS 来源,
  43. businesstype, pagesource, CAST(NULL AS STRING) AS endRoutePath,
  44. CAST(NULL AS STRING) AS isAdPlaying, CAST(NULL AS STRING) AS creativeCode, headvideoid AS 视频id,
  45. CAST(NULL AS STRING) AS auto_enter, CAST(NULL AS STRING) AS newPage, CAST(NULL AS STRING) AS pageStatus,
  46. hotsencetype, CAST(NULL AS STRING) AS path, subsessionid, sessionid
  47. FROM loghubods.ad_action_log_own
  48. WHERE dt='{day}' AND apptype='{apptype}' AND machinecode='{mc}'
  49. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  50. ),
  51. p AS (
  52. SELECT CAST(clienttimestamp AS BIGINT) AS ts, 'play' AS 来源,
  53. businesstype, pagesource, CAST(NULL AS STRING) AS endRoutePath,
  54. CAST(NULL AS STRING) AS isAdPlaying, CAST(NULL AS STRING) AS creativeCode, videoid AS 视频id,
  55. GET_JSON_OBJECT(extparams, '$.auto_enter') AS auto_enter,
  56. GET_JSON_OBJECT(extparams, '$.newPage') AS newPage,
  57. GET_JSON_OBJECT(extparams, '$.pageStatus') AS pageStatus,
  58. CAST(NULL AS STRING) AS hotsencetype, CAST(NULL AS STRING) AS path, subsessionid, sessionid
  59. FROM loghubods.video_play_log
  60. WHERE dt='{day}' AND apptype='{apptype}' AND mid='{mc}'
  61. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  62. ),
  63. s AS (
  64. SELECT CAST(clienttimestamp AS BIGINT) AS ts, 'simpleevent' AS 来源,
  65. businesstype, pagesource, endroutepath AS endRoutePath,
  66. GET_JSON_OBJECT(extparams, '$.isAdPlaying') AS isAdPlaying,
  67. GET_JSON_OBJECT(extparams, '$.creativeCode') AS creativeCode, videoid AS 视频id,
  68. CAST(NULL AS STRING) AS auto_enter, CAST(NULL AS STRING) AS newPage, CAST(NULL AS STRING) AS pageStatus,
  69. CAST(NULL AS STRING) AS hotsencetype, CAST(NULL AS STRING) AS path, subsessionid, sessionid
  70. FROM loghubods.simpleevent_log
  71. WHERE dt='{day}' AND apptype='{apptype}' AND machinecode='{mc}'
  72. AND (businesstype IS NULL OR businesstype <> 'openGIdError')
  73. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  74. ),
  75. u AS (
  76. SELECT CAST(clienttimestamp AS BIGINT) AS ts, 'useractive' AS 来源,
  77. businesstype, pagesource, CAST(NULL AS STRING) AS endRoutePath,
  78. CAST(NULL AS STRING) AS isAdPlaying, CAST(NULL AS STRING) AS creativeCode, CAST(NULL AS STRING) AS 视频id,
  79. CAST(NULL AS STRING) AS auto_enter, CAST(NULL AS STRING) AS newPage, CAST(NULL AS STRING) AS pageStatus,
  80. CAST(NULL AS STRING) AS hotsencetype, path, subsessionid, sessionid
  81. FROM loghubods.useractive_log
  82. WHERE dt='{day}' AND apptype='{apptype}' AND machinecode='{mc}'
  83. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  84. ),
  85. t AS (
  86. SELECT * FROM v
  87. UNION ALL SELECT * FROM a
  88. UNION ALL SELECT * FROM p
  89. UNION ALL SELECT * FROM s
  90. UNION ALL SELECT * FROM u
  91. )
  92. SELECT t.ts, t.来源, t.businesstype, t.pagesource, t.endRoutePath, t.isAdPlaying, t.creativeCode, t.视频id, b.title AS 视频标题,
  93. t.auto_enter, t.newPage, t.pageStatus, t.hotsencetype, t.path, t.subsessionid, t.sessionid
  94. FROM t
  95. LEFT JOIN videoods.dim_video b ON t.视频id = b.videoid
  96. ORDER BY t.ts
  97. """
  98. def text(value):
  99. return "" if pd.isna(value) else str(value)
  100. def behavior_definition(row):
  101. source = text(row["来源"])
  102. businesstype = text(row["businesstype"])
  103. pagesource = text(row["pagesource"])
  104. is_head_video_page = pagesource.endswith("user-videos-share")
  105. if source == "video" and is_head_video_page:
  106. return {"videoView": "头部视频曝光", "videoPlay": "头部视频播放"}.get(businesstype, "")
  107. if source == "ad" and is_head_video_page:
  108. return {
  109. "adRequest": "头部视频页广告请求",
  110. "adLoaded": "头部视频页广告加载",
  111. "adView": "头部视频页广告曝光",
  112. "adPlay": "头部视频页广告播放",
  113. }.get(businesstype, "")
  114. if source == "simpleevent":
  115. if businesstype == "pageView" and pagesource.endswith("category_55"):
  116. return "首页分类页曝光(分类55)"
  117. if is_head_video_page:
  118. return {
  119. "pageView": "视频分享页曝光",
  120. "detailRequest": "视频分享页详情接口请求成功",
  121. "userCaptureScreen": "用户在视频分享页截图",
  122. }.get(businesstype, "")
  123. return {
  124. "userPause": "视频暂停",
  125. "userActiveEnd": "小程序进入后台",
  126. }.get(businesstype, "")
  127. return ""
  128. def main():
  129. parser = argparse.ArgumentParser(description="查询单用户离线行为时间线")
  130. parser.add_argument("user_id", help="machinecode/mid")
  131. parser.add_argument("date", help="yyyyMMdd")
  132. parser.add_argument("apptype", nargs="?", default="0")
  133. parser.add_argument("--output-dir", type=Path, default=Path("."))
  134. args = parser.parse_args()
  135. datetime.strptime(args.date, "%Y%m%d")
  136. cli = ODPSClient()
  137. sql_args = {
  138. "mc": sql_text(args.user_id),
  139. "day": args.date,
  140. "apptype": sql_text(args.apptype),
  141. }
  142. df = cli.execute_sql(SQL_ALL.format(**sql_args))
  143. df = df[~df["businesstype"].isin(EXCLUDED_BUSINESSTYPES)]
  144. df = df.sort_values("ts", kind="stable").reset_index(drop=True)
  145. df = df.rename(
  146. columns={
  147. "pagestatus": "pageStatus",
  148. "newpage": "newPage",
  149. "endroutepath": "endRoutePath",
  150. "isadplaying": "isAdPlaying",
  151. "creativecode": "creativeCode",
  152. }
  153. )
  154. df["pageStatus"] = df["pageStatus"].map(
  155. lambda value: f"{text(value)}_{PAGESTATUS_MEAN[text(value)]}" if text(value) in PAGESTATUS_MEAN else text(value)
  156. )
  157. df.insert(0, "北京时间", pd.to_datetime(df["ts"], unit="ms", utc=True).dt.tz_convert("Asia/Shanghai").dt.tz_localize(None))
  158. df = df.drop(columns="ts")
  159. df.insert(df.columns.get_loc("businesstype") + 1, "中文行为定义", df.apply(behavior_definition, axis=1))
  160. output_dir = args.output_dir.expanduser()
  161. output_dir.mkdir(parents=True, exist_ok=True)
  162. out = output_dir / f"timeline_{safe_name(args.user_id[-12:])}_{args.date}.xlsx"
  163. with pd.ExcelWriter(out, engine="openpyxl", datetime_format="yyyy/mm/dd hh:mm:ss") as writer:
  164. df.to_excel(writer, sheet_name="行为路径", index=False)
  165. for cell in writer.sheets["行为路径"]["A"][1:]:
  166. cell.number_format = "yyyy/mm/dd hh:mm:ss"
  167. counts = df["来源"].value_counts().to_dict()
  168. for source in ["video", "ad", "play", "simpleevent", "useractive"]:
  169. print(f"[ROWS] {source}={counts.get(source, 0)}", flush=True)
  170. print(f"[XLSX] 日期={args.date} 事件数={len(df)} -> {out.resolve()}", flush=True)
  171. if __name__ == "__main__":
  172. main()