feat(taste): time-of-day / weekday context conditioning — #1531
Milestone #160 Opt 3 (temporal half). A new additive scoring term that boosts a candidate when its artist's play history concentrates in the CURRENT daypart × weekday-type cell, in the user's local timezone. - Migration 0046: recommendation_weight_profiles.context_time_weight (per-profile scoring weight, DEFAULT 1.0). - Query ListArtistContextPlayCountsForUser: per-artist completed-play counts split by the current cell (daypart night[22,5)/morning[5,12)/ afternoon[12,17)/evening[17,22) × weekday-vs-weekend) via started_at AT TIME ZONE users.timezone; 365-day window, skips excluded. - internal/recommendation/context.go: LoadContextAffinity computes each artist's shrunk cell-share minus the user's baseline share, clamped to [-1,1]; sparse artists shrink toward baseline (pseudo-count 5), unknown artists → 0 (cold-start neutral). - Score() gains context_affinity_score · ContextTimeWeight; both candidate loaders set it per candidate. - Tuning lab: ContextTimeWeight threaded through recsettings + admin API + web card ("Time-of-day weight" row) + Go/web tests. Shipped 1.0 both profiles (uniform start, re-bakeable). Device-class axis deferred to #1551 (needs a client_id → device-class mapping that doesn't exist yet). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,19 +24,26 @@ type ScoringInputs struct {
|
||||
// user's taste, negative reflects passive avoidance, 0 when there's no
|
||||
// profile signal (cold start / artist+tags absent from the profile).
|
||||
TasteMatchScore float64
|
||||
// ContextAffinityScore is the candidate artist's time-of-day/weekday
|
||||
// affinity for the CURRENT context (#1531), in [-1, +1]: positive when the
|
||||
// artist's plays concentrate in the current daypart × weekday-type cell
|
||||
// more than the user's baseline, negative when under-represented, 0 when
|
||||
// there's no history (cold-start neutral).
|
||||
ContextAffinityScore float64
|
||||
}
|
||||
|
||||
// ScoringWeights are the operator-tunable knobs. Defaults live in
|
||||
// config.RecommendationConfig and are propagated here per request.
|
||||
type ScoringWeights struct {
|
||||
BaseWeight float64
|
||||
LikeBoost float64
|
||||
RecencyWeight float64
|
||||
SkipPenalty float64
|
||||
JitterMagnitude float64
|
||||
ContextWeight float64
|
||||
SimilarityWeight float64
|
||||
TasteWeight float64
|
||||
BaseWeight float64
|
||||
LikeBoost float64
|
||||
RecencyWeight float64
|
||||
SkipPenalty float64
|
||||
JitterMagnitude float64
|
||||
ContextWeight float64
|
||||
SimilarityWeight float64
|
||||
TasteWeight float64
|
||||
ContextTimeWeight float64
|
||||
}
|
||||
|
||||
// Score computes the weighted-shuffle score per spec §6:
|
||||
@@ -48,6 +55,7 @@ type ScoringWeights struct {
|
||||
// + contextual_match_score * ContextWeight
|
||||
// + similarity_score * SimilarityWeight
|
||||
// + taste_match_score * TasteWeight
|
||||
// + context_affinity_score * ContextTimeWeight
|
||||
// + small_random_jitter
|
||||
//
|
||||
// Higher score = more likely to surface. rng is a function returning a
|
||||
@@ -63,6 +71,7 @@ func Score(in ScoringInputs, w ScoringWeights, now time.Time, rng func() float64
|
||||
s += in.ContextualMatchScore * w.ContextWeight
|
||||
s += in.SimilarityScore * w.SimilarityWeight
|
||||
s += in.TasteMatchScore * w.TasteWeight
|
||||
s += in.ContextAffinityScore * w.ContextTimeWeight
|
||||
s += (rng()*2 - 1) * w.JitterMagnitude
|
||||
return s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user