Live Coding Interview Prep

Course Content

Live Coding Interview Prep

7 sections · 50 lessons

Build a simple topic modeling pipeline (BERTopic-style).


What you need to know

Topic modelling answers "what are these 50,000 documents about?" without predefined labels.

The classic method, LDA, models each document as a mixture of word distributions. It works on long documents but struggles on short text — tweets, tickets, reviews — because a 12-word message has too few words to estimate a mixture from.

BERTopic's approach separates the two jobs:

  1. Embed — each document becomes a vector; meaning, not word overlap, decides similarity.
  2. Reduce — UMAP to about 5 dimensions so density clustering works.
  3. Cluster — HDBSCAN finds topic groups and leaves outliers out.
  4. Describe — c-TF-IDF picks each cluster's distinguishing words.
  5. Name (optional) — an LLM writes a 2–4 word label from keywords and examples.

Class-based TF-IDF (c-TF-IDF). Ordinary TF-IDF scores a word in a document. c-TF-IDF concatenates each cluster into one "class document" and scores words per class:

Text
score(word, cluster) = tf(word, cluster) × log(1 + A / f(word))tf(word, cluster) = count of the word in the cluster / total words in the clusterA                 = average number of words per clusterf(word)           = count of the word across all clusters

A word common in one cluster and rare elsewhere gets a high score; a word common everywhere ("order", "please") gets a low one.

Python
import refrom collections import Counterfrom collections.abc import Callableimport numpy as npSTOPWORDS = {"the", "and", "for", "that", "this", "with", "you", "your", "are", "was",             "have", "has", "not", "but", "can", "our", "from", "they", "still", "yet"}def tokenize(text: str) -> list[str]:    return [w for w in re.findall(r"[a-z]{3,}", text.lower()) if w not in STOPWORDS]def c_tf_idf(clusters: dict[int, list[str]], top_n: int = 5) -> dict[int, list[str]]:    """clusters: {label: [texts]} -> {label: most distinguishing terms}."""    counts = {c: Counter(w for t in texts for w in tokenize(t)) for c, texts in clusters.items()}    totals = {c: sum(cnt.values()) for c, cnt in counts.items()}    avg_words = sum(totals.values()) / len(totals) if totals else 0.0    corpus = sum(counts.values(), Counter())                 # word counts across all clusters    topics = {}    for c, cnt in counts.items():        scored = [(w, (n / totals[c]) * np.log(1 + avg_words / corpus[w])) for w, n in cnt.items()]        scored.sort(key=lambda pair: (-pair[1], pair[0]))    # score desc, then alphabetical        topics[c] = [w for w, _ in scored[:top_n]]    return topicsdef topic_model(texts: list[str], embed_fn: Callable, cluster_fn: Callable,                name_fn: Callable[[str], str] | None = None) -> list[dict]:    """cluster_fn(texts, embed_fn) -> {"clusters": {label: [texts]}, ...} (see the HDBSCAN question)."""    clusters = cluster_fn(texts, embed_fn)["clusters"]    topics = []    for label, terms in c_tf_idf(clusters).items():        name = ", ".join(terms[:3])        if name_fn:            examples = "\n".join(f"- {t}" for t in clusters[label][:5])            name = name_fn(f"Keywords: {', '.join(terms)}\nExamples:\n{examples}\n"                           "Give a 2-4 word topic name. Reply with the name only.").strip()        topics.append({"id": label, "name": name, "keywords": terms, "size": len(clusters[label])})    return sorted(topics, key=lambda t: -t["size"])

The tricky parts:

  • sum(counts.values(), Counter()) adds all clusters' counters into one corpus-wide counter; Counter supports +.
  • Sorting by (-score, word) breaks ties alphabetically, so results are reproducible run to run.
  • Normalising tf by cluster size stops a big cluster's words from dominating just because the cluster has more text.
  • cluster_fn is injected, so the same pipeline runs with cluster_texts from the HDBSCAN question (with UMAP as its reduce_fn) or with a stub in tests.

Complexity: embedding dominates. c-TF-IDF is O(total tokens) to count plus O(V log V) per cluster to sort V distinct words. Naming adds one LLM call per topic — tens of calls, not thousands. Memory O(vocabulary × clusters).

A real-life example

c-TF-IDF on three hand-made clusters of delivery-app complaints:

Python
clusters = {0: ["Refund not received for my order", "Refund still pending after cancelling",                "When will my refund reach my bank"],            1: ["Rider was late and the food was cold", "Order arrived late again",                "Late delivery, cold food"],            2: ["App crashes when I open my order", "App logs me out after update"]}for label, terms in c_tf_idf(clusters, top_n=3).items():    print(label, terms)# 0 ['refund', 'bank', 'cancelling']# 1 ['late', 'cold', 'food']# 2 ['app', 'crashes', 'logs']

Trace for cluster 0: after removing stopwords and words shorter than three letters, it has 12 words; refund appears 3 times in cluster 0 and 3 times in the whole corpus. The average cluster has (12 + 12 + 10) / 3 = 11.33 words.

wordtf in cluster 0corpus countscore
refund3 / 12 = 0.2530.25 × log(1 + 11.33 / 3) = 0.391
bank1 / 12 = 0.08310.083 × log(1 + 11.33) = 0.209
order1 / 12 = 0.08330.083 × log(1 + 11.33 / 3) = 0.130

"order" appears in every cluster, so it scores low even though it is in cluster 0. In cluster 2, "after" and "when" (0.19 each) only just miss the top three — a real pipeline uses a fuller stopword list. That is also why an LLM naming step helps: given the keywords and examples, it can write "App crashes and logouts".

Food-delivery and e-commerce companies run this weekly over free-text feedback, so a new complaint theme shows up as a new topic before it shows up in the ratings.

Follow-up questions to expect

  • "The outlier group is huge — is something wrong?" — Often 20–40% outliers is normal for short, noisy text. Read it: it frequently contains emerging topics too small to form a cluster yet. BERTopic can also reassign outliers to their nearest topic afterwards.
  • "Topic ids change every run?" — Yes; UMAP and HDBSCAN are not stable identifiers. Match topics across runs by keyword or centroid similarity, never store the raw id as a permanent label.
  • "How do you evaluate topics?" — Topic coherence scores, human review of sample documents per topic, and, if labels exist for a subset, agreement with them.