@@ -58,6 +58,9 @@ def create_value_safe_set(self, values: pd.Series) -> None:
5858 def analyze_tree (self , root : Node ) -> None :
5959 pass
6060
61+ def denormalize_safe_values (self ) -> None :
62+ pass
63+
6164
6265class BooleanConvertor (DataConvertor ):
6366 def __init__ (self ) -> None :
@@ -71,7 +74,7 @@ def to_float(self, value: Value) -> float:
7174 return 1.0 if value else 0.0
7275
7376 def from_interval (self , interval : Interval , rng : Random ) -> MicrodataValue :
74- value = _generate_float (interval , rng ) >= 0.5
77+ value = _generate_random_float (interval , rng ) >= 0.5
7578 return (value , 1.0 if value else 0.0 )
7679
7780 def create_value_safe_set (self , values : pd .Series ) -> None :
@@ -84,7 +87,8 @@ def __init__(self, values: Iterable[Value]) -> None:
8487 super ().__init__ ()
8588 # Fit up to 0.9999 so that the max bucket range is [0-1)
8689 self .scaler = MinMaxScaler (feature_range = (0.0 , 0.9999 )) # type: ignore
87- # This value-neutral fitting is only for passing unit tests.
90+ # This value-neutral fitting is only for passing unit tests, gets overridden
91+ # later by fit_transform().
8892 self .scaler .fit (np .array ([[0.0 ], [0.9999 ]]))
8993 self .final_round_precision = _get_round_precision (cast (Iterable [float ], values ))
9094
@@ -96,7 +100,7 @@ def to_float(self, value: Value) -> float:
96100 return round (float (value ), self .final_round_precision )
97101
98102 def from_interval (self , interval : Interval , rng : Random ) -> MicrodataValue :
99- value = _generate_float (interval , rng )
103+ value = _generate_random_float (interval , rng )
100104 if self .value_safe_flag is True :
101105 value = _convert_to_safe_value (value , self .safe_values )
102106 assert self .scaler is not None
@@ -115,7 +119,8 @@ def __init__(self) -> None:
115119 super ().__init__ ()
116120 # Fit up to 0.9999 so that the max bucket range is [0-1)
117121 self .scaler = MinMaxScaler (feature_range = (0.0 , 0.9999 )) # type: ignore
118- # This value-neutral fitting is only for passing unit tests.
122+ # This value-neutral fitting is only for passing unit tests, gets overridden
123+ # later by fit_transform().
119124 self .scaler .fit (np .array ([[0.0 ], [0.9999 ]]))
120125
121126 def column_type (self ) -> ColumnType :
@@ -126,7 +131,7 @@ def to_float(self, value: Value) -> float:
126131 return float (value )
127132
128133 def from_interval (self , interval : Interval , rng : Random ) -> MicrodataValue :
129- value = _generate_float (interval , rng )
134+ value = _generate_random_float (interval , rng )
130135 if self .value_safe_flag is True :
131136 value = _convert_to_safe_value (value , self .safe_values )
132137 assert self .scaler is not None
@@ -144,7 +149,8 @@ def __init__(self) -> None:
144149 super ().__init__ ()
145150 # Fit up to 0.9999 so that the max bucket range is [0-1)
146151 self .scaler = MinMaxScaler (feature_range = (0.0 , 0.9999 )) # type: ignore
147- # This value-neutral fitting is only for passing unit tests.
152+ # This value-neutral fitting is only for passing unit tests, gets overridden
153+ # later by fit_transform().
148154 self .scaler .fit (np .array ([[0.0 ], [0.9999 ]]))
149155
150156 def column_type (self ) -> ColumnType :
@@ -156,7 +162,7 @@ def to_float(self, value: Value) -> float:
156162 return float ((value - TIMESTAMP_REFERENCE ) / pd .Timedelta (1 , "s" ))
157163
158164 def from_interval (self , interval : Interval , rng : Random ) -> MicrodataValue :
159- value = _generate_float (interval , rng )
165+ value = _generate_random_float (interval , rng )
160166 if self .value_safe_flag is True :
161167 value = _convert_to_safe_value (value , self .safe_values )
162168 assert self .scaler is not None
@@ -177,19 +183,30 @@ def __init__(self, values: Iterable[Value]) -> None:
177183 if not isinstance (value , str ):
178184 raise TypeError (f"Not a `str` object in a string dtype column: { value } ." )
179185 self .value_map = sorted (cast (Set [str ], unique_values ))
186+
180187 # Note that self.safe_values is only used if self.value_safe_flag is False
181- self .safe_values : Set [int ] = set ()
188+ self .safe_values : Set [float ] = set ()
189+ # Fit up to 0.9999 so that the max bucket range is [0-1)
190+ self .scaler = MinMaxScaler (feature_range = (0.0 , 0.9999 )) # type: ignore
191+ # This value-neutral fitting is only for passing unit tests, gets overridden
192+ # later by fit_transform().
193+ self .scaler .fit (np .array ([[0.0 ], [0.9999 ]]))
182194
183195 def column_type (self ) -> ColumnType :
184196 return ColumnType .STRING
185197
186198 def to_float (self , value : Value ) -> float :
199+ # Note that value here is the string itself, not an index.
187200 index = bisect_left (self .value_map , cast (str , value ))
188201 assert index >= 0 and index < len (self .value_map )
189202 return float (index )
190203
191204 def from_interval (self , interval : Interval , rng : Random ) -> MicrodataValue :
205+ assert self .scaler is not None
206+ interval = _find_encapsulated_integer_interval (interval , self .scaler )
207+ # From here on intervals are integers (cast as float)
192208 if interval .is_singularity ():
209+ # convert to integer for value_map
193210 return (self .value_map [int (interval .min )], interval .min )
194211 else :
195212 return self ._map_interval (interval , rng )
@@ -220,19 +237,32 @@ def analyze_tree_walk(node: Node) -> None:
220237 # Avoid the cost of maintaining safe_values if in any
221238 # event all values are safe (i.e. self.value_safe_flag is True)
222239 if self .value_safe_flag is False and node .is_singularity () and node .is_over_threshold (low_threshold ):
223- self .safe_values .add (int (node .actual_intervals [0 ].min ))
240+ # Note that the values here are normalized
241+ self .safe_values .add (float (node .actual_intervals [0 ].min ))
224242 elif isinstance (node , Branch ):
225243 for child_node in node .children .values ():
226244 analyze_tree_walk (child_node )
227245
228246 analyze_tree_walk (root )
247+ # from .tree import _dump_tree
248+ # _dump_tree(root) # Debugging line to see the tree structure
249+
250+ def denormalize_safe_values (self ) -> None :
251+ assert self .scaler is not None
252+ if self .value_safe_flag is False and self .safe_values :
253+ # Convert normalized values back to original integer values
254+ denormalized_safe_values : Set [float ] = set ()
255+ for normalized_value in self .safe_values :
256+ original_value = _inverse_normalize_value (float (normalized_value ), self .scaler )
257+ denormalized_safe_values .add (float (round (original_value )))
258+ self .safe_values = denormalized_safe_values
229259
230260 def create_value_safe_set (self , values : pd .Series ) -> None :
231261 # Not needed
232262 pass
233263
234264
235- def _generate_float (interval : Interval , rng : Random ) -> float :
265+ def _generate_random_float (interval : Interval , rng : Random ) -> float :
236266 return rng .uniform (interval .min , interval .max )
237267
238268
@@ -326,6 +356,66 @@ def _inverse_normalize_value(value: float, scaler: MinMaxScaler) -> float:
326356 return float (inverse_transformed_value )
327357
328358
359+ def _find_encapsulated_integer_interval (interval : Interval , scaler : MinMaxScaler ) -> Interval :
360+ """
361+ Find the largest interval within the given interval where the inverse-transformed
362+ bounds correspond to integers (within machine precision).
363+
364+ Args:
365+ interval: The input interval in normalized space
366+ scaler: The MinMaxScaler used for inverse transformation
367+
368+ Returns:
369+ A new interval with bounds that are integer values (cast as floats)
370+ """
371+ interval_new = interval .copy ()
372+
373+ # Handle singularity case - bounds are already at the same point
374+ if interval .is_singularity ():
375+ # Convert the single value to its corresponding integer
376+ inverse_value = _inverse_normalize_value (interval .min , scaler )
377+ integer_value = float (round (inverse_value ))
378+ interval_new .min = integer_value
379+ interval_new .max = integer_value
380+ return interval_new
381+
382+ # Find the smallest integer >= the inverse-transformed interval.min
383+ min_inverse = _inverse_normalize_value (interval .min , scaler )
384+ min_integer = int (round (min_inverse ))
385+
386+ # If the current min already transforms to an integer (within precision), use it
387+ if abs (min_inverse - min_integer ) < 1e-10 :
388+ interval_new .min = float (min_integer )
389+ else :
390+ # Find the next integer
391+ next_integer = min_integer + 1 if min_inverse > min_integer else min_integer
392+ interval_new .min = float (next_integer )
393+
394+ # Find the largest integer <= the inverse-transformed interval.max
395+ max_inverse = _inverse_normalize_value (interval .max , scaler )
396+ max_integer = int (round (max_inverse ))
397+
398+ # Note that the max value of an Interval is exclusive, so we need to take care
399+ if abs (max_inverse - max_integer ) < 1e-10 :
400+ # If this is exact, then it will be included in the next higher min_integer
401+ interval_new .max = float (max_integer )
402+ else :
403+ # Find the previous integer
404+ prev_integer = max_integer - 1 if max_inverse < max_integer else max_integer
405+ # We add 1.0 because the max value is exclusive
406+ interval_new .max = float (prev_integer + 1.0 )
407+
408+ # Ensure the new interval is valid (min <= max)
409+ if interval_new .min > interval_new .max :
410+ # If no valid integer interval exists within bounds, throw an exception
411+ raise ValueError (
412+ f"No valid integer interval exists within bounds. "
413+ f"Min integer: { interval_new .min } , Max integer: { interval_new .max } "
414+ )
415+
416+ return interval_new
417+
418+
329419def _normalize (values : pd .Series , scaler : Optional [MinMaxScaler ]) -> pd .Series :
330420 if scaler is None :
331421 # Convertors that don't need normalization
@@ -370,6 +460,9 @@ def apply_convertors(convertors: list[DataConvertor], raw_data: pd.DataFrame) ->
370460def generate_microdata (
371461 buckets : Buckets , convertors : list [DataConvertor ], null_mappings : list [float ], rng : Random
372462) -> list [MicrodataRow ]:
463+ # print(buckets) # Debugging line to see the buckets
464+ for convertor in convertors :
465+ convertor .denormalize_safe_values ()
373466 microdata_rows : list [MicrodataRow ] = []
374467 for bucket in buckets :
375468 microdata_rows .extend (
@@ -415,4 +508,3 @@ def make_value_safe_columns_array(df: pd.DataFrame, value_safe_columns: list[int
415508 result [column ] = True
416509
417510 return result
418- return result
0 commit comments