Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Probabilistic & System-Scale Structures  ›  Lesson

Sampling a Stream You Can't Store

Reservoir Sampling 24 minKeep k items fairly from a stream of unknown length: each later item gets a k/(i+1) chance to swap in
You're building a piece ofQuarry — the storage engine
This piece — sample_counts(): Spot-checks a fair sample of a day's requests without storing the whole day.
Scenario Quarry needs to spot-check a fair sample of a day's requests without storing the full day, and has to prove the sample is actually fair, not just plausible.
Your task
Build sample_counts(stream, k, seed, trials). Run reservoir_sample trials times, reseeding deterministically each time (e.g. seed * 1000 + trial index), and return a dict mapping each stream item to how many trials it survived into the final reservoir. Every item's count should land near trials * k / len(stream). Example: sample_counts(["a","b","c","d","e"], 2, 7, 1000) gives every item a count near 400.

Sampling a Stream You Can't Store

In short: to pick k items uniformly at random from a stream whose total length you don't know in advance, keep the first k items, then for every later item i (0-indexed), draw a random j between 0 and i. If j < k, that item swaps into slot j of the reservoir; otherwise it's dropped. Item i ends up with exactly a k / (i + 1) chance of surviving, and working through the algebra shows every item in the whole stream — no matter when it arrived — ends up with the same k / n chance of being in the final reservoir. The reservoir never grows past k items, however long the stream runs.

A k-cell reservoir, and one item fighting to get in

Audit a fair sample of a day's requests

Quarry can't store a full day of request logs just to pick a few for a spot-check, but it can prove the sample it does keep is fair by re-running the draw many times and counting how often each request survives:

python

With 5 requests and a reservoir of 2, every request should appear in roughly 1000 * 2 / 5 = 400 of the 1000 trials. Every seeded trial is reproducible — the same seed and trial index always draw the same j values, which is what makes sample_counts itself deterministic even though each individual draw is random.

Watch one stream of 5 items fill a 2-cell reservoir, replace once, get rejected once, then replace again:

watch it move · array

Each later item rolls a random j in [0, i]. j < k swaps it in; j >= k rejects it. The reservoir never grows past k cells.

Item 0, "p", fills the first empty slot — nothing to roll yet. 2 cells. At index 0.

1 / 5

Why item 0 doesn't get an unfair head start

The naive worry is that early items had "more chances" to be knocked out, so they should be rarer in the reservoir than late ones. The algebra says otherwise: item i's chance of surviving every later round multiplies out to exactly k / n, the same for every i:

What does this print?
from fractions import Fraction

def survival_chance(i, n, k):
    # P(kept at step i) * P(not bumped at each later DRAW, which only
    # happens once the reservoir is full — steps before k are free fills)
    p = Fraction(1) if i < k else Fraction(k, i + 1)
    for later in range(max(i + 1, k), n):
        p *= Fraction(later, later + 1)   # P(not bumped) = 1 - 1/(later+1) = later/(later+1)
    return p

print(survival_chance(0, 5, 2), survival_chance(4, 5, 2))

Sampling while filtering out noise

Bot traffic shouldn't count toward the sample at all — not even toward the position i that decides replacement odds. Skipping banned items without adjusting i would quietly break the k/n fairness guarantee:

broken — fix itIndexError

This crashes (or silently breaks fairness) once a ban lands early in the stream. Fix it so position is counted only over items that survive the filter.

Quick practice

What does this print?
import random
rng = random.Random(3)
print(rng.randint(0, 4), rng.randint(0, 5))
What does this print?
k, n = 3, 10
print(k / n == 3 / 10)

🎯 Your turn

Build sample_counts(stream, k, seed, trials): run reservoir_sample trials times, reseeding deterministically each time, and return a dict mapping each stream item to how many trials it survived into the final reservoir.

  • sample_counts(["a", "b", "c", "d", "e"], 2, seed=7, trials=1000) should give every item a count near 1000 * 2 / 5 = 400.

Hint: seed each trial with something that changes every time but stays deterministic, like seed * 1000 + trial_index. reservoir_sample itself is the standard Algorithm R from above.

Press ▶ Run, try the Live App chips, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upFill the reservoir first

Write seed_reservoir(stream, k), returning the first k items of stream as a list — the reservoir's starting state, before any random draws happen. seed_reservoir(['p','q','r','s','t'], 2) -> ['p', 'q'].

arrays
DrillThe replacement chance

Write inclusion_probability(i, k), returning the chance (as a float) that the item arriving at 1-indexed position i (with i > k) replaces something in a k-cell reservoir: k / i. inclusion_probability(5, 2) -> 0.4.

probability
BuildAlgorithm R

Write reservoir_sample(stream, k, seed): keep the first k items, then for each later item at 0-indexed position i, draw j = rng.randint(0, i) from a random.Random(seed) generator and replace slot j if j < k. reservoir_sample(['p','q','r','s','t'], 2, 4) -> ['t', 'q'].

reservoir-sampling
BossSample while filtering

Write reservoir_sample_exclude(stream, k, seed, banned): reservoir-sample stream, skipping any item in banned entirely — including from the position count that drives the random draw. reservoir_sample_exclude(['a','bot','b','c'], 2, 0, {'bot'}) -> ['a', 'c'].

reservoir-sampling
CapstoneProve the sample is fair

Write weighted_check(counts, n, k, trials, tolerance): given sample_counts's output, return True only if every item's count is within tolerance (a fraction, e.g. 0.3) of the expected trials * k / n. weighted_check({'a': 390, 'b': 410}, 2, 2, 400, 0.1) -> True.

probabilistic-structuresprobability
sample_counts.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
sample_counts(stream, k, seed, trials) → dictHow many of `trials` reservoir-sample runs kept each stream item, proving the k/n fairness guarantee.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.