Embedding Recommender: More-Like-This, Offline Evaluation, Co-occurrence, Cold Start and MMR
Hands-on lab · IDE in your browser

Embedding Recommender: More-Like-This, Offline Evaluation, Co-occurrence, Cold Start and MMR

Build a bookshop's recommendations from blurb embeddings and click history: more-like-this by cosine similarity, hit rate and MRR on held-out sessions, taste profiles, co-occurrence, a count-weighted blend that still recommends books nobody has clicked, and MMR and author caps for lists people want.

Time
50 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

The job

Marrow & Page, an online bookshop, wants "more like this" and "recommended for you". You have 300 books with blurbs and 950 customers' clicks, and you find out which signal wins, and why the winner never recommends this month's new books.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    More like this

    Marrow & Page, an online bookshop, wants a "more like this" row under every book and a "recommended for you" row on the home page.

    You writebook_text()more_like_this()
  2. 2

    Measure it offline

    Before a recommender reaches customers, replay history.

    You writerank()evaluate()clicks()
  3. 3

    What people read together

    Blurbs say what a book is about.

  4. 4

    New books nobody has clicked

    Co-occurrence wins on hit rate but scores every new book 0, so a bookshop using only it never shows this month's books.

  5. 5

    Lists people want

    The best ten by score can be five books by one author.

    You writemmr()cap_authors()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: More like this

Marrow & Page, an online bookshop, wants a "more like this" row under every book and a "recommended for you" row on the home page. You have its 300 books (books.json) and 950 customers' clicks (sessions.json). Start with the text: embed each book, and books whose embeddings point the same way are alike.

Do this

1. Write book_text(book) in recommender.py: "<title> by <author>. <blurb>".

2. Write more_like_this(E, i, k): the k books most similar to book i. The rows of E are unit length, so E @ E[i] is every book's cosine similarity to i. Leave out i itself and books out of stock (IN_STOCK).

3. Run. It embeds the catalogue (cached after the first run) and shows two books' neighbours.

recommender.py, the file you edit83 lines
"""Marrow & Page's "more like this" and "recommended for you", built from embeddings and from what people read."""
import numpy as np

import harness as H

BOOKS, SESSIONS = H.load()
IX = {b["id"]: i for i, b in enumerate(BOOKS)}
IN_STOCK = np.array([b["in_stock"] for b in BOOKS])
NEW = np.array([b["added"] == "2026-09" for b in BOOKS])  # added this month: nobody has clicked them yet


def zscore(x):
    return (x - x.mean()) / (x.std() + 1e-9)


# ---------- Step 1: more like this ----------

def book_text(book):
    """The text to embed for a book: "<title> by <author>. <blurb>"."""
    # TODO (Step 1): "<title> by <author>. <blurb>".
    raise NotImplementedError("Step 1: write book_text()")


def more_like_this(E, i, k=5):
    """The k row indices most similar to book i by cosine similarity (E rows are unit length): not i itself, and only
    books in stock (IN_STOCK). Most similar first."""
    # TODO (Step 1): E @ E[i], then leave out i and books out of stock; the k best, best first.
    raise NotImplementedError("Step 1: write more_like_this()")


# ---------- Step 2: measure it offline ----------

def rank(scores, history, k=10):
    """The k best row indices by score, leaving out the history (row indices) and books out of stock."""
    raise NotImplementedError("rank() arrives in Step 2")


def evaluate(score_fn, only_new=None, k=10):
    """{"hit", "mrr", "n"} over SESSIONS["test"]: score_fn(history row indices) gives a score per book; a hit is the
    target in rank(..., k); mrr averages 1/position (0 for a miss). only_new=True or False keeps only the customers
    whose target is (or is not) a NEW book."""
    raise NotImplementedError("evaluate() arrives in Step 2")


def clicks():
    """How many times each book was clicked in SESSIONS["train"], as an array in row order."""
    raise NotImplementedError("clicks() arrives in Step 2")


def profile(E, history, decay=0.8):
    """Scores from a taste profile: the history's embeddings summed with weight decay**age (the last click has age
    0, weight 1), then the dot product of every book with that profile."""
    raise NotImplementedError("profile() arrives in Step 2")


# ---------- Step 3: what people read together ----------

def cooccurrence():
    """A books x books matrix: C[a, b] = the number of train sessions containing both a and b (a != b), divided by
    sqrt(clicks[a] * clicks[b]), 0 where either has no clicks. The diagonal is 0."""
    raise NotImplementedError("cooccurrence() arrives in Step 3")


# ---------- Step 4: new books ----------

def blend(co_scores, emb_scores, counts, k=3):
    """w * zscore(co_scores) + (1 - w) * zscore(emb_scores), per book, where w = counts / (counts + k): a book with
    many clicks is judged by who read it, a book nobody has clicked by its text."""
    raise NotImplementedError("blend() arrives in Step 4")


# ---------- Step 5: lists people want ----------

def mmr(scores, E, history, k=10, lam=0.7, pool=50):
    """Maximal marginal relevance: from the top `pool` of rank(scores, history, pool), pick k one at a time, each time
    the book with the highest lam * relevance - (1 - lam) * (its highest similarity to a book already picked).
    Relevance is the score rescaled to 0..1 over the pool (max 1, min 0)."""
    raise NotImplementedError("mmr() arrives in Step 5")


def cap_authors(ranked, k=10, per_author=2):
    """The first k of ranked (row indices, best first) keeping at most per_author books by any one author."""
    raise NotImplementedError("cap_authors() arrives in Step 5")
Provided for you:books.jsonharness.pysessions.jsontry_it.py

Frequently asked questions

Are embeddings good for recommendations?

They find items with similar descriptions and work for items nobody has interacted with yet. Where click history exists, co-occurrence usually predicts better, so production systems blend the two.

How do you evaluate a recommender offline?

Hide each test user's last interaction, recommend from the rest of their history, and measure how often the hidden item is in the top k (hit rate) and how high it ranks (MRR).

What is MMR in recommendations?

Maximal marginal relevance re-ranks a list so each new item balances its relevance against its similarity to the items already chosen, trading a little accuracy for variety.

Recommendations from embeddings, measured

Embeddings turn item descriptions into vectors, so similar items are a dot product away. Whether they make good recommendations is a question for offline evaluation. You embed a catalogue, build more-like-this and taste-profile recommenders, score them by hit rate and MRR on held-out sessions, compare them with co-occurrence from click history, blend the two with a per-item weight that fixes cold start, and use MMR and business rules to shape the final list.