| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- """全局树热度状态的加载与格式化。"""
- from __future__ import annotations
- from collections import defaultdict
- from types import SimpleNamespace
- from typing import Any
- from supply_infra.scoring.ranking import rank_with_scores
- from supply_infra.db.repositories.category_tree_weight_repo import CategoryTreeWeightRepository
- from supply_infra.db.repositories.global_tree_category_repo import GlobalTreeCategoryRepository
- from supply_infra.db.session import get_session
- def load_tree_state(biz_dt: str) -> tuple[dict[int, Any], dict[int | None, list[int]], dict[int, Any]]:
- with get_session() as session:
- categories = [
- SimpleNamespace(id=int(row.id), name=row.name, parent_id=row.parent_id, level=row.level)
- for row in GlobalTreeCategoryRepository(session).list_active_categories()
- ]
- weights = [
- SimpleNamespace(
- category_id=int(row.category_id),
- total_score=row.total_score,
- hung_word_count=row.hung_word_count,
- real_rov_7d_avg=row.real_rov_7d_avg,
- real_rov_7d_count=row.real_rov_7d_count,
- real_vov_7d_avg=row.real_vov_7d_avg,
- real_vov_7d_count=row.real_vov_7d_count,
- )
- for row in CategoryTreeWeightRepository(session).list_by_biz_dt(biz_dt)
- ]
- by_id = {row.id: row for row in categories}
- children: dict[int | None, list[int]] = defaultdict(list)
- for row in categories:
- parent_id = int(row.parent_id) if row.parent_id not in (None, 0) else None
- children[parent_id].append(row.id)
- for ids in children.values():
- ids.sort()
- return by_id, children, {row.category_id: row for row in weights}
- def format_heat_score(weight: Any | None) -> str:
- """格式化原始 total_score:只保留两位小数,无数据时明确为 null。"""
- if weight is None or weight.total_score is None:
- return "null"
- return f"{float(weight.total_score):.2f}"
- def has_hung_demand(weight: Any | None) -> bool:
- """判断分类自身是否挂有需求。"""
- return weight is not None and int(weight.hung_word_count or 0) > 0
- def format_demand_count(weight: Any | None) -> str:
- """格式化节点挂载需求数量;无需求时返回空字符串。"""
- if not has_hung_demand(weight):
- return ""
- return str(int(weight.hung_word_count or 0))
- HEAT_LEVEL_DEFINITION: dict[str, dict[str, str | float | None]] = {
- "S": {"label": "高热", "min_global_rank_score": 0.90},
- "A": {"label": "较高热", "min_global_rank_score": 0.75},
- "B": {"label": "中热", "min_global_rank_score": 0.50},
- "C": {"label": "较低热", "min_global_rank_score": 0.25},
- "D": {"label": "低热", "min_global_rank_score": 0.00},
- "U": {"label": "数据不足", "min_global_rank_score": None},
- }
- def global_heat_positions(weights: dict[int, Any]) -> dict[int, dict[str, float | int]]:
- """返回节点 ``total_score`` 在整棵有分节点中的排名位置。"""
- return rank_with_scores([
- (category_id, float(weight.total_score))
- for category_id, weight in weights.items()
- if weight is not None and weight.total_score is not None
- ])
- def heat_level(position: dict[str, float | int] | None) -> str:
- """按整树排名分映射批次热度等级;无分节点单列 U。"""
- if position is None:
- return "U"
- score = float(position["normalized_score"])
- for level in ("S", "A", "B", "C"):
- threshold = HEAT_LEVEL_DEFINITION[level]["min_global_rank_score"]
- if threshold is not None and score >= float(threshold):
- return level
- return "D"
- def format_rank(position: dict[str, float | int] | None) -> str:
- if position is None:
- return "无排名"
- rank = float(position["rank"])
- rank_text = str(int(rank)) if rank.is_integer() else f"{rank:.1f}"
- return f"{rank_text}/{int(position['total'])}"
- def path(category_id: int | None, by_id: dict[int, Any]) -> str:
- names: list[str] = []
- current = by_id.get(category_id) if category_id is not None else None
- while current is not None:
- names.append(current.name or str(current.id))
- parent_id = int(current.parent_id) if current.parent_id not in (None, 0) else None
- current = by_id.get(parent_id) if parent_id is not None else None
- return " > ".join(reversed(names))
|