fix(growth): rank the rate board by a smoothed score

Show the real percentage on the card, but sort with delta / (baseline + prior) so a small baseline cannot dominate. Same-day publishes stay off the rate board.
This commit is contained in:
kairee committed 2026-11-02 22:06:41 +08:00
1 parent 0f2ac612f5
commit 39a6537f67
14 files changed
+158 -16

No files matched your search

+15
View File
@@ -12,6 +12,7 @@ from reaction_board.analytics import ( # noqa: E402
METRICS,
age_days,
compute_growth,
smoothed_growth_rate,
content_hours,
eb_rate,
effective_shrink_minutes,
@@ -161,6 +162,19 @@ def test_compute_growth():
assert negative.per_day == -5.0
def test_smoothed_growth_rate():
# 80→512:真实增速 5.4;prior=500 时排序分 432/580
assert abs(smoothed_growth_rate(432, 80, 500) - 432 / 580) < 1e-12
assert abs(smoothed_growth_rate(432, 5000, 500) - 432 / 5500) < 1e-12
assert abs(smoothed_growth_rate(432, 80, 1000) - 432 / 1080) < 1e-12
# prior=0 时排序分退回真实相对增速(基线 ≥ 1)
assert smoothed_growth_rate(432, 80, 0) == 432 / 80
assert smoothed_growth_rate(None, 80, 500) is None
assert smoothed_growth_rate(432, None, 500) is None
assert smoothed_growth_rate(10, 0, 0) is None
assert smoothed_growth_rate(10, 80, -1) is None
def test_normalize_cover_and_hosts():
assert normalize_cover(None) is None
assert normalize_cover("") is None
@@ -251,6 +265,7 @@ def main_test():
test_score_percentiles()
test_interaction_rate_thresholds()
test_compute_growth()
test_smoothed_growth_rate()
test_normalize_cover_and_hosts()
test_content_efficiency_helpers()
test_age_and_maturity()
+4
View File
@@ -146,6 +146,7 @@ def test_analytics_defaults_and_validation():
assert cfg.analytics.video_shrink_minutes == 60
assert cfg.analytics.shrink_minutes is None
assert cfg.analytics.home_growth_days == "auto"
assert cfg.analytics.growth_rate_prior == 500
assert cfg.schedule.recent_snapshot_minutes == 60
assert cfg.schedule.recent_video_days == 2
assert cfg.schedule.snapshot_at == "23:15"
@@ -159,6 +160,7 @@ def test_analytics_defaults_and_validation():
"video_shrink_minutes": 30,
"shrink_minutes": 90,
"home_growth_days": 1,
"growth_rate_prior": 1000,
}
data["schedule"] = {
"discover_minutes": 30,
@@ -176,6 +178,7 @@ def test_analytics_defaults_and_validation():
assert cfg.analytics.video_shrink_minutes == 30
assert cfg.analytics.shrink_minutes == 90
assert cfg.analytics.home_growth_days == 1
assert cfg.analytics.growth_rate_prior == 1000
assert cfg.schedule.recent_snapshot_minutes == 15
assert cfg.schedule.recent_video_days == 3
@@ -194,6 +197,7 @@ def test_analytics_defaults_and_validation():
({"home_growth_days": 3}, "home_growth_days"),
({"home_growth_days": "7"}, "home_growth_days"),
({"home_growth_days": 0}, "home_growth_days"),
({"growth_rate_prior": -1}, "growth_rate_prior"),
):
data = base_config()
data["analytics"] = bad
+43
View File
@@ -492,6 +492,7 @@ def test_about_payload():
"video_shrink_minutes": 60,
"shrink_minutes": None,
"home_growth_days": "auto",
"growth_rate_prior": 500,
}
assert about["heat_weights"]["view"] == 0.30
assert about["followers"]["videos_total"] == 3
@@ -748,12 +749,18 @@ def test_same_day_growth_baselines():
assert 106 not in by_aid and 107 not in by_aid
assert by_aid[103]["growth"]["baseline"] == "zero"
assert by_aid[103]["growth"]["delta"] == 200
assert by_aid[103]["growth"]["rate"] is None
assert by_aid[103]["growth"]["gap_days"] == 0
assert by_aid[105]["growth"]["baseline"] == "open"
assert by_aid[105]["growth"]["delta"] == 300
assert by_aid[101]["growth"]["baseline"] == "snapshot"
assert by_aid[101]["growth"]["delta"] == 100
rate = q.list_videos(sort="growth_rate", growth_days=1, page_size=20)
rate_aids = {item["aid"] for item in rate["items"]}
assert 103 not in rate_aids
assert 101 in rate_aids and 105 in rate_aids
seven = q.list_videos(sort="growth", growth_days=7)
seven_aids = {item["aid"] for item in seven["items"]}
assert 103 not in seven_aids
@@ -774,6 +781,41 @@ def test_same_day_growth_baselines():
db.close()
def test_growth_rate_sort_uses_prior():
"""小基数的真实增速可以更高,排序分用基线 + prior,不按 raw rate 排。"""
with tempfile.TemporaryDirectory() as tmp:
db, q = setup(tmp)
try:
repo = Repository(db)
latest = DATES[-1]
previous_day = DATES[-2]
start = int(day_bounds(latest)[0])
pub = int(day_bounds("2026-09-01")[0])
for aid, views in ((301, (80, 512)), (302, (2000, 4000))):
repo.upsert_from_fav(
resource(aid, up_mid=1, up_name="UP A", pubdate=pub),
FOLDER.media_id, was_added=True, now=start,
)
repo.upsert_snapshot(
previous_day, aid, metrics=_view_metrics(views[0]),
ok=True, err=None, captured_at=start - 86400,
)
repo.upsert_snapshot(
latest, aid, metrics=_view_metrics(views[1]),
ok=True, err=None, captured_at=start + 60,
)
DASHBOARD_CACHE.clear()
ranked = q.list_videos(sort="growth_rate", growth_days=1, page_size=20)
order = [item["aid"] for item in ranked["items"]]
assert ranked["growth_rate_prior"] == 500
assert order.index(302) < order.index(301)
small = next(item for item in ranked["items"] if item["aid"] == 301)
assert abs(small["growth"]["rate"] - 432 / 80) < 1e-12
assert small["growth"]["delta"] == 432
finally:
db.close()
def test_system_tasks_sorted_by_started_at_desc():
"""最近任务按 started_at 倒序;缺开始时间的排末尾(含进行中无 finished)。"""
import json
@@ -835,6 +877,7 @@ def main_test():
test_snapshot_recent_next_run_follows_completion()
test_overview_adjacent_previous_with_seed_and_dual_fields()
test_same_day_growth_baselines()
test_growth_rate_sort_uses_prior()
print("OK: Web 查询层总览/榜单/详情/UP/时长效率/粉丝/候选/系统/访问统计测试全部通过")