growth_category_tree.py 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. """Build the growth heat map from global_category_v2 and its daily weights."""
  2. from __future__ import annotations
  3. from typing import Any
  4. from sqlalchemy import select
  5. from supply_infra.db.models.global_category_content_weight import (
  6. GlobalCategoryContentWeight,
  7. )
  8. from supply_infra.db.models.global_v2 import GlobalCategoryV2
  9. from supply_infra.db.repositories.global_category_content_weight_repo import (
  10. GlobalCategoryContentWeightRepository,
  11. )
  12. from supply_infra.db.session import get_session
  13. GROWTH_DIM_META: list[dict[str, str]] = [
  14. {"key": "read_rate", "label": "阅读率"},
  15. {"key": "avg_read_rate", "label": "平均阅读率"},
  16. {"key": "like_rate", "label": "点赞率"},
  17. ]
  18. def _normalize_parent_id(parent_id: int | None, category_ids: set[int]) -> int | None:
  19. if parent_id in (None, 0) or int(parent_id) not in category_ids:
  20. return None
  21. return int(parent_id)
  22. def _build_growth_tree(
  23. categories: list[GlobalCategoryV2],
  24. weights: list[GlobalCategoryContentWeight],
  25. ) -> list[dict[str, Any]]:
  26. category_ids = {int(category.stable_id) for category in categories}
  27. children_by_parent: dict[int | None, list[GlobalCategoryV2]] = {}
  28. for category in categories:
  29. parent_id = _normalize_parent_id(category.parent_stable_id, category_ids)
  30. children_by_parent.setdefault(parent_id, []).append(category)
  31. for children in children_by_parent.values():
  32. children.sort(key=lambda row: (row.level or 0, int(row.stable_id)))
  33. weight_by_stable_id = {int(row.stable_id): row for row in weights}
  34. def to_node(category: GlobalCategoryV2, ancestors: frozenset[int]) -> dict[str, Any]:
  35. stable_id = int(category.stable_id)
  36. weight = weight_by_stable_id.get(stable_id)
  37. count = int(weight.source_element_count or 0) if weight else 0
  38. next_ancestors = ancestors | {stable_id}
  39. children = [
  40. to_node(child, next_ancestors)
  41. for child in children_by_parent.get(stable_id, [])
  42. if int(child.stable_id) not in next_ancestors
  43. ]
  44. return {
  45. "id": stable_id,
  46. "name": category.name,
  47. "level": category.level,
  48. "description": category.description,
  49. "weights": {
  50. "read_rate": float(weight.read_rate_score) if weight else None,
  51. "avg_read_rate": float(weight.avg_read_rate_score) if weight else None,
  52. "like_rate": float(weight.like_rate_score) if weight else None,
  53. },
  54. "counts": {
  55. "read_rate": count,
  56. "avg_read_rate": count,
  57. "like_rate": count,
  58. },
  59. "children": children,
  60. }
  61. return [to_node(root, frozenset()) for root in children_by_parent.get(None, [])]
  62. def build_growth_category_tree(biz_dt: str | None = None) -> dict[str, Any]:
  63. """Return the V2 category tree with the requested/latest complete daily scores."""
  64. with get_session() as session:
  65. categories = list(
  66. session.scalars(
  67. select(GlobalCategoryV2).order_by(
  68. GlobalCategoryV2.level, GlobalCategoryV2.stable_id
  69. )
  70. ).all()
  71. )
  72. weight_repo = GlobalCategoryContentWeightRepository(session)
  73. resolved_dt = biz_dt or weight_repo.get_latest_completed_biz_dt()
  74. weights = weight_repo.list_completed_models_by_biz_dt(resolved_dt) if resolved_dt else []
  75. nodes = _build_growth_tree(categories, weights)
  76. return {
  77. "biz_dt": resolved_dt,
  78. "dims": GROWTH_DIM_META,
  79. "nodes": nodes,
  80. }