feat(analytics): lower maturity window to 2 days and reword about page
This commit is contained in:
1 parent
b405fc5fd7
commit
e7034cfb84
11 files changed
+66
-28
No files matched your search
@@ -36,7 +36,7 @@ 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_MATURITY_DAYS = 2
|
||||
DEFAULT_SHRINK_MINUTES = 120.0
|
||||
_IQR_TO_SIGMA = 1.349
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ DEFAULT_HEAT_WEIGHTS: dict[str, float] = {
|
||||
|
||||
DEFAULT_ANALYTICS: dict[str, Any] = {
|
||||
"min_duration_seconds": 600,
|
||||
"maturity_days": 14,
|
||||
"maturity_days": 2,
|
||||
"min_view_for_efficiency": 0,
|
||||
"shrink_minutes": None,
|
||||
}
|
||||
@@ -459,7 +459,7 @@ def _build_config(data: Mapping[str, Any], path: Path) -> AppConfig:
|
||||
minimum=0,
|
||||
),
|
||||
maturity_days=_as_int(
|
||||
analytics_raw.get("maturity_days", 14),
|
||||
analytics_raw.get("maturity_days", 2),
|
||||
"analytics.maturity_days",
|
||||
minimum=0,
|
||||
),
|
||||
|
||||
@@ -738,7 +738,7 @@ class DashboardQueries:
|
||||
]
|
||||
|
||||
if sort in duration_sorts:
|
||||
_, latest_end = day_bounds(latest)
|
||||
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 = [
|
||||
@@ -747,7 +747,7 @@ class DashboardQueries:
|
||||
and int(item["duration"] or 0) >= minimum_seconds
|
||||
and int((item["metrics"] or {}).get("view", 0)) >= min_view
|
||||
and is_mature(
|
||||
item["pubdate"], latest_end, analytics.maturity_days
|
||||
item["pubdate"], latest_start, analytics.maturity_days
|
||||
)
|
||||
]
|
||||
if sort == "interaction":
|
||||
@@ -1396,10 +1396,24 @@ class DashboardQueries:
|
||||
preview_row = conn.execute(
|
||||
"SELECT v FROM kv WHERE k = 'preview:seeded'"
|
||||
).fetchone()
|
||||
fans_start_date = None
|
||||
track_fans = [
|
||||
int(mid) for mid in cfg.track_fans if not isinstance(mid, bool)
|
||||
]
|
||||
if track_fans:
|
||||
placeholders = ",".join("?" for _ in track_fans)
|
||||
row = conn.execute(
|
||||
f"SELECT MIN(stat_date) AS d FROM up_stat "
|
||||
f"WHERE ok = 1 AND mid IN ({placeholders})",
|
||||
track_fans,
|
||||
).fetchone()
|
||||
fans_start_date = None if row is None else row["d"]
|
||||
return {
|
||||
"data_date": latest,
|
||||
"snapshot_days": self._snapshot_days(conn),
|
||||
"snapshot_first_date": first_date,
|
||||
"fans_start_date": fans_start_date,
|
||||
"track_fans": track_fans,
|
||||
"schema_version": int(self.db.schema_version or 0),
|
||||
"preview": bool(preview_row and str(preview_row["v"]) == "1"),
|
||||
"counts": counts,
|
||||
|
||||
@@ -14,6 +14,9 @@ export async function renderAbout(root) {
|
||||
const coverage = followers.videos_total
|
||||
? Math.round((followers.videos_covered / followers.videos_total) * 100)
|
||||
: 0;
|
||||
const fansStart = data.fans_start_date
|
||||
? escapeHtml(data.fans_start_date)
|
||||
: "暂无";
|
||||
|
||||
root.innerHTML = `<section class="page about-page">
|
||||
<header class="page-head">
|
||||
@@ -23,19 +26,20 @@ export async function renderAbout(root) {
|
||||
<section class="panel">
|
||||
<h2>数据来源与更新</h2>
|
||||
<ul class="about-list">
|
||||
<li>视频元数据、播放与互动来自 B 站公开接口;粉丝数来自 <code>relation/stat</code>,匿名可用。</li>
|
||||
<li>发现与收藏夹同步每小时运行一次,每日快照在 00:01 采集;最新数据日:<strong>${escapeHtml(data.data_date || "暂无")}</strong>。</li>
|
||||
<li>当前快照覆盖 <strong>${data.snapshot_days}</strong> 天${data.snapshot_first_date ? `(起始 ${escapeHtml(data.snapshot_first_date)})` : ""};历史快照无法从 B 站回填,只能从部署当天开始积累。</li>
|
||||
<li>数据库 schema 版本 <code>${data.schema_version}</code>;${data.preview ? "当前为预览数据,不影响真实数据库。" : "当前为真实数据库。"}</li>
|
||||
<li>所有数据来自 B 站以及部分 MCN 统计网站。</li>
|
||||
<li>新视频人工入库;库内视频数据每天更新一次;最新数据日:<strong>${escapeHtml(data.data_date || "暂无")}</strong>。</li>
|
||||
<li>当前视频数据覆盖 <strong>${data.snapshot_days}</strong> 天${data.snapshot_first_date ? `(起始 ${escapeHtml(data.snapshot_first_date)})` : ""};历史快照无法从 B 站回填,只能从部署当天开始积累。</li>
|
||||
<li>咻咻满粉丝数据的起点日期为 <strong>${fansStart}</strong>。</li>
|
||||
</ul>
|
||||
${data.preview ? `<p class="section-note">当前为预览数据,不影响真实数据库。</p>` : ""}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>三个时间概念</h2>
|
||||
<ul class="about-list">
|
||||
<li><strong>发布时间</strong>(<code>pubdate</code>):视频在 B 站上线的时刻。</li>
|
||||
<li><strong>入库时间</strong>(<code>first_seen</code>):本站第一次看到并写入该视频的时刻。人工加收藏或搜索确认都可能导致入库晚于发布。</li>
|
||||
<li><strong>快照日期</strong>(<code>stat_date</code>):每天采集一次播放与互动,写入 <code>snapshot</code>。缺失日不插值,曲线直接断开。</li>
|
||||
<li><strong>发布时间</strong>(<code>pubdate</code>):视频在 B 站发布的时刻。</li>
|
||||
<li><strong>入库时间</strong>(<code>first_seen</code>):本站第一次录入该视频的时刻。</li>
|
||||
<li><strong>快照日期</strong>(<code>stat_date</code>):每天更新一次视频的播放与互动数据。缺失日不插值,曲线直接断开。</li>
|
||||
</ul>
|
||||
<p class="section-note">因此,同一个视频的“发布后 7 天数据”和“入库后 7 天数据”可能不是同一段区间;详情页会同时展示发布时间与入库时间。</p>
|
||||
</section>
|
||||
@@ -59,7 +63,7 @@ export async function renderAbout(root) {
|
||||
<h2>榜单筛选</h2>
|
||||
<ul class="about-list">
|
||||
<li>内容效率与互动效率只统计时长 ≥ <strong>${analytics.min_duration_seconds}</strong> 秒的视频,排除结构不同的短视频与切片。</li>
|
||||
<li>只统计发布满 <strong>${analytics.maturity_days}</strong> 天的视频;reaction 视频通常在两周内接近平台期,过早比较会系统性低估新视频。</li>
|
||||
<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>
|
||||
@@ -75,8 +79,7 @@ export async function renderAbout(root) {
|
||||
<section class="panel">
|
||||
<h2>UP 粉丝快照</h2>
|
||||
<ul class="about-list">
|
||||
<li>视频入库时记录一次 UP 当前粉丝数,写入 <code>video.up_follower</code>;同一 UP 的旧视频不会被新快照覆盖。</li>
|
||||
<li><code>up_profile.follower_first</code> 保留首次采集值,<code>follower_latest</code> 随刷新更新。</li>
|
||||
<li>视频入库时记录一次 UP 当前粉丝数,后续更新视频数据不刷新 UP 的入库时粉丝数。</li>
|
||||
<li>历史视频的粉丝数是回填值,会标记 <code>backfilled</code>;它与真实的“入库时”存在时间差,暂不用于榜单排序。</li>
|
||||
<li>当前覆盖:${followers.videos_covered} / ${followers.videos_total} 条视频(${coverage}%),涉及 ${followers.ups_tracked} 个 UP;缺失 ${followers.videos_missing} 条,可运行 <code>app.py backfill-followers</code> 补齐。</li>
|
||||
</ul>
|
||||
@@ -92,5 +95,13 @@ export async function renderAbout(root) {
|
||||
<li>样本不足(少于 2 天快照、单视频 UP)会显示提示,不伪造 0。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>技术信息</h2>
|
||||
<ul class="about-list">
|
||||
<li>数据库 schema 版本 <code>${data.schema_version}</code>。</li>
|
||||
<li>${data.preview ? "当前为预览数据,不影响真实数据库。" : "当前为真实数据库。"}</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>`;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user