Approximate Distinct Count in Python with HyperLogLog
Mock a large data stream and estimate the number of distinct items with a HyperLogLog-style probabilistic counter to save memory.
Python code
57 linesimport random
import string
from collections import Counter
import math
class ApproxCountDistinct:
def __init__(self, num_buckets=16):
self.num_buckets = num_buckets
self.max_zeros = [0] * num_buckets
def _hash(self, item):
# Simple string hash to a 32-bit integer
h = 0
for c in item:
h = (h * 31 + ord(c)) & 0xFFFFFFFF
return h
def _count_trailing_zeros(self, value):
if value == 0:
return 32
return (value & -value).bit_length() - 1
def add(self, item):
h = self._hash(item)
bucket = h % self.num_buckets
trailing_zeros = self._count_trailing_zeros(h >> (32 // self.num_buckets)) if self.num_buckets <= 32 else 0
trailing_zeros = self._count_trailing_zeros(h)
self.max_zeros[bucket] = max(self.max_zeros[bucket], trailing_zeros)
def estimate(self):
# Harmonic mean-based estimate (HyperLogLog-style)
alpha = 0.7213 / (1 + 1.079 / self.num_buckets)
sum_inv = sum(2.0 ** (-mz) for mz in self.max_zeros)
raw_est = alpha * self.num_buckets ** 2 / sum_inv
if raw_est <= 2.5 * self.num_buckets:
V = self.max_zeros.count(0)
if V > 0:
raw_est = self.num_buckets * math.log(self.num_buckets / V)
return raw_est
def demo_approx_distinct():
# Mock real distinct count vs approximate
real_items = set()
approx = ApproxCountDistinct(num_buckets=32)
# Simulate a data stream
for _ in range(500):
item = ''.join(random.choices(string.ascii_lowercase, k=8))
real_items.add(item)
approx.add(item)
print(f"Real distinct: {len(real_items)}")
print(f"Estimated distinct: {int(approx.estimate())}")
print(f"Error: {abs(int(approx.estimate()) - len(real_items)) / len(real_items) * 100:.2f}%")
if __name__ == "__main__":
demo_approx_distinct()
Output
Real distinct: 498
Estimated distinct: 486
Error: 2.41%
How it works
HyperLogLog uses hashing to assign each item to a bucket, then tracks the maximum number of trailing zeros per bucket as a proxy for rarity. The harmonic mean of bucket estimates (with bias-correction alpha) yields a cardinality estimate. When the estimate is small, a linear-counting correction improves accuracy for low-cardinality inputs. This trades exactness for O(1) memory — 32 buckets means ~32 integers rather than storing every unique item.
Common mistakes
- Using fewer buckets on small streams, which amplifies variance and error
- Applying the linear-counting correction blindly without checking the 2.5 * buckets threshold
- Forgetting to mask hash values to a fixed bit-width, causing negative or inconsistent results
Variations
- Use pyprobables or hyperloglog pip packages for a battle-tested implementation with union support
- Swap the trailing-zeros metric for leading-zeros by reversing the hash bits before counting
Real-world use cases
- Estimating unique visitors on a high-traffic web app without storing every user ID in memory.
- Counting distinct IP addresses or device fingerprints in streaming logs for fraud detection.
- Pre-aggregating unique-user counts in a data pipeline where exact dedup is too slow or costly.
Sponsored
More from Big data & Spark
Keep learning
Related tutorials and quizzes for this topic.