build_report.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. #!/usr/bin/env python3
  2. """Build deterministic user and population reports from behavior and social CSVs."""
  3. from __future__ import annotations
  4. import argparse
  5. from pathlib import Path
  6. import pandas as pd
  7. GROUP_ORDER = ["明确举报人", "审核人", "疑似举报人", "正常用户"]
  8. ID_COLUMNS = {"用户分类", "子分类", "统计周期", "用户id"}
  9. TEXT_COLUMNS = {
  10. "操作系统", "机型", "地域", "来源其他场景值",
  11. "来源上游用户shareid", "来源群id",
  12. }
  13. GROUP_FEATURES = [
  14. "有效来源群数",
  15. "来源群访问UV合计",
  16. "来源群有效播放UV合计",
  17. "来源群有效播放用户率",
  18. ]
  19. def ordered_groups(detail: pd.DataFrame) -> list[str]:
  20. present = detail["用户分类"].drop_duplicates().tolist()
  21. return [group for group in GROUP_ORDER if group in present] + [
  22. group for group in present if group not in GROUP_ORDER
  23. ]
  24. def numeric_summary(detail: pd.DataFrame) -> pd.DataFrame:
  25. numeric = [column for column in detail.columns if column not in ID_COLUMNS | TEXT_COLUMNS]
  26. rows = []
  27. for group_name in ordered_groups(detail):
  28. group = detail[detail["用户分类"] == group_name]
  29. for feature in numeric:
  30. values = pd.to_numeric(group[feature], errors="coerce").fillna(0)
  31. rows.append(
  32. {
  33. "用户分类": group_name,
  34. "用户数": group["用户id"].nunique(),
  35. "特征": feature,
  36. "取值范围": f"{values.min():g}~{values.max():g}",
  37. "均值": values.mean(),
  38. "中位数": values.median(),
  39. "P90": values.quantile(0.9),
  40. "覆盖用户数": int(values.gt(0).sum()),
  41. "覆盖用户比例": values.gt(0).mean(),
  42. }
  43. )
  44. return pd.DataFrame(rows)
  45. def group_summary(detail: pd.DataFrame) -> pd.DataFrame:
  46. missing = [column for column in GROUP_FEATURES if column not in detail.columns]
  47. if missing:
  48. return pd.DataFrame(columns=["用户分类", "总人数", *GROUP_FEATURES])
  49. rows = []
  50. for group_name in ordered_groups(detail):
  51. group = detail[detail["用户分类"] == group_name].copy()
  52. for column in GROUP_FEATURES:
  53. group[column] = pd.to_numeric(group[column], errors="coerce").fillna(0)
  54. has_group = group["有效来源群数"] > 0
  55. visit_uv = group["来源群访问UV合计"].sum()
  56. play_uv = group["来源群有效播放UV合计"].sum()
  57. rows.append(
  58. {
  59. "用户分类": group_name,
  60. "总人数": group["用户id"].nunique(),
  61. "有来源群用户数": int(has_group.sum()),
  62. "有来源群用户占比": has_group.mean(),
  63. "访问来源群数合计": int(group["有效来源群数"].sum()),
  64. "来源群访问用户数合计(按群UV求和)": int(visit_uv),
  65. "来源群有效播放用户数合计(按群UV求和)": int(play_uv),
  66. "来源群有效播放用户率": play_uv / visit_uv if visit_uv else 0,
  67. "有来源群但有效播放UV为0用户数": int(
  68. (has_group & group["来源群有效播放UV合计"].eq(0)).sum()
  69. ),
  70. "有来源群但有效播放UV为0用户占比": (
  71. (has_group & group["来源群有效播放UV合计"].eq(0)).sum()
  72. / has_group.sum()
  73. if has_group.sum()
  74. else 0
  75. ),
  76. }
  77. )
  78. return pd.DataFrame(rows)
  79. def main() -> None:
  80. parser = argparse.ArgumentParser(description=__doc__)
  81. parser.add_argument("--behavior-detail", type=Path, required=True)
  82. parser.add_argument("--social-dir", type=Path)
  83. parser.add_argument("--output", type=Path, required=True)
  84. args = parser.parse_args()
  85. behavior = pd.read_csv(args.behavior_detail)
  86. detail = behavior.copy()
  87. social = group_detail = relationships = None
  88. if args.social_dir:
  89. social = pd.read_csv(args.social_dir / "user_social_features.csv")
  90. group_detail = pd.read_csv(args.social_dir / "group_detail.csv")
  91. relationships = pd.read_csv(args.social_dir / "relationship_detail.csv")
  92. detail = behavior.merge(social, on="用户id", how="left", validate="one_to_one")
  93. summary = numeric_summary(detail.copy())
  94. source_group_summary = group_summary(detail.copy())
  95. args.output.parent.mkdir(parents=True, exist_ok=True)
  96. with pd.ExcelWriter(args.output, engine="openpyxl") as writer:
  97. detail.to_excel(writer, sheet_name="用户完整明细", index=False)
  98. summary.to_excel(writer, sheet_name="人群特征聚合", index=False)
  99. source_group_summary.to_excel(writer, sheet_name="来源群聚合", index=False)
  100. if group_detail is not None:
  101. group_detail.to_excel(writer, sheet_name="来源群逐群明细", index=False)
  102. if relationships is not None:
  103. relationships.to_excel(writer, sheet_name="分享关系明细", index=False)
  104. print(f"users={len(detail)} unique_mids={detail['用户id'].nunique()} output={args.output}")
  105. if __name__ == "__main__":
  106. main()