feat: add freshness decay to tackle analytics

This commit is contained in:
ik
2026-09-21 18:02:23 +07:00
parent 91019cbf3a
commit e4e10bb4e9
7 changed files with 819 additions and 6 deletions
+14 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from math import exp, log
from fastapi import APIRouter, Query
from sqlalchemy import select
@@ -12,6 +13,12 @@ from ..models import CatchReport, Fish, ModerationStatus, Spot, Waterbody
from ..schemas import TackleCombinationOut
router = APIRouter()
FRESHNESS_HALF_LIFE_HOURS = 12.5
def _age_hours(value: datetime, now: datetime) -> float:
observed_at = value if value.tzinfo is not None else value.replace(tzinfo=timezone.utc)
return max(0.0, (now - observed_at).total_seconds() / 3600)
@router.get("/api/v1/analytics/tackle", response_model=list[TackleCombinationOut])
@@ -53,14 +60,19 @@ def tackle_combinations(
catches = len(unique_reports)
unique_players = len(players)
last_seen = max(report.reported_at for report in unique_reports.values())
freshness_score = round(sum(
exp(-_age_hours(report.reported_at, now) * log(2) / FRESHNESS_HALF_LIFE_HOURS)
for report in unique_reports.values()
) / catches * 100)
enough = catches >= min_samples and unique_players >= min_players
result.append(TackleCombinationOut(
role=role, value=value, catches=catches, unique_players=unique_players,
last_seen_at=last_seen, status="recommendation" if enough else "insufficient_data",
last_seen_at=last_seen, freshness_score=freshness_score,
status="recommendation" if enough else "insufficient_data",
explanation=(
"Достаточно независимых наблюдений для рекомендации."
if enough else
f"Данных мало: нужно минимум {min_samples} наблюдения и {min_players} независимых игрока."
),
))
return sorted(result, key=lambda item: (item.status != "recommendation", -item.catches, -item.unique_players, item.role, item.value))
return sorted(result, key=lambda item: (item.status != "recommendation", -item.freshness_score, -item.catches, -item.unique_players, item.role, item.value))
+1
View File
@@ -118,6 +118,7 @@ class TackleCombinationOut(BaseModel):
catches: int
unique_players: int
last_seen_at: datetime
freshness_score: int
status: str
explanation: str