Skip to content

Commit 831afd9

Browse files
committed
Fix API contract and compatibility bugs.
Validate window, sync docs, dedup cardinality estimator.
1 parent 71468cc commit 831afd9

4 files changed

Lines changed: 57 additions & 63 deletions

File tree

README.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,26 @@ Use ``pip install hyperloglog`` to install from PyPI.
99
Usage:
1010
=====
1111

12-
::
12+
.. code-block:: python
1313
1414
import hyperloglog
1515
hll = hyperloglog.HyperLogLog(0.01) # accept 1% counting error
1616
hll.add("hello")
17-
print len(hll) # 1
17+
print(len(hll)) # 1
1818
hll.add("hello")
19-
print len(hll) # 1 as items aren't added more than once
19+
print(len(hll)) # 1 as items aren't added more than once
2020
hll.add("hello again")
21-
print len(hll) # 2
21+
print(len(hll)) # 2
2222
2323
If we add a further 1000 random strings (giving a total of 1002 strings) we'll have a count roughly within 1% of the true value, in this case it counts 1007 (within +/- 10.2 of the true value)
2424

25-
::
25+
.. code-block:: python
2626
2727
# add 1000 random 30 char strings to hll
2828
import random
2929
import string
3030
[hll.add("".join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for n in range(30)])) for m in range(1000)]
31-
print len(hll) # 1007
31+
print(len(hll)) # 1007
3232
3333
3434
Changes:

hyperloglog/hll.py

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,28 @@ def bit_length_vec(arr):
8181

8282

8383
def get_rho_vec(w, max_width):
84-
rho = max_width - bit_length_vec(w) + 1
84+
# compare before subtracting: bit_length_vec may return an unsigned
85+
# dtype (NumPy 2.x), where a negative rho would silently wrap around
86+
bits = bit_length_vec(w)
8587

86-
if np.count_nonzero(rho <= 0):
88+
if np.count_nonzero(bits > max_width):
8789
raise ValueError('w overflow')
8890

89-
return rho
91+
return max_width - bits + 1
92+
93+
94+
def get_estimate(M, m, p, alpha):
95+
# small-range linear counting below the empirical threshold, otherwise
96+
# the HLL raw estimate (bias-corrected while E <= 5m)
97+
V = np.count_nonzero(M == 0)
98+
99+
if V > 0:
100+
H = m * math.log(m / V)
101+
if H <= get_treshold(p):
102+
return H
103+
104+
E = alpha * (m ** 2) / np.power(2.0, -M, dtype=float).sum()
105+
return (E - estimate_bias(E, p)) if E <= 5 * m else E
90106

91107

92108
class HyperLogLog(object):
@@ -124,6 +140,10 @@ def __setstate__(self, d):
124140
for key in d:
125141
setattr(self, key, d[key])
126142

143+
# normalize M from pickles created by pre-NumPy versions (plain list)
144+
# or under the other NumPy major version (different counter dtype)
145+
self.M = np.asarray(self.M, HLL_COUNTER_TYPE)
146+
127147
def add(self, value):
128148
"""
129149
Adds the item to the HyperLogLog
@@ -152,7 +172,8 @@ def add_bulk(self, values):
152172
# w = <x_{p}x_{p+1}..>
153173
# M[j] = max(M[j], rho(w))
154174

155-
assert not isinstance(values, (bytes, str)) and hasattr(values, '__iter__')
175+
if isinstance(values, (bytes, str)) or not hasattr(values, '__iter__'):
176+
raise TypeError('values must be a non-string iterable')
156177

157178
x = np.fromiter((int.from_bytes(sha1(packb(value)).digest()[:8], byteorder='big') for value in values), np.uint64)
158179
j = x & (self.m - 1)
@@ -180,32 +201,17 @@ def update(self, *others):
180201
self.M = np.maximum.reduce(ml)
181202

182203
def __eq__(self, other):
183-
if self.m != other.m:
184-
raise ValueError('Counters precisions should be equal')
185-
186-
return np.array_equal(self.M, other.M)
204+
if not isinstance(other, HyperLogLog):
205+
return NotImplemented
187206

188-
def __ne__(self, other):
189-
return not self.__eq__(other)
207+
return self.m == other.m and np.array_equal(self.M, other.M)
190208

191209
def __len__(self):
192210
return round(self.card())
193211

194-
def _Ep(self):
195-
E = self.alpha * (self.m ** 2) / np.power(2.0, -self.M, dtype=float).sum()
196-
return (E - estimate_bias(E, self.p)) if E <= 5 * self.m else E
197-
198212
def card(self):
199213
"""
200214
Returns the estimate of the cardinality
201215
"""
202-
203-
#count number of registers equal to 0
204-
V = np.count_nonzero(self.M == 0)
205-
206-
if V > 0:
207-
H = self.m * math.log(self.m / V)
208-
return H if H <= get_treshold(self.p) else self._Ep()
209-
else:
210-
return self._Ep()
216+
return get_estimate(self.M, self.m, self.p, self.alpha)
211217

hyperloglog/shll.py

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from hashlib import sha1
1010
from msgpack import packb
11-
from .hll import get_treshold, estimate_bias, get_alpha, get_rho
11+
from .hll import get_alpha, get_rho, get_estimate
1212

1313

1414
class SlidingHyperLogLog(object):
@@ -27,14 +27,17 @@ def __init__(self, error_rate, window, lpfm=None):
2727

2828
self.window = window
2929

30+
if not (window > 0):
31+
raise ValueError('window must be > 0')
32+
3033
if lpfm is not None:
3134
m = len(lpfm)
32-
p = round(math.log(m, 2))
3335

34-
if (1 << p) != m:
36+
if m == 0 or (m & (m - 1)) != 0:
3537
raise ValueError('List length is not power of 2')
3638

37-
self.LPFM = lpfm
39+
p = m.bit_length() - 1
40+
self.LPFM = list(lpfm)
3841

3942
else:
4043
if not (0 < error_rate < 1):
@@ -58,6 +61,10 @@ def __setstate__(self, d):
5861
for key in d:
5962
setattr(self, key, d[key])
6063

64+
# normalize LPFM from pickles created by older versions, which used
65+
# None for empty registers and ascending timestamp order
66+
self.LPFM = [tuple(sorted(reg, reverse=True)) if reg else tuple() for reg in self.LPFM]
67+
6168
@classmethod
6269
def from_list(cls, lpfm, window):
6370
return cls(None, window, lpfm)
@@ -103,6 +110,9 @@ def update(self, *others):
103110
if self.m != item.m:
104111
raise ValueError('Counters precisions should be equal')
105112

113+
if self.window != item.window:
114+
raise ValueError('Counters windows should be equal')
115+
106116
for j, lpfms_j in enumerate(zip(self.LPFM, *list(item.LPFM for item in others))):
107117
Rmax = None
108118
tmp = []
@@ -122,20 +132,13 @@ def update(self, *others):
122132
self.LPFM[j] = tuple(tmp)
123133

124134
def __eq__(self, other):
125-
if self.m != other.m:
126-
raise ValueError('Counters precisions should be equal')
127-
128-
return self.LPFM == other.LPFM
135+
if not isinstance(other, SlidingHyperLogLog):
136+
return NotImplemented
129137

130-
def __ne__(self, other):
131-
return not self.__eq__(other)
138+
return self.m == other.m and self.window == other.window and self.LPFM == other.LPFM
132139

133140
def __len__(self):
134-
raise NotImplemented
135-
136-
def _Ep(self, M):
137-
E = self.alpha * (self.m ** 2) / np.power(2.0, -M, dtype=float).sum()
138-
return (E - estimate_bias(E, self.p)) if E <= 5 * self.m else E
141+
raise NotImplementedError('use card(timestamp) to estimate cardinality')
139142

140143
def card(self, timestamp, window=None):
141144
"""
@@ -150,14 +153,7 @@ def card(self, timestamp, window=None):
150153
_t = timestamp - window
151154
M = np.fromiter((np.max(np.fromiter((R for ts, R in lpfm if ts >= _t), int), initial=0) if lpfm else 0 for lpfm in self.LPFM), int)
152155

153-
#count number or registers equal to 0
154-
V = np.count_nonzero(M == 0)
155-
156-
if V > 0:
157-
H = self.m * math.log(self.m / V)
158-
return H if H <= get_treshold(self.p) else self._Ep(M)
159-
else:
160-
return self._Ep(M)
156+
return get_estimate(M, self.m, self.p, self.alpha)
161157

162158
def card_wlist(self, timestamp, window_list):
163159
"""
@@ -188,13 +184,5 @@ def card_wlist(self, timestamp, window_list):
188184

189185
res = []
190186
for M in M_list:
191-
#count number of registers equal to 0
192-
V = M.count(0)
193-
M = np.array(M, int)
194-
195-
if V > 0:
196-
H = self.m * math.log(self.m / V)
197-
res.append(H if H <= get_treshold(self.p) else self._Ep(M))
198-
else:
199-
res.append(self._Ep(M))
187+
res.append(get_estimate(np.array(M, int), self.m, self.p, self.alpha))
200188
return res

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env python
22

3-
from distutils.core import setup
3+
from setuptools import setup
44

5-
version = '0.1.5'
5+
version = '0.1.6'
66

77
setup(
88
name='hyperloglog',

0 commit comments

Comments
 (0)