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

Count-Min & HyperLogLog

Sketches 26 minCount occurrences in a d x w grid and take the min to query; estimate distinct counts from the longest run of leading zeros seen
You're building a piece ofQuarry — the storage engine
This piece — count_min_estimate(): Counts how often each of millions of possible event types fires, in bounded memory.
Scenario Quarry's live dashboard tracks how often each of millions of possible event types fires, in a fixed, bounded amount of memory.
Your task
Build count_min_estimate(events, queries, d, w, seed). Build a d x w counter grid, increment one cell per row for every event (using _hash_to_row), then return a dict mapping each key in queries to the MINIMUM of its d cells. Example: count_min_estimate(["cat","cat","dog","fox"], ["cat","dog","fox"], d=2, w=4, seed=5) -> {"cat": 2, "dog": 1, "fox": 2}.

Count-Min & HyperLogLog

In short: a count-min sketch counts occurrences of many keys in a fixed d × w grid instead of one counter per key. Every event increments one cell per row — a different column in each row, picked by a different hash function. A query reads all d of a key's cells and reports the minimum, because a cell can only be inflated by collisions with other keys, never deflated: the smallest cell is always the closest to the truth, and the estimate is never below the real count. HyperLogLog answers a different question — not "how many of X" but "how many distinct keys overall" — using the same "give up exactness, buy back memory" trade: it tracks the longest run of leading zero bits seen per bucket, because seeing a long run of zeros is rare enough that it implies a lot of distinct hashes were tried.

A 2x4 counter grid from the real events cat,cat,dog,fox: "cat" reads correctly at 2 because row 0's cell is never shared, "dog" reads correctly at 1, but "fox" shares a cell with dog in row 0 and with cat in row 1, so it reads 2 instead of its true count of 1 — a collision can only push the estimate up, never down

Count events by key without a dict per key

Quarry's live dashboard tracks how often each of millions of possible event types fires, in bounded memory:

python

With only 4 columns per row, "dog" and "fox" collide in row 0, and "cat" and "fox" collide in row 1 — so "fox"'s two cells are 2 and 3, and its estimate comes out as min(2, 3) = 2, one more than its true count of 1. "dog" still reads correctly at 1, because row 1's cell for "dog" wasn't shared with anything. More rows means more independent chances to dodge a collision — that's the entire reason count-min uses more than one hash function.

Note the hash: Python's own hash() changes between runs unless PYTHONHASHSEED is fixed (see the Bloom filters lesson), so this uses hashlib.md5 keyed on a seed string instead — fully reproducible, on any machine, forever.

Watch the grid fill as 4 events arrive, then see the query take the min:

watch it move · grid

Each event lights one cell per row. Querying a key takes the MIN across its d cells — collisions can only push an estimate up, never down.

Event 1, "cat", lights row 0 col 2 and row 1 col 2. 2 by 4 table. 8 of 8 cells filled.

1 / 7

How many distinct visitors, without a set of them all

Counting distinct keys with a set() costs memory proportional to how many distinct keys there are. HyperLogLog trades exactness for a handful of small registers: hash each item, use its first few bits to pick a register, and in that register keep the longest run of leading zeros seen in the rest of the hash — because getting a hash with many leading zeros is exponentially rare, and seeing one at all is evidence that many distinct items have been tried:

python

A register holding 5 means "the longest zero-run assigned to this bucket was 5 bits" — rare for a small stream, likely once thousands of distinct items have passed through. Averaging that signal across many registers (the hll_registers / estimate_cardinality exercises below) turns it into a cardinality estimate using a small, fixed amount of memory regardless of whether a million items or a billion passed through.

Quick practice

What does this print?
def leading_zero_bits(h, width):
    if h == 0:
        return width
    count = 0
    bit = 1 << (width - 1)
    while bit and not (h & bit):
        count += 1
        bit >>= 1
    return count

print(leading_zero_bits(0, 4), leading_zero_bits(0b1111, 4))
What does this print?
d, w = 3, 100
print(d * w < 100 * 100)

🎯 Your turn

Build count_min_estimate(events, queries, d, w, seed): build a d x w counter grid, increment one cell per row for every event in events (using _hash_to_row), then return a dict mapping each key in queries to the minimum of its d cells.

  • count_min_estimate(["cat","cat","dog","fox"], ["cat","dog","fox"], d=2, w=4, seed=5) → {"cat": 2, "dog": 1, "fox": 2}

Hint: table = [[0] * w for _ in range(d)]. For each event, increment table[r][_hash_to_row(event, r, seed, w)] for every row r. Each query's answer is min(table[r][_hash_to_row(query, r, seed, w)] for r in range(d)).

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-upCount leading zero bits

Write leading_zero_bits(h, width), the number of leading 0 bits in h when written in width bits. leading_zero_bits(0b00100000, 8) -> 2.

bit-manipulation
DrillOne stable hash function per row

Write hash_to_row(key, row, seed, w), a deterministic (not Python's hash()) column index in [0, w) for key, that also depends on row and seed so different rows give different columns. Use hashlib.md5. hash_to_row('cat', 0, 5, 4) -> 2.

sketches
BuildBuild the counter grid

Write count_min_table(events, d, w, seed), returning the filled d x w grid itself (a list of lists), before any querying. count_min_table(['cat','cat','dog'], 2, 3, 5) -> [[1, 2, 0], [0, 3, 0]].

sketches
BossHyperLogLog's registers

Write hll_registers(items, bucket_bits, seed): hash each item to a 32-bit value (hashlib.md5), use its low bucket_bits bits to pick one of 2 ** bucket_bits registers, and keep the longest run of leading zeros (+1) seen in the REST of the hash for that register. len(hll_registers(['a','b','c'], 2, 9)) -> 4.

probabilistic-structures
CapstoneTurn registers into a count

Write estimate_cardinality(registers), the HyperLogLog cardinality estimate: alpha * m * m / sum(2 ** -r for r in registers), where m = len(registers) and alpha = 0.7213 / (1 + 1.079 / m) for m >= 128, else 0.7213. Round to 2 decimal places. estimate_cardinality([0, 6, 0, 5]) -> 5.64.

probabilistic-structures
count_min_estimate.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
count_min_estimate(events, queries, d, w, seed) → dictEstimated occurrence counts for each queried key, from a fixed-size d x w counter grid.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.