Recommender Systems — Stop Cold-Start Empty Recommendations
New users see empty recommendations? 48-hour implicit pipeline lag caused 35% retention loss.
20+ years shipping production ML systems and the infrastructure behind them. Notes here come from systems that actually shipped.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Collaborative filtering predicts what you'll like based on similar users' behaviour patterns.
- Content-based filtering matches items to your taste by analysing their attributes (genre, tags, features).
- Cold-start problem: both fail when no interaction data exists — use popularity fallback with Bayesian averaging.
- Production systems combine both into a hybrid: collaborative for serendipity, content-based for new-item coverage.
- Performance: item-based collaborative filtering scales better than user-based because item relationships are temporally stable.
- Biggest mistake: optimising RMSE instead of ranking metrics (NDCG) — RMSE gains don't correlate with user engagement.
Recommender systems are algorithms that predict what a user will want from a large catalog of items — products, movies, articles, songs. They exist because information overload is real: Netflix has 17,000+ titles, Amazon sells 350 million products, and Spotify hosts 100 million tracks.
Without a recommender, users drown in choice and churn. The core problem isn't just showing relevant items — it's doing so when you have zero data on a new user or item, which is the cold-start problem. Real systems like YouTube, TikTok, and Pinterest live or die by how well they handle this, because empty recommendations on day one mean lost users forever.
Three fundamental approaches exist. Collaborative filtering (used by Amazon's 'Customers who bought this also bought') relies on patterns across many users — if User A and User B both liked X, User A will probably like Y that User B liked. It works brilliantly at scale but fails on new items with no interaction history.
Content-based filtering (used by Spotify's 'Discover Weekly' early versions) recommends by matching item features — genre, director, keywords — to a user's profile. It solves cold-start for new items but creates filter bubbles, never surprising you. Hybrid systems (like Netflix's current algorithm) combine both, using content features to bootstrap collaborative signals and vice versa.
The cold-start problem is the hardest engineering challenge in production recommenders. A new user has no history; a new movie has no ratings. Real systems handle this with fallback strategies: popularity-based recommendations (top 10 trending), demographic segmentation (new users in Japan see anime), or explicit onboarding quizzes (Pinterest asks you to pick interests).
More advanced approaches use meta-data embeddings — representing a new movie by its director, cast, and synopsis vectors — to place it in the same latent space as existing items. The key insight: you never serve a truly empty recommendation; you always have a non-personalized or weakly-personalized baseline.
Evaluation metrics must reflect the cold-start reality. Offline metrics like RMSE (root mean squared error) or precision@k measure accuracy but miss the business impact. Production systems track engagement metrics: click-through rate (CTR), dwell time, session length, and churn rate.
The most telling metric is coverage — what fraction of your catalog gets recommended at least once. A system that only recommends Taylor Swift to everyone has great accuracy but zero coverage and terrible cold-start handling. Real-world teams also run A/B tests on cold-start cohorts specifically, measuring time-to-first-engagement and 7-day retention for new users versus existing ones.
Imagine you walk into a bookshop and the owner says, 'You loved Harry Potter? Then you'll love Percy Jackson — everyone who bought Harry Potter also grabbed that one.' That's a recommender system. It's software that watches what you and thousands of people like you have done, then quietly whispers, 'Hey, you'll probably like this next.' Netflix uses one. Spotify uses one. Amazon uses one. They're the engine behind every 'You might also like…' moment on the internet.
Every minute, Netflix has to decide what thumbnail to show 238 million subscribers. Spotify has to pick the next song for 600 million listeners. Amazon has to choose which product lands at the top of your feed. Getting this right is worth billions of dollars — Netflix once offered a $1 million prize just to improve their recommendation accuracy by 10%. Recommender systems are not a nice-to-have; they are the core revenue engine of the modern internet.
Before recommender systems existed, discovery was broken. You had to know what you were looking for. Search only helps when you already have a name in mind. But most of the time, you don't know what you want until someone shows it to you. Recommenders solve the 'unknown unknown' problem — surfacing things you'd love but would never have searched for. They turn a passive catalog of a million items into a personalised shop of ten perfect ones.
By the end of this article, you'll understand the two dominant families of recommender algorithms — collaborative filtering and content-based filtering — know when to use each one, and have working Python code that builds both from scratch. You'll also understand the cold-start problem (the dirty secret nobody warns you about) and be able to answer the questions interviewers actually ask about this topic.
Why Cold-Start Kills Recommendations — And What to Do About It
Recommender systems are algorithms that predict user preferences by modeling interactions between users and items. The core mechanic is simple: given a sparse matrix of past behavior (clicks, purchases, ratings), the system infers which unseen items a user is likely to engage with. The cold-start problem occurs when there is zero or minimal interaction data for a new user or new item — the matrix is too sparse for collaborative filtering to work.
In practice, the most common approach is collaborative filtering, which finds patterns across users (user-based) or items (item-based). It relies on the assumption that users who agreed in the past will agree again. Matrix factorization (e.g., SVD, ALS) decomposes the interaction matrix into latent factors, achieving O(k·(m+n)) complexity for k factors, m users, and n items. Content-based filtering uses item attributes instead of user behavior, avoiding cold-start for new items but not for new users.
Use collaborative filtering when you have thousands of interactions per user and a stable catalog. For cold-start scenarios — new users, new items, or seasonal catalogs — hybrid approaches that blend content signals with collaborative signals are mandatory. Without them, your system returns empty or random recommendations, destroying user trust and engagement on day one.
Collaborative Filtering: Trusting the Crowd's Taste
Collaborative filtering is the most powerful and most widely used recommender technique. The core idea is beautifully simple: find users who behaved like you in the past, and recommend what they liked that you haven't seen yet. You're not analysing the content at all — you're analysing patterns in human behaviour.
There are two flavours. User-based collaborative filtering asks: 'Which users are most similar to you?' Item-based collaborative filtering asks: 'Which items are most similar to this item, based on who rated both?' Amazon famously switched to item-based in 2003 because it scales better — comparing millions of items is more stable than comparing millions of constantly-changing users.
The maths behind similarity is usually cosine similarity or Pearson correlation. Cosine similarity measures the angle between two rating vectors — a score of 1 means identical taste, 0 means no overlap. The beauty of this approach is that it's content-agnostic. It doesn't care if you're recommending films, songs, or tax software. If the behaviour data is there, it works.
The critical weakness is the cold-start problem: if a new user has no history, or a new item has no ratings, collaborative filtering is blind. You can't find similar users for someone with zero interactions.
import numpy as np from sklearn.metrics.pairwise import cosine_similarity # --- Data Setup --- # Rows = users, Columns = movies # Rating scale: 1-5, 0 = not yet watched user_movie_ratings = np.array([ # Inception, Interstellar, The Dark Knight, Toy Story, Finding Nemo [5, 4, 5, 1, 0], # Alice [4, 5, 4, 0, 1], # Bob [0, 3, 5, 2, 1], # Carol [1, 0, 1, 5, 5], # David [2, 1, 0, 4, 5], # Eve ]) user_names = ["Alice", "Bob", "Carol", "David", "Eve"] movie_names = ["Inception", "Interstellar", "The Dark Knight", "Toy Story", "Finding Nemo"] # --- Step 1: Compute user-to-user similarity --- # cosine_similarity returns a matrix where [i][j] is how similar user i is to user j user_similarity_matrix = cosine_similarity(user_movie_ratings) print("=== User Similarity Matrix ===") print(f"{'':10}", end="") for name in user_names: print(f"{name:12}", end="") print() for i, name in enumerate(user_names): print(f"{name:10}", end="") for score in user_similarity_matrix[i]: print(f"{score:.3f} ", end="") print() # --- Step 2: Generate recommendations for a target user --- def recommend_movies_for_user(target_user_index, top_n_users=2, top_n_movies=2): """ Find the most similar users to the target user, then recommend movies those users rated highly that the target user hasn't seen. """ target_user_name = user_names[target_user_index] target_ratings = user_movie_ratings[target_user_index] # Get similarity scores for the target user vs everyone else similarity_scores = user_similarity_matrix[target_user_index] # Sort users by similarity, excluding the target user themselves (similarity = 1.0) similar_user_indices = np.argsort(similarity_scores)[::-1] similar_user_indices = [i for i in similar_user_indices if i != target_user_index] # Take the top N most similar users top_similar_users = similar_user_indices[:top_n_users] print(f"\n=== Recommendations for {target_user_name} ===") print(f"Movies {target_user_name} has NOT watched: ", end="") unwatched = [movie_names[j] for j in range(len(movie_names)) if target_ratings[j] == 0] print(", ".join(unwatched)) print(f"Most similar users: {[user_names[i] for i in top_similar_users]}") # Accumulate weighted scores for each unwatched movie movie_scores = {} for similar_user_idx in top_similar_users: similarity_weight = similarity_scores[similar_user_idx] for movie_idx, rating in enumerate(user_movie_ratings[similar_user_idx]): # Only consider movies the TARGET user hasn't watched if target_ratings[movie_idx] == 0 and rating > 0: movie_name = movie_names[movie_idx] # Weight the rating by how similar this user is to the target weighted_score = rating * similarity_weight movie_scores[movie_name] = movie_scores.get(movie_name, 0) + weighted_score # Sort by score descending and return top N ranked_recommendations = sorted(movie_scores.items(), key=lambda item: item[1], reverse=True) print(f"\nTop {top_n_movies} recommendations:") for rank, (movie, score) in enumerate(ranked_recommendations[:top_n_movies], start=1): print(f" {rank}. {movie} (weighted score: {score:.3f})") # Run recommendations for Alice (index 0) and David (index 3) recommend_movies_for_user(target_user_index=0) recommend_movies_for_user(target_user_index=3)
Content-Based Filtering: Recommending by DNA, Not by Crowd
Content-based filtering flips the whole approach. Instead of asking 'what did similar users like?', it asks 'what are the properties of items this specific user has liked, and which other items share those properties?'
Think of it as building a DNA profile of your taste. If you've listened to three jazz albums with upbeat tempo and trumpet solos, content-based filtering finds more albums with those exact characteristics — no other user's data required. This makes it immune to the cold-start problem for new users (as long as they rate a few items) and new items (as long as the item has metadata).
The standard implementation uses TF-IDF vectorisation on item metadata (genre, tags, description, cast) to represent each item as a vector in feature space. Then cosine similarity finds which items land closest to each other in that space.
The weakness is the **filter bubble**: content-based systems will only ever recommend more of what you already like. You rated sci-fi thrillers? You'll get more sci-fi thrillers — forever. It can't surprise you. Collaborative filtering can, because it's discovering what the crowd knows that your own history doesn't reveal.
Production systems almost always combine both approaches — this is called a hybrid recommender — using collaborative filtering for serendipity and content-based for specificity.
import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity # --- Movie Catalog with Metadata --- # In production this would come from a database. Here we define it inline. movie_catalog = pd.DataFrame({ 'title': [ 'Inception', 'Interstellar', 'The Dark Knight', 'Toy Story', 'Finding Nemo', 'Avengers: Endgame', 'The Prestige', 'Up' ], # 'tags' is a space-separated string of features — genre, mood, themes. # TF-IDF will treat each word as a feature dimension. 'tags': [ 'sci-fi thriller mind-bending dreams heist christopher-nolan', 'sci-fi space drama time-travel emotion christopher-nolan', 'action thriller dark superhero crime christopher-nolan', 'animation family adventure friendship comedy pixar', 'animation family ocean adventure comedy pixar', 'action superhero adventure sci-fi ensemble marvel', 'thriller mystery magic drama christopher-nolan', 'animation family adventure emotion loss pixar' ] }) # --- Step 1: Build the TF-IDF Feature Matrix --- # TF-IDF converts text tags into numeric vectors. # Words that appear in every movie (like 'the') get low weight; # distinctive words (like 'christopher-nolan') get high weight. tfidf_vectorizer = TfidfVectorizer(stop_words='english') tfidf_feature_matrix = tfidf_vectorizer.fit_transform(movie_catalog['tags']) print(f"Feature matrix shape: {tfidf_feature_matrix.shape}") print(f"(That's {tfidf_feature_matrix.shape[0]} movies x {tfidf_feature_matrix.shape[1]} unique tag features)\n") # --- Step 2: Compute Item-to-Item Cosine Similarity --- # Each row in the matrix represents a movie as a point in tag-space. # cosine_similarity measures the angle between any two movies' vectors. item_similarity_matrix = cosine_similarity(tfidf_feature_matrix, tfidf_feature_matrix) # Build a lookup: movie title -> row index title_to_index = pd.Series(movie_catalog.index, index=movie_catalog['title']) # --- Step 3: The Recommendation Function --- def get_content_based_recommendations(liked_movie_title, top_n=3): """ Given a movie the user liked, find the most similar movies based purely on their content/tag profiles. """ if liked_movie_title not in title_to_index: print(f"Movie '{liked_movie_title}' not found in catalog.") return movie_index = title_to_index[liked_movie_title] # Get the similarity row for this movie — a score vs every other movie similarity_scores = list(enumerate(item_similarity_matrix[movie_index])) # Sort by similarity score, highest first # Exclude index 0 because that's the movie itself (similarity = 1.0) similarity_scores_sorted = sorted( similarity_scores, key=lambda pair: pair[1], reverse=True ) # Skip the first result (it's the same movie) top_similar_movies = similarity_scores_sorted[1: top_n + 1] print(f"Because you liked '{liked_movie_title}', you might enjoy:") print(f" (Tags: {movie_catalog.loc[movie_index, 'tags']})\n") for rank, (idx, score) in enumerate(top_similar_movies, start=1): recommended_title = movie_catalog.loc[idx, 'title'] recommended_tags = movie_catalog.loc[idx, 'tags'] print(f" {rank}. {recommended_title} (similarity: {score:.3f})") print(f" Tags: {recommended_tags}") print() # --- Run recommendations --- get_content_based_recommendations('Inception', top_n=3) get_content_based_recommendations('Toy Story', top_n=3)
The Cold-Start Problem and How Real Systems Handle It
Here's the dirty secret of recommender systems that textbooks gloss over: both major approaches fail at the exact moment you need them most — when you have no data.
A new user has no rating history. Collaborative filtering can't find similar users. Content-based filtering has no liked items to extract preferences from. A new item (a film released today) has no ratings yet. Collaborative filtering will never surface it. This is the cold-start problem, and it's the difference between an academic exercise and a production system.
Here's how real systems handle it:
1. Onboarding surveys. Spotify and Netflix both ask new users to pick a few genres or artists they love. This seeds the profile immediately so content-based filtering has something to work with from minute one.
2. Popularity-based fallback. When you have nothing else, recommend the most popular items in the relevant category. It's not personalised, but it's not random noise either. A new user on a music app gets the top 50 chart, not a blank screen.
3. Demographic proxies. If you know a user's age, location, or device type (from sign-up), you can bootstrap recommendations from other users with the same demographic profile — even before they interact with any content.
4. Matrix Factorisation for sparse data. Techniques like SVD (Singular Value Decomposition) or ALS (Alternating Least Squares) decompose your ratings matrix into latent factors that can generalise even when most ratings are missing. This is what Netflix's production system is based on.
import pandas as pd import numpy as np # --- Simulated movie ratings data --- # Each row is one rating event: which user rated which movie and how. ratings_data = [ {'user_id': 'alice', 'movie': 'Inception', 'rating': 5}, {'user_id': 'alice', 'movie': 'Interstellar', 'rating': 4}, {'user_id': 'alice', 'movie': 'The Dark Knight', 'rating': 5}, {'user_id': 'bob', 'movie': 'Inception', 'rating': 4}, {'user_id': 'bob', 'movie': 'Interstellar', 'rating': 5}, {'user_id': 'bob', 'movie': 'Toy Story', 'rating': 3}, {'user_id': 'carol', 'movie': 'The Dark Knight', 'rating': 4}, {'user_id': 'carol', 'movie': 'Avengers: Endgame','rating': 5}, {'user_id': 'carol', 'movie': 'Toy Story', 'rating': 4}, {'user_id': 'david', 'movie': 'Toy Story', 'rating': 5}, {'user_id': 'david', 'movie': 'Finding Nemo', 'rating': 5}, {'user_id': 'eve', 'movie': 'Avengers: Endgame','rating': 4}, {'user_id': 'eve', 'movie': 'Inception', 'rating': 3}, ] ratings_df = pd.DataFrame(ratings_data) # --- Build the Popularity Scorecard --- # A good popularity score isn't just average rating — it must account for # the number of ratings too. A film with 1,000 ratings of 4.0 is safer # to recommend than one with 2 ratings of 5.0. # We use a Bayesian average: (n / (n + m)) * mean_rating + (m / (n + m)) * global_mean # Where n = number of ratings for this film, m = minimum ratings threshold global_mean_rating = ratings_df['rating'].mean() minimum_votes_threshold = 2 # need at least 2 ratings to be trusted movie_stats = ratings_df.groupby('movie').agg( total_ratings=('rating', 'count'), mean_rating=('rating', 'mean') ).reset_index() def bayesian_average(row, global_mean, min_votes): """Pulls films with few ratings toward the global mean, reducing noise.""" n = row['total_ratings'] mean = row['mean_rating'] # As n grows large, this approaches the true mean_rating. # With n=1, it's heavily pulled toward global_mean. return (n / (n + min_votes)) * mean + (min_votes / (n + min_votes)) * global_mean movie_stats['bayesian_score'] = movie_stats.apply( bayesian_average, axis=1, global_mean=global_mean_rating, min_votes=minimum_votes_threshold ) popularity_ranked = movie_stats.sort_values('bayesian_score', ascending=False).reset_index(drop=True) print("=== Popularity Fallback Catalog (for new users) ===") print(f"Global mean rating across all movies: {global_mean_rating:.2f}\n") print(popularity_ranked[['movie', 'total_ratings', 'mean_rating', 'bayesian_score']].to_string(index=False)) # --- The Cold-Start Decision Router --- def get_recommendations(user_id, user_history, all_ratings_df, top_n=3): """ Routes to the right strategy based on how much data we have for this user. - No history: popularity fallback (cold start) - Has history: could call collaborative or content-based (placeholder here) """ print(f"\n=== Fetching recommendations for: {user_id} ===") if len(user_history) == 0: # COLD START: no interactions yet — serve popularity list print("Status: NEW USER (cold start) — serving popularity-based fallback\n") already_watched = set() # new user has watched nothing else: print(f"Status: RETURNING USER — has rated {len(user_history)} movies\n") already_watched = set(user_history.keys()) # In a real system you'd call collaborative or content-based here. # We show the fallback logic pathway for illustration. print("(Would call collaborative/content-based system here in production)\n") # Show popularity fallback recommendations, excluding already-seen items recommendations = [ row for _, row in popularity_ranked.iterrows() if row['movie'] not in already_watched ][:top_n] for rank, movie_row in enumerate(recommendations, start=1): print(f" {rank}. {movie_row['movie']} " f"(score: {movie_row['bayesian_score']:.3f}, " f"ratings: {int(movie_row['total_ratings'])})") # Simulate a brand new user with zero history get_recommendations('new_signup_frank', user_history={}, all_ratings_df=ratings_df) # Simulate a returning user who has watched some films get_recommendations('alice', user_history={'Inception': 5, 'Interstellar': 4}, all_ratings_df=ratings_df)
Hybrid Recommenders: Getting the Best of Both Worlds
Pure collaborative or content-based systems each have fatal flaws. Hybrid recommenders combine them to cancel out weaknesses. Most production recommenders at scale — Netflix, Spotify, YouTube — are hybrids under the hood.
There are three common hybrid strategies:
Weighted hybrid: Compute scores from both collaborative and content-based models, then blend them with a tunable weight. Weight = 0.7 collaborative + 0.3 content-based is a common starting point.
Cascade hybrid: Use content-based to narrow the candidate pool (e.g., only items in genres the user has liked), then re-rank with collaborative filtering. This reduces the search space and injects serendipity from the crowd.
Feature-augmented hybrid: Add the latent factors from matrix factorisation (collaborative) as additional features into the content-based model. This lets the content-based model leverage behavioural signals without its own cold-start blindness.
Choosing the right hybrid architecture depends on your data density and latency budget. Weighted hybrids are simplest to implement but require careful offline tuning of the blending parameter. Cascades are more complex but offer control over each stage's output quality. Feature augmentation is used by Netflix and is the most powerful — but it requires a mature ML infrastructure.
| Strategy | Pros | Cons | Best for |
|---|---|---|---|
| Weighted | Simple to implement; easy to tune | Linear combination assumes independence | Teams with limited ML resources |
| Cascade | Each stage is independently optimisable | Higher latency; error propagates | High-traffic systems with strict control |
| Feature-augmented | Most powerful; state-of-the-art results | Complex infrastructure; risk of overfitting | Companies with dedicated ML teams |
The fundamental trade-off: more integration increases model power but also increases system complexity and maintenance cost. Start with a weighted hybrid, measure the gap, and only add complexity when it moves a core product metric.
Evaluating Recommender Systems: Metrics That Actually Matter
A recommender that scores 0.95 RMSE on a test set can still produce terrible recommendations. Why? Because RMSE measures how close predicted ratings are to actual ratings — it doesn't care about the order of the list. A user doesn't care if you predicted 4.2 instead of 4.1; they care whether the first item shown is something they'd love.
This is the fundamental insight that Netflix's 2009 prize exposed: optimising for RMSE barely moved business metrics. What matters is ranking quality.
OFFLINE METRICS (computed on held-out data): - Precision@K: fraction of top-K recommendations that the user actually interacted with. - Recall@K: fraction of all interacted items that appeared in the top-K. - NDCG@K (Normalised Discounted Cumulative Gain): gives more weight to correct recommendations at the top of the list. The standard metric for academic recommender evaluation. - Mean Average Precision (MAP): average of precision over all relevant item positions.
ONLINE METRICS (measured in production via A/B test): - Click-through rate (CTR): % of recommendation impressions that got a click. - Conversion rate: % of clicks that led to a purchase or follow. - User engagement: time spent, session length, return rate. - Diversity: how many different categories appear in recommendations. Measured by intra-list distance or category entropy.
The gap between offline and online metrics is notorious. A model that beats the baseline by 5% NDCG often shows no CTR lift — because offline tests use static snapshots while online users are exposed to the recommendations and their behaviour changes. This is called position bias and feedback loop effects.
The evaluation pipeline should include: 1. Historical train/test split (time-based, not random). 2. Offline ranking metrics (NDCG@10, Precision@5). 3. Replay-simulation: replay historical logs pretending your new model was live — measure how many of those recommendations would have been clicked. 4. Online A/B test with one-week minimum runtime.
If you only have budget for one metric, track NDCG@10. If you have two, add Precision@5. Industry experience shows these correlate best with long-term user retention.
Popularity-Based Systems: The Baseline You Can’t Ignore
Most teams skip naive popularity-based recommenders because they’re not sexy. That’s a mistake. These systems set your floor — the minimum acceptable quality. If your fancy neural hybrid can’t beat ‘most viewed this week’ for cold users, you have an architecture problem.
The math is brutally simple: rank items by total interactions. But raw counts favor old content. IMDB solved this with a weighted score that balances average rating against the number of votes. Here’s the formula: weighted_score = (v / (v + m)) R + (m / (v + m)) C, where R is item average, v is vote count, m is minimum votes to qualify, and C is the global mean.
The dirty secret? Most production systems still use a variant of this for their default homepage. Netflix’s ‘Trending Now’ is popularity with a recency decay. Don’t overthink your baseline until this fails.
import pandas as pd import numpy as np def weighted_rating(df, rating_col='rating', count_col='vote_count', m=1000): """Calculate IMDB-style weighted rating.""" C = df[rating_col].mean() v = df[count_col] R = df[rating_col] return (v / (v + m)) * R + (m / (v + m)) * C # Example with MovieLens-style data movies = pd.DataFrame({ 'title': ['The Shawshank Redemption', 'Plan 9 from Outer Space', 'The Godfather'], 'rating': [9.3, 2.5, 9.2], 'vote_count': [25000, 200, 18000] }) movies['weighted_score'] = weighted_rating(movies) print(movies[['title', 'weighted_score']].sort_values('weighted_score', ascending=False))
Content-Based Filtering: When Metadata is Your Only Friend
Content-based filtering recommends items by similarity to what users already liked. No crowd needed. You build a profile per user from item features — genres, tags, embeddings. Then you vectorize everything and compute cosine similarity.
The classic trap: teams dump in raw text and expect magic. You must clean and weight your features. Genre overlap matters more than director name. TF-IDF on plot summaries works better than bag-of-words. And never — ever — use one-hot encoding on high-cardinality features like actor lists unless you want a sparse disaster.
Here’s where it shines: cold-start for new items. A new movie has zero ratings, but it has genres and a description. The content-based system can immediately slot it into existing user profiles. No waiting for interactions. This is your first line of defense against cold items — not collaborative filtering.
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity import pandas as pd # Mini dataset with genre features movies = pd.DataFrame({ 'title': ['Inception', 'The Dark Knight', 'The Notebook'], 'genres': ['sci-fi action thriller', 'action crime thriller', 'romance drama'] }) # Vectorize genres using TF-IDF (one-hot is too sparse) tfidf = TfidfVectorizer() genre_matrix = tfidf.fit_transform(movies['genres']) # Compute cosine similarity sim_matrix = cosine_similarity(genre_matrix, genre_matrix) # Recommend for Inception (index 0) inception_idx = 0 scores = list(enumerate(sim_matrix[inception_idx])) sorted_scores = sorted(scores, key=lambda x: x[1], reverse=True)[1:3] print('Movies similar to Inception:') for idx, score in sorted_scores: print(f' {movies.iloc[idx]["title"]}: {score:.3f}')
New Users See Empty Recommendations — Cold-Start Cascade
- Every recommender must have a cascading fallback from personalised → popular → curated — never assume you'll have data.
- Bayesian averaging prevents one-hit-wonder items from dominating the fallback list.
- Monitor the 'cold-start coverage ratio' — percentage of new users who receive at least 3 recommendations within 5 minutes.
SELECT count(*) FROM interactions WHERE user_id=42 AND timestamp > NOW() - INTERVAL '7 days'Check if the user exists in the similarity matrix: grep '42' /data/user_similarity.npy | head -1Check last index time: ls -la /data/item_features/ | tail -1Run a manual feature vectorisation: python -c "from src.features import build_item_vector; print(build_item_vector(98765))"Compute per-user recommendation diversity: python -c "from src.eval import compute_diversity; print(compute_diversity(1000))"Inspect the similarity matrix for near-zero variance: python -c "import numpy as np; mat=np.load('sim.npy'); print(np.var(mat, axis=1).mean())"Run evaluation on the held-out test set: python src/evaluate.py --model v2 --test data/test.parquet | grep NDCGCheck for dataset shift: python src/detect_shift.py --reference data/train.parquet --current data/this_week.parquetkubectl exec -it recommender-pod-0 -- curl localhost:8080/metrics | grep request_duration_secondsProfile with: kubectl exec -it recommender-pod-0 -- jcmd 1 JFR.start duration=60s filename=profile.jfr| Aspect | Collaborative Filtering | Content-Based Filtering |
|---|---|---|
| Core idea | Find similar users or items based on ratings behaviour | Find similar items based on their attributes/metadata |
| Data required | User interaction history (ratings, clicks, views) | Item metadata (genre, tags, description, features) |
| Cold-start (new user) | Fails — no history to find similar users | Partially works after a few explicit ratings |
| Cold-start (new item) | Fails — no one has rated it yet | Works immediately if metadata exists |
| Serendipity | High — can surface unexpected discoveries via crowd wisdom | Low — trapped in a filter bubble of known preferences |
| Scalability | Expensive at scale; item-based is more stable than user-based | Scales well; similarity precomputed from item features |
| Best used when | Large, dense interaction dataset exists | Rich item metadata available; niche or new catalog |
| Real-world example | Amazon 'customers also bought', Netflix row ordering | Pandora Music Genome Project, news article recommenders |
| File | Command / Code | Purpose |
|---|---|---|
| collaborative_filtering.py | from sklearn.metrics.pairwise import cosine_similarity | Collaborative Filtering |
| content_based_filtering.py | from sklearn.feature_extraction.text import TfidfVectorizer | Content-Based Filtering |
| cold_start_popularity_fallback.py | ratings_data = [ | The Cold-Start Problem and How Real Systems Handle It |
| popularity_baseline.py | def weighted_rating(df, rating_col='rating', count_col='vote_count', m=1000): | Popularity-Based Systems |
| content_based.py | from sklearn.feature_extraction.text import TfidfVectorizer | Content-Based Filtering |
Key takeaways
Common mistakes to avoid
5 patternsUsing raw average ratings for popularity fallback
Forgetting to normalise ratings before computing cosine similarity
Treating the recommendation problem as a prediction problem instead of a ranking problem
Using user-based collaborative filtering at scale with millions of users
No guardrails for diversity and recency in the final ranking
Interview Questions on This Topic
Explain the cold-start problem in recommender systems. How would you handle it for a new user who signs up on day one with zero interaction history?
What is the difference between user-based and item-based collaborative filtering? Why did Amazon move to item-based, and what trade-offs does that involve?
You've built a recommender system and your RMSE on the test set is excellent, but user engagement hasn't improved. What could explain this, and how would you diagnose and fix it?
How would you design a real-time recommendation pipeline for a news website with millions of articles published daily?
What is the difference between Recall@K and NDCG@K, and when would you use each?
Frequently Asked Questions
Collaborative filtering recommends items based on the behaviour of similar users — it looks at who liked what and finds patterns across many people. Content-based filtering recommends items based on their own attributes — it profiles items by genre, tags, or features and matches them to a specific user's demonstrated taste. In practice, most production systems combine both approaches into a hybrid recommender.
The cold-start problem occurs when a recommender system can't make good recommendations because it lacks data — either a new user has no interaction history, or a new item has no ratings. The standard solution is a popularity-based fallback for new users (recommend trending items using a Bayesian average score), onboarding surveys to seed initial preferences, and content-based filtering for new items that have metadata but no ratings yet.
No — the collaborative and content-based approaches described here work purely with linear algebra (cosine similarity, matrix operations) and are often good enough for many applications. Deep learning recommenders (like two-tower neural networks or transformer-based models) offer better performance at massive scale but require far more data and infrastructure. Start simple with cosine similarity and only add complexity when you can measure that it moves a real metric.
Track a mix of offline and online metrics. Offline: NDCG@10, Precision@5, Recall@20. Online (A/B test): click-through rate (CTR), conversion rate, time spent, diversity index, and return rate within 7 days. The most actionable single metric is NDCG@10, but you must also monitor feedback loops — e.g., does the model collapse into recommending only popular items? Use a diversity guardrail to detect this.
A filter bubble happens when a recommender only shows you content similar to what you've already consumed, trapping you in a narrow taste space. Content-based filtering is especially prone to this. To avoid it, inject collaborative filtering signals (which introduce serendipity from the crowd), add random exploration (epsilon-greedy), or explicitly optimise for diversity using algorithms like xQuAD. Spotify's Discover Weekly is a textbook example of a filter bubble breaker.
20+ years shipping production ML systems and the infrastructure behind them. Notes here come from systems that actually shipped.
That's ML Basics. Mark it forged?
7 min read · try the examples if you haven't