feat(recommender): Thompson-sampling bandit over candidate sources (M6)
ci / recommender / recommender (push) Failing after 4s
ci / db / db (push) Failing after 2m21s

Nightly Beta(α, β) per user and source from wave impressions (reward:
complete or like, ×0.99 daily decay) stored in user_profiles_rec.bandit;
the wave adds 0.2·(θ − 0.5) per candidate source, sources without data
stay neutral. The user's exploration share now follows how well `fresh`
does against the other sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 22:31:24 +03:00
1 parent f7722e28ad
commit ff0aafdb1e
6 files changed
+130 -2

No files matched your search

@@ -15,6 +15,7 @@ import numpy as np
import psycopg
from pipu_rec.train.als import ALSModel, normalize, train_als
from pipu_rec.train.bandit import update_bandits
from pipu_rec.train.metrics import evaluate
from pipu_rec.train.signals import DISLIKE, LIKE, Interaction, event_weight, implicit_scores
@@ -157,9 +158,11 @@ def train_all(dsn: str, now: float | None = None) -> dict[str, Any]:
if not conn.execute("SELECT pg_try_advisory_lock(%s)", (ADVISORY_LOCK,)).fetchone()[0]: # type: ignore[index]
return {"skipped": "another training is running"}
try:
# Бандит считается всегда — ему хватает и пары сессий волны, в отличие от ALS.
bandit_users = update_bandits(conn, now)
interactions = load_interactions(conn)
if len(interactions) < MIN_INTERACTIONS:
return {"skipped": f"only {len(interactions)} interactions"}
return {"skipped": f"only {len(interactions)} interactions", "bandit_users": bandit_users}
started = time.monotonic()
offline = offline_metrics(interactions, now)
model = train_als(implicit_scores(interactions, now))
@@ -171,6 +174,7 @@ def train_all(dsn: str, now: float | None = None) -> dict[str, Any]:
"users": len(model.users),
"items": len(model.items),
"interactions": len(interactions),
"bandit_users": bandit_users,
"train_s": round(time.monotonic() - started, 1),
}
if not should_activate(offline, active_metrics(conn, "als")):