Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
Every recommender model, however sophisticated, is trained on one artifact: a table of (user, item, signal, timestamp) rows built from raw event logs. Getting from a firehose of page-view and click events — full of duplicates, bot traffic, accidental double-clicks, and inconsistent item IDs — to a clean interaction table is unglamorous but foundational. Skip this step or do it sloppily and you'll spend weeks debugging a model that's actually fine, because the data feeding it is corrupted: the same view logged three times, a deprecated item ID that no longer maps to a real item, or a session boundary that's arbitrary. Practitioners who've done this before know: budget real time for this stage, write assertions that catch schema drift, and never assume the raw logs are already what you need.
The demo takes a batch of raw, messy events and reduces them to a clean interaction table: deduplicating, filtering bot-like rapid-fire clicks, and collapsing multiple views into a single strongest signal per (user, item).
from collections import defaultdict
RAW_EVENTS = [
{"user": "u1", "item": "i1", "type": "view", "ts": 100},
{"user": "u1", "item": "i1", "type": "view", "ts": 100}, # duplicate log line
{"user": "u1", "item": "i1", "type": "click", "ts": 101},
{"user": "u1", "item": "i2", "type": "click", "ts": 102},
{"user": "u1", "item": "i2", "type": "click", "ts": 102.05}, # bot/double-click
{"user": "u2", "item": "i3", "type": "purchase", "ts": 200},
{"user": "u2", "item": "i3", "type": "click", "ts": 199},
]
SIGNAL_RANK = {"view": 1, "click": 2, "purchase": 3}
def clean_events(raw):
seen = set()
deduped = []
for e in raw:
key = (e["user"], e["item"], e["type"], round(e["ts"], 1))
if key in seen:
continue
seen.add(key)
deduped.append(e)
return deduped
def filter_bot_bursts(events, min_gap=0.5):
# Drop same (user, item) events fired implausibly close together.
events = sorted(events, key=lambda e: (e["user"], e["item"], e["ts"]))
kept, last_ts = [], {}
for e in events:
key = (e["user"], e["item"])
if key in last_ts and e["ts"] - last_ts[key] < min_gap:
continue
last_ts[key] = e["ts"]
kept.append(e)
return kept
def collapse_to_strongest_signal(events):
strongest = defaultdict(lambda: ("view", 0))
for e in events:
key = (e["user"], e["item"])
cur_type, cur_rank = strongest[key]
rank = SIGNAL_RANK[e["type"]]
if rank > cur_rank:
strongest[key] = (e["type"], rank)
return {k: v[0] for k, v in strongest.items()}
pipeline = collapse_to_strongest_signal(filter_bot_bursts(clean_events(RAW_EVENTS)))
for (user, item), signal in sorted(pipeline.items()):
print(f"{user} x {item}: {signal}")python3 main.py