demand_priority.py 6.8 KB

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