Skip to content

Commit 5d5b473

Browse files
committed
variable sized root ranges
1 parent 09dc53c commit 5d5b473

10 files changed

Lines changed: 248 additions & 68 deletions

File tree

syndiffix/anonymizer.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ def _random_uniform(interval: FlatteningInterval, seed: Hash) -> int:
3939
return int(seed) % (interval.upper - interval.lower + 1) + interval.lower
4040

4141

42+
def _random_uniform_float(lower: float, upper: float, seed: Hash) -> float:
43+
# Convert hash to a float between 0 and 1
44+
u = (int(seed) & 0x7FFFFFFFFFFFFFFF) / 0x7FFFFFFFFFFFFFFF
45+
# Scale to the desired range
46+
return lower + u * (upper - lower)
47+
48+
4249
def _random_normal(sd: float, seed: Hash) -> float:
4350
u1 = (int(seed) & 0x7FFFFFFF) / 0x7FFFFFFF
4451
u1 = max(u1, sys.float_info.epsilon)
@@ -293,3 +300,16 @@ def noisy_row_limit(salt: bytes, seed: Hash, row_count: int, row_fraction: int)
293300
noise = _random_uniform(FlatteningInterval(-noise_range, noise_range), noise_seed)
294301

295302
return real_row_limit + noise
303+
304+
305+
def generate_root_buffers(anonymization_context: AnonymizationContext) -> tuple[float, float]:
306+
root_buffers = anonymization_context.anonymization_params.root_buffers
307+
salt = anonymization_context.anonymization_params.salt
308+
309+
lower_seed = _crypto_hash_salted_seed(salt, _mix_seed("lower_buffer", anonymization_context.bucket_seed))
310+
upper_seed = _crypto_hash_salted_seed(salt, _mix_seed("upper_buffer", anonymization_context.bucket_seed))
311+
312+
lower_buffer = _random_uniform_float(root_buffers.lower_low, root_buffers.lower_high, lower_seed)
313+
upper_buffer = _random_uniform_float(root_buffers.upper_low, root_buffers.upper_high, upper_seed)
314+
315+
return lower_buffer, upper_buffer

syndiffix/common.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ class Column:
3333
Columns = tuple[Column, ...]
3434

3535

36+
@dataclass(frozen=True)
37+
class RootBuffer:
38+
upper_high: float = 0.95
39+
upper_low: float = 0.85
40+
lower_high: float = 0.15
41+
lower_low: float = 0.05
42+
43+
3644
@dataclass(frozen=True)
3745
class FlatteningInterval:
3846
lower: int = 2
@@ -63,6 +71,7 @@ class AnonymizationParams:
6371
low_count_params: SuppressionParams = SuppressionParams()
6472
outlier_count: FlatteningInterval = FlatteningInterval()
6573
top_count: FlatteningInterval = FlatteningInterval()
74+
root_buffers: RootBuffer = RootBuffer()
6675
layer_noise_sd: float = 1.0
6776

6877

syndiffix/microdata.py

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,16 @@
1717
)
1818
from sklearn.preprocessing import MinMaxScaler
1919

20+
from .anonymizer import generate_root_buffers, hash_strings
2021
from .bucket import Buckets
21-
from .common import ColumnId, ColumnType, Value, check_column_names_or_ids
22+
from .common import (
23+
AnonymizationContext,
24+
AnonymizationParams,
25+
ColumnId,
26+
ColumnType,
27+
Value,
28+
check_column_names_or_ids,
29+
)
2230
from .interval import Interval, Intervals
2331
from .tree import Branch, Leaf, Node
2432

@@ -33,10 +41,15 @@
3341

3442

3543
class DataConvertor(ABC):
36-
def __init__(self) -> None:
44+
def __init__(self, column: str, anonymization_params: AnonymizationParams) -> None:
3745
self.scaler: Optional[MinMaxScaler] = None
3846
self.value_safe_flag: bool = False
3947

48+
base_seed = hash_strings(iter([str(column)]))
49+
self.lower_buffer, self.upper_buffer = generate_root_buffers(
50+
AnonymizationContext(base_seed, anonymization_params)
51+
)
52+
4053
@abstractmethod
4154
def column_type(self) -> ColumnType:
4255
pass
@@ -64,8 +77,8 @@ def denormalize_safe_values(self) -> None:
6477

6578

6679
class BooleanConvertor(DataConvertor):
67-
def __init__(self) -> None:
68-
super().__init__()
80+
def __init__(self, column: str, anonymization_params: AnonymizationParams) -> None:
81+
super().__init__(column, anonymization_params)
6982

7083
def column_type(self) -> ColumnType:
7184
return ColumnType.BOOLEAN
@@ -84,13 +97,12 @@ def create_value_safe_set(self, values: pd.Series) -> None:
8497

8598

8699
class RealConvertor(DataConvertor):
87-
def __init__(self, values: Iterable[Value]) -> None:
88-
super().__init__()
89-
# Fit up to 0.9999 so that the max bucket range is [0-1)
90-
self.scaler = MinMaxScaler(feature_range=(0.0, 0.9999)) # type: ignore
100+
def __init__(self, column: str, anonymization_params: AnonymizationParams, values: Iterable[Value]) -> None:
101+
super().__init__(column, anonymization_params)
102+
self.scaler = MinMaxScaler(feature_range=(self.lower_buffer, self.upper_buffer)) # type: ignore
91103
# This value-neutral fitting is only for passing unit tests, gets overridden
92104
# later by fit_transform().
93-
self.scaler.fit(np.array([[0.0], [0.9999]]))
105+
self.scaler.fit(np.array([[self.lower_buffer], [self.upper_buffer]]))
94106
self.final_round_precision = _get_round_precision(cast(Iterable[float], values))
95107

96108
def column_type(self) -> ColumnType:
@@ -116,13 +128,12 @@ def create_value_safe_set(self, values: pd.Series) -> None:
116128

117129

118130
class IntegerConvertor(DataConvertor):
119-
def __init__(self) -> None:
120-
super().__init__()
121-
# Fit up to 0.9999 so that the max bucket range is [0-1)
122-
self.scaler = MinMaxScaler(feature_range=(0.0, 0.9999)) # type: ignore
131+
def __init__(self, column: str, anonymization_params: AnonymizationParams) -> None:
132+
super().__init__(column, anonymization_params)
133+
self.scaler = MinMaxScaler(feature_range=(self.lower_buffer, self.upper_buffer)) # type: ignore
123134
# This value-neutral fitting is only for passing unit tests, gets overridden
124135
# later by fit_transform().
125-
self.scaler.fit(np.array([[0.0], [0.9999]]))
136+
self.scaler.fit(np.array([[self.lower_buffer], [self.upper_buffer]]))
126137

127138
def column_type(self) -> ColumnType:
128139
return ColumnType.INTEGER
@@ -146,13 +157,12 @@ def create_value_safe_set(self, values: pd.Series) -> None:
146157

147158

148159
class TimestampConvertor(DataConvertor):
149-
def __init__(self) -> None:
150-
super().__init__()
151-
# Fit up to 0.9999 so that the max bucket range is [0-1)
152-
self.scaler = MinMaxScaler(feature_range=(0.0, 0.9999)) # type: ignore
160+
def __init__(self, column: str, anonymization_params: AnonymizationParams) -> None:
161+
super().__init__(column, anonymization_params)
162+
self.scaler = MinMaxScaler(feature_range=(self.lower_buffer, self.upper_buffer)) # type: ignore
153163
# This value-neutral fitting is only for passing unit tests, gets overridden
154164
# later by fit_transform().
155-
self.scaler.fit(np.array([[0.0], [0.9999]]))
165+
self.scaler.fit(np.array([[self.lower_buffer], [self.upper_buffer]]))
156166

157167
def column_type(self) -> ColumnType:
158168
return ColumnType.TIMESTAMP
@@ -177,8 +187,8 @@ def create_value_safe_set(self, values: pd.Series) -> None:
177187

178188

179189
class StringConvertor(DataConvertor):
180-
def __init__(self, values: Iterable[Value]) -> None:
181-
super().__init__()
190+
def __init__(self, column: str, anonymization_params: AnonymizationParams, values: Iterable[Value]) -> None:
191+
super().__init__(column, anonymization_params)
182192
unique_values = set()
183193
for v in values:
184194
if not pd.isna(v):
@@ -196,7 +206,6 @@ def __init__(self, values: Iterable[Value]) -> None:
196206

197207
# Note that self.safe_values is only used if self.value_safe_flag is False
198208
self.safe_values: Set[float] = set()
199-
# Fit up to 0.9999 so that the max bucket range is [0-1)
200209
self.scaler = MinMaxScaler(feature_range=(0.0, 0.9999)) # type: ignore
201210
# This value-neutral fitting is only for passing unit tests, gets overridden
202211
# later by fit_transform().
@@ -231,7 +240,7 @@ def _map_interval(self, interval: Interval, rng: Random) -> MicrodataValue:
231240
min_value = int(interval.min)
232241
# max_value is inclusive
233242
max_value = min(int(interval.max) - 1, len(self.value_map) - 1)
234-
# The latter term in the above line can 0 (not sure why TODO: check)
243+
# The latter term in the above line can be 0 (not sure why TODO: check)
235244
max_value = max(min_value, max_value)
236245
value = rng.randint(min_value, max_value)
237246
if self.value_safe_flag is True or value in self.safe_values:
@@ -343,19 +352,19 @@ def _microdata_row_generator(
343352
yield [_generate(i, c, nm, rng) for i, c, nm in zip(intervals, convertors, null_mappings)]
344353

345354

346-
def get_convertor(df: pd.DataFrame, column: str) -> DataConvertor:
355+
def get_convertor(df: pd.DataFrame, column: str, anonymization_params: AnonymizationParams) -> DataConvertor:
347356
dtype = df.dtypes[column]
348357
if is_integer_dtype(dtype):
349-
return IntegerConvertor()
358+
return IntegerConvertor(column, anonymization_params)
350359
elif is_float_dtype(dtype):
351-
return RealConvertor(df[column])
360+
return RealConvertor(column, anonymization_params, df[column])
352361
elif is_bool_dtype(dtype):
353-
return BooleanConvertor()
362+
return BooleanConvertor(column, anonymization_params)
354363
elif is_datetime64_dtype(dtype):
355-
return TimestampConvertor()
364+
return TimestampConvertor(column, anonymization_params)
356365
elif is_string_dtype(dtype):
357366
# Note above is `True` for `object` dtype, but `StringConvertor` will assert values are `str`.
358-
return StringConvertor(df[column])
367+
return StringConvertor(column, anonymization_params, df[column])
359368
else:
360369
raise TypeError(f"Dtype {dtype} is not supported.")
361370

syndiffix/synthesizer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ def __init__(
9898
)
9999
counters_factory = GenericPidCountersFactory(len(pids.columns), max_low_count)
100100

101+
self.columns = tuple(raw_data.columns)
101102
self.raw_dtypes = raw_data.dtypes
102103

103-
self.column_convertors = [get_convertor(raw_data, column) for column in raw_data.columns]
104+
self.column_convertors = [get_convertor(raw_data, column, anonymization_params) for column in raw_data.columns]
104105
for col_id, convertor in enumerate(self.column_convertors):
105106
convertor.set_value_safe_flag(self.value_safe_columns_array[col_id])
106107
self.column_is_integral = [self._is_integral(convertor.column_type()) for convertor in self.column_convertors]

tests/clustering/test_measures.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ def test_measure_all() -> None:
2020
np.array(
2121
[
2222
[1.00, 0.2, 0.15, 0.02, 0.05],
23-
[0.2, 1.00, 0.18, 0.02, 0.06],
24-
[0.15, 0.18, 1.00, 0.04, 0.07],
23+
[0.2, 1.00, 0.14, 0.02, 0.04],
24+
[0.15, 0.14, 1.00, 0.04, 0.08],
2525
[0.02, 0.02, 0.04, 1.00, 0.01],
26-
[0.05, 0.06, 0.07, 0.01, 1.00],
26+
[0.05, 0.04, 0.08, 0.01, 1.00],
2727
]
2828
),
2929
)
3030

31-
assert np.array_equal(np.round(measures.entropy_1dim, 3), np.array([9.214, 9.207, 5.160, 0.118, 1.350]))
31+
print(np.round(measures.entropy_1dim, 3))
32+
assert np.array_equal(np.round(measures.entropy_1dim, 3), np.array([9.218, 9.212, 5.164, 0.118, 1.350]))

tests/conftest.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99

1010
SALT = bytes([])
1111
NOISELESS_SUPPRESSION = SuppressionParams(layer_sd=0.0)
12+
NOISELESS_ROOT_BUFFERS = RootBuffer(upper_high=0.9, upper_low=0.9, lower_high=0.1, lower_low=0.1)
13+
# NOISELESS_ROOT_BUFFERS = RootBuffer(upper_high=1.0, upper_low=1.0, lower_high=0.0, lower_low=0.0)
1214

1315
NOISELESS_PARAMS = AnonymizationParams(
1416
low_count_params=NOISELESS_SUPPRESSION,
17+
root_buffers=NOISELESS_ROOT_BUFFERS,
1518
layer_noise_sd=0.0,
1619
outlier_count=FlatteningInterval(upper=FlatteningInterval().lower),
1720
top_count=FlatteningInterval(upper=FlatteningInterval().lower),
@@ -41,7 +44,9 @@ def _load_csv(path: str, columns: list[str] | None) -> pd.DataFrame:
4144
df = pd.read_csv(path, keep_default_na=False, na_values=[""], low_memory=False)
4245
if columns is not None:
4346
df = df[columns]
44-
return apply_convertors([get_convertor(df, column) for column in df.columns], df)
47+
# Create default anonymization params for loading CSV data
48+
params = NOISELESS_PARAMS
49+
return apply_convertors([get_convertor(df, column, params) for column in df.columns], df)
4550

4651

4752
def load_forest(

0 commit comments

Comments
 (0)