1- """Core DDSketch implementation."""
1+ """Core DDSketch implementation.
2+
3+ Optimized for high throughput with efficient bucket indexing and quantile queries.
4+ """
25
36from typing import Literal , Union
47from .mapping .logarithmic import LogarithmicMapping
811from .storage .contiguous import ContiguousStorage
912from .storage .sparse import SparseStorage
1013
14+
1115class DDSketch :
1216 """
1317 DDSketch implementation for quantile approximation with relative-error guarantees.
@@ -21,6 +25,9 @@ class DDSketch:
2125 by Charles Masson, Jee E. Rim and Homin K. Lee
2226 """
2327
28+ __slots__ = ('relative_accuracy' , 'cont_neg' , 'mapping' , 'positive_store' ,
29+ 'negative_store' , 'count' , 'zero_count' , '_min' , '_max' , '_sum' )
30+
2431 def __init__ (
2532 self ,
2633 relative_accuracy : float ,
@@ -54,7 +61,6 @@ def __init__(
5461 self .relative_accuracy = relative_accuracy
5562 self .cont_neg = cont_neg
5663
57-
5864 # Initialize mapping scheme
5965 if mapping_type == 'logarithmic' :
6066 self .mapping = LogarithmicMapping (relative_accuracy )
@@ -71,31 +77,53 @@ def __init__(
7177 self .positive_store = SparseStorage (strategy = bucket_strategy )
7278 self .negative_store = SparseStorage (strategy = bucket_strategy ) if cont_neg else None
7379
74- self .count = 0
75- self .zero_count = 0
80+ self .count = 0.0
81+ self .zero_count = 0.0
82+
83+ # Summary stats
84+ self ._min = float ('+inf' )
85+ self ._max = float ('-inf' )
86+ self ._sum = 0.0
7687
77- def insert (self , value : Union [int , float ]) -> None :
88+ def insert (self , value : Union [int , float ], weight : float = 1.0 ) -> None :
7889 """
7990 Insert a value into the sketch.
8091
8192 Args:
8293 value: The value to insert.
94+ weight: The weight of the value (default 1.0).
8395
8496 Raises:
8597 ValueError: If value is negative and cont_neg is False.
8698 """
99+ # Cache method lookups for hot path optimization
87100 if value > 0 :
88- bucket_idx = self .mapping .compute_bucket_index (value )
89- self .positive_store .add (bucket_idx )
101+ # Most common case: positive values
102+ # Inline the hot path with cached local references
103+ compute_idx = self .mapping .compute_bucket_index
104+ self .positive_store .add (compute_idx (value ), weight )
90105 elif value < 0 :
91106 if self .cont_neg :
92- bucket_idx = self .mapping .compute_bucket_index ( - value )
93- self .negative_store .add (bucket_idx )
107+ compute_idx = self .mapping .compute_bucket_index
108+ self .negative_store .add (compute_idx ( - value ), weight )
94109 else :
95110 raise ValueError ("Negative values not supported when cont_neg is False" )
96111 else :
97- self .zero_count += 1
98- self .count += 1
112+ self .zero_count += weight
113+
114+ # Track summary stats - combined update
115+ self .count += weight
116+ self ._sum += value * weight
117+ # Update min/max - use local to avoid repeated attribute access
118+ if value < self ._min :
119+ self ._min = value
120+ if value > self ._max :
121+ self ._max = value
122+
123+ # Alias for API compatibility
124+ def add (self , value : Union [int , float ], weight : float = 1.0 ) -> None :
125+ """Alias for insert()."""
126+ self .insert (value , weight )
99127
100128 def delete (self , value : Union [int , float ]) -> None :
101129 """
@@ -125,6 +153,7 @@ def delete(self, value: Union[int, float]) -> None:
125153
126154 if deleted :
127155 self .count -= 1
156+ self ._sum -= value
128157
129158 def quantile (self , q : float ) -> float :
130159 """
@@ -146,32 +175,52 @@ def quantile(self, q: float) -> float:
146175
147176 rank = q * (self .count - 1 )
148177
149- if self .cont_neg :
150- neg_count = self .negative_store .total_count
178+ if self .cont_neg and self . negative_store is not None :
179+ neg_count = self .negative_store .count
151180 if rank < neg_count :
152- # Handle negative values
153- curr_count = 0
154- if self .negative_store .min_index is not None :
155- for idx in range (self .negative_store .max_index , self .negative_store .min_index - 1 , - 1 ):
156- bucket_count = self .negative_store .get_count (idx )
157- curr_count += bucket_count
158- if curr_count > rank :
159- return - self .mapping .compute_value_from_index (idx )
181+ # Handle negative values - use reversed rank
182+ reversed_rank = neg_count - rank - 1
183+ key = self .negative_store .key_at_rank (reversed_rank , lower = False )
184+ return - self .mapping .compute_value_from_index (key )
160185 rank -= neg_count
161186
162187 if rank < self .zero_count :
163- return 0
188+ return 0.0
164189 rank -= self .zero_count
165190
166- curr_count = 0
167- if self .positive_store .min_index is not None :
168- for idx in range (self .positive_store .min_index , self .positive_store .max_index + 1 ):
169- bucket_count = self .positive_store .get_count (idx )
170- curr_count += bucket_count
171- if curr_count > rank :
172- return self .mapping .compute_value_from_index (idx )
173-
174- return float ('inf' )
191+ # Use key_at_rank for consistency with storage implementation
192+ key = self .positive_store .key_at_rank (rank )
193+ return self .mapping .compute_value_from_index (key )
194+
195+ # Alias for API compatibility
196+ def get_quantile_value (self , quantile : float ) -> float :
197+ """Alias for quantile()."""
198+ try :
199+ return self .quantile (quantile )
200+ except ValueError :
201+ return None
202+
203+ @property
204+ def avg (self ) -> float :
205+ """Return the exact average of values added to the sketch."""
206+ if self .count == 0 :
207+ return 0.0
208+ return self ._sum / self .count
209+
210+ @property
211+ def sum (self ) -> float :
212+ """Return the exact sum of values added to the sketch."""
213+ return self ._sum
214+
215+ @property
216+ def min (self ) -> float :
217+ """Return the minimum value added to the sketch."""
218+ return self ._min
219+
220+ @property
221+ def max (self ) -> float :
222+ """Return the maximum value added to the sketch."""
223+ return self ._max
175224
176225 def merge (self , other : 'DDSketch' ) -> None :
177226 """
@@ -185,12 +234,20 @@ def merge(self, other: 'DDSketch') -> None:
185234 """
186235 if self .relative_accuracy != other .relative_accuracy :
187236 raise ValueError ("Cannot merge sketches with different relative accuracies" )
237+
238+ if other .count == 0 :
239+ return
188240
189241 self .positive_store .merge (other .positive_store )
190- if self .cont_neg and other .cont_neg :
242+ if self .cont_neg and other .cont_neg and other . negative_store is not None :
191243 self .negative_store .merge (other .negative_store )
192- elif other .cont_neg and sum ( other .negative_store . counts . values ()) > 0 :
244+ elif other .cont_neg and other .negative_store is not None and other . negative_store . count > 0 :
193245 raise ValueError ("Cannot merge sketch containing negative values when cont_neg is False" )
194246
195247 self .zero_count += other .zero_count
196- self .count += other .count
248+ self .count += other .count
249+ self ._sum += other ._sum
250+ if other ._min < self ._min :
251+ self ._min = other ._min
252+ if other ._max > self ._max :
253+ self ._max = other ._max
0 commit comments