export-software-dedup-table.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import csv
  2. from collections import Counter, defaultdict
  3. from pathlib import Path
  4. from openpyxl import Workbook
  5. from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
  6. ROOT = Path(__file__).resolve().parents[1]
  7. OUTPUT_ROOT = ROOT / "outputs" / "overnight-quality-1780682025651"
  8. SOURCE = OUTPUT_ROOT / "manual-review-sample.csv"
  9. CSV_OUT = OUTPUT_ROOT / "software-client-list-dedup.csv"
  10. XLSX_OUT = OUTPUT_ROOT / "software-client-list-dedup.xlsx"
  11. SUMMARY_OUT = OUTPUT_ROOT / "software-client-list-dedup-summary.md"
  12. HEADERS = [
  13. "brief编号",
  14. "策略",
  15. "排名",
  16. "平台",
  17. "博主名称",
  18. "综合分",
  19. "brief匹配分",
  20. "参考风格分",
  21. "证据加分",
  22. "证据风险扣分",
  23. "推荐理由",
  24. "风险提示",
  25. "主页链接",
  26. "人工复核标签",
  27. ]
  28. STRATEGY_PRIORITY = {
  29. "result-first-broad": 50,
  30. "result-first": 40,
  31. "video-enhanced": 30,
  32. "result-first-risk": 20,
  33. "baseline-live": 10,
  34. }
  35. def to_num(row, key):
  36. try:
  37. return float(str(row.get(key, "")).strip() or 0)
  38. except ValueError:
  39. return 0.0
  40. def norm(text):
  41. return " ".join(str(text or "").strip().lower().split())
  42. def dedupe_key(row):
  43. link = norm(row.get("主页链接"))
  44. if link:
  45. return (norm(row.get("brief编号")), norm(row.get("平台")), link)
  46. return (norm(row.get("brief编号")), norm(row.get("平台")), norm(row.get("博主名称")))
  47. def quality_key(row):
  48. return (
  49. STRATEGY_PRIORITY.get(row.get("策略"), 0),
  50. to_num(row, "综合分"),
  51. to_num(row, "brief匹配分"),
  52. to_num(row, "参考风格分"),
  53. to_num(row, "证据加分"),
  54. -to_num(row, "证据风险扣分"),
  55. -to_num(row, "排名"),
  56. )
  57. def load_rows():
  58. with SOURCE.open("r", encoding="utf-8-sig", newline="") as handle:
  59. rows = list(csv.DictReader(handle))
  60. for row in rows:
  61. for header in HEADERS:
  62. row.setdefault(header, "")
  63. return rows
  64. def dedupe_rows(rows):
  65. kept = {}
  66. all_by_key = defaultdict(list)
  67. for row in rows:
  68. key = dedupe_key(row)
  69. all_by_key[key].append(row)
  70. if key not in kept or quality_key(row) > quality_key(kept[key]):
  71. kept[key] = dict(row)
  72. unique_rows = list(kept.values())
  73. unique_rows.sort(
  74. key=lambda row: (
  75. row.get("brief编号", ""),
  76. -to_num(row, "综合分"),
  77. -to_num(row, "brief匹配分"),
  78. -to_num(row, "参考风格分"),
  79. -to_num(row, "证据加分"),
  80. to_num(row, "证据风险扣分"),
  81. row.get("平台", ""),
  82. row.get("博主名称", ""),
  83. )
  84. )
  85. rank_by_brief = defaultdict(int)
  86. for row in unique_rows:
  87. brief = row.get("brief编号", "")
  88. rank_by_brief[brief] += 1
  89. row["排名"] = str(rank_by_brief[brief])
  90. row["人工复核标签"] = ""
  91. return unique_rows, all_by_key
  92. def write_csv(rows):
  93. with CSV_OUT.open("w", encoding="utf-8-sig", newline="") as handle:
  94. writer = csv.DictWriter(handle, fieldnames=HEADERS, extrasaction="ignore")
  95. writer.writeheader()
  96. writer.writerows(rows)
  97. def write_xlsx(rows, original_count, duplicate_key_count):
  98. wb = Workbook()
  99. ws = wb.active
  100. ws.title = "去重名单"
  101. ws.append(HEADERS)
  102. for row in rows:
  103. ws.append([row.get(header, "") for header in HEADERS])
  104. header_fill = PatternFill("solid", fgColor="1F4E78")
  105. header_font = Font(color="FFFFFF", bold=True)
  106. thin = Side(style="thin", color="D9E2F3")
  107. for cell in ws[1]:
  108. cell.fill = header_fill
  109. cell.font = header_font
  110. cell.alignment = Alignment(horizontal="center", vertical="center")
  111. cell.border = Border(bottom=thin)
  112. widths = {
  113. "A": 20,
  114. "B": 18,
  115. "C": 8,
  116. "D": 14,
  117. "E": 24,
  118. "F": 10,
  119. "G": 12,
  120. "H": 12,
  121. "I": 10,
  122. "J": 12,
  123. "K": 110,
  124. "L": 85,
  125. "M": 56,
  126. "N": 16,
  127. }
  128. for col, width in widths.items():
  129. ws.column_dimensions[col].width = width
  130. for row in ws.iter_rows(min_row=2):
  131. for cell in row:
  132. cell.alignment = Alignment(vertical="top", wrap_text=True)
  133. for idx in [3, 6, 7, 8, 9, 10]:
  134. row[idx - 1].alignment = Alignment(horizontal="center", vertical="top", wrap_text=True)
  135. ws.freeze_panes = "A2"
  136. ws.auto_filter.ref = ws.dimensions
  137. summary = wb.create_sheet("去重说明")
  138. summary_rows = [
  139. ["项目", "值"],
  140. ["来源文件", str(SOURCE)],
  141. ["原始行数", original_count],
  142. ["去重后行数", len(rows)],
  143. ["删除重复行数", original_count - len(rows)],
  144. ["存在重复的博主键数量", duplicate_key_count],
  145. ["去重口径", "同一 brief编号 + 平台 + 主页链接;没有主页链接时使用 brief编号 + 平台 + 博主名称"],
  146. ["保留规则", "优先 result-first-broad,其次 result-first、video-enhanced、result-first-risk、baseline-live;同优先级下保留综合分/brief匹配/参考风格/证据加分更高且风险扣分更低的一条"],
  147. ["排名规则", "去重后按每个 brief 重新从 1 排名"],
  148. ]
  149. for item in summary_rows:
  150. summary.append(item)
  151. summary.column_dimensions["A"].width = 22
  152. summary.column_dimensions["B"].width = 130
  153. for cell in summary[1]:
  154. cell.fill = header_fill
  155. cell.font = header_font
  156. for row in summary.iter_rows():
  157. for cell in row:
  158. cell.alignment = Alignment(vertical="top", wrap_text=True)
  159. wb.save(XLSX_OUT)
  160. def write_summary(rows, original_count, duplicate_key_count):
  161. brief_counts = Counter(row["brief编号"] for row in rows)
  162. strategy_counts = Counter(row["策略"] for row in rows)
  163. lines = [
  164. "# 软件端去重名单导出说明",
  165. "",
  166. f"- 来源:`{SOURCE}`",
  167. f"- 原始行数:{original_count}",
  168. f"- 去重后行数:{len(rows)}",
  169. f"- 删除重复行数:{original_count - len(rows)}",
  170. f"- 存在重复的博主键数量:{duplicate_key_count}",
  171. f"- CSV:`{CSV_OUT}`",
  172. f"- XLSX:`{XLSX_OUT}`",
  173. "",
  174. "## 各 brief 行数",
  175. "",
  176. ]
  177. for brief, count in sorted(brief_counts.items()):
  178. lines.append(f"- {brief}: {count}")
  179. lines.extend(["", "## 保留策略分布", ""])
  180. for strategy, count in sorted(strategy_counts.items()):
  181. lines.append(f"- {strategy}: {count}")
  182. lines.extend(
  183. [
  184. "",
  185. "## 去重与排序口径",
  186. "",
  187. "- 去重口径:同一 `brief编号 + 平台 + 主页链接` 视为同一博主;没有主页链接时用 `brief编号 + 平台 + 博主名称`。",
  188. "- 保留规则:优先发布策略 `result-first-broad`、`result-first`,再保留诊断策略;同优先级下看综合分、brief 匹配、参考风格、证据加分和风险扣分。",
  189. "- 排名规则:去重后按每个 brief 重新排序。",
  190. ]
  191. )
  192. SUMMARY_OUT.write_text("\n".join(lines), encoding="utf-8-sig")
  193. def main():
  194. rows = load_rows()
  195. unique_rows, all_by_key = dedupe_rows(rows)
  196. duplicate_key_count = sum(1 for values in all_by_key.values() if len(values) > 1)
  197. write_csv(unique_rows)
  198. write_xlsx(unique_rows, len(rows), duplicate_key_count)
  199. write_summary(unique_rows, len(rows), duplicate_key_count)
  200. print(f"rows_in={len(rows)}")
  201. print(f"rows_out={len(unique_rows)}")
  202. print(f"dropped={len(rows) - len(unique_rows)}")
  203. print(f"duplicate_keys={duplicate_key_count}")
  204. print(f"csv={CSV_OUT}")
  205. print(f"xlsx={XLSX_OUT}")
  206. print(f"summary={SUMMARY_OUT}")
  207. if __name__ == "__main__":
  208. main()