Popularity bias and feedback loops
What it is
A ranking system trained on its own logged interactions learns to prefer what it already showed. The loop:
ranker shows item A high
│
▼
users click A (partly because it was high)
│
▼
training data says "A is good"
│
▼
ranker learns to show A higher
│
└──────────────────────────► (repeat)
Meanwhile item B, never shown, generates no clicks, so the
training data says nothing about it, and the default
interpretation of "no clicks" is "not good".
Two distinct effects that get conflated and have different fixes:
Popularity bias is a static property of the data: popular items have more interactions, so a model trained on interaction counts predicts popularity rather than relevance. It exists even in a single training run on historical data.
The feedback loop is a dynamic property of the deployed system: the model's own decisions generate the next training set, so any bias compounds over training cycles. It only appears once the model is in production and its output feeds back.
Commonly confused with position bias. Position bias is about where on the page an item appeared; popularity bias is about whether it was shown at all. Position bias is correctable with propensity weighting on rank; the feedback loop is not, because there is no observation to reweight for an item that was never displayed.
The problem it solves, or rather causes
The failure is slow, invisible in daily metrics, and expensive to reverse.
Month 1 Catalogue: 2M items. Items appearing in any top-20: 340k
Month 4 Items appearing in any top-20: 180k
Month 8 Items appearing in any top-20: 95k
Month 12 Items appearing in any top-20: 61k
Engagement metrics over the same period: flat or slightly up.
Every daily metric looks fine. Click-through rate holds, session length holds, conversion holds. What has happened is that the system has quietly stopped being able to surface 97 percent of the catalogue, and the consequences arrive later:
- New items cannot break in. A genuinely better product uploaded today has no interaction history, so it ranks low, so it gets no interactions. Cold start becomes permanent rather than temporary.
- The long tail dies. Users with niche interests are served the head, so they leave, and their departure is invisible because they were a small share of clicks.
- Supply-side incentives break. On a marketplace, sellers learn that new listings cannot gain traction, which changes who lists at all.
- The model becomes untestable. Offline evaluation on logged data cannot show the problem, because the logs contain only what was shown.
Mechanics
Where the bias enters
1. TRAINING LABELS
"Clicked" as a positive label conflates relevance with
exposure. An item clicked 10,000 times may be clicked because
it is good or because it was shown 10 million times.
2. FEATURES
A popularity feature (view count, purchase count, rating count)
is the most predictive single feature in almost every ranking
model, which is exactly the problem: the model leans on it
because it works, and it works because of the loop.
3. NEGATIVE SAMPLING
"Shown and not clicked" is a reasonable negative. "Never shown"
is NOT a negative, and treating unlabelled items as negatives
is how the loop is written directly into the objective.
4. EVALUATION
Offline metrics computed on logged impressions can only reward
ranking things the old system showed. A model that surfaces
genuinely new items measures WORSE, because those items are
unjudged and scored as irrelevant.
Point 4 is the one that makes the problem self-concealing: the evaluation methodology actively penalises the fix.
Diagnosing it
Four measurements, and the first two should be on a dashboard permanently.
# 1. CATALOGUE COVERAGE. What fraction of items are ever shown?
coverage = len({i for imp in impressions for i in imp.items}) / catalogue_size
# Track weekly. A monotone decline is the signal.
# 2. GINI COEFFICIENT of impressions. How concentrated is exposure?
def gini(counts):
x = np.sort(np.asarray(counts, dtype=float))
n = len(x)
# 0 = perfectly equal exposure, 1 = one item gets everything.
return (2 * np.sum((np.arange(1, n + 1)) * x) / (n * np.sum(x))
- (n + 1) / n)
# Compare impression Gini against a RELEVANCE baseline: if
# relevance is genuinely concentrated, high Gini is correct.
# 3. LONG-TAIL COVERAGE at k.
tail = {i for i in catalogue if popularity[i] < percentile_80}
tail_share = sum(1 for r in results if r in tail) / len(results)
# 4. NEW-ITEM TIME-TO-FIRST-IMPRESSION.
# Median days from an item entering the catalogue to its first
# appearance in a top-20. If this is rising, cold start is
# becoming permanent.
The Gini comparison is the one that avoids a false alarm. Some concentration is correct: if 5 percent of items really are what most people want, uniform exposure would be worse for users. The question is whether impression concentration exceeds relevance concentration, and that requires a relevance estimate independent of the logs, which is what the interventions below produce.
Fix 1: exploration
The structural fix, because it generates the missing observations.
def rank_with_exploration(scored, k=20, epsilon=0.10):
"""Reserve a fraction of slots for items the model is
uncertain about, not for random items. Random exploration
wastes slots on things we already know are bad."""
n_explore = max(1, int(k * epsilon))
exploit = scored[:k - n_explore]
# Thompson sampling over the uncertainty in each item's
# estimated relevance. Items with few impressions have wide
# posteriors and are sampled more often, which is exactly
# the exploration we want.
pool = [c for c in candidates if c not in exploit]
explore = sorted(pool, key=lambda c: -np.random.beta(
a=1 + c.clicks, b=1 + c.impressions - c.clicks))[:n_explore]
return interleave(exploit, explore)
Thompson sampling rather than epsilon-greedy because the exploration is targeted at uncertainty rather than spread uniformly: an item with 3 impressions and 1 click has a wide posterior and gets explored; an item with 50,000 impressions and a 0.2 percent click rate has a narrow one and does not. That is far more sample-efficient at the same slot cost.
The cost is real and should be stated: exploration slots have lower expected immediate engagement. At 10 percent of slots, expect a small measurable dip in short-term CTR. The argument is that it is an investment with a measurable return, and the return is visible in catalogue coverage and in new-item time-to-first-impression rather than in this week's CTR, which is why it needs to be agreed in advance rather than defended after the dip.
Fix 2: inverse propensity weighting
Correct the training data for known exposure bias.
# Weight each observation by the inverse probability that it was
# shown. An item shown rarely and clicked counts for much more
# than one shown constantly and clicked.
def ips_weight(impression, propensities, clip=0.01):
p = max(propensities[impression.item_id, impression.position], clip)
return 1.0 / p
The clipping matters and is the practical caveat. Very low propensities produce enormous weights and the variance of the estimator explodes, so a handful of rare observations dominate training. Clipping trades a little bias for a large variance reduction, and the clip value is a real hyperparameter rather than a detail.
And propensity has to be knowable. If ranking is deterministic, the propensity of an item that was never shown is zero and the weight is infinite. Which is why exploration and IPS are complements rather than alternatives: exploration randomises exposure enough to make propensities estimable, and IPS uses that randomisation to debias the training data. Neither works alone.
Fix 3: change what the model is trained to predict
NAIVE: predict P(click | shown)
-> learns exposure as much as relevance
BETTER: predict P(click | shown, position) and use the model
at a fixed reference position
-> removes position bias, not exposure bias
BETTER: two-tower / two-model decomposition:
P(click) = P(examined | position) x P(relevant | examined)
Train the examination model on position and layout,
the relevance model on the residual.
-> This is the standard "unbiased LTR" formulation.
ALSO: exclude or regularise raw popularity features. If
popularity is a feature, the model will use it, and it
is the most predictive feature available, so it will
dominate. Some systems drop it entirely and let
popularity emerge from genuine relevance signals.
Dropping the popularity feature is a bigger intervention than it sounds and often the right one. The counter-argument is that popularity carries real signal (popular items are often genuinely better), and the response is that the model can learn that from the content and interaction features that caused the popularity, without the direct feedback path.
Fix 4: calibrated re-ranking
Enforce exposure properties on the final list, as a constraint rather than an objective.
def calibrate(scored, k=20, tail_floor=0.15):
"""Guarantee a minimum share of long-tail items, accepting a
small relevance cost. A constraint, not a soft penalty,
because a soft penalty gets tuned away."""
tail_slots = int(k * tail_floor)
head = [x for x in scored if not is_tail(x)][:k - tail_slots]
tail = [x for x in scored if is_tail(x)][:tail_slots]
return merge_by_score(head, tail)
This is the same shape as the diversity constraint in the multi-stage funnel: a property of the set that a pointwise ranker cannot express, so it is applied to the set.
Production evidence
Chaney, Stewart and Engelhardt, "How Algorithmic Confounding in Recommendation Systems Increases Homogeneity and Decreases Utility" (RecSys 2018) is the primary simulation study of the feedback loop, showing that a system trained on its own output homogenises recommendations and reduces user utility over successive cycles even when each individual model is well-trained.
Joachims, Swaminathan and Schnabel, "Unbiased Learning-to-Rank with Biased Feedback" (WSDM 2017) is the reference for inverse propensity weighting in ranking, including the variance problem and clipping.
Abdollahpouri et al.'s work on popularity bias in recommendation documents the long-tail-coverage and Gini measurements and the calibrated re-ranking approach.
Netflix's and Spotify's published work on exploration describes production bandit systems reserving slots for uncertain items, with Spotify's work on podcast and playlist recommendation explicitly framing exploration as an investment against catalogue collapse.
YouTube's published recommendation work discusses "position bias" correction with a shallow tower predicting examination probability separately from relevance, which is the two-tower decomposition above deployed at scale.
Bandit frameworks in production (LinUCB, Thompson sampling) are documented across industry write-ups from Yahoo's early news recommendation work (Li et al., 2010) onward, and Yahoo's is notable for the offline evaluation methodology using randomised logged data, which is the only unbiased way to evaluate this offline.
The debate
The case for aggressive exploration: the feedback loop is a slow catastrophe, the metrics that detect it are not the metrics anyone watches, and by the time catalogue coverage has collapsed the recovery takes as long as the decline did. Exploration is cheap insurance and it is the only intervention that generates the data needed to evaluate the others.
The case against: exploration has a measurable cost in immediate engagement, and for a small catalogue where the head genuinely is what users want, aggressively surfacing the tail makes the product worse. Concentration is not automatically bias; sometimes the popular things are popular because they are better.
The case for IPS instead: it corrects the data without spending user-facing slots, so it is free in engagement terms. And it only works where propensities are estimable, which requires randomisation, which is exploration.
My position: run exploration at a small fixed budget, measure catalogue coverage and new-item time-to-first-impression as standing metrics, and treat popularity as a feature to be removed rather than a signal to be trusted.
The exploration budget is the decision I hold most firmly, and I would set it at roughly 5 to 10 percent of slots with Thompson sampling rather than uniform randomisation, because targeted exploration is far more sample-efficient at the same cost. Crucially, it has to be agreed in advance, because it produces a small immediate dip in engagement and the argument for it is a long-run one. A team that ships exploration and then defends it against a CTR regression will lose, so the agreement is "we accept a 1 to 2 percent CTR cost and we will judge it on coverage and new-item metrics in six months".
The measurement I would insist on is catalogue coverage and new-item time-to-first-impression on the standing dashboard, because the failure is invisible in every metric anyone currently watches. A system can lose 90 percent of its effective catalogue over a year with flat engagement, and the first visible symptom is usually a supply-side complaint rather than a metric.
On popularity as a feature I would take the stronger position: remove it and let popularity emerge. It is the most predictive single feature in almost any ranking model, which is exactly why it is dangerous: the model leans on it, it works because of the loop, and the loop tightens. The genuine signal in popularity is available through the content and interaction features that caused it.
Where I would be careful: concentration is not automatically a problem. Before intervening I would compare impression Gini against a relevance-based Gini, because if relevance genuinely is concentrated then flattening exposure makes the product worse for most users. The intervention is justified when exposure concentration exceeds relevance concentration, and establishing that requires exploration data, which is another reason exploration comes first.
Follow-up Q&A
"What is the feedback loop, exactly?" The ranker shows an item, users click it partly because it was shown prominently, that click becomes a positive training label, and the model learns to show it higher. Meanwhile an item never shown generates no clicks, and the default interpretation of no clicks is "not good". So the model's own decisions generate its next training set and any bias compounds over training cycles. It is distinct from popularity bias, which is a static property of historical data and exists even in a single training run.
"How would you detect it?" Four measurements, and the first two belong on a permanent dashboard. Catalogue coverage: what fraction of items appear in any top-20, tracked weekly, where a monotone decline is the signal. Gini coefficient of impressions, compared against a relevance-based Gini, because some concentration is correct and the problem is when exposure concentration exceeds relevance concentration. Long-tail share of results. And median time-to-first-impression for new items, which tells you whether cold start has become permanent.
"Why won't engagement metrics catch it?" Because they stay flat. A system can go from 340,000 items appearing in top-20 results to 61,000 over a year with click-through rate, session length and conversion all steady, because the head is genuinely engaging. The consequences are delayed: new items cannot break in, users with niche interests leave quietly, and supply-side participants stop listing. The first visible symptom is usually a seller complaint rather than a metric.
"What is the fix?" Exploration is the structural one, because it is the only intervention that generates the missing observations. I would reserve roughly 5 to 10 percent of slots and use Thompson sampling rather than uniform randomisation, so exploration targets items with wide posteriors, the ones with few impressions, rather than spreading uniformly across items we already know are bad. Then inverse propensity weighting on the training data, which needs the randomisation exploration provides to make propensities estimable at all.
"Why Thompson sampling rather than epsilon-greedy?" Sample efficiency at the same slot cost. Epsilon-greedy spends its exploration budget uniformly, including on items with 50,000 impressions and a well-established 0.2 percent click rate, where there is nothing left to learn. Thompson sampling draws from each item's posterior, so an item with three impressions and one click has a wide posterior and gets explored, and a well-measured item does not. Same cost, far more information.
"Exploration costs engagement. How do you get it approved?" By agreeing the trade in advance rather than defending it afterwards. The proposal is: we accept a one to two percent click-through cost, and we judge it in six months on catalogue coverage and new-item time-to-first-impression rather than on this quarter's CTR. A team that ships exploration and then argues about a CTR dip loses that argument, because the cost is immediate and visible and the benefit is delayed and diffuse. The agreement has to precede the dip.
"Can you fix it with IPS alone, without spending slots?" No, and the reason is structural. Inverse propensity weighting reweights observations by the probability they were shown, and an item that was never shown has a propensity of zero and an infinite weight. There is no observation to reweight. So IPS corrects for position bias within what was shown, and it cannot correct for exposure bias about what was never shown. Exploration generates the propensity variation IPS needs, which is why they are complements.
"What about the popularity feature?" I would remove it, which is a stronger position than most people take. It is the single most predictive feature in almost any ranking model, and that is exactly the problem: the model leans on it, it is predictive because of the loop, and using it tightens the loop. The genuine signal in popularity, that popular things are often better, is available through the content and interaction features that caused the popularity, without the direct feedback path.
"Isn't concentrated exposure sometimes correct?" Yes, and that is the check that prevents a false alarm. If 5 percent of items genuinely are what most users want, uniform exposure makes the product worse. So the test is whether impression Gini exceeds a relevance-based Gini, not whether impression Gini is high. Establishing the relevance baseline requires exposure data that is not itself biased, which is exploration data again, so exploration comes first in the sequence for that reason too.
Common misconceptions
"Popularity bias and position bias are the same problem." Position bias is about where on the page an item appeared and is correctable with propensity weighting. Popularity bias is about whether it was shown at all, and there is no observation to reweight.
"Offline evaluation will show the problem." It cannot. Offline metrics on logged impressions reward ranking what the old system showed, and a model surfacing genuinely new items measures worse because those items are unjudged.
"Exploration means showing random items." Random exploration wastes slots on items already known to be bad. Uncertainty-targeted exploration gets far more information per slot.
"Not clicked means not relevant." Not shown means not observed. Treating unshown items as negatives writes the feedback loop directly into the training objective.
"High concentration means bias." Only if it exceeds the concentration of genuine relevance. Establishing that requires unbiased exposure data.
Interview delivery note
Distinguish the two effects immediately, because conflating them is the common error: "There are two things here. Popularity bias is static: popular items have more interactions so a model trained on counts predicts popularity. The feedback loop is dynamic: the model's own decisions generate its next training set, so bias compounds over cycles. And neither is position bias, which is about where on the page an item appeared and is the one that's actually correctable with propensity weighting."
Make the failure concrete and name why it hides: "The thing that makes this dangerous is that it's invisible in every metric anyone watches. I've seen a system go from 340,000 items appearing in any top-20 to 61,000 over a year, with click-through rate, session length and conversion all flat. The head is genuinely engaging, so engagement holds while the system quietly loses the ability to surface 97 percent of the catalogue."
Give the fix with its mechanism, not just its name: "Exploration is the structural fix because it's the only thing that generates the missing observations. Five to ten percent of slots, Thompson sampling rather than uniform, so it targets items with wide posteriors instead of spending budget on items with fifty thousand impressions where there's nothing left to learn."
The line that shows you have shipped this: "and the exploration cost has to be agreed in advance. It produces an immediate one to two percent CTR dip and the benefit shows up in coverage metrics six months later. A team that ships exploration and then argues about the dip loses, because the cost is visible and the benefit isn't yet."
And show the check that avoids over-correcting: "though before intervening I'd compare impression Gini against a relevance-based Gini, because some concentration is correct. If five percent of items genuinely are what people want, flattening exposure makes the product worse. The problem is when exposure concentration exceeds relevance concentration."
Further reading
- Chaney, Stewart and Engelhardt, "How Algorithmic Confounding in Recommendation Systems Increases Homogeneity and Decreases Utility" (RecSys 2018).
- Joachims, Swaminathan and Schnabel, "Unbiased Learning-to-Rank with Biased Feedback" (WSDM 2017).
- Abdollahpouri, Burke and Mobasher, "Managing Popularity Bias in Recommender Systems with Personalized Re-ranking" (2019).
- Li, Chu, Langford and Schapire, "A Contextual-Bandit Approach to Personalized News Article Recommendation" (WWW 2010), including the unbiased offline evaluation method.
- Zhao et al., "Recommending What Video to Watch Next: A Multitask Ranking System" (RecSys 2019), for the shallow-tower position-bias correction in production.