anonymization-lib 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anonymization_lib/__init__.py +7 -0
- anonymization_lib/io/__init__.py +4 -0
- anonymization_lib/io/data_import.py +90 -0
- anonymization_lib/io/export.py +87 -0
- anonymization_lib/metrics/__init__.py +5 -0
- anonymization_lib/metrics/k_anonymity.py +391 -0
- anonymization_lib/metrics/l_diversity.py +393 -0
- anonymization_lib/metrics/t_closeness.py +618 -0
- anonymization_lib/techniques/__init__.py +6 -0
- anonymization_lib/techniques/generalization.py +907 -0
- anonymization_lib/techniques/pipeline.py +134 -0
- anonymization_lib/techniques/substitution.py +343 -0
- anonymization_lib/techniques/suppression.py +255 -0
- anonymization_lib/utils.py +153 -0
- anonymization_lib/visualization/Suggestion.py +730 -0
- anonymization_lib/visualization/__init__.py +4 -0
- anonymization_lib/visualization/visualization.py +845 -0
- anonymization_lib-1.4.0.dist-info/METADATA +302 -0
- anonymization_lib-1.4.0.dist-info/RECORD +22 -0
- anonymization_lib-1.4.0.dist-info/WHEEL +5 -0
- anonymization_lib-1.4.0.dist-info/licenses/LICENSE +21 -0
- anonymization_lib-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
from pyspark.sql import DataFrame
|
|
2
|
+
from pyspark.sql import functions as F
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AnonymizationAdvisor:
|
|
6
|
+
"""
|
|
7
|
+
Suggests anonymization improvements based on k-anonymity analysis.
|
|
8
|
+
|
|
9
|
+
The advisor evaluates the current level of k-anonymity of a dataset
|
|
10
|
+
and analyzes the influence of each quasi-identifier on the formation
|
|
11
|
+
of equivalence groups.
|
|
12
|
+
|
|
13
|
+
For each quasi-identifier, the advisor evaluates two complementary
|
|
14
|
+
properties:
|
|
15
|
+
|
|
16
|
+
1. Cardinality frequency:
|
|
17
|
+
Measures the proportion of distinct values in the column relative
|
|
18
|
+
to the total number of records. High-cardinality attributes may
|
|
19
|
+
have strong individual identifying power.
|
|
20
|
+
|
|
21
|
+
2. Equivalence-group reduction:
|
|
22
|
+
Estimates how the number of equivalence groups would change if
|
|
23
|
+
the attribute were removed from the quasi-identifier set. A large
|
|
24
|
+
reduction indicates that the attribute contributes significantly
|
|
25
|
+
to the fragmentation of the dataset.
|
|
26
|
+
|
|
27
|
+
Using both metrics avoids misleading recommendations when multiple
|
|
28
|
+
high-cardinality quasi-identifiers coexist. For example, removing
|
|
29
|
+
one unique identifier may produce no reduction in equivalence groups
|
|
30
|
+
if another unique identifier remains in the quasi-identifier set.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
quasi_identifiers : list of str
|
|
35
|
+
Columns used to form equivalence groups (quasi-identifiers).
|
|
36
|
+
These are attributes that could potentially re-identify
|
|
37
|
+
individuals when combined.
|
|
38
|
+
|
|
39
|
+
k : int
|
|
40
|
+
Target k-anonymity value. Each equivalence group should contain
|
|
41
|
+
at least ``k`` records for the dataset to satisfy the configured
|
|
42
|
+
k-anonymity requirement.
|
|
43
|
+
|
|
44
|
+
The value must be an integer greater than 1. Boolean values are
|
|
45
|
+
not accepted.
|
|
46
|
+
|
|
47
|
+
Notes
|
|
48
|
+
-----
|
|
49
|
+
The advisor does not modify the input dataset or automatically apply
|
|
50
|
+
anonymization techniques. It only analyzes the dataset and generates
|
|
51
|
+
recommendations that can guide subsequent anonymization decisions.
|
|
52
|
+
|
|
53
|
+
The analysis is performed using Spark DataFrame operations so that
|
|
54
|
+
large datasets can be processed in a distributed manner.
|
|
55
|
+
|
|
56
|
+
The generated recommendations combine two complementary indicators:
|
|
57
|
+
|
|
58
|
+
- ``cardinality_frequency``:
|
|
59
|
+
Ratio between the number of distinct values in a quasi-identifier
|
|
60
|
+
and the total number of records.
|
|
61
|
+
|
|
62
|
+
- ``group_reduction_frequency``:
|
|
63
|
+
Relative reduction in the number of equivalence groups when the
|
|
64
|
+
quasi-identifier is removed from the current quasi-identifier set.
|
|
65
|
+
|
|
66
|
+
The risk thresholds used by the advisor are heuristic and are intended
|
|
67
|
+
to provide practical guidance rather than a formal privacy guarantee.
|
|
68
|
+
The actual anonymization strategy should also consider the semantics
|
|
69
|
+
of each attribute and the intended use of the dataset.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
HIGH_CARDINALITY_THRESHOLD = 0.50
|
|
73
|
+
MEDIUM_CARDINALITY_THRESHOLD = 0.10
|
|
74
|
+
|
|
75
|
+
HIGH_GROUP_REDUCTION_THRESHOLD = 0.50
|
|
76
|
+
MEDIUM_GROUP_REDUCTION_THRESHOLD = 0.20
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
quasi_identifiers: list,
|
|
81
|
+
k: int,
|
|
82
|
+
):
|
|
83
|
+
if (
|
|
84
|
+
not isinstance(quasi_identifiers, list)
|
|
85
|
+
or not quasi_identifiers
|
|
86
|
+
):
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"'quasi_identifiers' must be a non-empty list."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not all(
|
|
92
|
+
isinstance(column, str) and column.strip()
|
|
93
|
+
for column in quasi_identifiers
|
|
94
|
+
):
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"All quasi-identifiers must be non-empty strings."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if len(quasi_identifiers) != len(set(quasi_identifiers)):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"'quasi_identifiers' must not contain duplicates."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
not isinstance(k, int)
|
|
106
|
+
or isinstance(k, bool)
|
|
107
|
+
or k <= 1
|
|
108
|
+
):
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"'k' must be an integer greater than 1."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self.quasi_identifiers = quasi_identifiers
|
|
114
|
+
self.k = k
|
|
115
|
+
|
|
116
|
+
def suggest(
|
|
117
|
+
self,
|
|
118
|
+
df: DataFrame,
|
|
119
|
+
) -> "AnonymizationAdvisorResult":
|
|
120
|
+
"""
|
|
121
|
+
Analyzes the dataset and generates recommendations to improve
|
|
122
|
+
k-anonymity.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
df : DataFrame
|
|
127
|
+
Input Spark DataFrame.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
AnonymizationAdvisorResult
|
|
132
|
+
Object containing the global privacy summary and the
|
|
133
|
+
column-level anonymization recommendations.
|
|
134
|
+
|
|
135
|
+
Raises
|
|
136
|
+
------
|
|
137
|
+
ValueError
|
|
138
|
+
If the input is None, is not compatible with a Spark
|
|
139
|
+
DataFrame, or does not contain all configured
|
|
140
|
+
quasi-identifiers.
|
|
141
|
+
"""
|
|
142
|
+
if df is None:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"Input DataFrame cannot be None."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Compatible with standard Spark and Spark Connect.
|
|
148
|
+
if not hasattr(df, "groupBy"):
|
|
149
|
+
raise ValueError(
|
|
150
|
+
"Input must be a Spark-compatible DataFrame."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
missing_columns = [
|
|
154
|
+
column
|
|
155
|
+
for column in self.quasi_identifiers
|
|
156
|
+
if column not in df.columns
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
if missing_columns:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"Columns not found in DataFrame: {missing_columns}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
equivalence_groups = self._build_equivalence_groups(
|
|
165
|
+
df,
|
|
166
|
+
self.quasi_identifiers,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
(
|
|
170
|
+
summary_df,
|
|
171
|
+
current_groups,
|
|
172
|
+
total_records,
|
|
173
|
+
) = self._build_summary(
|
|
174
|
+
df,
|
|
175
|
+
equivalence_groups,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
suggestions_df = self._build_suggestions(
|
|
179
|
+
df,
|
|
180
|
+
current_groups,
|
|
181
|
+
total_records,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return AnonymizationAdvisorResult(
|
|
185
|
+
summary_df=summary_df,
|
|
186
|
+
suggestions_df=suggestions_df,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _build_equivalence_groups(
|
|
190
|
+
self,
|
|
191
|
+
df: DataFrame,
|
|
192
|
+
columns: list,
|
|
193
|
+
) -> DataFrame:
|
|
194
|
+
"""
|
|
195
|
+
Builds equivalence groups using the selected quasi-identifiers.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
df : DataFrame
|
|
200
|
+
Input Spark DataFrame.
|
|
201
|
+
|
|
202
|
+
columns : list of str
|
|
203
|
+
Columns used to define the equivalence groups.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
DataFrame
|
|
208
|
+
DataFrame containing one row per equivalence group and a
|
|
209
|
+
``group_size`` column with the number of records belonging
|
|
210
|
+
to each group.
|
|
211
|
+
"""
|
|
212
|
+
return (
|
|
213
|
+
df
|
|
214
|
+
.groupBy(*columns)
|
|
215
|
+
.agg(
|
|
216
|
+
F.count("*").alias("group_size")
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def _build_summary(
|
|
221
|
+
self,
|
|
222
|
+
df: DataFrame,
|
|
223
|
+
equivalence_groups: DataFrame,
|
|
224
|
+
):
|
|
225
|
+
"""
|
|
226
|
+
Builds the global k-anonymity summary.
|
|
227
|
+
|
|
228
|
+
The total number of records is calculated from the sizes of the
|
|
229
|
+
already generated equivalence groups instead of executing an
|
|
230
|
+
additional ``df.count()`` action over the original dataset.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
df : DataFrame
|
|
235
|
+
Input Spark DataFrame. It is used to create the resulting
|
|
236
|
+
summary DataFrame.
|
|
237
|
+
|
|
238
|
+
equivalence_groups : DataFrame
|
|
239
|
+
DataFrame containing the equivalence groups and their sizes.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
tuple
|
|
244
|
+
A tuple containing:
|
|
245
|
+
|
|
246
|
+
- summary_df:
|
|
247
|
+
Spark DataFrame with the global k-anonymity statistics.
|
|
248
|
+
|
|
249
|
+
- total_groups:
|
|
250
|
+
Number of equivalence groups in the dataset.
|
|
251
|
+
|
|
252
|
+
- total_records:
|
|
253
|
+
Number of records in the dataset.
|
|
254
|
+
"""
|
|
255
|
+
stats = (
|
|
256
|
+
equivalence_groups
|
|
257
|
+
.agg(
|
|
258
|
+
F.count("*").alias(
|
|
259
|
+
"total_groups"
|
|
260
|
+
),
|
|
261
|
+
|
|
262
|
+
F.sum("group_size").alias(
|
|
263
|
+
"total_records"
|
|
264
|
+
),
|
|
265
|
+
|
|
266
|
+
F.sum(
|
|
267
|
+
F.when(
|
|
268
|
+
F.col("group_size") < self.k,
|
|
269
|
+
1,
|
|
270
|
+
).otherwise(0)
|
|
271
|
+
).alias(
|
|
272
|
+
"risky_groups"
|
|
273
|
+
),
|
|
274
|
+
|
|
275
|
+
F.sum(
|
|
276
|
+
F.when(
|
|
277
|
+
F.col("group_size") < self.k,
|
|
278
|
+
F.col("group_size"),
|
|
279
|
+
).otherwise(0)
|
|
280
|
+
).alias(
|
|
281
|
+
"risky_records"
|
|
282
|
+
),
|
|
283
|
+
|
|
284
|
+
F.min("group_size").alias(
|
|
285
|
+
"current_k"
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
.first()
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
total_groups = stats["total_groups"] or 0
|
|
292
|
+
total_records = stats["total_records"] or 0
|
|
293
|
+
risky_groups = stats["risky_groups"] or 0
|
|
294
|
+
risky_records = stats["risky_records"] or 0
|
|
295
|
+
current_k = stats["current_k"]
|
|
296
|
+
|
|
297
|
+
risky_records_frequency = (
|
|
298
|
+
round(
|
|
299
|
+
risky_records / total_records,
|
|
300
|
+
4,
|
|
301
|
+
)
|
|
302
|
+
if total_records > 0
|
|
303
|
+
else 0.0
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
summary_df = df.sparkSession.createDataFrame(
|
|
307
|
+
[(
|
|
308
|
+
self.k,
|
|
309
|
+
current_k,
|
|
310
|
+
total_records,
|
|
311
|
+
total_groups,
|
|
312
|
+
risky_groups,
|
|
313
|
+
risky_records,
|
|
314
|
+
risky_records_frequency,
|
|
315
|
+
)],
|
|
316
|
+
[
|
|
317
|
+
"target_k",
|
|
318
|
+
"current_k",
|
|
319
|
+
"total_records",
|
|
320
|
+
"total_equivalence_groups",
|
|
321
|
+
"risky_groups",
|
|
322
|
+
"risky_records",
|
|
323
|
+
"risky_records_frequency",
|
|
324
|
+
],
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
summary_df,
|
|
329
|
+
total_groups,
|
|
330
|
+
total_records,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def _build_suggestions(
|
|
334
|
+
self,
|
|
335
|
+
df: DataFrame,
|
|
336
|
+
current_groups: int,
|
|
337
|
+
total_records: int,
|
|
338
|
+
) -> DataFrame:
|
|
339
|
+
"""
|
|
340
|
+
Builds column-level anonymization suggestions.
|
|
341
|
+
|
|
342
|
+
Each quasi-identifier is evaluated using two complementary
|
|
343
|
+
indicators:
|
|
344
|
+
|
|
345
|
+
``cardinality_frequency``
|
|
346
|
+
Ratio between the number of distinct values in the column
|
|
347
|
+
and the total number of records.
|
|
348
|
+
|
|
349
|
+
``group_reduction_frequency``
|
|
350
|
+
Relative reduction in the number of equivalence groups that
|
|
351
|
+
would occur if the column were removed from the current
|
|
352
|
+
quasi-identifier set.
|
|
353
|
+
|
|
354
|
+
Combining both indicators allows the advisor to detect
|
|
355
|
+
high-cardinality attributes even when their marginal removal
|
|
356
|
+
produces little or no reduction because another identifying
|
|
357
|
+
attribute remains in the quasi-identifier set.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
df : DataFrame
|
|
362
|
+
Input Spark DataFrame.
|
|
363
|
+
|
|
364
|
+
current_groups : int
|
|
365
|
+
Number of equivalence groups generated using all configured
|
|
366
|
+
quasi-identifiers.
|
|
367
|
+
|
|
368
|
+
total_records : int
|
|
369
|
+
Total number of records in the dataset.
|
|
370
|
+
|
|
371
|
+
Returns
|
|
372
|
+
-------
|
|
373
|
+
DataFrame
|
|
374
|
+
Spark DataFrame containing cardinality information,
|
|
375
|
+
equivalence-group impact, risk level, and a suggested
|
|
376
|
+
anonymization action for each quasi-identifier.
|
|
377
|
+
"""
|
|
378
|
+
qis = self.quasi_identifiers
|
|
379
|
+
|
|
380
|
+
# Calculate all column cardinalities in a single Spark action.
|
|
381
|
+
cardinalities = (
|
|
382
|
+
df
|
|
383
|
+
.agg(*[
|
|
384
|
+
F.countDistinct(
|
|
385
|
+
F.col(column)
|
|
386
|
+
).alias(column)
|
|
387
|
+
for column in qis
|
|
388
|
+
])
|
|
389
|
+
.first()
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Calculate the number of equivalence groups that would remain
|
|
393
|
+
# after removing each quasi-identifier.
|
|
394
|
+
#
|
|
395
|
+
# When there is only one quasi-identifier, removing it leaves
|
|
396
|
+
# all records in one equivalence group.
|
|
397
|
+
if len(qis) == 1:
|
|
398
|
+
groups_without = {
|
|
399
|
+
qis[0]: 1
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
else:
|
|
403
|
+
groups_without = (
|
|
404
|
+
df
|
|
405
|
+
.agg(*[
|
|
406
|
+
F.countDistinct(
|
|
407
|
+
F.struct(*[
|
|
408
|
+
F.col(other_column)
|
|
409
|
+
for other_column in qis
|
|
410
|
+
if other_column != column
|
|
411
|
+
])
|
|
412
|
+
).alias(column)
|
|
413
|
+
for column in qis
|
|
414
|
+
])
|
|
415
|
+
.first()
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
rows = []
|
|
419
|
+
|
|
420
|
+
for column in qis:
|
|
421
|
+
cardinality = (
|
|
422
|
+
cardinalities[column]
|
|
423
|
+
or 0
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
groups_without_column = (
|
|
427
|
+
groups_without[column]
|
|
428
|
+
or 0
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
cardinality_frequency = (
|
|
432
|
+
cardinality / total_records
|
|
433
|
+
if total_records > 0
|
|
434
|
+
else 0.0
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
group_reduction = (
|
|
438
|
+
current_groups
|
|
439
|
+
- groups_without_column
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
group_reduction_frequency = (
|
|
443
|
+
group_reduction / current_groups
|
|
444
|
+
if current_groups > 0
|
|
445
|
+
else 0.0
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
rows.append((
|
|
449
|
+
column,
|
|
450
|
+
cardinality,
|
|
451
|
+
cardinality_frequency,
|
|
452
|
+
current_groups,
|
|
453
|
+
groups_without_column,
|
|
454
|
+
group_reduction,
|
|
455
|
+
group_reduction_frequency,
|
|
456
|
+
))
|
|
457
|
+
|
|
458
|
+
result = df.sparkSession.createDataFrame(
|
|
459
|
+
rows,
|
|
460
|
+
[
|
|
461
|
+
"column",
|
|
462
|
+
"cardinality",
|
|
463
|
+
"cardinality_frequency",
|
|
464
|
+
"current_equivalence_groups",
|
|
465
|
+
"groups_without_column",
|
|
466
|
+
"group_reduction",
|
|
467
|
+
"group_reduction_frequency",
|
|
468
|
+
],
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
high_risk_condition = (
|
|
472
|
+
(
|
|
473
|
+
F.col("cardinality_frequency")
|
|
474
|
+
>= self.HIGH_CARDINALITY_THRESHOLD
|
|
475
|
+
)
|
|
476
|
+
|
|
|
477
|
+
(
|
|
478
|
+
F.col("group_reduction_frequency")
|
|
479
|
+
>= self.HIGH_GROUP_REDUCTION_THRESHOLD
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
medium_risk_condition = (
|
|
484
|
+
(
|
|
485
|
+
F.col("cardinality_frequency")
|
|
486
|
+
>= self.MEDIUM_CARDINALITY_THRESHOLD
|
|
487
|
+
)
|
|
488
|
+
|
|
|
489
|
+
(
|
|
490
|
+
F.col("group_reduction_frequency")
|
|
491
|
+
>= self.MEDIUM_GROUP_REDUCTION_THRESHOLD
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
result
|
|
497
|
+
.withColumn(
|
|
498
|
+
"risk_level",
|
|
499
|
+
F.when(
|
|
500
|
+
high_risk_condition,
|
|
501
|
+
F.lit("High"),
|
|
502
|
+
)
|
|
503
|
+
.when(
|
|
504
|
+
medium_risk_condition,
|
|
505
|
+
F.lit("Medium"),
|
|
506
|
+
)
|
|
507
|
+
.otherwise(
|
|
508
|
+
F.lit("Low")
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
.withColumn(
|
|
512
|
+
"suggested_action",
|
|
513
|
+
F.when(
|
|
514
|
+
F.col("cardinality_frequency")
|
|
515
|
+
>= self.HIGH_CARDINALITY_THRESHOLD,
|
|
516
|
+
F.lit(
|
|
517
|
+
"Consider suppression or strong generalization"
|
|
518
|
+
),
|
|
519
|
+
)
|
|
520
|
+
.when(
|
|
521
|
+
F.col("group_reduction_frequency")
|
|
522
|
+
>= self.HIGH_GROUP_REDUCTION_THRESHOLD,
|
|
523
|
+
F.lit(
|
|
524
|
+
"Consider generalization"
|
|
525
|
+
),
|
|
526
|
+
)
|
|
527
|
+
.when(
|
|
528
|
+
(
|
|
529
|
+
F.col("cardinality_frequency")
|
|
530
|
+
>= self.MEDIUM_CARDINALITY_THRESHOLD
|
|
531
|
+
)
|
|
532
|
+
|
|
|
533
|
+
(
|
|
534
|
+
F.col("group_reduction_frequency")
|
|
535
|
+
>= self.MEDIUM_GROUP_REDUCTION_THRESHOLD
|
|
536
|
+
),
|
|
537
|
+
F.lit(
|
|
538
|
+
"Consider moderate generalization"
|
|
539
|
+
),
|
|
540
|
+
)
|
|
541
|
+
.otherwise(
|
|
542
|
+
F.lit(
|
|
543
|
+
"Low priority for anonymization"
|
|
544
|
+
)
|
|
545
|
+
),
|
|
546
|
+
)
|
|
547
|
+
.orderBy(
|
|
548
|
+
F.when(F.col("risk_level") == "High", 3)
|
|
549
|
+
.when(F.col("risk_level") == "Medium", 2)
|
|
550
|
+
.otherwise(1)
|
|
551
|
+
.desc(),
|
|
552
|
+
F.desc("cardinality_frequency"),
|
|
553
|
+
F.desc("group_reduction_frequency"),
|
|
554
|
+
F.desc("cardinality"),
|
|
555
|
+
)
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class AnonymizationAdvisorResult:
|
|
560
|
+
"""
|
|
561
|
+
Stores the results generated by AnonymizationAdvisor.
|
|
562
|
+
|
|
563
|
+
Attributes
|
|
564
|
+
----------
|
|
565
|
+
summary_df : DataFrame
|
|
566
|
+
Global statistics describing the current equivalence groups
|
|
567
|
+
and the current k-anonymity level.
|
|
568
|
+
|
|
569
|
+
suggestions_df : DataFrame
|
|
570
|
+
Column-level anonymization recommendations based on attribute
|
|
571
|
+
cardinality and equivalence-group impact.
|
|
572
|
+
"""
|
|
573
|
+
|
|
574
|
+
def __init__(
|
|
575
|
+
self,
|
|
576
|
+
summary_df: DataFrame,
|
|
577
|
+
suggestions_df: DataFrame,
|
|
578
|
+
):
|
|
579
|
+
self.summary_df = summary_df
|
|
580
|
+
self.suggestions_df = suggestions_df
|
|
581
|
+
|
|
582
|
+
def get_summary_df(self) -> DataFrame:
|
|
583
|
+
"""
|
|
584
|
+
Returns the summary DataFrame.
|
|
585
|
+
|
|
586
|
+
Returns
|
|
587
|
+
-------
|
|
588
|
+
DataFrame
|
|
589
|
+
Spark DataFrame containing the global k-anonymity summary.
|
|
590
|
+
"""
|
|
591
|
+
return self.summary_df
|
|
592
|
+
|
|
593
|
+
def get_suggestions_df(self) -> DataFrame:
|
|
594
|
+
"""
|
|
595
|
+
Returns the suggestions DataFrame.
|
|
596
|
+
|
|
597
|
+
Returns
|
|
598
|
+
-------
|
|
599
|
+
DataFrame
|
|
600
|
+
Spark DataFrame containing the column-level anonymization
|
|
601
|
+
recommendations.
|
|
602
|
+
"""
|
|
603
|
+
return self.suggestions_df
|
|
604
|
+
|
|
605
|
+
def show_summary(
|
|
606
|
+
self,
|
|
607
|
+
truncate=False,
|
|
608
|
+
):
|
|
609
|
+
"""
|
|
610
|
+
Displays a global privacy assessment of the dataset.
|
|
611
|
+
|
|
612
|
+
The summary includes:
|
|
613
|
+
|
|
614
|
+
- target_k:
|
|
615
|
+
Desired k-anonymity level defined by the user.
|
|
616
|
+
|
|
617
|
+
- current_k:
|
|
618
|
+
Current k-anonymity level of the dataset, calculated as the
|
|
619
|
+
size of the smallest equivalence group.
|
|
620
|
+
|
|
621
|
+
- total_records:
|
|
622
|
+
Total number of records in the dataset.
|
|
623
|
+
|
|
624
|
+
- total_equivalence_groups:
|
|
625
|
+
Number of distinct equivalence groups generated from the
|
|
626
|
+
selected quasi-identifiers.
|
|
627
|
+
|
|
628
|
+
- risky_groups:
|
|
629
|
+
Number of equivalence groups whose size is smaller than
|
|
630
|
+
the target k value.
|
|
631
|
+
|
|
632
|
+
- risky_records:
|
|
633
|
+
Total number of records belonging to risky equivalence
|
|
634
|
+
groups.
|
|
635
|
+
|
|
636
|
+
- risky_records_frequency:
|
|
637
|
+
Proportion of records affected by risky groups with respect
|
|
638
|
+
to the total dataset size.
|
|
639
|
+
|
|
640
|
+
Interpretation
|
|
641
|
+
--------------
|
|
642
|
+
A dataset satisfies k-anonymity when:
|
|
643
|
+
|
|
644
|
+
current_k >= target_k
|
|
645
|
+
|
|
646
|
+
and therefore:
|
|
647
|
+
|
|
648
|
+
risky_groups = 0
|
|
649
|
+
risky_records = 0
|
|
650
|
+
"""
|
|
651
|
+
self.summary_df.show(
|
|
652
|
+
truncate=truncate
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
def show_suggestions(
|
|
656
|
+
self,
|
|
657
|
+
truncate=False,
|
|
658
|
+
):
|
|
659
|
+
"""
|
|
660
|
+
Displays anonymization recommendations for each quasi-identifier.
|
|
661
|
+
|
|
662
|
+
The analysis combines the individual cardinality of each
|
|
663
|
+
quasi-identifier with the impact of removing that attribute from
|
|
664
|
+
the current equivalence-group definition.
|
|
665
|
+
|
|
666
|
+
The output includes:
|
|
667
|
+
|
|
668
|
+
- column:
|
|
669
|
+
Evaluated quasi-identifier.
|
|
670
|
+
|
|
671
|
+
- cardinality:
|
|
672
|
+
Number of distinct non-null values in the column.
|
|
673
|
+
|
|
674
|
+
- cardinality_frequency:
|
|
675
|
+
Ratio between the number of distinct values and the total
|
|
676
|
+
number of records.
|
|
677
|
+
|
|
678
|
+
Values close to 1 indicate that the attribute has a large
|
|
679
|
+
number of distinct values relative to the dataset size and
|
|
680
|
+
may therefore have strong identifying power.
|
|
681
|
+
|
|
682
|
+
- current_equivalence_groups:
|
|
683
|
+
Number of equivalence groups generated using all selected
|
|
684
|
+
quasi-identifiers.
|
|
685
|
+
|
|
686
|
+
- groups_without_column:
|
|
687
|
+
Number of equivalence groups that would remain if the column
|
|
688
|
+
were excluded from the quasi-identifier set.
|
|
689
|
+
|
|
690
|
+
- group_reduction:
|
|
691
|
+
Absolute reduction in equivalence groups obtained by removing
|
|
692
|
+
the column.
|
|
693
|
+
|
|
694
|
+
- group_reduction_frequency:
|
|
695
|
+
Relative reduction in equivalence groups with respect to the
|
|
696
|
+
original number of groups.
|
|
697
|
+
|
|
698
|
+
- risk_level:
|
|
699
|
+
Heuristic classification of the quasi-identifier as High,
|
|
700
|
+
Medium, or Low risk.
|
|
701
|
+
|
|
702
|
+
The classification considers both cardinality frequency and
|
|
703
|
+
equivalence-group reduction.
|
|
704
|
+
|
|
705
|
+
- suggested_action:
|
|
706
|
+
Suggested anonymization strategy based on the observed
|
|
707
|
+
characteristics of the quasi-identifier.
|
|
708
|
+
|
|
709
|
+
Interpretation
|
|
710
|
+
--------------
|
|
711
|
+
``cardinality_frequency`` and ``group_reduction_frequency`` measure
|
|
712
|
+
different properties.
|
|
713
|
+
|
|
714
|
+
A high cardinality frequency indicates that the attribute has
|
|
715
|
+
substantial individual identifying power.
|
|
716
|
+
|
|
717
|
+
A high group reduction frequency indicates that removing the
|
|
718
|
+
attribute significantly reduces the fragmentation of the dataset.
|
|
719
|
+
|
|
720
|
+
These measures should be interpreted together. For example, two
|
|
721
|
+
unique identifiers may both have a cardinality frequency close to
|
|
722
|
+
1 while individually producing almost no group reduction because
|
|
723
|
+
the other unique identifier continues to separate every record.
|
|
724
|
+
|
|
725
|
+
The risk levels and suggested actions are heuristic guidance.
|
|
726
|
+
They do not themselves constitute a formal privacy guarantee.
|
|
727
|
+
"""
|
|
728
|
+
self.suggestions_df.show(
|
|
729
|
+
truncate=truncate
|
|
730
|
+
)
|