feat(db): add video pages and up follower snapshot schema
This commit is contained in:
1 parent
707ed7d7d7
commit
4942d1d161
10 files changed
+521
-13
No files matched your search
@@ -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] = {}
|
||||
|
||||
Reference in new issue
Block a user