Hybrid Recommendation Strategies¶
Combine multiple recommenders for more robust and effective recommendation systems.
Overview¶
Purpose: Leverage strengths of different recommender types by combining them strategically.
Common Patterns: - Ensemble (weighted combination) - Fallback (primary with backup) - Stage-based (filter then rank) - Context-based (switch by scenario)
Why Hybrid?¶
Different recommenders excel in different scenarios:
| Recommender | Strength | Weakness |
|---|---|---|
| RankingRecommender | Warm items with interactions | Cold-start items |
| ContextualBanditsRecommender | Exploration | Requires interaction data |
item_subset / retriever |
Hard business constraints | Not a model — narrows candidates before ranking |
Solution: Combine them!
Pattern 1: Fallback Strategy¶
Use a primary recommender, fall back to secondary when needed.
Cold-Start Fallback¶
def hybrid_recommend_with_fallback(user_id, item_id, user_features, top_k=5):
"""
Use RankingRecommender for warm items,
ContextualBanditsRecommender for cold items (exploration).
"""
# Check if item has sufficient interaction data
interaction_count = get_item_interaction_count(item_id)
if interaction_count >= MIN_INTERACTIONS:
# Warm item: use collaborative filtering
recommender = ranking_recommender
else:
# Cold item: explore with bandits
recommender = bandits_recommender
interactions_df = pd.DataFrame({"USER_ID": [user_id]})
users_df = pd.DataFrame({"USER_ID": [user_id], **user_features})
return recommender.recommend(interactions_df, users_df, top_k=top_k)
Error Fallback¶
def safe_recommend_with_fallback(user_id, user_features, top_k=5):
"""
Try ML recommender, fall back to popular items on failure.
"""
try:
# Primary: ML-based recommender
recommendations = ml_recommender.recommend(
interactions=interactions_df,
users=users_df,
top_k=top_k
)
return recommendations, 'ml'
except Exception as e:
logger.error(f"ML recommender failed: {e}")
# Fallback: Popular items
recommendations = get_popular_items(top_k)
return recommendations, 'popular'
Pattern 2: Ensemble (Weighted Combination)¶
Combine scores from multiple recommenders.
Score-Level Ensemble¶
def ensemble_recommend(user_id, user_features, top_k=5, weights=None):
"""
Combine scores from multiple recommenders with weighted average.
"""
if weights is None:
weights = {'ranking': 0.6, 'bandits': 0.4}
interactions_df = pd.DataFrame({"USER_ID": [user_id]})
users_df = pd.DataFrame({"USER_ID": [user_id], **user_features})
# Get scores from each recommender
scores_ranking = ranking_recommender.score_items(interactions_df, users_df)
scores_bandits = bandits_recommender.score_items(interactions_df, users_df)
# Normalize scores (0-1 range)
scores_ranking_norm = (scores_ranking - scores_ranking.min()) / (scores_ranking.max() - scores_ranking.min())
scores_bandits_norm = (scores_bandits - scores_bandits.min()) / (scores_bandits.max() - scores_bandits.min())
# Weighted combination
ensemble_scores = (
weights['ranking'] * scores_ranking_norm +
weights['bandits'] * scores_bandits_norm
)
# Get top-k
top_items = ensemble_scores.iloc[0].nlargest(top_k).index.tolist()
return np.array([top_items])
Rank-Level Ensemble¶
def rank_fusion_recommend(user_id, user_features, top_k=5):
"""
Combine rankings from multiple recommenders (Borda count).
"""
interactions_df = pd.DataFrame({"USER_ID": [user_id]})
users_df = pd.DataFrame({"USER_ID": [user_id], **user_features})
# Get recommendations from each
recs_a = ranking_recommender.recommend(interactions_df, users_df, top_k=20)
recs_b = bandits_recommender.recommend(interactions_df, users_df, top_k=20)
# Borda count: assign points based on rank
item_scores = {}
for rank, item in enumerate(recs_a[0]):
item_scores[item] = item_scores.get(item, 0) + (20 - rank)
for rank, item in enumerate(recs_b[0]):
item_scores[item] = item_scores.get(item, 0) + (20 - rank)
# Sort by combined score
sorted_items = sorted(item_scores.items(), key=lambda x: x[1], reverse=True)
top_items = [item for item, score in sorted_items[:top_k]]
return np.array([top_items])
Pattern 3: Stage-Based Pipeline¶
Multi-stage filtering and ranking.
Filter → Rank¶
def staged_recommend(user_id, user_features, top_k=5):
"""
Stage 1: Business rules produce an allowed candidate set
Stage 2: RankingRecommender ranks within that set
"""
# Stage 1: Your policy / compliance / inventory filter
candidate_items = filter_catalog_by_business_rules(
user_id=user_id,
user_location=user_features['location'],
user_age=user_features['age'],
)
# Stage 2: Rank with ML
ranking_recommender.set_item_subset(candidate_items)
interactions_df = pd.DataFrame({"USER_ID": [user_id]})
users_df = pd.DataFrame({"USER_ID": [user_id], **user_features})
recommendations = ranking_recommender.recommend(
interactions=interactions_df,
users=users_df,
top_k=top_k
)
return recommendations
Coarse → Fine Ranking¶
def two_stage_ranking(user_id, user_features, top_k=5):
"""
Stage 1: Fast model retrieves top-100 candidates
Stage 2: Expensive model re-ranks to top-k
"""
# Stage 1: Fast retrieval (e.g., lightweight model)
fast_recommender.set_item_subset(all_items)
candidates_df = fast_recommender.score_items(interactions_df, users_df)
top_100_items = candidates_df.iloc[0].nlargest(100).index.tolist()
# Stage 2: Precise ranking (e.g., DeepFM)
precise_recommender.set_item_subset(top_100_items)
final_recommendations = precise_recommender.recommend(
interactions=interactions_df,
users=users_df,
top_k=top_k
)
return final_recommendations
Pattern 4: Context-Based Switching¶
Choose recommender based on context.
User-Based Switching¶
def context_aware_recommend(user_id, user_features, top_k=5):
"""
Choose recommender based on user characteristics.
"""
user_history_count = get_user_interaction_count(user_id)
if user_history_count < 5:
# New user: explore with bandits
recommender = bandits_recommender
strategy = 'bandits_cold'
elif user_history_count < 20:
# Medium history: use bandits for exploration
recommender = bandits_recommender
strategy = 'bandits'
else:
# Rich history: use collaborative filtering
recommender = ranking_recommender
strategy = 'collaborative'
interactions_df = pd.DataFrame({"USER_ID": [user_id]})
users_df = pd.DataFrame({"USER_ID": [user_id], **user_features})
recommendations = recommender.recommend(interactions_df, users_df, top_k=top_k)
# Log strategy used
logger.info(f"Used {strategy} for user {user_id}")
return recommendations
Time-Based Switching¶
def time_aware_recommend(user_id, user_features, current_hour, top_k=5):
"""
Use different recommenders for different times of day.
"""
if 9 <= current_hour <= 17:
# Business hours: prioritize work-related items
recommender = work_recommender
elif 18 <= current_hour <= 22:
# Evening: prioritize entertainment
recommender = entertainment_recommender
else:
# Off-hours: use general recommender
recommender = general_recommender
recommendations = recommender.recommend(...)
return recommendations
Pattern 5: Diversity Enhancement¶
Combine recommenders to increase diversity.
def diverse_recommend(user_id, user_features, top_k=5):
"""
Mix recommendations from different sources for diversity.
"""
# Get recommendations from each source
collaborative_recs = ranking_recommender.recommend(
interactions_df, users_df, top_k=10
)[0]
bandits_recs = bandits_recommender.recommend(
interactions_df, users_df, top_k=10
)[0]
# Interleave for diversity
diverse_recs = []
for i in range(top_k):
if i % 2 == 0 and len(collaborative_recs) > i//2:
diverse_recs.append(collaborative_recs[i//2])
elif len(bandits_recs) > i//2:
diverse_recs.append(bandits_recs[i//2])
return np.array([diverse_recs[:top_k]])
Complete Example: Production Hybrid System¶
class HybridRecommender:
"""
Production-ready hybrid recommender combining multiple strategies.
"""
def __init__(self, ranking_rec, bandits_rec, rule_rec):
self.ranking_rec = ranking_rec
self.bandits_rec = bandits_rec
self.rule_rec = rule_rec
self.min_item_interactions = 10
self.min_user_interactions = 5
def recommend(self, user_id, user_features, top_k=5):
"""
Multi-strategy hybrid recommendation.
"""
# Stage 1: Business rules filter
valid_items = self.rule_rec.get_valid_items(user_id, user_features)
# Stage 2: Choose primary recommender
user_history = get_user_interaction_count(user_id)
if user_history < self.min_user_interactions:
# New user: explore with bandits
primary_rec = self.bandits_rec
strategy = 'bandits_cold'
elif user_history < 20:
# Growing user: explore with bandits
primary_rec = self.bandits_rec
strategy = 'bandits'
else:
# Established user: collaborative filtering
primary_rec = self.ranking_rec
strategy = 'collaborative'
# Stage 3: Get primary recommendations
primary_rec.set_item_subset(valid_items)
interactions_df = pd.DataFrame({"USER_ID": [user_id]})
users_df = pd.DataFrame({"USER_ID": [user_id], **user_features})
try:
primary_recs = primary_rec.recommend(
interactions_df, users_df, top_k=top_k
)
except Exception as e:
logger.error(f"Primary recommender failed: {e}")
# Fallback to rule-based
primary_recs = self.rule_rec.recommend(
interactions_df, users_df, top_k=top_k
)
strategy = 'fallback_rules'
# Stage 4: Boost cold items (diversification)
final_recs = self._boost_cold_items(primary_recs[0], top_k)
# Log strategy used
statsd.increment('recommender.strategy', tags=[f'strategy:{strategy}'])
return np.array([final_recs])
def _boost_cold_items(self, recommendations, top_k):
"""
Replace last 20% of recommendations with cold items for exploration.
"""
num_cold = max(1, int(top_k * 0.2))
warm_recs = recommendations[:-num_cold]
# Get cold item recommendations via exploration
cold_recs = self.bandits_rec.recommend(interactions_df, users_df, top_k=num_cold)[0]
return list(warm_recs) + list(cold_recs)
Best Practices¶
1. Weight Tuning¶
# A/B test different weights
weights_configs = [
{'ranking': 0.7, 'bandits': 0.3},
{'ranking': 0.6, 'bandits': 0.4},
{'ranking': 0.5, 'bandits': 0.5}
]
for weights in weights_configs:
recommendations = ensemble_recommend(..., weights=weights)
ndcg = evaluate(recommendations, ...)
print(f"Weights {weights}: NDCG = {ndcg}")
2. Monitor Strategy Usage¶
def monitored_hybrid_recommend(user_id, user_features, top_k=5):
strategy = choose_strategy(user_id, user_features)
# Log strategy usage
statsd.increment('hybrid.strategy', tags=[f'strategy:{strategy}'])
recommendations = execute_strategy(strategy, ...)
return recommendations
3. Graceful Degradation¶
recommender_priority = [
('primary', ml_recommender),
('secondary', bandits_recommender),
('tertiary', popular_items)
]
for name, recommender in recommender_priority:
try:
return recommender.recommend(...)
except Exception as e:
logger.warning(f"{name} failed: {e}")
continue
Next Steps¶
- RankingRecommender - Learn about collaborative filtering
- Production Guide - Deploy hybrid systems
- Evaluation Guide - Evaluate hybrid approaches