P08 — End-to-End Recommendation System
Run it first. There is a companion page that builds this project's machinery as numbered, independently runnable blocks and then assembles them into one measured system: P08 hands-on — block by block (
handson/h*.py). Every number on it was produced by running the code. Read it alongside the milestones below.
Medium · 66 hours · Weeks 84–89 · Stage 4 · Python
The bar is higher here. This is your professional domain. A project that would be a strong result in Stage 1 is a mediocre one in Stage 4. Score yourself against what a specialist would expect, not against what a newcomer would achieve.
Table of Contents
- The Loop, Instantiated
- Why This Project Matters
- Prerequisites
- Duration and Size
- Central Technical Questions
- Architecture
- Showcase — Do This Before You Start
- Implementation Milestones
- Concepts To Study
- Primary-Source Readings
- Experiments
- Benchmarks and Metrics
- Correctness Tests
- Failure Tests
- Expected Difficulties
- Scope Boundaries
- Deliverables
- Exit Criteria
- Extension Ideas
- Connections
- References
The Loop, Instantiated
| Step | For this project |
|---|---|
| 1. Problem | Given a user's history and a catalogue that turns over daily, choose \(k\) items they will engage with — where "engage" is a proxy for a thing you cannot measure |
| 2. Constraints | Cold items appear constantly (news). Cold users are the majority. Feedback is implicit, biased, and delayed. Latency budget is tens of milliseconds |
| 3. Naive design | Yours. Most people build: average the embeddings of clicked items, ANN search, return top-k |
| 4. Predicted failure | Predict what breaks first. Candidates: popularity collapse, duplicate stories, staleness, filter bubble, cold start |
| 5. Minimal implementation | Exactly the naive design above, measured |
| 6. Correctness | No already-seen item; no duplicates; every returned item exists and passes filters |
| 7. Instrumentation | Per-stage latency, candidate-set overlap, exposure distribution, freshness distribution |
| 8. Baseline | Four baselines: popularity, recency, content-similarity, random. Random is not a joke — it calibrates every other number |
| 9. Bottleneck | Is quality limited by retrieval recall, by ranking, or by the user representation? Design an experiment that separates them |
| 10. Hypothesis | EMA profile beats mean profile for users with drifting interests, above a drift rate you specify |
| 11. Modification | EMA with a swept decay rate |
| 12. Experiment | Decay sweep × user segment × drift rate |
| 13. Failure analysis | Which users got worse? Segment before concluding |
| 14. Report | Including the metric that improved while the system got worse |
Why This Project Matters
You build these professionally. So the value here is not "learn recommenders" — it is learn to evaluate one honestly, which is a genuinely different and rarer skill.
The central hazard: almost every accuracy metric can be improved by recommending popular items, because popular items are popular. A system that quietly collapses onto the head of the catalogue will show a rising NDCG and a falling product. The only defence is a metric suite that includes coverage, novelty, and exposure concentration alongside accuracy, and the discipline to report all of them every time.
The second reason is specific to news, and to you: a news recommender is a system where the catalogue turns over faster than the user model converges. That makes cold start the normal case rather than the exception, and it makes freshness a first-class metric rather than a tie-breaker. Most recommender literature assumes a stable catalogue and does not transfer.
Prerequisites
- P02 complete — retrieval calls your index, not a library's
- P01/P13 helpful — you can generate embeddings and reason about their geometry
- From
math.md: §Ranking Metrics (2 h), §Implicit Feedback and Bias (2 h)
Duration and Size
Medium, 66 hours, 6 weeks. Shorter than comparable projects because you start with domain knowledge. That is deliberate, and the exit bar compensates.
| Tier | Contents | Hours |
|---|---|---|
| MVI | Item embeddings, mean user profile, ANN retrieval, score-and-sort ranking, seen-filtering, and the four baselines with a full metric suite. | 32 |
| Standard | + EMA profiles at multiple decay rates, hybrid retrieval, a feature-based reranker, freshness and diversity terms, near-duplicate detection, cold-start handling, calibration measurement. | 66 |
| Extension | A two-tower retrieval model trained on your interactions; or a bandit explore/exploit layer with measured regret. | +30–45 |
Central Technical Questions
- What is a user's interest, as a mathematical object? A point, a distribution, a set of points, a trajectory? Each choice makes different failures possible.
- How fast should a profile forget? Derive the relationship between decay rate and effective memory before you sweep it.
- Where does recall actually get lost — retrieval or ranking? Most teams optimise the wrong stage because they never separate them.
- What does diversity cost in accuracy, and is the trade favourable? Measure it; do not assume.
- How do you evaluate a recommender offline when the logs were produced by a different policy? This is the hardest question in the project and the one with no clean answer.
- Which of your improvements is just popularity bias in disguise?
The EMA profile, derived
An exponentially-weighted profile updates as \(u_t = \alpha x_t + (1-\alpha) u_{t-1}\), giving item \(i\) interactions ago a weight \(\alpha(1-\alpha)^i\). The half-life — interactions until an item's weight halves — is \(h = \ln(0.5)/\ln(1-\alpha)\):
| α | half-life (interactions) | effective window 1/α | weight on last 10 interactions |
|---|---|---|---|
| 0.02 | 34.31 | 50.0 | 18.3% |
| 0.05 | 13.51 | 20.0 | 40.1% |
| 0.10 | 6.58 | 10.0 | 65.1% |
| 0.20 | 3.11 | 5.0 | 89.3% |
| 0.30 | 1.94 | 3.3 | 97.2% |
| 0.50 | 1.00 | 2.0 | 99.9% |
At α=0.5 the profile is essentially "the last two things you clicked". At α=0.02 it is a slow-moving average that will not notice a genuine interest change for a month. The mean profile is the α→0 limit with equal weights, and it can never adapt at all.
Choose α from a stated assumption about how fast interests drift, then test that assumption. In P09 you will simulate populations with known drift rates and can finally check whether your chosen α was right — which is the single best reason to build the simulator.
Recall propagation through the pipeline
Retrieval-then-ranking is a funnel, and the funnel has a ceiling:
\[ \text{recall}_{\text{end-to-end}}@k \le \text{recall}_{\text{retrieval}}@K \]
If retrieval recall@K is 0.90, no ranker — however good — can exceed 0.90 end-to-end. This is obvious once stated and routinely ignored: teams spend quarters on ranking models while the retrieval stage silently caps them.
Your ANN index's recall from P02 is therefore a hard ceiling on your recommender's quality, and E5 measures exactly how much of it you are losing. That linkage between two of your own projects is one of the most satisfying measurements in the journey.
Popularity concentration
If item popularity follows a Zipf distribution with exponent α, the head's share of total engagement mass is:
| Zipf α | top 1% of items | top 10% of items |
|---|---|---|
| 0.5 | 9.4% | 31.1% |
| 0.8 | 30.0% | 57.1% |
| 1.0 | 53.0% | 76.5% |
| 1.2 | 75.1% | 90.3% |
At α=1.0, recommending only the top 1% of the catalogue captures 53% of all
engagement. A trivial bestseller list will beat a mediocre personalised model on
accuracy metrics, and it will do so while covering 1% of the catalogue. This is why
the popularity baseline is mandatory and why coverage is reported alongside NDCG,
always. tools/metrics.py computes both.
Architecture
interactions ──► profile builder ──┬─ mean vector
├─ EMA vector (α swept)
└─ multi-interest: cluster history, keep top-c centroids
│
catalogue ──► embeddings ──► ANN index (P02)│
▼
┌──── retrieval (candidates, K≈500) ────┐
│ ANN by profile · ANN per interest │
│ recency pool · popularity pool │
└──────────────┬────────────────────────┘
│ union, dedupe by id
▼
filters: seen · blocked · region · age
▼
ranking: score = w1·sim + w2·freshness
+ w3·quality − w4·redundancy
▼
diversity pass (MMR) ──► near-duplicate collapse ──► top-k
Freshness for news is not a tie-breaker. Model it explicitly, e.g. an exponential decay \(f(a) = e^{-a/\tau}\) in article age \(a\), with \(\tau\) a swept parameter. A relevance-only ranker on a news corpus surfaces last month's best article over today's good one, forever.
MMR (maximal marginal relevance) selects greedily: \(\arg\max_i [\lambda \cdot \text{rel}(i) - (1-\lambda)\max_{j \in S}\text{sim}(i,j)]\). One parameter, one line, and it produces the accuracy/diversity frontier of E7.
Showcase — Do This Before You Start
W6 · walkthroughs/w6_popularity.py · ~40 minutes
A working miniature of this project: a bestseller list beating every personalised recommender on NDCG by 2.5x while covering 0.5% of the catalogue.
cd walkthroughs && python3 w6_popularity.py
It is 80-ish lines and it surfaces this project's central surprise in an evening rather than in week six. Run it before committing the weeks.
Implementation Milestones
| # | Milestone | Hours | Done when |
|---|---|---|---|
| 1 | Dataset: real or realistic articles with timestamps, categories, text; interaction log generator or real logs | 6 | Documented, with its popularity Zipf α measured |
| 2 | Embeddings + the P02 index over the catalogue | 4 | Index recall@K measured on this corpus |
| 3 | The four baselines: random, popularity, recency, content-similarity | 5 | All four scored on the full metric suite. Do this before anything clever |
| 4 | Mean-profile retrieval + ranking | 5 | Beats (or fails to beat) the baselines — report either way |
| 5 | EMA profiles, α ∈ {0.02…0.5} | 5 | Half-life table reproduced empirically |
| 6 | Full metric suite via tools/metrics.py | 4 | recall, precision, NDCG, MRR, coverage, novelty, Gini, ILD, freshness, calibration |
| 7 | Filters: seen, blocked, region, max age | 4 | Correct and measured for selectivity — links straight to P03's E3 |
| 8 | Near-duplicate detection and collapse | 6 | Measured duplicate rate before/after |
| 9 | Freshness term with swept τ | 4 | Freshness/accuracy frontier plotted |
| 10 | MMR diversity with swept λ | 5 | Accuracy/diversity frontier plotted |
| 11 | Feature-based reranker (GBDT over sim, freshness, popularity, category match) | 8 | Beats score-and-sort, or is honestly reported as not doing so |
| 12 | Cold-start paths: new user, new item | 5 | Both measured separately from the warm case |
| 13 | Experiments + report | 5 | All rows filled |
Concepts To Study
- Implicit feedback: clicks are not ratings; absence of a click is not a negative
- Position and presentation bias: the strongest predictor of a click is where the item was shown
- Two-stage retrieval/ranking and the recall ceiling
- User representation: mean, EMA, multi-interest clustering, sequence models
- Content-based vs collaborative vs hybrid, and why news is content-heavy
- Cold start: item cold start dominates in news; user cold start dominates in acquisition
- Freshness modelling: decay functions, and half-life as a product decision
- Diversity: MMR, determinantal point processes (know they exist), intra-list distance
- Filter bubbles and feedback loops: the recommender shapes the data that trains its successor
- Ranking metrics: NDCG's discount as a modelling assumption, MAP, MRR — and which fits a feed
- Beyond-accuracy metrics: coverage, novelty, serendipity, Gini
- Calibration: does the topic mix of recommendations match the user's history mix?
- Off-policy evaluation: IPS, capped IPS, doubly robust — enough to know why naive offline replay is biased
Primary-Source Readings
Budget: 10 hours.
| Reading | Why | Hours |
|---|---|---|
| Covington, P., Adams, J., Sargin, E. Deep Neural Networks for YouTube Recommendations. RecSys 2016 | Two-stage architecture; the "example age" feature is the freshness lesson | 1.5 |
| Steck, H. Calibrated Recommendations. RecSys 2018 | Why accuracy-optimal recommendations are miscalibrated, with a fix | 1.5 |
| Carbonell, J., Goldstein, J. The Use of MMR... SIGIR 1998 | MMR, in four pages | 0.5 |
| Chaney, A. J. B., Stewart, B. M., Engelhardt, B. E. How Algorithmic Confounding in Recommendation Systems Increases Homogeneity and Decreases Utility. RecSys 2018 | The feedback loop, simulated. Sets up P09 directly | 2 |
| Wu, F. et al. MIND: A Large-scale Dataset for News Recommendation. ACL 2020 | News-specific evaluation and its pitfalls | 1.5 |
| Cañamares, R., Castells, P. Should I Follow the Crowd? A Probabilistic Analysis of the Effectiveness of Popularity in Recommender Systems. SIGIR 2018 | Why popularity baselines are so hard to beat, analysed | 1.5 |
| Hu, Y., Koren, Y., Volinsky, C. Collaborative Filtering for Implicit Feedback Datasets. ICDM 2008 | The implicit-feedback formulation | 1.5 |
Experiments
| # | Experiment | Sweep | Predict first |
|---|---|---|---|
| E1 | Baselines | random / popularity / recency / content-sim | Predict the popularity baseline's NDCG. You will underestimate it |
| E2 | Profile type | mean vs EMA(α ∈ {0.02…0.5}) | Optimal α, overall and by user segment |
| E3 | Profile type × history length | × {1–3, 4–10, 11–50, 50+ interactions} | An interaction: predict where mean beats EMA |
| E4 | Candidate-set size K | {50,100,500,1000,5000} | Where does more retrieval stop helping? |
| E5 | Retrieval recall ceiling | ANN efSearch ∈ {16…256} | End-to-end quality vs retrieval recall — the linkage to P02 |
| E6 | Freshness weight τ | {1 h, 6 h, 24 h, 7 d, ∞} | Accuracy/freshness frontier |
| E7 | Diversity λ | {0, 0.3, 0.5, 0.7, 1.0} | Accuracy/ILD frontier; predict the elbow |
| E8 | Deduplication | on/off | Duplicate rate, and its effect on perceived quality |
| E9 | Reranker | score-and-sort vs GBDT | Lift, and which feature carries it |
| E10 | Cold start | new users (<3 interactions), new items (<1 h old) | Measured separately; the aggregate hides both |
| E11 | Popularity-bias audit | every configuration above | Coverage and Gini per config. Which "wins" are just head collapse? |
| E12 | Calibration | topic distribution of recs vs history | Predict the direction of the miscalibration |
| E13 | Latency budget | per stage: retrieval / filter / rank / diversify | Predict which stage dominates. It is usually not the one you think |
| E14 | Stability | same user, consecutive requests | How much does the list churn? Excessive churn is a real product bug |
E11 is the experiment that makes this a Stage 4 project. For every configuration you evaluate, record coverage and Gini next to NDCG. Then find at least one config where NDCG improved and coverage collapsed, and write it up. That is the honest- evaluation skill the project exists to build.
E5 is the linkage experiment. Sweep your ANN's efSearch, measure retrieval recall and end-to-end NDCG at each point, and plot them together. You will find a knee beyond which better retrieval buys nothing — that knee is your correct operating point, and almost nobody computes it.
Benchmarks and Metrics
All computed by tools/metrics.py. Report the whole suite for
every configuration. A table with only NDCG is a rejected result.
| Family | Metrics |
|---|---|
| Accuracy | recall@k, precision@k, NDCG@k, MRR — k always stated |
| Catalogue | coverage, Gini of exposure, novelty (bits) |
| List quality | intra-list diversity, duplicate rate, freshness distribution |
| Fit | calibration error between recommendation and history topic mixes |
| Serving | p50/p95/p99 end-to-end and per stage |
| Stability | rank correlation between consecutive requests for an unchanged user |
| Segmented | every accuracy metric, split by history length and by user activity decile |
Segmented reporting is not optional. An aggregate metric on a Zipf-distributed user population is dominated by heavy users. A change that helps the top decile and harms everyone else looks like a win in aggregate, and it is the most common way a recommender gets worse while its dashboard improves.
Correctness Tests
- Never recommend a seen item. Zero, across the full evaluation.
- No duplicate ids within one list.
- Every returned item exists and satisfies every active filter.
- Exactly k items returned, or fewer with an explicit reason logged.
- Determinism: same user state + same seed → same list.
- Metric implementations verified against hand-computed examples — the worked cases
in
metrics.py's demo. - No future leakage: an item published after the request time can never appear. This is the label-leakage bug of recommenders and it inflates offline metrics enormously.
- Empty history produces a sensible cold-start list, not a crash.
- Empty candidate set after filtering degrades gracefully.
- Profile update is order-dependent for EMA and order-independent for mean. Test both properties — getting this backwards is a real bug.
Failure Tests
| Injection | Required behaviour |
|---|---|
| Embedding service returns zeros for 10% of items | Detected, not silently ranked at the origin |
| Item published in the future (clock skew) | Rejected by the freshness filter |
| User with 10,000 interactions | Profile build stays within the latency budget |
| Catalogue with 90% near-duplicates | Dedup keeps the list usable |
| All candidates filtered out | Graceful fallback, logged |
| Interaction log with duplicated events | Profile not double-weighted |
| Stale index (30 minutes behind) | Freshness degrades measurably — quantify it; it is P15's question |
| One category comprising 80% of the catalogue | Diversity and calibration must respond |
| Adversarial engagement (a bot clicking one topic) | Profile poisoning; measure how fast, then bound it |
Expected Difficulties
- You will beat the baselines by less than you expect, or not at all. See the Zipf table. This is the expected outcome and it is a legitimate result; report it and diagnose it rather than tuning until the number looks better.
- Offline evaluation is biased by the logging policy. Your logs record what the old system showed. Items never shown have no positives and score as failures. Read the off-policy material, state the bias in the report, and note that P09 exists precisely to escape it.
- Metric selection will tempt you. Decide the primary metric before running the sweep and write it down.
- Segment or be fooled. Always.
- Freshness and accuracy fight, and the accuracy metric always wins offline because offline data cannot express "this was stale when shown". Note the limitation explicitly; it is another P09 motivation.
- The domain-expertise trap: you will be tempted to jump to the sophisticated design. Build the naive one and measure it first — the whole method depends on it, and knowing the answer in advance is exactly when the discipline matters most.
Scope Boundaries
In scope: content-based and hybrid retrieval, profile construction, ranking, diversity, freshness, dedup, cold start, offline evaluation, a full metric suite.
Out of scope: training a large neural ranker; real user traffic; a production serving stack; multi-objective optimisation beyond a weighted sum; sequence models (extension); a feature store; real-time profile updates (P07 does that, and P15 integrates it).
Deliverables
recsys/— pipeline, four baselines, metric suite, sweep runnerREPORT.mdcentred on E11 (the popularity-bias audit) and E5 (the recall ceiling)- A comparison matrix: every configuration × every metric, one table. This is the portfolio artifact
- Notebook entries for E2, E5, E11
- A written statement of the offline-evaluation bias, and what P09 will do about it
Exit Criteria
- All four baselines implemented and scored on the full suite
- Your system compared against all four, with an honest verdict per metric
- E2 + E3 complete: EMA vs mean, swept, segmented by history length
- E5 complete: retrieval recall ceiling measured, knee identified
- E11 complete: at least one configuration documented where accuracy rose and coverage fell
- All metrics reported segmented, not only aggregate
- Cold-start paths measured separately for new users and new items
- Latency budget measured per stage
-
REPORT.mdwritten with a falsified prediction and the offline-bias limitation stated
Extension Ideas
- Two-tower retrieval trained on your interactions, compared against content embeddings at equal latency.
- Bandit exploration (Thompson sampling or LinUCB) with measured regret and coverage effects.
- Sequence model (GRU4Rec-style) as the profile, compared against EMA. The direct test of "is a learned sequence better than an exponential average", which is a question with a real answer in your domain.
- Off-policy evaluation with capped IPS, compared against naive replay — quantifying the bias you flagged.
Connections
Backward: P02 is retrieval and sets the recall ceiling. P03 provides filtered search and the selectivity regime. P01/P13 provide embeddings.
Forward:
- → P09: the simulator evaluates this pipeline under known ground truth, escaping offline bias. Keep the pipeline's interface stable and swappable
- → P10: A/B testing over the same pipeline
- → P15: the serving path of the integrated system
References
- Covington, P., Adams, J., Sargin, E. Deep Neural Networks for YouTube Recommendations. RecSys 2016.
- Steck, H. Calibrated Recommendations. RecSys 2018.
- Carbonell, J., Goldstein, J. The Use of MMR, Diversity-Based Reranking for Reordering Documents and Producing Summaries. SIGIR 1998.
- Chaney, A. J. B., Stewart, B. M., Engelhardt, B. E. How Algorithmic Confounding in Recommendation Systems Increases Homogeneity and Decreases Utility. RecSys 2018.
- Cañamares, R., Castells, P. Should I Follow the Crowd? SIGIR 2018.
- Hu, Y., Koren, Y., Volinsky, C. Collaborative Filtering for Implicit Feedback Datasets. ICDM 2008.
- Wu, F. et al. MIND: A Large-scale Dataset for News Recommendation. ACL 2020.
- Joachims, T., Swaminathan, A., Schnabel, T. Unbiased Learning-to-Rank with Biased Feedback. WSDM 2017.
- Ricci, F., Rokach, L., Shapira, B. (eds.) Recommender Systems Handbook, 3rd ed. Springer, 2022.
- Kunaver, M., Požrl, T. Diversity in recommender systems — A survey. Knowledge-Based Systems 123, 2017.