diff --git a/app/admin/repositories/stats.py b/app/admin/repositories/stats.py index 202a5ba..3256812 100644 --- a/app/admin/repositories/stats.py +++ b/app/admin/repositories/stats.py @@ -280,12 +280,12 @@ def dashboard_overview( period_active_user_ids = ( login_user_ids | compare_start_user_ids | coupon_event_user_ids | coupon_claim_user_ids ) - period_retained_new_user_ids = period_new_user_ids & period_active_user_ids - period_retention_rate = ( - round(len(period_retained_new_user_ids) / len(period_new_user_ids), 4) - if period_new_user_ids - else None - ) + # 留存口径(2026-07-05 产品改):次日留存——窗口内每天 D,取 **D-1 日(前日)新增**用户, + # 统计其 D 日活跃(登录/开始比价/开始领券)比例,逐日累加。默认窗口=昨日单天,即 + # 「前日新增用户的昨日留存」。原口径(窗口内新增∩窗口内活跃)在单日窗口下≈100% 无意义 + # (注册即登录,当天新增必然当天活跃)。逐日 cohort 在下方 trend 循环内顺带累计。 + retention_cohort_total = 0 + retention_retained_total = 0 trend_points: list[dict] = [] for cur_date in _date_range(period_from, period_to): day_start_utc, day_end_utc, day_start_local, day_end_local = _period_bounds( @@ -313,15 +313,25 @@ def dashboard_overview( CouponPromptEngagement.engage_type == "claim_started", ) ) + daily_active_user_ids = ( + daily_login_user_ids + | daily_compare_start_user_ids + | daily_coupon_event_user_ids + | daily_coupon_claim_user_ids + ) + # 次日留存:cohort = 前一日(D-1)新增用户,留存 = 其中当日(D)活跃者(口径见上)。 + cohort_ids = _user_id_set( + select(User.id).where( + User.created_at >= day_start_utc - timedelta(days=1), + User.created_at < day_end_utc - timedelta(days=1), + ) + ) + retention_cohort_total += len(cohort_ids) + retention_retained_total += len(cohort_ids & daily_active_user_ids) trend_points.append( { "date": cur_date, - "active_users": len( - daily_login_user_ids - | daily_compare_start_user_ids - | daily_coupon_event_user_ids - | daily_coupon_claim_user_ids - ), + "active_users": len(daily_active_user_ids), "new_users": _count( User, User.created_at >= day_start_utc, @@ -330,6 +340,11 @@ def dashboard_overview( "comparisons": _count(ComparisonRecord, *daily_comparison_conds), } ) + period_retention_rate = ( + round(retention_retained_total / retention_cohort_total, 4) + if retention_cohort_total + else None + ) period_coin_conds = ( CoinTransaction.created_at >= start_local, @@ -476,11 +491,13 @@ def dashboard_overview( "users": { "new": len(period_new_user_ids), "active": len(period_active_user_ids), - "retained_new_users": len(period_retained_new_user_ids), + "retained_new_users": retention_retained_total, + "retention_cohort": retention_cohort_total, "retention_rate": period_retention_rate, "retention_note": ( - "口径:登录(last_login_at)+开始比价(real_compare_start)+" - "开始领券(real_coupon_start/claim_started),按用户去重" + "次日留存:窗口内每天取前一日新增用户,统计其当日活跃" + "(登录/开始比价/开始领券,按用户去重)比例,逐日累加;" + "默认窗口=昨日,即前日新增用户的昨日留存" ), }, "comparison": { diff --git a/app/admin/schemas/dashboard.py b/app/admin/schemas/dashboard.py index 56f2c15..b91c6b9 100644 --- a/app/admin/schemas/dashboard.py +++ b/app/admin/schemas/dashboard.py @@ -43,7 +43,10 @@ class DashboardComparison(BaseModel): class DashboardPeriodUsers(BaseModel): new: int active: int + # 次日留存(2026-07-05 起):retained_new_users = 窗口内逐日「前一日新增且当日活跃」用户数之和, + # retention_cohort = 对应的前一日新增基数之和,retention_rate = 两者之比。 retained_new_users: int + retention_cohort: int = 0 retention_rate: float | None = None retention_note: str