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.
@@ -0,0 +1,393 @@
1
+ from pyspark.sql import DataFrame
2
+ from pyspark.sql import functions as F
3
+
4
+ from ..utils import EquivalenceGroups
5
+
6
+
7
+ class LDiversity(EquivalenceGroups):
8
+ """
9
+ Calculates distinct l-diversity for a dataset based on
10
+ quasi-identifiers and one sensitive attribute.
11
+
12
+ L-diversity extends k-anonymity by requiring each equivalence group
13
+ to contain sufficient diversity in its sensitive attribute.
14
+
15
+ This implementation uses distinct l-diversity. An equivalence group
16
+ satisfies l-diversity when it contains at least ``l_threshold``
17
+ distinct values of the sensitive attribute.
18
+
19
+ Parameters
20
+ ----------
21
+ quasi_identifiers : list of str
22
+ Columns used to form equivalence groups (quasi-identifiers).
23
+ These are attributes that could potentially re-identify
24
+ individuals when combined.
25
+
26
+ sensitive_attribute : str
27
+ Column representing the sensitive attribute whose diversity
28
+ must be protected (e.g., disease, salary category, diagnosis).
29
+
30
+ l_threshold : int, optional
31
+ Minimum number of distinct sensitive-attribute values required
32
+ in every equivalence group.
33
+
34
+ The value must be an integer greater than 1.
35
+ Default is 2.
36
+
37
+ Notes
38
+ -----
39
+ The implementation uses Spark DataFrame operations and performs
40
+ the computation in a distributed manner.
41
+
42
+ The metric implemented is distinct l-diversity. Other variants,
43
+ such as entropy l-diversity or recursive (c, l)-diversity, are not
44
+ computed by this class.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ quasi_identifiers=None,
50
+ sensitive_attribute=None,
51
+ l_threshold: int = 2
52
+ ):
53
+ if quasi_identifiers is None:
54
+ quasi_identifiers = []
55
+
56
+ if not isinstance(quasi_identifiers, list):
57
+ raise ValueError(
58
+ "'quasi_identifiers' must be a list of column names."
59
+ )
60
+
61
+ if not quasi_identifiers:
62
+ raise ValueError(
63
+ "At least one quasi-identifier must be provided."
64
+ )
65
+
66
+ if not all(
67
+ isinstance(col, str) and col.strip()
68
+ for col in quasi_identifiers
69
+ ):
70
+ raise ValueError(
71
+ "'quasi_identifiers' must contain only non-empty strings."
72
+ )
73
+
74
+ if len(quasi_identifiers) != len(set(quasi_identifiers)):
75
+ raise ValueError(
76
+ "'quasi_identifiers' must not contain duplicate columns."
77
+ )
78
+
79
+ if (
80
+ not isinstance(sensitive_attribute, str)
81
+ or not sensitive_attribute.strip()
82
+ ):
83
+ raise ValueError(
84
+ "'sensitive_attribute' must be a non-empty column name."
85
+ )
86
+
87
+ if sensitive_attribute in quasi_identifiers:
88
+ raise ValueError(
89
+ "'sensitive_attribute' must not be included "
90
+ "in 'quasi_identifiers'."
91
+ )
92
+
93
+ if (
94
+ not isinstance(l_threshold, int)
95
+ or isinstance(l_threshold, bool)
96
+ or l_threshold <= 1
97
+ ):
98
+ raise ValueError(
99
+ "'l_threshold' must be an integer greater than 1."
100
+ )
101
+
102
+ self.quasi_identifiers = quasi_identifiers
103
+ self.sensitive_attribute = sensitive_attribute
104
+ self.l_threshold = l_threshold
105
+
106
+ def summary(self, df: DataFrame):
107
+ """
108
+ Computes distinct l-diversity in a distributed manner.
109
+
110
+ The method first creates equivalence groups using the configured
111
+ quasi-identifiers. For each equivalence group, it calculates the
112
+ number of distinct values of the sensitive attribute.
113
+
114
+ An equivalence group violates l-diversity when:
115
+
116
+ l_diversity < l_threshold
117
+
118
+ The method also calculates aggregated statistics describing the
119
+ l-diversity values of all equivalence groups.
120
+
121
+ Parameters
122
+ ----------
123
+ df : pyspark.sql.DataFrame
124
+ Input dataset containing the quasi-identifiers and the
125
+ sensitive attribute.
126
+
127
+ Returns
128
+ -------
129
+ LDiversityResult
130
+ Container object with:
131
+
132
+ - summary_df:
133
+ Aggregated statistics describing l-diversity across all
134
+ equivalence groups.
135
+
136
+ - violating_groups:
137
+ Equivalence groups whose number of distinct sensitive
138
+ values is below the configured l-threshold.
139
+
140
+ Raises
141
+ ------
142
+ ValueError
143
+ If the input DataFrame is None, is not Spark-compatible, or
144
+ does not contain all required columns.
145
+
146
+ Notes
147
+ -----
148
+ The method is lazy. It builds the Spark transformations required
149
+ to calculate the metric, but execution occurs when an action such
150
+ as ``show()``, ``collect()`` or ``count()`` is performed on one
151
+ of the returned DataFrames.
152
+ """
153
+ if df is None:
154
+ raise ValueError(
155
+ "Input DataFrame cannot be None."
156
+ )
157
+
158
+ # Compatible with standard Spark and Spark Connect.
159
+ if not hasattr(df, "groupBy"):
160
+ raise ValueError(
161
+ "Input must be a Spark-compatible DataFrame."
162
+ )
163
+
164
+ self._validate_columns(
165
+ df,
166
+ self.quasi_identifiers + [self.sensitive_attribute]
167
+ )
168
+
169
+ # ------------------------------------------------------
170
+ # EQUIVALENCE GROUPS AND DISTINCT L-DIVERSITY
171
+ # ------------------------------------------------------
172
+
173
+ equivalence_groups = self._build_equivalence_groups(
174
+ df,
175
+ self.quasi_identifiers,
176
+ extra_aggs=[
177
+ F.countDistinct(
178
+ self.sensitive_attribute
179
+ ).alias("l_diversity")
180
+ ]
181
+ )
182
+
183
+ # ------------------------------------------------------
184
+ # VIOLATING GROUPS
185
+ # ------------------------------------------------------
186
+
187
+ violating_groups = (
188
+ equivalence_groups
189
+ .filter(
190
+ F.col("l_diversity") < self.l_threshold
191
+ )
192
+ )
193
+
194
+ # ------------------------------------------------------
195
+ # SUMMARY
196
+ # ------------------------------------------------------
197
+
198
+ summary_df = (
199
+ equivalence_groups
200
+ .agg(
201
+ F.count("*").alias(
202
+ "num_equivalence_groups"
203
+ ),
204
+ F.min("l_diversity").alias(
205
+ "l_value"
206
+ ),
207
+ F.max("l_diversity").alias(
208
+ "max_l"
209
+ ),
210
+ F.round(
211
+ F.avg("l_diversity"),
212
+ 2
213
+ ).alias(
214
+ "avg_l"
215
+ ),
216
+ F.sum(
217
+ F.when(
218
+ F.col("l_diversity")
219
+ < self.l_threshold,
220
+ 1
221
+ ).otherwise(0)
222
+ ).alias(
223
+ "num_violating_groups"
224
+ )
225
+ )
226
+ .withColumn(
227
+ "l_threshold",
228
+ F.lit(self.l_threshold)
229
+ )
230
+ .withColumn(
231
+ "satisfies_l_diversity",
232
+ F.col("l_value")
233
+ >= F.col("l_threshold")
234
+ )
235
+ )
236
+
237
+ return LDiversityResult(
238
+ summary_df=summary_df,
239
+ violating_groups=violating_groups
240
+ )
241
+
242
+
243
+ class LDiversityResult:
244
+ """
245
+ Container for l-diversity results.
246
+
247
+ The object stores both the aggregated summary generated by the
248
+ l-diversity analysis and the equivalence groups that violate the
249
+ configured l-threshold.
250
+
251
+ Attributes
252
+ ----------
253
+ summary_df : pyspark.sql.DataFrame
254
+ Aggregated statistics of l-diversity values across all
255
+ equivalence groups.
256
+
257
+ violating_groups : pyspark.sql.DataFrame
258
+ Equivalence groups whose number of distinct sensitive-attribute
259
+ values is below the configured l-threshold.
260
+ """
261
+
262
+ def __init__(
263
+ self,
264
+ summary_df: DataFrame,
265
+ violating_groups: DataFrame
266
+ ):
267
+ """
268
+ Initializes the result object.
269
+
270
+ Parameters
271
+ ----------
272
+ summary_df : pyspark.sql.DataFrame
273
+ Aggregated statistics describing the l-diversity values
274
+ across all equivalence groups.
275
+
276
+ violating_groups : pyspark.sql.DataFrame
277
+ Equivalence groups that do not satisfy the configured
278
+ l-diversity threshold.
279
+ """
280
+ self.summary_df = summary_df
281
+ self.violating_groups = violating_groups
282
+
283
+ def show_summary(self):
284
+ """
285
+ Displays aggregated statistics of l-diversity.
286
+
287
+ The summary includes:
288
+
289
+ - num_equivalence_groups:
290
+ Total number of equivalence groups.
291
+
292
+ - l_value:
293
+ Minimum number of distinct sensitive values found among all
294
+ equivalence groups. This represents the l-diversity value
295
+ of the complete dataset.
296
+
297
+ - max_l:
298
+ Maximum number of distinct sensitive values found in an
299
+ equivalence group.
300
+
301
+ - avg_l:
302
+ Average number of distinct sensitive values across all
303
+ equivalence groups.
304
+
305
+ - num_violating_groups:
306
+ Number of equivalence groups whose l-diversity is below the
307
+ configured threshold.
308
+
309
+ - l_threshold:
310
+ Minimum l-diversity value required by the user.
311
+
312
+ - satisfies_l_diversity:
313
+ Boolean indicating whether every equivalence group satisfies
314
+ the configured l-diversity requirement.
315
+
316
+ Interpretation
317
+ --------------
318
+ The dataset satisfies the configured l-diversity requirement when:
319
+
320
+ l_value >= l_threshold
321
+
322
+ and therefore:
323
+
324
+ num_violating_groups = 0
325
+ """
326
+ self.summary_df.show(
327
+ truncate=False
328
+ )
329
+
330
+ def show_violating_groups(
331
+ self,
332
+ n: int = 10,
333
+ sort: bool = False
334
+ ):
335
+ """
336
+ Displays equivalence groups that do not satisfy l-diversity.
337
+
338
+ Parameters
339
+ ----------
340
+ n : int, optional
341
+ Number of violating equivalence groups to display.
342
+ Default is 10.
343
+
344
+ sort : bool, optional
345
+ Whether to sort violating groups by their l-diversity value
346
+ in ascending order.
347
+
348
+ When enabled, the groups with the lowest diversity and
349
+ therefore the greatest potential privacy risk are displayed
350
+ first.
351
+
352
+ Sorting may increase computational cost for large datasets
353
+ because it requires an additional distributed ordering
354
+ operation.
355
+
356
+ Default is False.
357
+ """
358
+ df = self.violating_groups
359
+
360
+ if sort:
361
+ df = df.orderBy(
362
+ F.col("l_diversity").asc()
363
+ )
364
+
365
+ df.show(
366
+ n,
367
+ truncate=False
368
+ )
369
+
370
+ def get_summary_df(self) -> DataFrame:
371
+ """
372
+ Returns aggregated statistics of l-diversity.
373
+
374
+ Returns
375
+ -------
376
+ pyspark.sql.DataFrame
377
+ DataFrame containing the global l-diversity assessment,
378
+ aggregated statistics and the configured privacy threshold.
379
+ """
380
+ return self.summary_df
381
+
382
+ def get_violating_groups(self) -> DataFrame:
383
+ """
384
+ Returns equivalence groups that do not satisfy l-diversity.
385
+
386
+ Returns
387
+ -------
388
+ pyspark.sql.DataFrame
389
+ DataFrame containing the equivalence groups whose number of
390
+ distinct sensitive-attribute values is below the configured
391
+ l-threshold.
392
+ """
393
+ return self.violating_groups