|
@@ -1,5 +1,5 @@
|
|
|
"""
|
|
"""
|
|
|
-定时任务:从 ODPS 同步策略需求天级表到 MySQL,并对新词做归属分类。
|
|
|
|
|
|
|
+定时任务:从 ODPS 同步策略需求天级表到 MySQL,并对新词做归属分类与热度统计。
|
|
|
|
|
|
|
|
流程:
|
|
流程:
|
|
|
1. 比对当天 ODPS / MySQL 行数,相同则跳过写入
|
|
1. 比对当天 ODPS / MySQL 行数,相同则跳过写入
|
|
@@ -7,17 +7,22 @@
|
|
|
3. 查询当天全部 demand_name,按空格分词写入 set
|
|
3. 查询当天全部 demand_name,按空格分词写入 set
|
|
|
4. 过滤 demand_belong_category 中已存在的词
|
|
4. 过滤 demand_belong_category 中已存在的词
|
|
|
5. 剩余词按 100 词一批调用 demand_belong_category_agent
|
|
5. 剩余词按 100 词一批调用 demand_belong_category_agent
|
|
|
|
|
+6. 遍历 demand_belong_category 全部词,按策略统计热度写入 demand_popularity_stats
|
|
|
"""
|
|
"""
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import logging
|
|
import logging
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
|
|
+from decimal import Decimal
|
|
|
from typing import Any
|
|
from typing import Any
|
|
|
|
|
|
|
|
from agents.demand_belong_category_agent.run import main as classify_demand_words
|
|
from agents.demand_belong_category_agent.run import main as classify_demand_words
|
|
|
from supply_infra.db.repositories.demand_belong_category_repo import (
|
|
from supply_infra.db.repositories.demand_belong_category_repo import (
|
|
|
DemandBelongCategoryRepository,
|
|
DemandBelongCategoryRepository,
|
|
|
)
|
|
)
|
|
|
|
|
+from supply_infra.db.repositories.demand_popularity_stats_repo import (
|
|
|
|
|
+ DemandPopularityStatsRepository,
|
|
|
|
|
+)
|
|
|
from supply_infra.db.repositories.multi_demand_pool_di_repo import MultiDemandPoolDiRepository
|
|
from supply_infra.db.repositories.multi_demand_pool_di_repo import MultiDemandPoolDiRepository
|
|
|
from supply_infra.db.session import get_session
|
|
from supply_infra.db.session import get_session
|
|
|
from supply_infra.odps.client import get_odps_client
|
|
from supply_infra.odps.client import get_odps_client
|
|
@@ -25,6 +30,18 @@ from supply_infra.odps.client import get_odps_client
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
_WORD_BATCH_SIZE = 100
|
|
_WORD_BATCH_SIZE = 100
|
|
|
|
|
+_STATS_UPSERT_BATCH = 200
|
|
|
|
|
+
|
|
|
|
|
+# 策略名 → 统计字段前缀;去年同期阳历/阴历合并为 plat_ly_pop
|
|
|
|
|
+_STRATEGY_METRIC: dict[str, str] = {
|
|
|
|
|
+ "新热事件": "ext_pop",
|
|
|
|
|
+ "逐月": "plat_sust_pop",
|
|
|
|
|
+ "去年同期阳历": "plat_ly_pop",
|
|
|
|
|
+ "去年同期阴历": "plat_ly_pop",
|
|
|
|
|
+ "当下供需gap": "recent_pop",
|
|
|
|
|
+}
|
|
|
|
|
+_TARGET_STRATEGIES = list(_STRATEGY_METRIC.keys())
|
|
|
|
|
+_METRIC_KEYS = ("ext_pop", "plat_sust_pop", "plat_ly_pop", "recent_pop")
|
|
|
|
|
|
|
|
RowKey = tuple[str, str]
|
|
RowKey = tuple[str, str]
|
|
|
|
|
|
|
@@ -143,9 +160,112 @@ def _sync_diff(partition_date: str) -> dict[str, Any]:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _calc_metric_stats(weights: list[float]) -> tuple[Decimal, Decimal, int]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 计算单策略 max / avg / count。
|
|
|
|
|
+ 权重为 0 不参与平均值;全部为 0(或无有效权重)时 avg 默认为 0。
|
|
|
|
|
+ """
|
|
|
|
|
+ count = len(weights)
|
|
|
|
|
+ if count == 0:
|
|
|
|
|
+ return Decimal("0.00"), Decimal("0.00"), 0
|
|
|
|
|
+
|
|
|
|
|
+ max_val = max(weights)
|
|
|
|
|
+ nonzero = [w for w in weights if w != 0]
|
|
|
|
|
+ avg_val = sum(nonzero) / len(nonzero) if nonzero else 0.0
|
|
|
|
|
+ return (
|
|
|
|
|
+ Decimal(str(round(max_val, 2))),
|
|
|
|
|
+ Decimal(str(round(avg_val, 2))),
|
|
|
|
|
+ count,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _empty_metric_stats() -> dict[str, Decimal | int]:
|
|
|
|
|
+ result: dict[str, Decimal | int] = {}
|
|
|
|
|
+ for key in _METRIC_KEYS:
|
|
|
|
|
+ result[f"{key}_max"] = Decimal("0.00")
|
|
|
|
|
+ result[f"{key}_avg"] = Decimal("0.00")
|
|
|
|
|
+ result[f"{key}_count"] = 0
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _build_stats_row(
|
|
|
|
|
+ demand_category_id: int,
|
|
|
|
|
+ demand_word_name: str,
|
|
|
|
|
+ biz_dt: str,
|
|
|
|
|
+ strategy_weights: list[tuple[str, float | None]],
|
|
|
|
|
+) -> dict[str, Any]:
|
|
|
|
|
+ """按策略分组后汇总为 demand_popularity_stats 一行。"""
|
|
|
|
|
+ grouped: dict[str, list[float]] = {key: [] for key in _METRIC_KEYS}
|
|
|
|
|
+ for strategy, weight in strategy_weights:
|
|
|
|
|
+ metric = _STRATEGY_METRIC.get(strategy)
|
|
|
|
|
+ if metric is None or weight is None:
|
|
|
|
|
+ continue
|
|
|
|
|
+ grouped[metric].append(float(weight))
|
|
|
|
|
+
|
|
|
|
|
+ row: dict[str, Any] = {
|
|
|
|
|
+ "demand_category_id": demand_category_id,
|
|
|
|
|
+ "demand_word_name": demand_word_name[:128],
|
|
|
|
|
+ "biz_dt": biz_dt,
|
|
|
|
|
+ **_empty_metric_stats(),
|
|
|
|
|
+ }
|
|
|
|
|
+ for key in _METRIC_KEYS:
|
|
|
|
|
+ max_val, avg_val, count = _calc_metric_stats(grouped[key])
|
|
|
|
|
+ row[f"{key}_max"] = max_val
|
|
|
|
|
+ row[f"{key}_avg"] = avg_val
|
|
|
|
|
+ row[f"{key}_count"] = count
|
|
|
|
|
+ return row
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def compute_popularity_stats(biz_dt: str) -> dict[str, Any]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 遍历 demand_belong_category 全部词,各自 LIKE 查询当天指定策略权重,
|
|
|
|
|
+ 分策略统计后写入 demand_popularity_stats。
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ biz_dt: 业务日期 (YYYYMMDD)
|
|
|
|
|
+ """
|
|
|
|
|
+ with get_session() as session:
|
|
|
|
|
+ categories = DemandBelongCategoryRepository(session).list_active_id_name()
|
|
|
|
|
+
|
|
|
|
|
+ if not categories:
|
|
|
|
|
+ logger.info("Popularity stats: no demand_belong_category rows, skip")
|
|
|
|
|
+ return {"words": 0, "upserted": 0}
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("Popularity stats: processing %d words for biz_dt=%s", len(categories), biz_dt)
|
|
|
|
|
+
|
|
|
|
|
+ rows: list[dict[str, Any]] = []
|
|
|
|
|
+ upserted = 0
|
|
|
|
|
+
|
|
|
|
|
+ with get_session() as session:
|
|
|
|
|
+ pool_repo = MultiDemandPoolDiRepository(session)
|
|
|
|
|
+ stats_repo = DemandPopularityStatsRepository(session)
|
|
|
|
|
+
|
|
|
|
|
+ for idx, (category_id, name) in enumerate(categories, start=1):
|
|
|
|
|
+ matches = pool_repo.list_weights_by_name_like(
|
|
|
|
|
+ biz_dt, name, _TARGET_STRATEGIES
|
|
|
|
|
+ )
|
|
|
|
|
+ rows.append(_build_stats_row(category_id, name, biz_dt, matches))
|
|
|
|
|
+
|
|
|
|
|
+ if len(rows) >= _STATS_UPSERT_BATCH:
|
|
|
|
|
+ upserted += stats_repo.upsert_rows(rows)
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "Popularity stats progress: %d/%d words, batch upserted",
|
|
|
|
|
+ idx,
|
|
|
|
|
+ len(categories),
|
|
|
|
|
+ )
|
|
|
|
|
+ rows = []
|
|
|
|
|
+
|
|
|
|
|
+ if rows:
|
|
|
|
|
+ upserted += stats_repo.upsert_rows(rows)
|
|
|
|
|
+
|
|
|
|
|
+ result = {"words": len(categories), "upserted": upserted}
|
|
|
|
|
+ logger.info("Popularity stats completed: %s", result)
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def sync_multi_demand_pool_odps_to_mysql(partition_date: str | None = None) -> dict:
|
|
def sync_multi_demand_pool_odps_to_mysql(partition_date: str | None = None) -> dict:
|
|
|
"""
|
|
"""
|
|
|
- 从 ODPS 增量同步策略需求天级数据到 MySQL,并对新词做归属分类。
|
|
|
|
|
|
|
+ 从 ODPS 增量同步策略需求天级数据到 MySQL,并对新词做归属分类与热度统计。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
partition_date: 分区日期 (YYYYMMDD),默认当天
|
|
partition_date: 分区日期 (YYYYMMDD),默认当天
|
|
@@ -184,11 +304,13 @@ def sync_multi_demand_pool_odps_to_mysql(partition_date: str | None = None) -> d
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
classify_stats = _classify_words(partition_date)
|
|
classify_stats = _classify_words(partition_date)
|
|
|
|
|
+ popularity_stats = compute_popularity_stats(partition_date)
|
|
|
|
|
|
|
|
result = {
|
|
result = {
|
|
|
"partition_date": partition_date,
|
|
"partition_date": partition_date,
|
|
|
**sync_stats,
|
|
**sync_stats,
|
|
|
"classify": classify_stats,
|
|
"classify": classify_stats,
|
|
|
|
|
+ "popularity": popularity_stats,
|
|
|
"synced_at": datetime.now().isoformat(),
|
|
"synced_at": datetime.now().isoformat(),
|
|
|
}
|
|
}
|
|
|
logger.info("Multi demand pool sync completed: %s", result)
|
|
logger.info("Multi demand pool sync completed: %s", result)
|