feat(db): add video pages and up follower snapshot schema

This commit is contained in:
kairee committed 2026-11-01 11:14:36 +08:00
1 parent 707ed7d7d7
commit 4942d1d161
10 files changed
+521 -13

No files matched your search

+125
View File
@@ -35,6 +35,9 @@ DEFAULT_HEAT_WEIGHTS: dict[str, float] = {
MIN_VIEW_FOR_INTERACTION = 100
MIN_VIEW_FOR_GROWTH_RATE = 200
MIN_EFFICIENCY_DURATION = 600
DEFAULT_MATURITY_DAYS = 14
DEFAULT_SHRINK_MINUTES = 120.0
_IQR_TO_SIGMA = 1.349
@@ -83,6 +86,128 @@ def _metric_value(row: Mapping[str, object], metric: str) -> float:
return 0.0
def content_hours(duration_seconds: object) -> float | None:
"""把视频总时长(秒)换算为内容小时;缺失或非正数返回 ``None``。
多 P 视频的 ``duration`` 已是各 P 之和,因此这里不需要再乘 P 数。
"""
if duration_seconds is None or isinstance(duration_seconds, bool):
return None
try:
seconds = float(duration_seconds)
except (TypeError, ValueError):
return None
if seconds <= 0:
return None
return seconds / 3600.0
def views_per_hour(view: object, duration_seconds: object) -> float | None:
"""播放/内容小时;时长缺失或非正数时返回 ``None``,不做除零。"""
hours = content_hours(duration_seconds)
if hours is None:
return None
return _metric_value({"view": view}, "view") / hours
def interactions_per_hour(
metrics: Mapping[str, object] | None,
duration_seconds: object,
*,
interaction_metrics: Sequence[str] = ("like", "coin", "favorite"),
) -> float | None:
"""互动/内容小时;统计缺失或时长无效时返回 ``None``。"""
hours = content_hours(duration_seconds)
if hours is None or metrics is None:
return None
interactions = sum(_metric_value(metrics, metric) for metric in interaction_metrics)
return interactions / hours
def age_days(pubdate: object, reference: int | float) -> int | None:
"""发布到参考时点(通常为最新数据日)的整天数;无效发布时间返回 ``None``。"""
if pubdate is None or isinstance(pubdate, bool):
return None
try:
published = int(pubdate)
now = int(reference)
except (TypeError, ValueError):
return None
if published <= 0:
return None
return max(0, (now - published) // 86400)
def is_mature(
pubdate: object,
reference: int | float,
maturity_days: int = DEFAULT_MATURITY_DAYS,
) -> bool:
"""是否已达到成熟窗口;发布时间缺失视为未成熟。"""
days = age_days(pubdate, reference)
return days is not None and days >= max(0, int(maturity_days))
def median_of(values: Iterable[object]) -> float | None:
"""忽略无效值的中位数;没有有效值返回 ``None``。"""
numbers: list[float] = []
for value in values:
if value is None or isinstance(value, bool):
continue
try:
numbers.append(float(value))
except (TypeError, ValueError):
continue
if not numbers:
return None
return float(_stat_median(numbers))
def effective_shrink_minutes(
per_up_minutes: Iterable[object],
fallback: float = DEFAULT_SHRINK_MINUTES,
) -> float:
"""自适应收缩强度:各 UP 总内容分钟的中位数,缺失时回退固定值。"""
positives: list[float] = []
for minutes in per_up_minutes:
if minutes is None or isinstance(minutes, bool):
continue
try:
value = float(minutes)
except (TypeError, ValueError):
continue
if value > 0:
positives.append(value)
value = median_of(positives)
if value is None or value <= 0:
return float(fallback)
return value
def eb_rate(
total: object,
exposure: object,
prior_rate: float,
prior_strength: float,
) -> float | None:
"""Gamma–Poisson 经验贝叶斯收缩后的速率。
``exposure`` 与 ``prior_strength`` 必须使用同一单位(本项目中为分钟),
返回单位与 ``prior_rate`` 一致(每分钟)。
"""
if prior_strength < 0:
raise ValueError("prior_strength 不能为负")
try:
total_value = max(0.0, float(total))
exposure_value = max(0.0, float(exposure))
prior = max(0.0, float(prior_rate))
except (TypeError, ValueError):
return None
if prior_strength <= 0:
return None if exposure_value <= 0 else total_value / exposure_value
return (total_value + prior_strength * prior) / (exposure_value + prior_strength)
def normalized_weights(weights: Mapping[str, float] | None = None) -> dict[str, float]:
source = weights or DEFAULT_HEAT_WEIGHTS
result: dict[str, float] = {}