|
|
@@ -0,0 +1,141 @@
|
|
|
+"""
|
|
|
+category_tree_weight 四维热度全局排名归一化打分。
|
|
|
+
|
|
|
+在 category_tree_weight 全部节点写入完成后执行:
|
|
|
+1. 对 ext_pop / plat_sust_pop / plat_ly_pop / recent_pop 各自独立做全局排名
|
|
|
+2. 将排名映射为 [1/n, 1] 区间的归一化分(count>0 的节点参与排名,其余为 0)
|
|
|
+3. 四维分相加写入 total_score
|
|
|
+"""
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import logging
|
|
|
+from decimal import Decimal
|
|
|
+from typing import Any
|
|
|
+
|
|
|
+from supply_infra.db.repositories.category_tree_weight_repo import (
|
|
|
+ CategoryTreeWeightRepository,
|
|
|
+)
|
|
|
+from supply_infra.db.session import get_session
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+POP_DIM_KEYS: tuple[str, ...] = (
|
|
|
+ "ext_pop",
|
|
|
+ "plat_sust_pop",
|
|
|
+ "plat_ly_pop",
|
|
|
+ "recent_pop",
|
|
|
+)
|
|
|
+
|
|
|
+_UPDATE_BATCH = 500
|
|
|
+
|
|
|
+
|
|
|
+def _dec(value: float, places: int = 8) -> Decimal:
|
|
|
+ return Decimal(str(round(float(value), places)))
|
|
|
+
|
|
|
+
|
|
|
+def rank_to_scores(items: list[tuple[int, float]]) -> dict[int, float]:
|
|
|
+ """
|
|
|
+ 按 avg 降序做全局排名,映射为归一化分。
|
|
|
+
|
|
|
+ items: [(category_id, avg), ...],仅含 count>0 的节点。
|
|
|
+ 同分节点取平均名次;score = (n - avg_rank + 1) / n,范围 (0, 1]。
|
|
|
+ """
|
|
|
+ if not items:
|
|
|
+ return {}
|
|
|
+
|
|
|
+ sorted_items = sorted(items, key=lambda x: (-x[1], x[0]))
|
|
|
+ n = len(sorted_items)
|
|
|
+ scores: dict[int, float] = {}
|
|
|
+ i = 0
|
|
|
+ while i < n:
|
|
|
+ j = i
|
|
|
+ avg = sorted_items[i][1]
|
|
|
+ while j < n and sorted_items[j][1] == avg:
|
|
|
+ j += 1
|
|
|
+ avg_rank = (i + 1 + j) / 2.0
|
|
|
+ score = (n - avg_rank + 1) / n
|
|
|
+ for k in range(i, j):
|
|
|
+ scores[sorted_items[k][0]] = score
|
|
|
+ i = j
|
|
|
+ return scores
|
|
|
+
|
|
|
+
|
|
|
+def _materialize_weight_row(row: Any) -> dict[str, Any]:
|
|
|
+ """在 session 内抽出标量,避免 DetachedInstanceError。"""
|
|
|
+ payload: dict[str, Any] = {
|
|
|
+ "category_id": int(row.category_id),
|
|
|
+ "biz_dt": str(row.biz_dt),
|
|
|
+ }
|
|
|
+ for dim in POP_DIM_KEYS:
|
|
|
+ payload[f"{dim}_count"] = int(getattr(row, f"{dim}_count", 0) or 0)
|
|
|
+ payload[f"{dim}_avg"] = float(getattr(row, f"{dim}_avg", 0) or 0)
|
|
|
+ return payload
|
|
|
+
|
|
|
+
|
|
|
+def _build_rank_score_rows(
|
|
|
+ rows: list[dict[str, Any]],
|
|
|
+) -> list[dict[str, Any]]:
|
|
|
+ """根据已写入的权重行计算各节点排名分。"""
|
|
|
+ dim_scores: dict[str, dict[int, float]] = {}
|
|
|
+ for dim in POP_DIM_KEYS:
|
|
|
+ candidates: list[tuple[int, float]] = []
|
|
|
+ for row in rows:
|
|
|
+ count = int(row[f"{dim}_count"])
|
|
|
+ if count <= 0:
|
|
|
+ continue
|
|
|
+ avg = float(row[f"{dim}_avg"])
|
|
|
+ candidates.append((int(row["category_id"]), avg))
|
|
|
+ dim_scores[dim] = rank_to_scores(candidates)
|
|
|
+
|
|
|
+ updates: list[dict[str, Any]] = []
|
|
|
+ for row in rows:
|
|
|
+ category_id = int(row["category_id"])
|
|
|
+ biz_dt = str(row["biz_dt"])
|
|
|
+ score_values = {
|
|
|
+ f"{dim}_score": dim_scores[dim].get(category_id, 0.0)
|
|
|
+ for dim in POP_DIM_KEYS
|
|
|
+ }
|
|
|
+ total = sum(score_values.values())
|
|
|
+ updates.append(
|
|
|
+ {
|
|
|
+ "category_id": category_id,
|
|
|
+ "biz_dt": biz_dt,
|
|
|
+ **{col: _dec(score_values[col]) for col in (
|
|
|
+ "ext_pop_score",
|
|
|
+ "plat_sust_pop_score",
|
|
|
+ "plat_ly_pop_score",
|
|
|
+ "recent_pop_score",
|
|
|
+ )},
|
|
|
+ "total_score": _dec(total),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return updates
|
|
|
+
|
|
|
+
|
|
|
+def update_category_tree_rank_scores(biz_dt: str) -> dict[str, Any]:
|
|
|
+ """在 category_tree_weight 全部写入后,按 biz_dt 更新四维排名分与 total_score。"""
|
|
|
+ with get_session() as session:
|
|
|
+ repo = CategoryTreeWeightRepository(session)
|
|
|
+ rows = [
|
|
|
+ _materialize_weight_row(row)
|
|
|
+ for row in repo.list_by_biz_dt(biz_dt)
|
|
|
+ ]
|
|
|
+
|
|
|
+ if not rows:
|
|
|
+ logger.info("Category tree rank scores: no rows, skip biz_dt=%s", biz_dt)
|
|
|
+ return {"biz_dt": biz_dt, "nodes": 0, "updated": 0}
|
|
|
+
|
|
|
+ updates = _build_rank_score_rows(rows)
|
|
|
+ updated = 0
|
|
|
+ with get_session() as session:
|
|
|
+ repo = CategoryTreeWeightRepository(session)
|
|
|
+ for i in range(0, len(updates), _UPDATE_BATCH):
|
|
|
+ updated += repo.update_rank_scores(updates[i : i + _UPDATE_BATCH])
|
|
|
+
|
|
|
+ result = {
|
|
|
+ "biz_dt": biz_dt,
|
|
|
+ "nodes": len(updates),
|
|
|
+ "updated": updated,
|
|
|
+ }
|
|
|
+ logger.info("Category tree rank scores completed: %s", result)
|
|
|
+ return result
|