Source code for corerec.retrieval.popularity
"""
Popularity-based Retrieval
Simple baseline that returns most popular items.
Useful for cold-start users and as a component in ensembles.
"""
import time
from typing import Any, Dict, List, Optional
import numpy as np
from .base import BaseRetriever, Candidate, RetrievalResult
[docs]
class PopularityRetriever(BaseRetriever):
"""
Retriever that returns most popular items.
"Popular" can mean different things:
- Most interactions (views, clicks, purchases)
- Highest average rating
- Most recent trending
This is a simple but effective baseline, especially for:
- Cold-start users with no history
- Fallback when other retrievers fail
- Diversity injection in ensembles
Example:
retriever = PopularityRetriever()
retriever.fit(item_ids, interaction_counts)
candidates = retriever.retrieve(user_id=None, top_k=50)
"""
[docs]
def __init__(
self,
name: str = "popularity",
time_decay: Optional[float] = None,
):
"""
Args:
name: identifier for this retriever
time_decay: if set, apply exponential decay based on recency
"""
super().__init__(name=name)
self.time_decay = time_decay
# populated by fit()
self.item_ids: List[Any] = []
self.popularity_scores: np.ndarray = np.array([])
self._sorted_indices: np.ndarray = np.array([])
[docs]
def fit(
self,
item_ids: List[Any],
scores: Optional[List[float]] = None,
interaction_counts: Optional[List[int]] = None,
timestamps: Optional[List[float]] = None,
**kwargs
) -> "PopularityRetriever":
"""
Compute popularity scores for items.
Args:
item_ids: unique identifiers for items
scores: pre-computed popularity scores (if available)
interaction_counts: raw interaction counts to use as popularity
timestamps: if provided with time_decay, applies recency weighting
Provide either scores or interaction_counts.
"""
self.item_ids = list(item_ids)
if scores is not None:
self.popularity_scores = np.asarray(scores, dtype=float)
elif interaction_counts is not None:
self.popularity_scores = np.asarray(interaction_counts, dtype=float)
else:
# default: uniform popularity
self.popularity_scores = np.ones(len(item_ids))
# apply time decay if configured
if self.time_decay is not None and timestamps is not None:
ts = np.asarray(timestamps)
max_ts = ts.max()
decay = np.exp(-self.time_decay * (max_ts - ts))
self.popularity_scores = self.popularity_scores * decay
# pre-sort for fast retrieval
self._sorted_indices = np.argsort(self.popularity_scores)[::-1]
self._is_fitted = True
return self
[docs]
def retrieve(
self,
query: Any = None,
top_k: int = 100,
exclude_items: Optional[List[Any]] = None,
**kwargs
) -> RetrievalResult:
"""
Retrieve most popular items.
Args:
query: ignored (popularity is query-independent)
top_k: number of items to return
exclude_items: items to exclude from results
Returns:
RetrievalResult with most popular items
"""
self._check_fitted()
start = time.perf_counter()
exclude_set = set(exclude_items) if exclude_items else set()
candidates = []
for idx in self._sorted_indices:
item_id = self.item_ids[idx]
if item_id in exclude_set:
continue
candidates.append(Candidate(
item_id=item_id,
score=float(self.popularity_scores[idx]),
source=self.name,
))
if len(candidates) >= top_k:
break
elapsed = (time.perf_counter() - start) * 1000
return RetrievalResult(
candidates=candidates,
query_id=query,
retriever_name=self.name,
timing_ms=elapsed,
)
[docs]
def get_item_popularity(self, item_id: Any) -> float:
"""Get popularity score for a specific item."""
try:
idx = self.item_ids.index(item_id)
return float(self.popularity_scores[idx])
except ValueError:
return 0.0