feat(recommender): LightGBM lambdarank ranker with impression features (M6)
The wave logs each served candidate's feature vector to rec_impression_features. Nightly training joins them with impression outcomes, validates on the last week and activates the model only when it beats the v1 order and the active model. The service loads the active ranker and uses its order in place of the v1 score. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fb07f962eb
commit
58bd14ef38
12 files changed
+551
-14
No files matched your search
@@ -159,3 +159,25 @@ def test_onboarding_seeds_taste(world: dict[str, Any]) -> None:
|
||||
profile = load_profile(conn, user)
|
||||
assert len(profile.scores) >= 10, "chosen artist's tracks become the initial taste"
|
||||
assert not profile.played
|
||||
|
||||
|
||||
def test_impression_features_roundtrip(world: dict[str, Any]) -> None:
|
||||
from pipu_rec.train.ranker import load_rows, train_and_save
|
||||
from pipu_rec.wave.engine import FEATURE_NAMES
|
||||
|
||||
session, user, track = uid(), world["users"][0], world["tracks"][0]
|
||||
with psycopg.connect(DSN, autocommit=True) as conn:
|
||||
conn.execute("INSERT INTO wave_sessions (id, user_id) VALUES (%s, %s)", (session, user))
|
||||
conn.execute(
|
||||
"INSERT INTO wave_impressions (wave_session_id, position, track_id, candidate_source, outcome) "
|
||||
"VALUES (%s, 0, %s, 'audio_sim', 'complete')",
|
||||
(session, track),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO rec_impression_features (wave_session_id, track_id, features) VALUES (%s, %s, %s)",
|
||||
(session, track, [0.5] * len(FEATURE_NAMES)),
|
||||
)
|
||||
rows = load_rows(conn)
|
||||
assert any(r[0] == session and r[2] == "complete" for r in rows)
|
||||
result = train_and_save(conn, time.time())
|
||||
assert "skipped" in result["ranker"], "too few impressions — v1 stays"
|
||||
Reference in new issue
Block a user