run_first_layer_source_metrics.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. #!/usr/bin/env python3
  2. from pathlib import Path
  3. import sys
  4. import pandas as pd
  5. from odps_module import ODPSClient
  6. BASE = Path(__file__).resolve().parent
  7. CONFIG = {
  8. "realtime": (
  9. BASE / "first_layer_source_metrics_realtime_20260721_apptype4_version1578.sql",
  10. BASE / "first_layer_source_metrics_realtime_20260721_apptype4_version1578.csv",
  11. ),
  12. "offline": (
  13. BASE / "first_layer_source_metrics_offline_20260716_20260720_apptype4_all_versions.sql",
  14. BASE / "first_layer_source_metrics_offline_20260716_20260720_apptype4_all_versions.csv",
  15. ),
  16. "offline_20260611_1507": (
  17. BASE / "first_layer_source_metrics_offline_20260611_apptype0_version1507.sql",
  18. BASE / "first_layer_source_metrics_offline_20260611_apptype0_version1507.csv",
  19. ),
  20. "offline_20260717_20260720_special_layer": (
  21. BASE / "first_layer_source_metrics_offline_20260717_20260720_apptype4_all_versions_no_qywx_filter_special_layer.sql",
  22. BASE / "first_layer_source_metrics_offline_20260717_20260720_apptype4_all_versions_no_qywx_filter_special_layer.csv",
  23. ),
  24. "offline_20260721_1578_special_layer": (
  25. BASE / "first_layer_source_metrics_offline_20260721_apptype4_version1578_no_qywx_filter_special_layer.sql",
  26. BASE / "first_layer_source_metrics_offline_20260721_apptype4_version1578_no_qywx_filter_special_layer.csv",
  27. ),
  28. "realtime_20260722_special_layer": (
  29. BASE / "first_layer_source_metrics_realtime_20260722_apptype4_version1578_no_qywx_filter_special_layer.sql",
  30. BASE / "first_layer_source_metrics_realtime_20260722_apptype4_version1578_no_qywx_filter_special_layer.csv",
  31. ),
  32. "offline_20260716_20260721_20260722_all_special_layer": (
  33. BASE / "first_layer_source_metrics_offline_20260716_20260721_20260722_apptype4_all_versions_no_qywx_filter_special_layer.sql",
  34. BASE / "first_layer_source_metrics_offline_20260716_20260721_20260722_apptype4_all_versions_no_qywx_filter_special_layer.csv",
  35. ),
  36. "realtime_20260723_all_special_layer": (
  37. BASE / "first_layer_source_metrics_realtime_20260723_apptype4_all_versions_no_qywx_filter_special_layer.sql",
  38. BASE / "first_layer_source_metrics_realtime_20260723_apptype4_all_versions_no_qywx_filter_special_layer.csv",
  39. ),
  40. }
  41. SOURCES = ["头部", "推荐", "全部"]
  42. SOURCE_KEYS = {"头部": "head", "推荐": "recommend", "全部": "all"}
  43. FACT_SUFFIXES = ["曝光PV", "曝光UV", "播放PV", "播放UV", "分享PV", "分享UV", "裂变层UV"]
  44. FACT_COLUMNS = ["首层UV", *[f"{source}{suffix}" for source in SOURCES for suffix in FACT_SUFFIXES]]
  45. RATE_SUFFIXES = [
  46. "曝光PV/首层UV", "播放PV/首层UV", "分享PV/首层UV",
  47. "STR(分享PV/曝光PV)", "T0裂变率(裂变层UV/首层UV)", "裂变层UV/曝光PV",
  48. ]
  49. SPECIAL_LAYER_OUTPUTS = [
  50. CONFIG["realtime_20260722_special_layer"][1],
  51. CONFIG["offline_20260721_1578_special_layer"][1],
  52. CONFIG["offline_20260717_20260720_special_layer"][1],
  53. ]
  54. SPECIAL_LAYER_COMBINED = BASE / "first_layer_source_metrics_20260717_20260720_offline_all_20260721_offline_1578_20260722_realtime_1578_no_qywx_filter_special_layer.csv"
  55. SPECIAL_LAYER_DAILY = BASE / "first_layer_source_metrics_daily_compare_20260717_20260722_no_qywx_filter_special_layer.csv"
  56. ALL_VERSION_SPECIAL_LAYER_OUTPUTS = [
  57. CONFIG["realtime_20260723_all_special_layer"][1],
  58. CONFIG["offline_20260716_20260721_20260722_all_special_layer"][1],
  59. CONFIG["offline_20260717_20260720_special_layer"][1],
  60. ]
  61. ALL_VERSION_SPECIAL_LAYER_COMBINED = BASE / "first_layer_source_metrics_20260716_20260722_offline_20260723_realtime_apptype4_all_versions_no_qywx_filter_special_layer.csv"
  62. ALL_VERSION_SPECIAL_LAYER_DAILY = BASE / "first_layer_source_metrics_daily_compare_20260716_20260723_apptype4_all_versions_no_qywx_filter_special_layer.csv"
  63. def column_mapping():
  64. result = {
  65. "stat_date": "日期", "app_type": "产品类型", "version_code": "版本号",
  66. "bucket": "尾号", "first_layer_uv": "首层UV",
  67. }
  68. fields = {
  69. "exposure_pv": "曝光PV", "exposure_uv": "曝光UV",
  70. "play_pv": "播放PV", "play_uv": "播放UV",
  71. "share_pv": "分享PV", "share_uv": "分享UV", "fission_uv": "裂变层UV",
  72. }
  73. for source, key in SOURCE_KEYS.items():
  74. for field, label in fields.items():
  75. result[f"{key}_{field}"] = f"{source}{label}"
  76. return result
  77. def group_name(bucket):
  78. return "实验组(0、1)" if bucket in {"0", "1"} else "对照组(其余14桶)"
  79. def format_percent(value):
  80. return "" if pd.isna(value) else f"{value * 100:.4f}%"
  81. def add_rates(data):
  82. result = data.copy()
  83. first_layer_uv = result["首层UV"].replace(0, pd.NA)
  84. for source in SOURCES:
  85. exposure_pv = result[f"{source}曝光PV"].replace(0, pd.NA)
  86. result[f"{source}曝光PV/首层UV"] = result[f"{source}曝光PV"] / first_layer_uv
  87. result[f"{source}播放PV/首层UV"] = result[f"{source}播放PV"] / first_layer_uv
  88. result[f"{source}分享PV/首层UV"] = result[f"{source}分享PV"] / first_layer_uv
  89. result[f"{source}STR(分享PV/曝光PV)"] = result[f"{source}分享PV"] / exposure_pv
  90. result[f"{source}T0裂变率(裂变层UV/首层UV)"] = result[f"{source}裂变层UV"] / first_layer_uv
  91. result[f"{source}裂变层UV/曝光PV"] = result[f"{source}裂变层UV"] / exposure_pv
  92. return result
  93. def add_relative_changes(data):
  94. result = data.copy()
  95. result["首层UV相对对照组变化率"] = ""
  96. exp = result.index[(result["行类型"] == "分组聚合") & (result["分组"] == "实验组(0、1)")]
  97. ctrl = result.index[(result["行类型"] == "分组聚合") & (result["分组"] == "对照组(其余14桶)")]
  98. if len(exp) == 1 and len(ctrl) == 1:
  99. exp_mean = result.loc[exp[0], "首层UV"] / 2
  100. ctrl_mean = result.loc[ctrl[0], "首层UV"] / 14
  101. if ctrl_mean:
  102. change = format_percent(exp_mean / ctrl_mean - 1)
  103. mask = (result["分组"] == "实验组(0、1)") & result["行类型"].isin(["分组聚合", "每桶均值"])
  104. result.loc[mask, "首层UV相对对照组变化率"] = change
  105. for source in SOURCES:
  106. for suffix in RATE_SUFFIXES:
  107. metric = f"{source}{suffix}"
  108. change_column = f"{metric}相对对照组变化率"
  109. result[change_column] = ""
  110. for row_type in ["分组聚合", "每桶均值"]:
  111. exp_idx = result.index[(result["行类型"] == row_type) & (result["分组"] == "实验组(0、1)")]
  112. ctrl_idx = result.index[(result["行类型"] == row_type) & (result["分组"] == "对照组(其余14桶)")]
  113. if len(exp_idx) == 1 and len(ctrl_idx) == 1:
  114. control = result.loc[ctrl_idx[0], metric]
  115. if pd.notna(control) and control != 0:
  116. result.loc[exp_idx[0], change_column] = format_percent(result.loc[exp_idx[0], metric] / control - 1)
  117. return result
  118. def format_output(data):
  119. result = data.copy()
  120. for source in SOURCES:
  121. for suffix in ["曝光PV/首层UV", "播放PV/首层UV", "分享PV/首层UV"]:
  122. result[f"{source}{suffix}"] = result[f"{source}{suffix}"].round(4)
  123. for suffix in ["STR(分享PV/曝光PV)", "T0裂变率(裂变层UV/首层UV)", "裂变层UV/曝光PV"]:
  124. result[f"{source}{suffix}"] = result[f"{source}{suffix}"].map(format_percent)
  125. result[FACT_COLUMNS] = result[FACT_COLUMNS].round(0).astype("Int64")
  126. ordered = ["日期", "产品类型", "版本号", "行类型", "分组", "尾号", "首层UV", "首层UV相对对照组变化率"]
  127. for source in SOURCES:
  128. ordered.extend([
  129. f"{source}曝光PV", f"{source}曝光UV",
  130. f"{source}曝光PV/首层UV", f"{source}曝光PV/首层UV相对对照组变化率",
  131. f"{source}播放PV", f"{source}播放UV",
  132. f"{source}播放PV/首层UV", f"{source}播放PV/首层UV相对对照组变化率",
  133. f"{source}分享PV", f"{source}分享UV",
  134. f"{source}分享PV/首层UV", f"{source}分享PV/首层UV相对对照组变化率",
  135. f"{source}STR(分享PV/曝光PV)", f"{source}STR(分享PV/曝光PV)相对对照组变化率",
  136. f"{source}裂变层UV",
  137. f"{source}T0裂变率(裂变层UV/首层UV)", f"{source}T0裂变率(裂变层UV/首层UV)相对对照组变化率",
  138. f"{source}裂变层UV/曝光PV", f"{source}裂变层UV/曝光PV相对对照组变化率",
  139. ])
  140. return result[ordered]
  141. def build_daily(data, stat_date):
  142. daily = data[data["日期"] == stat_date].copy()
  143. daily["尾号"] = daily["尾号"].astype(str)
  144. daily["行类型"] = "尾号明细"
  145. daily["分组"] = daily["尾号"].map(group_name)
  146. detail = daily[["日期", "产品类型", "版本号", "行类型", "分组", "尾号", *FACT_COLUMNS]]
  147. aggregates, means = [], []
  148. for group, buckets in [("实验组(0、1)", {"0", "1"}), ("对照组(其余14桶)", set("23456789abcdef"))]:
  149. selected = detail[detail["尾号"].isin(buckets)]
  150. common = {"日期": stat_date, "产品类型": detail["产品类型"].iloc[0], "版本号": detail["版本号"].iloc[0], "分组": group}
  151. aggregates.append({
  152. **common, "行类型": "分组聚合", "尾号": "0、1" if len(buckets) == 2 else "其余14桶",
  153. **selected[FACT_COLUMNS].sum().to_dict(),
  154. })
  155. means.append({
  156. **common, "行类型": "每桶均值", "尾号": "2桶均值" if len(buckets) == 2 else "14桶均值",
  157. **selected[FACT_COLUMNS].mean().to_dict(),
  158. })
  159. result = pd.concat([detail, pd.DataFrame(aggregates), pd.DataFrame(means)], ignore_index=True)
  160. return format_output(add_relative_changes(add_rates(result)))
  161. def main():
  162. mode = sys.argv[1] if len(sys.argv) > 1 else "realtime"
  163. if mode == "combine_all_version_special_layer":
  164. result = pd.concat([pd.read_csv(path, dtype=str) for path in ALL_VERSION_SPECIAL_LAYER_OUTPUTS], ignore_index=True)
  165. result = result.sort_values("日期", ascending=False, kind="stable").reset_index(drop=True)
  166. if len(result) != 160 or result["日期"].nunique() != 8:
  167. raise ValueError(f"合并结果异常:rows={len(result)}, dates={result['日期'].nunique()}")
  168. result.to_csv(ALL_VERSION_SPECIAL_LAYER_COMBINED, index=False, encoding="utf-8-sig")
  169. daily = result[result["行类型"] == "分组聚合"].copy()
  170. daily.to_csv(ALL_VERSION_SPECIAL_LAYER_DAILY, index=False, encoding="utf-8-sig")
  171. print(f"[CSV] {ALL_VERSION_SPECIAL_LAYER_COMBINED} ({len(result)} rows)")
  172. print(f"[CSV] {ALL_VERSION_SPECIAL_LAYER_DAILY} ({len(daily)} rows)")
  173. return
  174. if mode == "combine_special_layer":
  175. result = pd.concat([pd.read_csv(path, dtype=str) for path in SPECIAL_LAYER_OUTPUTS], ignore_index=True)
  176. result.to_csv(SPECIAL_LAYER_COMBINED, index=False, encoding="utf-8-sig")
  177. daily = result[result["行类型"] == "分组聚合"].copy()
  178. daily.to_csv(SPECIAL_LAYER_DAILY, index=False, encoding="utf-8-sig")
  179. print(f"[CSV] {SPECIAL_LAYER_COMBINED} ({len(result)} rows)")
  180. print(f"[CSV] {SPECIAL_LAYER_DAILY} ({len(daily)} rows)")
  181. return
  182. if mode not in CONFIG:
  183. raise SystemExit(f"usage: python3 run_first_layer_source_metrics.py [{'|'.join(CONFIG)}|combine_special_layer|combine_all_version_special_layer]")
  184. sql_file, output_file = CONFIG[mode]
  185. data = ODPSClient().execute_sql(sql_file.read_text(encoding="utf-8")).rename(columns=column_mapping())
  186. data["日期"] = data["日期"].astype(str)
  187. reports = [build_daily(data, stat_date) for stat_date in sorted(data["日期"].unique(), reverse=True)]
  188. result = pd.concat(reports, ignore_index=True)
  189. result.to_csv(output_file, index=False, encoding="utf-8-sig")
  190. print(f"[CSV] {output_file}", flush=True)
  191. summary = result[result["行类型"] == "分组聚合"][[
  192. "日期", "版本号", "分组", "首层UV", "首层UV相对对照组变化率",
  193. "全部曝光PV/首层UV", "全部播放PV/首层UV", "全部分享PV/首层UV",
  194. "全部T0裂变率(裂变层UV/首层UV)", "全部裂变层UV/曝光PV",
  195. ]]
  196. print(summary.to_string(index=False), flush=True)
  197. if __name__ == "__main__":
  198. main()