Classic ML · Easy · 15 min

A recommender in 40 lines

Cosine similarity between products, a weighted average and a sorted list: an item-based recommender on 14 customers and 10 products of a workshop.

R is a 14 customers × 10 products matrix with ratings from 1 to 5 and 0 where there is none. Write four functions:

cosine(a, b): dot product over the product of the norms, treating 0 as just another zero. When a norm is 0, return 0.

itemSimilarity(R): the 10 × 10 matrix S with the cosine between **columns** (products) i and j.

predict(R, S, u, i, k): the rating customer u would give product i: among the products u did rate (other than i) with S[i][j] > 0, take the k most similar and compute the weighted mean Σ S[i][j] · R[u][j] / Σ S[i][j]. With no neighbours, 0.

recommend(R, S, u, n, k): for every product u has not rated, its prediction; return the best n as { item, score } (product index and rating rounded to two decimals), highest first and, on a tie, by index. The preview lets you pick a customer and see their recommendations.

Challenges 0/4

  • Cosine: equal, perpendicular and an empty vector
  • The similarity matrix is symmetric, with ones on the diagonal
  • A prediction with three neighbours, and 0 with none
  • The recommendations of a mechanics customer and a bodywork one

function cosine(a, b) {
  // dot / (|a| · |b|); 0 when a norm is 0
  return 0;
}

function itemSimilarity(R) {
  // S[i][j] = cosine of columns i and j
  return R[0].map(() => R[0].map(() => 0));
}

function predict(R, S, u, i, k) {
  // neighbours: products j != i rated by u with S[i][j] > 0; the k most similar; weighted mean; 0 without neighbours
  return 0;
}

function recommend(R, S, u, n, k) {
  // unrated products with their prediction, best n as { item, score } (two decimals), ties by index
  return [];
}

const S = itemSimilarity(R);
console.log(recommend(R, S, 2, 3, 3).map((r) => PRODUCTS[r.item] + ' ' + r.score));
Console output appears here (console.log).

The idea

An item-based recommender makes a simple bet: if whoever bought filters also bought oil, you show oil to the next person who rates filters well. The "similarity" between two products is the cosine between their rating columns, and the predicted rating is a mean of what the customer already rated, weighted by that similarity.

It is what many small shops use with good results, and it fits in 40 lines with no libraries. In the preview, customers 1 to 8 buy mechanics and the rest bodywork and cleaning: you will see the recommendations respect that split without anyone programming it. The data is synthetic and says so.

Two honest limits. With little data, a product nobody rated is never recommended (the cold start), and a new customer gets nothing until they rate something. And cosine over zeros rewards popular products: if you dislike that, centre each row by subtracting the customer's mean before computing.

All of this runs with the ratings on your machine. You do not need to send your customers' purchase history to anyone to recommend well in a hundred-product catalogue.

Run it on your machine

With Python, pandas and NumPy, on this exercise's CSV (CC BY 4.0). Here with every rated neighbour, without the k cut, so the scores differ a little:

curl -O https://vorluxai.com/data/labs/valoraciones-taller.csv
python3 -c "
import pandas as pd, numpy as np
df = pd.read_csv('valoraciones-taller.csv')
M = df.pivot(index='cliente', columns='producto', values='valoracion').fillna(0)
X = M.values; norms = np.linalg.norm(X, axis=0)
S = (X.T @ X) / np.outer(norms, norms)
u = M.index.get_loc('C03'); rated = X[u] > 0
pred = {M.columns[i]: round(float((S[i, rated] * X[u, rated]).sum() / S[i, rated].sum()), 2) for i in range(X.shape[1]) if not rated[i]}
print('C03 ->', sorted(pred.items(), key=lambda kv: -kv[1])[:3])
"

Check what you learned

1. What is the cosine computed between in an item-based recommender?
2. When is a product nobody has rated recommended?
3. Why is the prediction a weighted mean and not a simple one?
4. What data leaves your machine to recommend?

Go deeper: the Google Machine Learning Crash Course reference →

This in production, with your data? Let's talk for 15 minutes →