run_first_layer_source_metrics.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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. }
  17. SOURCES = ["头部", "推荐", "全部"]
  18. SOURCE_KEYS = {"头部": "head", "推荐": "recommend", "全部": "all"}
  19. FACT_SUFFIXES = ["曝光PV", "曝光UV", "播放PV", "播放UV", "分享PV", "分享UV", "裂变层UV"]
  20. FACT_COLUMNS = ["首层UV", *[f"{source}{suffix}" for source in SOURCES for suffix in FACT_SUFFIXES]]
  21. RATE_SUFFIXES = [
  22. "曝光PV/首层UV", "播放PV/首层UV", "分享PV/首层UV",
  23. "STR(分享PV/曝光PV)", "T0裂变率(裂变层UV/首层UV)", "裂变层UV/曝光PV",
  24. ]
  25. def column_mapping():
  26. result = {
  27. "stat_date": "日期", "app_type": "产品类型", "version_code": "版本号",
  28. "bucket": "尾号", "first_layer_uv": "首层UV",
  29. }
  30. fields = {
  31. "exposure_pv": "曝光PV", "exposure_uv": "曝光UV",
  32. "play_pv": "播放PV", "play_uv": "播放UV",
  33. "share_pv": "分享PV", "share_uv": "分享UV", "fission_uv": "裂变层UV",
  34. }
  35. for source, key in SOURCE_KEYS.items():
  36. for field, label in fields.items():
  37. result[f"{key}_{field}"] = f"{source}{label}"
  38. return result
  39. def group_name(bucket):
  40. return "实验组(0、1)" if bucket in {"0", "1"} else "对照组(其余14桶)"
  41. def format_percent(value):
  42. return "" if pd.isna(value) else f"{value * 100:.4f}%"
  43. def add_rates(data):
  44. result = data.copy()
  45. first_layer_uv = result["首层UV"].replace(0, pd.NA)
  46. for source in SOURCES:
  47. exposure_pv = result[f"{source}曝光PV"].replace(0, pd.NA)
  48. result[f"{source}曝光PV/首层UV"] = result[f"{source}曝光PV"] / first_layer_uv
  49. result[f"{source}播放PV/首层UV"] = result[f"{source}播放PV"] / first_layer_uv
  50. result[f"{source}分享PV/首层UV"] = result[f"{source}分享PV"] / first_layer_uv
  51. result[f"{source}STR(分享PV/曝光PV)"] = result[f"{source}分享PV"] / exposure_pv
  52. result[f"{source}T0裂变率(裂变层UV/首层UV)"] = result[f"{source}裂变层UV"] / first_layer_uv
  53. result[f"{source}裂变层UV/曝光PV"] = result[f"{source}裂变层UV"] / exposure_pv
  54. return result
  55. def add_relative_changes(data):
  56. result = data.copy()
  57. result["首层UV相对对照组变化率"] = ""
  58. exp = result.index[(result["行类型"] == "分组聚合") & (result["分组"] == "实验组(0、1)")]
  59. ctrl = result.index[(result["行类型"] == "分组聚合") & (result["分组"] == "对照组(其余14桶)")]
  60. if len(exp) == 1 and len(ctrl) == 1:
  61. exp_mean = result.loc[exp[0], "首层UV"] / 2
  62. ctrl_mean = result.loc[ctrl[0], "首层UV"] / 14
  63. if ctrl_mean:
  64. change = format_percent(exp_mean / ctrl_mean - 1)
  65. mask = (result["分组"] == "实验组(0、1)") & result["行类型"].isin(["分组聚合", "每桶均值"])
  66. result.loc[mask, "首层UV相对对照组变化率"] = change
  67. for source in SOURCES:
  68. for suffix in RATE_SUFFIXES:
  69. metric = f"{source}{suffix}"
  70. change_column = f"{metric}相对对照组变化率"
  71. result[change_column] = ""
  72. for row_type in ["分组聚合", "每桶均值"]:
  73. exp_idx = result.index[(result["行类型"] == row_type) & (result["分组"] == "实验组(0、1)")]
  74. ctrl_idx = result.index[(result["行类型"] == row_type) & (result["分组"] == "对照组(其余14桶)")]
  75. if len(exp_idx) == 1 and len(ctrl_idx) == 1:
  76. control = result.loc[ctrl_idx[0], metric]
  77. if pd.notna(control) and control != 0:
  78. result.loc[exp_idx[0], change_column] = format_percent(result.loc[exp_idx[0], metric] / control - 1)
  79. return result
  80. def format_output(data):
  81. result = data.copy()
  82. for source in SOURCES:
  83. for suffix in ["曝光PV/首层UV", "播放PV/首层UV", "分享PV/首层UV"]:
  84. result[f"{source}{suffix}"] = result[f"{source}{suffix}"].round(4)
  85. for suffix in ["STR(分享PV/曝光PV)", "T0裂变率(裂变层UV/首层UV)", "裂变层UV/曝光PV"]:
  86. result[f"{source}{suffix}"] = result[f"{source}{suffix}"].map(format_percent)
  87. result[FACT_COLUMNS] = result[FACT_COLUMNS].round(0).astype("Int64")
  88. ordered = ["日期", "产品类型", "版本号", "行类型", "分组", "尾号", "首层UV", "首层UV相对对照组变化率"]
  89. for source in SOURCES:
  90. ordered.extend([
  91. f"{source}曝光PV", f"{source}曝光UV",
  92. f"{source}曝光PV/首层UV", f"{source}曝光PV/首层UV相对对照组变化率",
  93. f"{source}播放PV", f"{source}播放UV",
  94. f"{source}播放PV/首层UV", f"{source}播放PV/首层UV相对对照组变化率",
  95. f"{source}分享PV", f"{source}分享UV",
  96. f"{source}分享PV/首层UV", f"{source}分享PV/首层UV相对对照组变化率",
  97. f"{source}STR(分享PV/曝光PV)", f"{source}STR(分享PV/曝光PV)相对对照组变化率",
  98. f"{source}裂变层UV",
  99. f"{source}T0裂变率(裂变层UV/首层UV)", f"{source}T0裂变率(裂变层UV/首层UV)相对对照组变化率",
  100. f"{source}裂变层UV/曝光PV", f"{source}裂变层UV/曝光PV相对对照组变化率",
  101. ])
  102. return result[ordered]
  103. def build_daily(data, stat_date):
  104. daily = data[data["日期"] == stat_date].copy()
  105. daily["尾号"] = daily["尾号"].astype(str)
  106. daily["行类型"] = "尾号明细"
  107. daily["分组"] = daily["尾号"].map(group_name)
  108. detail = daily[["日期", "产品类型", "版本号", "行类型", "分组", "尾号", *FACT_COLUMNS]]
  109. aggregates, means = [], []
  110. for group, buckets in [("实验组(0、1)", {"0", "1"}), ("对照组(其余14桶)", set("23456789abcdef"))]:
  111. selected = detail[detail["尾号"].isin(buckets)]
  112. common = {"日期": stat_date, "产品类型": detail["产品类型"].iloc[0], "版本号": detail["版本号"].iloc[0], "分组": group}
  113. aggregates.append({
  114. **common, "行类型": "分组聚合", "尾号": "0、1" if len(buckets) == 2 else "其余14桶",
  115. **selected[FACT_COLUMNS].sum().to_dict(),
  116. })
  117. means.append({
  118. **common, "行类型": "每桶均值", "尾号": "2桶均值" if len(buckets) == 2 else "14桶均值",
  119. **selected[FACT_COLUMNS].mean().to_dict(),
  120. })
  121. result = pd.concat([detail, pd.DataFrame(aggregates), pd.DataFrame(means)], ignore_index=True)
  122. return format_output(add_relative_changes(add_rates(result)))
  123. def main():
  124. mode = sys.argv[1] if len(sys.argv) > 1 else "realtime"
  125. if mode not in CONFIG:
  126. raise SystemExit("usage: python3 run_first_layer_source_metrics.py [realtime|offline]")
  127. sql_file, output_file = CONFIG[mode]
  128. data = ODPSClient().execute_sql(sql_file.read_text(encoding="utf-8")).rename(columns=column_mapping())
  129. data["日期"] = data["日期"].astype(str)
  130. reports = [build_daily(data, stat_date) for stat_date in sorted(data["日期"].unique(), reverse=True)]
  131. result = pd.concat(reports, ignore_index=True)
  132. result.to_csv(output_file, index=False, encoding="utf-8-sig")
  133. print(f"[CSV] {output_file}", flush=True)
  134. summary = result[result["行类型"] == "分组聚合"][[
  135. "日期", "版本号", "分组", "首层UV", "首层UV相对对照组变化率",
  136. "全部曝光PV/首层UV", "全部播放PV/首层UV", "全部分享PV/首层UV",
  137. "全部T0裂变率(裂变层UV/首层UV)", "全部裂变层UV/曝光PV",
  138. ]]
  139. print(summary.to_string(index=False), flush=True)
  140. if __name__ == "__main__":
  141. main()