19 — Diversity, Fairness, and Serendipity
“Accuracy alone makes echo chambers. The best feeds know when to surprise you.”
Introduction
Recommender systems optimized purely for predicted clicks converge to filter bubbles, popularity dominance, and demographic unfairness. Real products need to balance accuracy with diversity, novelty, serendipity, and fairness — both for users (better experience) and for content producers (fair exposure).
This post covers re-ranking strategies (MMR, DPP), exposure-aware fairness, calibrated recommendations, and how to measure beyond accuracy.
1. The Beyond-Accuracy Goals
flowchart LR
A[Accuracy<br/>NDCG, CTR] --- B[Diversity<br/>variety in slate]
B --- C[Novelty<br/>unfamiliar items]
C --- D[Serendipity<br/>unexpected & relevant]
D --- E[Fairness<br/>users + producers]
E --- F[Calibration<br/>match user's taste mix]
Each is a different lens; they often conflict pairwise.
2. Diversity Metrics
Intra-List Diversity (ILD)
\(\text{ILD}(L) = \frac{2}{|L|(|L|-1)} \sum_{i,j \in L, i<j} d(i, j)\)
where $d$ is a distance (1 − cosine similarity, or categorical Jaccard).
Category Coverage
Fraction of distinct categories in the slate.
Gini / Long-Tail Share
Catalog-wide: how spread is your recommendation traffic? Lower Gini → fairer to long-tail items.
3. Re-Ranking for Diversity
MMR — Maximal Marginal Relevance (Carbonell 1998)
\(\text{MMR} = \arg\max_{i \in C \setminus L}\left[\lambda \cdot s_i - (1-\lambda) \max_{j \in L} \text{sim}(i, j)\right]\)
Greedy: at each step pick the item that’s relevant but dissimilar to already-selected.
import numpy as np
def mmr_rerank(candidates, scores, sim, k=10, lam=0.7):
selected, remaining = [], list(range(len(candidates)))
while len(selected) < k and remaining:
if not selected:
i = int(np.argmax(scores[remaining]))
else:
max_sim = sim[np.ix_(remaining, selected)].max(axis=1)
obj = lam * scores[remaining] - (1 - lam) * max_sim
i = int(np.argmax(obj))
selected.append(remaining.pop(i))
return [candidates[i] for i in selected]
Determinantal Point Processes (DPP)
Probabilistic models that score sets by a determinant of a kernel matrix $L$:
\[P(L_S) \propto \det(L_S)\]The determinant rewards diverse subsets — high-volume parallelepipeds. Used at YouTube for slate diversity (Wilhelm 2018).
import numpy as np
def greedy_dpp(L_kernel, k):
"""Greedy MAP for DPP using sequential Cholesky updates."""
N = L_kernel.shape[0]
cis = np.zeros((k, N))
di2s = np.diag(L_kernel).copy()
selected = []
for i in range(k):
j = int(np.argmax(di2s))
selected.append(j)
L_ji = L_kernel[j]
if i > 0:
eis = (L_ji - cis[:i, :].T @ cis[:i, j]) / np.sqrt(di2s[j])
else:
eis = L_ji / np.sqrt(di2s[j])
cis[i] = eis
di2s -= eis**2
di2s[j] = -np.inf
return selected
Submodular Set Functions
Generalize MMR/DPP — anything that exhibits diminishing returns can be greedily optimized with a $1 - 1/e$ guarantee.
4. Calibrated Recommendations (Steck 2018)
If a user historically watches 60% drama / 30% comedy / 10% horror, the slate should reflect that mix — not just push the highest-scoring drama.
Formally, pick $L$ to minimize KL divergence between user’s category histogram and the slate’s:
\[D_{KL}(p_u \| q_L)\]Beautiful technique — combats the “rabbit hole” effect where models reinforce strongest signals.
5. Serendipity
Serendipity ≈ relevance × unexpectedness. Heuristic:
\[\text{Sere}(L, u) = \frac{1}{|L|}\sum_{i \in L} \text{rel}(u, i) \cdot \text{unexp}(i, u)\]where unexp = 1 − similarity to user’s average history.
Tricky to evaluate offline — usually validated via user studies.
6. Fairness for Producers
Equal Exposure
Each item gets a share of exposure proportional to its merit:
\[\frac{\text{Exposure}_i}{\text{Merit}_i} \approx \text{const}\]Group Fairness
Items grouped by protected attribute (gender of author, country of origin). Constraints:
- Demographic parity: equal exposure per group.
- Equal opportunity: equal click-through given relevance.
Stochastic Rankings (Singh & Joachims 2018)
Sample rankings from a distribution that satisfies fairness constraints in expectation.
7. Fairness for Users
- Recommendation quality parity across demographic slices.
- Avoidance of harmful recommendations (radicalization, addictive content).
- Privacy-preserving recommendations (differentially private MF).
8. Pros & Cons
| Method | Pros | Cons |
|---|---|---|
| MMR | Simple, plug-in | Doesn’t model set-level utility |
| DPP | Principled set diversity | Kernel design hard |
| Calibration | Matches user taste profile | Requires reliable category labels |
| Stochastic ranking | Strong fairness guarantees | Click variance up |
9. Production Integration
flowchart LR
A[Ranker top-N] --> B[Diversity re-rank<br/>MMR / DPP]
B --> C[Calibration adjust]
C --> D[Fairness budget<br/>exposure constraints]
D --> E[Top-K served]
E --> F[Metrics: NDCG + ILD + Gini + Calibration]
Re-rank stage is cheap — pure-logic, runs on CPU, easy to A/B independently of the model.
10. End-to-End: MMR + Calibration on MovieLens
import pandas as pd, numpy as np
from sklearn.metrics.pairwise import cosine_similarity
movies = pd.read_csv("movies.csv")
genres = movies["genres"].str.get_dummies("|").values # n x g
sim = cosine_similarity(genres)
# Assume scores from your model
scores = np.random.rand(len(movies))
# User's profile (e.g., from their history's genre mean)
user_profile = np.array([0.5, 0.3, 0.2] + [0]*(genres.shape[1]-3))
user_profile /= user_profile.sum()
# Calibrated MMR
def cal_mmr(scores, sim, user_profile, item_genres, k=10, lam=0.6, beta=0.2):
chosen = []
cand = list(range(len(scores)))
while len(chosen) < k:
best, best_obj = None, -1e9
for i in cand:
div = (max((sim[i, j] for j in chosen), default=0)) if chosen else 0
slate_dist = item_genres[chosen + [i]].mean(0) if chosen else item_genres[i]
slate_dist = slate_dist / max(slate_dist.sum(), 1e-9)
kl = np.sum(user_profile * (np.log(user_profile + 1e-9) - np.log(slate_dist + 1e-9)))
obj = lam*scores[i] - (1-lam)*div - beta*kl
if obj > best_obj: best, best_obj = i, obj
chosen.append(best); cand.remove(best)
return chosen
top = cal_mmr(scores, sim, user_profile, genres, k=10)
print(movies.iloc[top][["title","genres"]])
11. Pitfalls
- Adding diversity without measuring opportunity cost in CTR.
- Treating category labels as ground truth when they’re noisy (multi-label, missing).
- Diversity at slate but not over time — boring sessions for repeat users.
- Fairness intervention without causal analysis — may shift bias rather than fix it.
- Calibration to wrong reference distribution (global popularity vs personal taste).
12. Public Datasets
- MovieLens — clean category labels — https://grouplens.org/datasets/movielens/
- Spotify MPD — diverse playlists — https://www.aicrowd.com/challenges/spotify-million-playlist-dataset-challenge
- MovieLens-Fairness — added demographic — https://github.com/google-research-datasets/movielens-fairness
- Last.fm 360K — popularity bias studies — http://ocelma.net/MusicRecommendationDataset/
- Amazon Books — author/publisher fairness — https://nijianmo.github.io/amazon/
13. Further Reading
- Carbonell & Goldstein, The Use of MMR (SIGIR 1998)
- Steck, Calibrated Recommendations (RecSys 2018)
- Kulesza & Taskar, Determinantal Point Processes for Machine Learning (NOW 2012)
- Wilhelm et al., Practical Diversified Recommendations on YouTube with DPP (CIKM 2018)
- Singh & Joachims, Fairness of Exposure in Rankings (KDD 2018)
- Mehrotra et al., Towards a Fair Marketplace: Counterfactual Evaluation of the Trade-off between Relevance, Fairness and Satisfaction (CIKM 2018)