Back to home
☕️ ColdStart Cafe
An interactive exploration of the cold-start problem in recommendation systems
Every recommendation system faces a bootstrapping dilemma: how do you recommend things to a new user when you know nothing about them? This is the cold-start problem — and ColdStart Cafe lets you experience it firsthand.
Four recommendation algorithms compete in real time, starting from zero knowledge. You feed them signals — movie ratings, demographics, genre preferences, viewing history — and watch their accuracy shift with each new data point. Real-time metrics (precision@10, recall@10, NDCG@10) quantify the recovery, while an AI narrator explains what's happening and why at each step.
In Challenge Mode, the tables turn: study a real user's profile, pick 10 movies you think they'd love, and see if you can out-recommend the machines.
Built as a full-stack application with a Python/FastAPI backend running four distinct ML algorithms (popularity baseline, content-based filtering, collaborative filtering via SVD, and an adaptive hybrid ensemble) against the MovieLens 100K dataset, and a React/TypeScript frontend with animated visualizations, streaming AI narration, and a cafe-themed UI.
🛠️ Tech Stack
🚀 Features
Four competing algorithms — Popularity (Bayesian average), Content-Based (cosine similarity on genre vectors), Collaborative Filtering (SVD via scikit-surprise), and Hybrid (adaptive weighted ensemble) — each with distinct cold-start behavior
Progressive signal feeding — add ratings, demographics, genre preferences, and viewing history one at a time and watch algorithms re-compute in real time (~600ms per step)
Real-time metrics — precision@10, recall@10, and NDCG@10 computed against a hidden ground-truth user, visualized as animated line charts
Two-tier AI narration — 13 cafe-themed templates handle common transitions instantly; edge cases fall back to Claude API streaming via SSE, with graceful degradation if no API key is configured
Challenge Mode — study a target user's demographics and 3 seed ratings, pick 10 movies, then get scored against all four algorithms with an AI-generated verdict
Animated race visualization — spring-physics algorithm timeline with sparkle bursts on big jumps, signal filmstrip with entrance animations, and tabbed top-10 recommendation cards
Session management — in-memory state with TTL eviction, sliding-window rate limiting, and max concurrent session enforcement
Dockerized deployment — multi-stage builds, nginx SPA routing with API proxy, health checks, and CI pipeline running 146 backend tests + frontend type-checking
🧗♂️ Challenges
Designing a meaningful evaluation without data leakage — The system selects a random ground-truth user with 20+ ratings and 3+ genre diversity, holds out their high-rated movies as the "relevant set," then measures algorithm accuracy against it. The ground truth is stored server-side in a separate dict (never serialized to the client) to prevent Pydantic from accidentally exposing it in API responses.
Making SVD work with minimal data — scikit-surprise's SVD needs a ratings matrix, but a brand-new user has no entries. The solution uses demographic-based neighbor initialization: find users with similar age/gender/occupation, seed the trainset with their ratings, then train a 10-epoch SVD. This gives collaborative filtering something to work with even at 1-2 signals.
Streaming narration across two animation modes — Template narrations arrive as a complete string (needs local typing animation), while LLM narrations stream token-by-token via SSE (needs incremental rendering). Both
useTypingAnimationanduseNarrationStreamhooks are always called (React Rules of Hooks), withskip/enabledflags controlling which is active — avoiding conditional hook violations.Multi-worker session routing — In-memory sessions are process-local, so running uvicorn with multiple workers caused 404s when requests routed to the wrong process. Diagnosed via Railway deploy logs and resolved by pinning to a single async worker, which handles portfolio-level traffic without issue.
NumPy ABI compatibility — scikit-surprise 1.1.4's Cython extensions are compiled against NumPy 1.x, so
numpy>=2.0breaks at import time. Pinnednumpy>=1.26.0,<2.0.0andpython>=3.12,<3.13to stay within the compatibility window.
🧠 Learnings
Protocol-based interfaces over inheritance — Algorithms are stateless module-level functions satisfying a
RecommenderProtocol (structural subtyping). No base class, no__init__, no shared state. This made testing trivial — each algorithm is a pure function of(state, data) -> RecommenderResult.Two-tier architecture for LLM features — Templates handle the predictable cases (first rating, cold-start intro) instantly and for free; the LLM handles the long tail (unusual signal combinations, narrative variety). This pattern — deterministic fast path + LLM fallback — kept the app responsive while still feeling intelligent.
Separation of HTTP, orchestration, and storage layers — Routers handle HTTP concerns (parsing, status codes), engines handle business logic (signal application, algorithm execution, metric computation), and storage handles state (thread-safe session dict with TTL). This layering made it possible to test algorithms in isolation, test API behavior separately, and swap storage without touching business logic.
Designing for graceful degradation — No API key? Templates still work. Claude API errors? Static fallback narration. Session expired? Cafe-themed recovery UI with a retry CTA. Rate limit hit? 429 with
Retry-Afterheader. Every failure mode has a designed experience, not just an error message.Docker build context vs. Dockerfile location — In a monorepo, the build context (where
COPYpaths resolve from) and the Dockerfile location are independent. Railway builds from the repo root, soCOPY pyproject.tomlfails — it needsCOPY backend/pyproject.toml. A subtle but common monorepo deployment pitfall.