Recommender systems from the ground up
Content-based filtering
Content-based filtering turns items into feature vectors and a user into the average of what they liked. It works before anyone else has interacted.
Items as vectors
Content-based filtering ignores other users. It describes each item by its features: the things you can measure about it. A feature might be a genre, a tag, a TF-IDF score for each word, or an embedding. TF-IDF gives a word more weight when it is rare across the catalogue. An embedding is a list of numbers that puts like items near each other. The system then recommends items that look like the ones a user has liked. The model is a similarity function, and the feature set is the real design choice.
That makes the approach clear. Every result can be traced to a feature the user has shown interest in. That helps with explanations and with debugging. The approach is also brittle. If the features do not describe what people care about, no amount of tuning the similarity will fix it.
The choice of features decides what counts as similar. One-hot tags (a list of zeros with a one for each tag present) make every shared tag count the same. TF-IDF makes a rare tag count more. An embedding folds synonyms together. None of these is more “correct” than another. Each gives a different list of neighbours, and the reader should know which one the product uses.
The profile is an average
A user profile (one vector that stands for a user’s taste) is built from the items they consumed. It is the mean of their vectors, or a weighted mean where recent and high-rated items count more. Scoring is then a dot product or a cosine between the profile and every unseen item. Sort the scores and cut at the slot budget.
Cosine ignores length and compares direction. That is what you want when item vectors have different lengths. A dot product rewards long vectors, so scale to unit length first unless length means something. The choice looks small. It changes the ranking more than most settings you pick before training.
Weighting is where a content model becomes personal. A rating of five can count three times a rating of three. A like from last week can count more than a like from last year. The weights are settings with a product meaning. Choose them from behaviour, not from a grid search on a metric that never sees the page.
TF-IDF and cosine in scikit-learn
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
vectorizer = TfidfVectorizer(stop_words="english")
matrix = vectorizer.fit_transform(descriptions)
profile = matrix[liked_indices].mean(axis=0)
scores = cosine_similarity(profile, matrix).ravel()
print(scores.argsort()[::-1][:10])A profile from tags
Run the scorer and read the top three. Then weight the second liked item twice and see which recommendation moves to the front.
What it can and cannot do
It can recommend a brand-new item as soon as its features exist. It can also explain a result: you liked something similar. It cannot find a taste the user has never shown. It also tends to overspecialise. The profile keeps pointing at the same corner of the catalogue until every list looks alike.
The fix is not a better similarity. It is variety in the candidate set (the items the system is allowed to rank). It is also deliberate exploration and a profile that fades, so old likes stop dominating. Content-based filtering is a strong cold-start tool: it works when a user or an item is new. It is a weak serendipity tool: it rarely surprises. Knowing which job you are giving it is most of the design.
Content features are also the honest answer where data from other users is thin or sensitive. A new marketplace cannot wait for co-occurrence (users touching the same items). Neither can a regulated catalogue, or a library with one copy of each title. The trade is that the results can only be as good as the descriptions. Someone has to own the metadata.
Normalise item vectors before averaging them into a profile. Otherwise one long, feature-rich item can dominate a user’s taste for months.
Features are the model
Two systems with the same algorithm and different features act like different products. A coarse genre tag says little. A well-written description carries style, era and mood. Text features are cheap and strong. They also inherit every bias and every gap in the copy.
Normalise, deduplicate and version the feature pipeline. When the features change, the results change. Without a version you cannot tell whether the model got better or the catalogue was rewritten underneath it. A feature diff belongs next to the model diff.
Text preprocessing is modelling. Lowercasing, dropping stop words (common words such as “the”), stemming (cutting a word to its root) and splitting on punctuation all change the vector. A pipeline that does them differently at training and at serving time is a skew bug. Keep the preprocessing in one function. Version it with the features, and test it on the same fixture.
Not every word is a signal
Run the scorer, then remove the word music from every description and confirm the ranking barely changes. A term that appears everywhere carries no information.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does content-based filtering need that collaborative filtering does not?
