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:
@@ -67,6 +67,10 @@ var weightFields = map[string]weightField{
|
||||
get: func(w recommendation.ScoringWeights) float64 { return w.TasteWeight },
|
||||
set: func(w *recommendation.ScoringWeights, v float64) { w.TasteWeight = v },
|
||||
},
|
||||
"context_time_weight": {
|
||||
get: func(w recommendation.ScoringWeights) float64 { return w.ContextTimeWeight },
|
||||
set: func(w *recommendation.ScoringWeights, v float64) { w.ContextTimeWeight = v },
|
||||
},
|
||||
}
|
||||
|
||||
// applyWeightPatch validates and applies a partial update, returning
|
||||
|
||||
@@ -54,16 +54,19 @@ type TasteTuning struct {
|
||||
// here from config.RecommendationConfig — YAML is bootstrap-only,
|
||||
// rule: config in UI). Radio is seed-directed (the user picked a
|
||||
// direction), so taste is a lighter nudge than in the daily mixes.
|
||||
// ContextTimeWeight starts uniform (1.0) across both profiles pending
|
||||
// trend data (#1531); split them once the metrics view justifies it.
|
||||
func ShippedRadioWeights() recommendation.ScoringWeights {
|
||||
return recommendation.ScoringWeights{
|
||||
BaseWeight: 1.0,
|
||||
LikeBoost: 2.0,
|
||||
RecencyWeight: 1.0,
|
||||
SkipPenalty: 1.0,
|
||||
JitterMagnitude: 0.1,
|
||||
ContextWeight: 2.0,
|
||||
SimilarityWeight: 2.0,
|
||||
TasteWeight: 1.0,
|
||||
BaseWeight: 1.0,
|
||||
LikeBoost: 2.0,
|
||||
RecencyWeight: 1.0,
|
||||
SkipPenalty: 1.0,
|
||||
JitterMagnitude: 0.1,
|
||||
ContextWeight: 2.0,
|
||||
SimilarityWeight: 2.0,
|
||||
TasteWeight: 1.0,
|
||||
ContextTimeWeight: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,14 +74,15 @@ func ShippedRadioWeights() recommendation.ScoringWeights {
|
||||
// Must stay in sync with the pre-push literal in playlists/system.go.
|
||||
func ShippedDailyMixWeights() recommendation.ScoringWeights {
|
||||
return recommendation.ScoringWeights{
|
||||
BaseWeight: 1.0,
|
||||
LikeBoost: 2.0,
|
||||
RecencyWeight: 1.0,
|
||||
SkipPenalty: 2.0,
|
||||
JitterMagnitude: 0.1,
|
||||
ContextWeight: 0.5,
|
||||
SimilarityWeight: 1.5,
|
||||
TasteWeight: 1.5,
|
||||
BaseWeight: 1.0,
|
||||
LikeBoost: 2.0,
|
||||
RecencyWeight: 1.0,
|
||||
SkipPenalty: 2.0,
|
||||
JitterMagnitude: 0.1,
|
||||
ContextWeight: 0.5,
|
||||
SimilarityWeight: 1.5,
|
||||
TasteWeight: 1.5,
|
||||
ContextTimeWeight: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,41 +352,44 @@ func (s *Service) audit(
|
||||
|
||||
func upsertParams(profile string, w recommendation.ScoringWeights) dbq.UpsertWeightProfileDefaultsParams {
|
||||
return dbq.UpsertWeightProfileDefaultsParams{
|
||||
Profile: profile,
|
||||
BaseWeight: w.BaseWeight,
|
||||
LikeBoost: w.LikeBoost,
|
||||
RecencyWeight: w.RecencyWeight,
|
||||
SkipPenalty: w.SkipPenalty,
|
||||
JitterMagnitude: w.JitterMagnitude,
|
||||
ContextWeight: w.ContextWeight,
|
||||
SimilarityWeight: w.SimilarityWeight,
|
||||
TasteWeight: w.TasteWeight,
|
||||
Profile: profile,
|
||||
BaseWeight: w.BaseWeight,
|
||||
LikeBoost: w.LikeBoost,
|
||||
RecencyWeight: w.RecencyWeight,
|
||||
SkipPenalty: w.SkipPenalty,
|
||||
JitterMagnitude: w.JitterMagnitude,
|
||||
ContextWeight: w.ContextWeight,
|
||||
SimilarityWeight: w.SimilarityWeight,
|
||||
TasteWeight: w.TasteWeight,
|
||||
ContextTimeWeight: w.ContextTimeWeight,
|
||||
}
|
||||
}
|
||||
|
||||
func updateParams(profile string, w recommendation.ScoringWeights) dbq.UpdateWeightProfileParams {
|
||||
return dbq.UpdateWeightProfileParams{
|
||||
Profile: profile,
|
||||
BaseWeight: w.BaseWeight,
|
||||
LikeBoost: w.LikeBoost,
|
||||
RecencyWeight: w.RecencyWeight,
|
||||
SkipPenalty: w.SkipPenalty,
|
||||
JitterMagnitude: w.JitterMagnitude,
|
||||
ContextWeight: w.ContextWeight,
|
||||
SimilarityWeight: w.SimilarityWeight,
|
||||
TasteWeight: w.TasteWeight,
|
||||
Profile: profile,
|
||||
BaseWeight: w.BaseWeight,
|
||||
LikeBoost: w.LikeBoost,
|
||||
RecencyWeight: w.RecencyWeight,
|
||||
SkipPenalty: w.SkipPenalty,
|
||||
JitterMagnitude: w.JitterMagnitude,
|
||||
ContextWeight: w.ContextWeight,
|
||||
SimilarityWeight: w.SimilarityWeight,
|
||||
TasteWeight: w.TasteWeight,
|
||||
ContextTimeWeight: w.ContextTimeWeight,
|
||||
}
|
||||
}
|
||||
|
||||
func weightsFromRow(r dbq.RecommendationWeightProfile) recommendation.ScoringWeights {
|
||||
return recommendation.ScoringWeights{
|
||||
BaseWeight: r.BaseWeight,
|
||||
LikeBoost: r.LikeBoost,
|
||||
RecencyWeight: r.RecencyWeight,
|
||||
SkipPenalty: r.SkipPenalty,
|
||||
JitterMagnitude: r.JitterMagnitude,
|
||||
ContextWeight: r.ContextWeight,
|
||||
SimilarityWeight: r.SimilarityWeight,
|
||||
TasteWeight: r.TasteWeight,
|
||||
BaseWeight: r.BaseWeight,
|
||||
LikeBoost: r.LikeBoost,
|
||||
RecencyWeight: r.RecencyWeight,
|
||||
SkipPenalty: r.SkipPenalty,
|
||||
JitterMagnitude: r.JitterMagnitude,
|
||||
ContextWeight: r.ContextWeight,
|
||||
SimilarityWeight: r.SimilarityWeight,
|
||||
TasteWeight: r.TasteWeight,
|
||||
ContextTimeWeight: r.ContextTimeWeight,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user