demand_priority.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. """需求自身先验分:来源内排名归一化后再形成可比分。"""
  2. from __future__ import annotations
  3. from collections import defaultdict
  4. from typing import Any
  5. from supply_infra.scoring.posterior import format_posterior_value
  6. from supply_infra.scoring.ranking import rank_with_scores
  7. DEMAND_PRIORITY_SCORE_METHOD = {
  8. "name": "source_rank_mean_v1",
  9. "range": "0-100",
  10. "steps": [
  11. "同一需求在同一来源内的非零 weight 先取平均",
  12. "每个来源内部独立按 weight 降序排名并归一化到 (0,1]",
  13. "同一需求已有来源的归一分等权平均后乘 100",
  14. ],
  15. "missing_source_policy": "未出现的来源不补零",
  16. "warning": "不同来源的原始 weight 不可直接相加;本分也不可与分类节点 total_score 直接相加",
  17. }
  18. def _average(values: list[float]) -> float | None:
  19. return sum(values) / len(values) if values else None
  20. def build_demand_priority_index(pool_rows: list[Any]) -> dict[str, dict[str, Any]]:
  21. """基于一天的完整需求池,构建需求自身的来源可比先验分。"""
  22. weights: dict[tuple[str, str], list[float]] = defaultdict(list)
  23. rows_by_name: dict[str, list[Any]] = defaultdict(list)
  24. observed_strategies: dict[str, set[str]] = defaultdict(set)
  25. for row in pool_rows:
  26. demand_name = str(getattr(row, "demand_name", "") or "").strip()
  27. strategy = str(getattr(row, "strategy", "") or "").strip()
  28. if not demand_name:
  29. continue
  30. rows_by_name[demand_name].append(row)
  31. if strategy:
  32. observed_strategies[demand_name].add(strategy)
  33. raw_weight = getattr(row, "weight", None)
  34. if strategy and raw_weight is not None and float(raw_weight) != 0:
  35. weights[(strategy, demand_name)].append(float(raw_weight))
  36. source_averages = {
  37. key: float(_average(values))
  38. for key, values in weights.items()
  39. if values
  40. }
  41. candidates_by_source: dict[str, list[tuple[str, float]]] = defaultdict(list)
  42. for (strategy, demand_name), avg in source_averages.items():
  43. candidates_by_source[strategy].append((demand_name, avg))
  44. positions_by_source = {
  45. strategy: rank_with_scores(candidates)
  46. for strategy, candidates in candidates_by_source.items()
  47. }
  48. index: dict[str, dict[str, Any]] = {}
  49. for demand_name, demand_rows in rows_by_name.items():
  50. sources: list[dict[str, Any]] = []
  51. for strategy in sorted(observed_strategies[demand_name]):
  52. avg = source_averages.get((strategy, demand_name))
  53. position = positions_by_source.get(strategy, {}).get(demand_name)
  54. source_row_count = sum(
  55. 1
  56. for row in demand_rows
  57. if str(getattr(row, "strategy", "") or "").strip() == strategy
  58. )
  59. sources.append({
  60. "strategy": strategy,
  61. "raw_weight_avg": avg,
  62. "raw_weight_row_count": source_row_count if avg is not None else 0,
  63. "rank_in_source": float(position["rank"]) if position is not None else None,
  64. "source_demand_count": int(position["total"]) if position is not None else 0,
  65. "source_normalized_rank_score": (
  66. float(position["normalized_score"]) if position is not None else None
  67. ),
  68. })
  69. normalized_scores = [
  70. float(item["source_normalized_rank_score"])
  71. for item in sources
  72. if item["source_normalized_rank_score"] is not None
  73. ]
  74. prior_score = (
  75. 100 * sum(normalized_scores) / len(normalized_scores)
  76. if normalized_scores
  77. else None
  78. )
  79. rov_values = [
  80. float(row.real_rov_7d)
  81. for row in demand_rows
  82. if getattr(row, "real_rov_7d", None) is not None
  83. ]
  84. vov_values = [
  85. float(row.real_vov_7d)
  86. for row in demand_rows
  87. if getattr(row, "real_vov_7d", None) is not None
  88. ]
  89. index[demand_name] = {
  90. "demand_name": demand_name,
  91. "source_rank_score": round(prior_score, 4) if prior_score is not None else None,
  92. "valid_source_count": len(normalized_scores),
  93. "observed_source_count": len(sources),
  94. "sources": sources,
  95. "posterior_from_exact_pool_rows": {
  96. "rov_diff": {
  97. "value": max(rov_values) if rov_values else None,
  98. "has_data": bool(rov_values),
  99. },
  100. "vov_diff": {
  101. "value": max(vov_values) if vov_values else None,
  102. "has_data": bool(vov_values),
  103. },
  104. },
  105. "exact_pool_ids": sorted({int(row.id) for row in demand_rows}),
  106. }
  107. demand_positions = rank_with_scores([
  108. (demand_name, float(item["source_rank_score"]))
  109. for demand_name, item in index.items()
  110. if item["source_rank_score"] is not None
  111. ])
  112. for demand_name, item in index.items():
  113. position = demand_positions.get(demand_name)
  114. item["global_demand_rank"] = float(position["rank"]) if position is not None else None
  115. item["global_scored_demand_count"] = int(position["total"]) if position is not None else 0
  116. item["score_method"] = DEMAND_PRIORITY_SCORE_METHOD["name"]
  117. return index
  118. def format_demand_priority(item: dict[str, Any] | None) -> list[str]:
  119. """将需求自身排名证据格式化为 Agent 易读文本。"""
  120. if item is None:
  121. return ["需求自身来源归一分=—(无需求池记录)"]
  122. score = item.get("source_rank_score")
  123. rank = item.get("global_demand_rank")
  124. total = int(item.get("global_scored_demand_count") or 0)
  125. score_text = "—" if score is None else f"{float(score):.2f}/100"
  126. rank_text = "无排名" if rank is None else f"{float(rank):g}/{total}"
  127. lines = [
  128. f"需求自身来源归一分={score_text};全日需求自身排名={rank_text};"
  129. f"有效来源={int(item.get('valid_source_count') or 0)}",
  130. ]
  131. for source in item.get("sources") or []:
  132. raw = source.get("raw_weight_avg")
  133. source_rank = source.get("rank_in_source")
  134. normalized = source.get("source_normalized_rank_score")
  135. raw_text = "—" if raw is None else f"{float(raw):.4f}"
  136. source_rank_text = (
  137. "—"
  138. if source_rank is None
  139. else f"{float(source_rank):g}/{source['source_demand_count']}"
  140. )
  141. normalized_text = "—" if normalized is None else f"{float(normalized):.4f}"
  142. lines.append(
  143. f" 来源={source['strategy']} raw_weight_avg={raw_text} "
  144. f"来源内名次={source_rank_text} 来源归一分={normalized_text}"
  145. )
  146. lines.append(" 口径:来源内先排名归一化,再对已有来源取均值;禁止直接相加原始 weight。")
  147. posterior = item.get("posterior_from_exact_pool_rows") or {}
  148. rov = posterior.get("rov_diff") or {}
  149. vov = posterior.get("vov_diff") or {}
  150. if rov.get("has_data") or vov.get("has_data"):
  151. lines.append(
  152. " 词级后验:"
  153. f"rov_diff={format_posterior_value(rov.get('value'))} "
  154. f"vov_diff={format_posterior_value(vov.get('value'))}"
  155. )
  156. else:
  157. lines.append(" 词级后验:无验证数据(效果未知)")
  158. return lines