fix(analytics): shrink video efficiency and gate noisy boards by views

This commit is contained in:
kairee committed 2026-11-01 12:25:27 +08:00
1 parent e7034cfb84
commit bc3bef0743
12 files changed
+125 -40

No files matched your search

+59 -7
View File
@@ -699,6 +699,7 @@ class DashboardQueries:
latest = self._latest_date(conn) or self._today()
analytics = self.config_store.get().analytics
duration_sorts = {"efficiency", "interaction_efficiency"}
min_view_for_ranking = max(0, int(analytics.min_view_for_ranking))
within_field = None
if within_days is not None:
if within_days < 1:
@@ -716,7 +717,7 @@ class DashboardQueries:
)
aids = [int(video["aid"]) for video in videos]
snapshots = self._snapshot_map(conn, aids)
_, _, heat, percentiles = self._global_heat(conn)
videos_all, snapshots_all, heat, percentiles = self._global_heat(conn)
previous: dict[int, dict[str, Any]] = {}
if sort in {"growth", "growth_rate", "interaction"}:
@@ -740,16 +741,60 @@ class DashboardQueries:
if sort in duration_sorts:
latest_start, _ = day_bounds(latest)
minimum_seconds = max(0, int(analytics.min_duration_seconds))
min_view = max(0, int(analytics.min_view_for_efficiency))
items = [
item for item in items
if item["metrics"] is not None
and int(item["duration"] or 0) >= minimum_seconds
and int((item["metrics"] or {}).get("view", 0)) >= min_view
and int((item["metrics"] or {}).get("view", 0)) >= min_view_for_ranking
and is_mature(
item["pubdate"], latest_start, analytics.maturity_days
)
]
kappa_hours = max(0, int(analytics.video_shrink_minutes)) / 60.0
total_hours = 0.0
total_views = 0.0
total_interactions = 0.0
for video in videos_all:
hours = content_hours(video.get("duration"))
if hours is None:
continue
metrics = _metric_map(snapshots_all.get(int(video["aid"])))
if metrics is None:
continue
total_hours += hours
total_views += metrics["view"]
total_interactions += (
metrics["like"] + metrics["coin"] + metrics["favorite"]
)
prior_views = total_views / total_hours if total_hours > 0 else 0.0
prior_interactions = (
total_interactions / total_hours if total_hours > 0 else 0.0
)
for item in items:
hours = content_hours(item.get("duration")) or 0.0
metrics = item["metrics"] or {}
denominator = hours + kappa_hours
if hours <= 0 or denominator <= 0:
item["efficiency_score"] = None
item["interaction_efficiency_score"] = None
continue
view = int(metrics.get("view", 0))
interactions = (
int(metrics.get("like", 0))
+ int(metrics.get("coin", 0))
+ int(metrics.get("favorite", 0))
)
item["efficiency_score"] = (
view + kappa_hours * prior_views
) / denominator
item["interaction_efficiency_score"] = (
interactions + kappa_hours * prior_interactions
) / denominator
if sort == "heat":
items = [
item for item in items
if int((item["metrics"] or {}).get("view", 0)) >= min_view_for_ranking
]
if sort == "interaction":
items = [item for item in items if item["interaction_rate"] is not None]
if sort in {"growth", "growth_rate"}:
@@ -773,7 +818,11 @@ class DashboardQueries:
if sort == "interaction":
return item["interaction_rate"] if item["interaction_rate"] is not None else float("-inf")
if sort in {"efficiency", "interaction_efficiency"}:
value = item.get(sort)
field = (
"efficiency_score" if sort == "efficiency"
else "interaction_efficiency_score"
)
value = item.get(field)
return value if value is not None else float("-inf")
if sort in {"growth", "growth_rate"}:
field = "delta" if sort == "growth" else "rate"
@@ -800,13 +849,15 @@ class DashboardQueries:
duration_filter = {
"min_duration_seconds": max(0, int(analytics.min_duration_seconds)),
"maturity_days": max(0, int(analytics.maturity_days)),
"min_view": max(0, int(analytics.min_view_for_efficiency)),
"min_view": min_view_for_ranking,
"shrink_minutes": max(0, int(analytics.video_shrink_minutes)),
}
if not items:
available = False
reason = (
f"仅统计时长 ≥ {duration_filter['min_duration_seconds']} 秒、"
f"发布满 {duration_filter['maturity_days']} 天的视频"
f"发布满 {duration_filter['maturity_days']} 天、"
f"播放 ≥ {duration_filter['min_view']} 的视频"
)
return {
"sort": sort,
@@ -1425,7 +1476,8 @@ class DashboardQueries:
"analytics": {
"min_duration_seconds": cfg.analytics.min_duration_seconds,
"maturity_days": cfg.analytics.maturity_days,
"min_view_for_efficiency": cfg.analytics.min_view_for_efficiency,
"min_view_for_ranking": cfg.analytics.min_view_for_ranking,
"video_shrink_minutes": cfg.analytics.video_shrink_minutes,
"shrink_minutes": cfg.analytics.shrink_minutes,
},
"heat_weights": {
+3 -3
View File
@@ -51,8 +51,8 @@ export async function renderAbout(root) {
<li><strong>互动</strong>:点赞 + 投币 + 收藏。综合热度也使用全部七项指标,按配置权重加权。</li>
<li><strong>互动率</strong>:(点赞 + 投币 + 收藏) / 播放;播放不足 100 不计算。</li>
<li><strong>综合热度</strong>:每项先 <code>log1p</code>,再按中位数与 IQR 做稳健标准化,最后加权求和。分数只用于同一批数据内排序,不能跨日期比较。</li>
<li><strong>内容效率</strong>:播放 / 内容小时。内容小时 = 视频总时长(多 P 为各 P 之和)÷ 3600。</li>
<li><strong>互动效率</strong>:互动 / 内容小时,互动口径同上。</li>
<li><strong>内容效率</strong>:播放 / 内容小时。内容小时 = 视频总时长(多 P 为各 P 之和)÷ 3600。榜单里的单条视频用收缩后的估计:<code>(播放 + κ·μ₀) / (小时 + κ)</code>,κ 默认 ${analytics.video_shrink_minutes} 分钟、μ₀ 为全库播放/小时;详情页保留原始播放/小时。</li>
<li><strong>互动效率</strong>:互动 / 内容小时,互动口径与收缩方式同上。</li>
<li><strong>UP 内容效率</strong>:把该 UP 的全部视频合并后计算,并用 Gamma–Poisson 经验贝叶斯向全库均值收缩,避免单条视频或极小样本冲榜。收缩强度 κ ${analytics.shrink_minutes ? `固定为 ${analytics.shrink_minutes} 分钟` : "取全库 UP 总内容分钟的中位数"}。</li>
<li><strong>累计触达</strong>:总播放。它是规模指标,不代表吸引力;高产 UP 排在前列是正常结果。</li>
<li><strong>分 P</strong>:<code>pages</code> 是分 P 数,<code>duration</code> 是该 bvid 各 P 时长之和。B 站不公开单 P 播放,因此没有单 P 榜;分 P 数只用于详情展示,不参与排序。</li>
@@ -66,7 +66,7 @@ export async function renderAbout(root) {
<li>只统计发布满 <strong>${analytics.maturity_days}</strong> 天的视频;reaction 视频通常在发布前两天完成数据积累,两周内接近平台期,过早比较会系统性低估新视频。</li>
<li>不引入无界的“年龄惩罚”:超过成熟窗口后不再按天加分或扣分。</li>
<li><code>duration</code> 为空或 ≤ 0 的视频(通常只在失效视频上出现)不进入任何时长相关榜,也不做插补。</li>
<li>当前配置的最小播放门槛为 <strong>${analytics.min_view_for_efficiency}</strong>。</li>
<li>播放低于 <strong>${analytics.min_view_for_ranking}</strong> 的视频不进入内容效率、互动效率与综合热度榜;这是为了避免小样本噪声冲榜。</li>
</ul>
</section>
+6 -3
View File
@@ -83,10 +83,10 @@ function controls({ scope, sort, within, growthDays, q, folder, minVideos, folde
function videoPrimary(item, sort) {
if (sort === "efficiency") {
return { primary: "efficiency", label: "播放/小时", text: formatCount(item.efficiency) };
return { primary: "efficiency_score", label: "播放/小时·收缩", text: formatCount(item.efficiency_score) };
}
if (sort === "interaction_efficiency") {
return { primary: "interaction_efficiency", label: "互动/小时", text: formatCount(item.interaction_efficiency) };
return { primary: "interaction_efficiency_score", label: "互动/小时·收缩", text: formatCount(item.interaction_efficiency_score) };
}
if (sort === "interaction") {
return { primary: "view", label: "互动率", text: formatPercent(item.interaction_rate) };
@@ -143,8 +143,11 @@ function listHtml(data, scope, sort, startRank) {
function durationNote(data) {
const filter = data.duration_filter;
if (!filter) return "";
const shrink = filter.shrink_minutes
? `并按 ${filter.shrink_minutes} 分钟的先验暴露量向全库均值收缩`
: "";
return `<div class="section-note">内容效率与互动效率仅统计时长 ≥ ${filter.min_duration_seconds} 秒、
发布满 ${filter.maturity_days} 天的视频;时长缺失的视频不参与排序。</div>`;
发布满 ${filter.maturity_days} 天、播放 ≥ ${filter.min_view} 的视频${shrink};时长缺失的视频不参与排序。</div>`;
}
function bindControls(root, scope) {
+2 -2
View File
@@ -77,8 +77,8 @@ export async function renderVideo(root, { params, query }) {
<div><span>综合热度</span><strong>${formatScore(detail.heat)}</strong></div>
<div><span>全库百分位</span><strong>${detail.heat_percentile === null || detail.heat_percentile === undefined ? "—" : `前 ${Math.max(0, 100 - detail.heat_percentile).toFixed(0)}%`}</strong></div>
<div><span>播放日增</span><strong>${detail.growth?.available ? formatDelta(detail.growth.delta) : "—"}</strong></div>
<div><span>内容效率</span><strong>${detail.efficiency == null ? "—" : `${formatCount(detail.efficiency)}/小时`}</strong></div>
<div><span>互动效率</span><strong>${detail.interaction_efficiency == null ? "—" : `${formatCount(detail.interaction_efficiency)}/小时`}</strong></div>
<div><span>内容效率(原始)</span><strong>${detail.efficiency == null ? "—" : `${formatCount(detail.efficiency)}/小时`}</strong></div>
<div><span>互动效率(原始)</span><strong>${detail.interaction_efficiency == null ? "—" : `${formatCount(detail.interaction_efficiency)}/小时`}</strong></div>
<div><span>UP 粉丝(入库时)</span><strong>${formatCount(detail.up_follower)}</strong></div>
<div><span>快照数</span><strong>${samples}</strong></div>
<div><span>最近快照</span><strong>${escapeHtml(detail.snapshot_date || "—")}</strong></div>