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,7 @@
1
+ from .metrics import KAnonymity, LDiversity, TCloseness
2
+ from .techniques import Substitution, Suppression, Generalization
3
+ from .visualization import Visualization, AnonymizationAdvisor
4
+ from .io import DataExporter, DataImporter
5
+
6
+ __all__ = ["KAnonymity", "LDiversity", "TCloseness", "Substitution", "Suppression", "Generalization", "Visualization", "AnonymizationAdvisor", "DataExporter", "DataImporter"]
7
+
@@ -0,0 +1,4 @@
1
+ from .export import DataExporter
2
+ from .data_import import DataImporter
3
+
4
+ __all__ = ["DataExporter", "DataImporter"]
@@ -0,0 +1,90 @@
1
+ import os
2
+ from pyspark.sql import DataFrame, SparkSession
3
+
4
+
5
+ class DataImporter:
6
+ """
7
+ Utility class for importing datasets into Spark DataFrames.
8
+
9
+ Supported formats:
10
+ - csv
11
+ - parquet
12
+ - orc
13
+ - s3
14
+ """
15
+
16
+ VALID_FORMATS = {"csv", "parquet", "orc", "s3"}
17
+
18
+ @staticmethod
19
+ def import_data(
20
+ spark: SparkSession,
21
+ path: str,
22
+ file_format: str = "parquet",
23
+ header: bool = True,
24
+ infer_schema: bool = True
25
+ ) -> DataFrame:
26
+ """
27
+ Imports a dataset into a Spark DataFrame.
28
+
29
+ Parameters
30
+ ----------
31
+ spark : pyspark.sql.SparkSession
32
+ Active Spark session.
33
+ path : str
34
+ Source dataset path.
35
+ file_format : str, default="parquet"
36
+ Input format. Supported values are:
37
+ - 'csv'
38
+ - 'parquet'
39
+ - 'orc'
40
+ - 's3'
41
+ header : bool, default=True
42
+ Whether the CSV file contains a header row.
43
+ Ignored for non-CSV formats.
44
+ infer_schema : bool, default=True
45
+ Whether Spark should infer the schema automatically for CSV files.
46
+ Ignored for non-CSV formats.
47
+
48
+ Returns
49
+ -------
50
+ pyspark.sql.DataFrame
51
+ Imported Spark DataFrame.
52
+
53
+ Raises
54
+ ------
55
+ TypeError
56
+ If spark is not a SparkSession.
57
+ ValueError
58
+ If path or file_format are invalid.
59
+ FileNotFoundError
60
+ If the local source path does not exist.
61
+ """
62
+ if not hasattr(spark, "read"):
63
+ raise TypeError("spark must provide a Spark-compatible read interface.")
64
+
65
+ if not isinstance(path, str) or not path.strip():
66
+ raise ValueError("The source path must be a non-empty string.")
67
+
68
+ if file_format not in DataImporter.VALID_FORMATS:
69
+ raise ValueError(
70
+ f"Unsupported format. Use one of: {DataImporter.VALID_FORMATS}"
71
+ )
72
+
73
+ if file_format == "s3":
74
+ if not path.startswith("s3a://"):
75
+ raise ValueError("S3 paths must start with 's3a://'")
76
+ return spark.read.parquet(path)
77
+
78
+ if file_format == "csv":
79
+ return (
80
+ spark.read
81
+ .option("header", header)
82
+ .option("inferSchema", infer_schema)
83
+ .csv(path)
84
+ )
85
+
86
+ if file_format == "parquet":
87
+ return spark.read.parquet(path)
88
+
89
+ if file_format == "orc":
90
+ return spark.read.orc(path)
@@ -0,0 +1,87 @@
1
+ from pyspark.sql import DataFrame
2
+ import os
3
+
4
+
5
+ class DataExporter:
6
+ """
7
+ Utility class for exporting Spark DataFrames to different storage formats.
8
+
9
+ Supported formats:
10
+ - csv
11
+ - parquet
12
+ - orc
13
+ """
14
+
15
+ VALID_FORMATS = {"csv", "parquet", "orc"}
16
+ VALID_MODES = {"overwrite", "append", "ignore", "error", "errorifexists"}
17
+
18
+ @staticmethod
19
+ def export(
20
+ df: DataFrame,
21
+ path: str,
22
+ file_format: str = "parquet",
23
+ mode: str = "overwrite",
24
+ header: bool = True
25
+ ) -> str:
26
+ """
27
+ Exports a Spark DataFrame to the specified storage format.
28
+
29
+ Parameters
30
+ ----------
31
+ df : pyspark.sql.DataFrame
32
+ Spark DataFrame to export.
33
+ path : str
34
+ Destination path where the dataset will be written.
35
+ file_format : str, default="parquet"
36
+ Output format. Supported values are:
37
+ - 'csv'
38
+ - 'parquet'
39
+ - 'orc'
40
+ mode : str, default="overwrite"
41
+ Spark write mode. Supported values are:
42
+ - 'overwrite'
43
+ - 'append'
44
+ - 'ignore'
45
+ - 'error'
46
+ - 'errorifexists'
47
+ header : bool, default=True
48
+ Whether to include a header when exporting to CSV.
49
+ Ignored for Parquet and ORC.
50
+
51
+ Returns
52
+ -------
53
+ str
54
+ Confirmation message indicating that the export was completed.
55
+ """
56
+ if not hasattr(df, "write"):
57
+ raise TypeError("df must provide a Spark-compatible write interface.")
58
+
59
+ if file_format not in DataExporter.VALID_FORMATS:
60
+ raise ValueError(
61
+ f"Unsupported format. Use one of: {DataExporter.VALID_FORMATS}"
62
+ )
63
+
64
+ if mode not in DataExporter.VALID_MODES:
65
+ raise ValueError(
66
+ f"Unsupported mode. Use one of: {DataExporter.VALID_MODES}"
67
+ )
68
+
69
+ if not isinstance(path, str) or not path.strip():
70
+ raise ValueError("The destination path must be a non-empty string.")
71
+
72
+ writer = df.write.mode(mode)
73
+
74
+ if file_format == "csv":
75
+ writer.option("header", header).csv(path)
76
+
77
+ elif file_format == "parquet":
78
+ writer.parquet(path)
79
+
80
+ elif file_format == "orc":
81
+ writer.orc(path)
82
+
83
+ return (
84
+ f"Dataset successfully exported in '{file_format}' format to: "
85
+ f"{path}"
86
+ )
87
+
@@ -0,0 +1,5 @@
1
+ from .k_anonymity import KAnonymity
2
+ from .l_diversity import LDiversity
3
+ from .t_closeness import TCloseness
4
+
5
+ __all__ = ["KAnonymity", "LDiversity", "TCloseness"]
@@ -0,0 +1,391 @@
1
+ from pyspark.sql import DataFrame
2
+ from pyspark.sql import functions as F
3
+
4
+ from ..utils import EquivalenceGroups
5
+
6
+
7
+ class KAnonymity(EquivalenceGroups):
8
+ """
9
+ Calculates k-anonymity for a dataset based on a set of
10
+ quasi-identifiers.
11
+
12
+ K-anonymity measures whether each combination of quasi-identifier
13
+ values is shared by at least ``k`` records.
14
+
15
+ Records with the same values for all quasi-identifiers form an
16
+ equivalence group. The size of the smallest equivalence group
17
+ represents the k-anonymity level of the dataset.
18
+
19
+ Parameters
20
+ ----------
21
+ quasi_identifiers : list of str, optional
22
+ Columns used as quasi-identifiers to construct equivalence groups.
23
+
24
+ k_threshold : int, default=2
25
+ Minimum required size for every equivalence group.
26
+
27
+ A dataset satisfies the configured k-anonymity requirement when
28
+ every equivalence group contains at least ``k_threshold`` records.
29
+
30
+ Notes
31
+ -----
32
+ Equivalence groups are computed using native Spark operations, allowing
33
+ the calculation to be performed in a distributed manner.
34
+
35
+ No dataset records are collected to the driver.
36
+
37
+ The computation is lazy. Spark execution occurs only when an action is
38
+ performed on one of the resulting DataFrames.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ quasi_identifiers=None,
44
+ k_threshold: int = 2
45
+ ):
46
+ """
47
+ Initializes the k-anonymity metric.
48
+
49
+ Parameters
50
+ ----------
51
+ quasi_identifiers : list of str, optional
52
+ Columns used as quasi-identifiers.
53
+
54
+ Column names must be non-empty strings and duplicate columns
55
+ are not allowed.
56
+
57
+ k_threshold : int, default=2
58
+ Minimum required size of every equivalence group.
59
+
60
+ The value must be an integer greater than 1.
61
+
62
+ Raises
63
+ ------
64
+ ValueError
65
+ If ``quasi_identifiers`` is not a list, contains invalid or
66
+ duplicate column names, or ``k_threshold`` is not an integer
67
+ greater than 1.
68
+ """
69
+ if quasi_identifiers is not None:
70
+ if not isinstance(quasi_identifiers, list):
71
+ raise ValueError(
72
+ "'quasi_identifiers' must be a list of column names."
73
+ )
74
+
75
+ if not all(
76
+ isinstance(column, str) and column.strip()
77
+ for column in quasi_identifiers
78
+ ):
79
+ raise ValueError(
80
+ "'quasi_identifiers' must contain only "
81
+ "non-empty strings."
82
+ )
83
+
84
+ if len(quasi_identifiers) != len(set(quasi_identifiers)):
85
+ raise ValueError(
86
+ "'quasi_identifiers' must not contain "
87
+ "duplicate columns."
88
+ )
89
+
90
+ if (
91
+ not isinstance(k_threshold, int)
92
+ or isinstance(k_threshold, bool)
93
+ or k_threshold <= 1
94
+ ):
95
+ raise ValueError(
96
+ "'k_threshold' must be an integer greater than 1."
97
+ )
98
+
99
+ self.quasi_identifiers = quasi_identifiers or []
100
+ self.k_threshold = k_threshold
101
+
102
+ def summary(
103
+ self,
104
+ df: DataFrame
105
+ ):
106
+ """
107
+ Computes k-anonymity statistics for the input dataset.
108
+
109
+ Records are grouped according to their values for the configured
110
+ quasi-identifiers. Each resulting equivalence group contains a
111
+ ``group_size`` representing the number of records sharing that
112
+ combination of quasi-identifier values.
113
+
114
+ The k-anonymity value of the dataset is defined as the size of the
115
+ smallest equivalence group:
116
+
117
+ k = min(group_size)
118
+
119
+ Equivalence groups whose size is smaller than ``k_threshold`` are
120
+ considered violating groups.
121
+
122
+ Parameters
123
+ ----------
124
+ df : pyspark.sql.DataFrame
125
+ Input dataset containing the configured quasi-identifiers.
126
+
127
+ Returns
128
+ -------
129
+ KAnonymityResult
130
+ Container containing:
131
+
132
+ - ``summary_df``:
133
+ DataFrame with aggregated k-anonymity statistics.
134
+
135
+ - ``violating_groups``:
136
+ Equivalence groups whose size is below the configured
137
+ ``k_threshold``.
138
+
139
+ Raises
140
+ ------
141
+ ValueError
142
+ If the input DataFrame is None, is not Spark-compatible, or
143
+ does not contain one or more configured quasi-identifiers.
144
+
145
+ Notes
146
+ -----
147
+ The computation uses native Spark aggregations and is performed
148
+ in a distributed manner.
149
+
150
+ This method does not trigger Spark execution by itself. Both
151
+ returned DataFrames are lazy and are evaluated only when a Spark
152
+ action is performed.
153
+
154
+ ``summary_df`` and ``violating_groups`` share the same logical
155
+ equivalence-group computation. Without persistence, separate
156
+ actions on both result DataFrames may cause Spark to recompute
157
+ that shared lineage.
158
+ """
159
+ if df is None:
160
+ raise ValueError(
161
+ "Input DataFrame cannot be None."
162
+ )
163
+
164
+ # Compatible with standard Spark and Spark Connect.
165
+ if not hasattr(df, "groupBy"):
166
+ raise ValueError(
167
+ "Input must be a Spark-compatible DataFrame."
168
+ )
169
+
170
+ self._validate_columns(
171
+ df,
172
+ self.quasi_identifiers
173
+ )
174
+
175
+ # ------------------------------------------------------
176
+ # EQUIVALENCE GROUPS
177
+ # ------------------------------------------------------
178
+
179
+ equivalence_groups = self._build_equivalence_groups(
180
+ df,
181
+ self.quasi_identifiers
182
+ )
183
+
184
+ # ------------------------------------------------------
185
+ # VIOLATING GROUPS
186
+ # ------------------------------------------------------
187
+
188
+ violating_groups = equivalence_groups.filter(
189
+ F.col("group_size") < self.k_threshold
190
+ )
191
+
192
+ # ------------------------------------------------------
193
+ # SUMMARY
194
+ # ------------------------------------------------------
195
+
196
+ summary_df = (
197
+ equivalence_groups
198
+ .agg(
199
+ F.count("*").alias(
200
+ "num_equivalence_groups"
201
+ ),
202
+ F.min("group_size").alias(
203
+ "k_value"
204
+ ),
205
+ F.max("group_size").alias(
206
+ "max_group_size"
207
+ ),
208
+ F.round(
209
+ F.avg("group_size"),
210
+ 2
211
+ ).alias(
212
+ "avg_group_size"
213
+ ),
214
+ F.sum(
215
+ F.when(
216
+ F.col("group_size") < self.k_threshold,
217
+ 1
218
+ ).otherwise(0)
219
+ ).alias(
220
+ "num_violating_groups"
221
+ )
222
+ )
223
+ .withColumn(
224
+ "k_threshold",
225
+ F.lit(self.k_threshold)
226
+ )
227
+ .withColumn(
228
+ "satisfies_k_anonymity",
229
+ F.col("k_value")
230
+ >= F.col("k_threshold")
231
+ )
232
+ )
233
+
234
+ return KAnonymityResult(
235
+ summary_df=summary_df,
236
+ violating_groups=violating_groups
237
+ )
238
+
239
+
240
+ class KAnonymityResult:
241
+ """
242
+ Container for k-anonymity results.
243
+
244
+ The object stores both the aggregated summary of the k-anonymity
245
+ analysis and the equivalence groups that violate the configured
246
+ k-threshold.
247
+
248
+ Parameters
249
+ ----------
250
+ summary_df : pyspark.sql.DataFrame
251
+ DataFrame containing aggregated k-anonymity statistics.
252
+
253
+ violating_groups : pyspark.sql.DataFrame
254
+ Equivalence groups whose size is below the configured k-threshold.
255
+
256
+ Notes
257
+ -----
258
+ The stored DataFrames remain lazy Spark DataFrames. Creating this
259
+ result object does not trigger Spark execution.
260
+ """
261
+
262
+ def __init__(
263
+ self,
264
+ summary_df: DataFrame,
265
+ violating_groups: DataFrame
266
+ ):
267
+ """
268
+ Initializes the k-anonymity result container.
269
+
270
+ Parameters
271
+ ----------
272
+ summary_df : pyspark.sql.DataFrame
273
+ Aggregated statistics of k-anonymity across equivalence groups.
274
+
275
+ violating_groups : pyspark.sql.DataFrame
276
+ Subset of equivalence groups whose size is below the configured
277
+ k-threshold, representing potential privacy risks.
278
+ """
279
+ self.summary_df = summary_df
280
+ self.violating_groups = violating_groups
281
+
282
+ def show_summary(self) -> None:
283
+ """
284
+ Displays the aggregated k-anonymity summary.
285
+
286
+ Returns
287
+ -------
288
+ None
289
+ The summary is displayed using Spark's ``show`` method.
290
+
291
+ Notes
292
+ -----
293
+ Calling this method triggers Spark execution.
294
+ """
295
+ self.summary_df.show(
296
+ truncate=False
297
+ )
298
+
299
+ def show_violating_groups(
300
+ self,
301
+ n: int = 10,
302
+ sort: bool = False
303
+ ) -> None:
304
+ """
305
+ Displays equivalence groups that do not satisfy k-anonymity.
306
+
307
+ Parameters
308
+ ----------
309
+ n : int, default=10
310
+ Maximum number of violating groups to display.
311
+
312
+ sort : bool, default=False
313
+ Whether to sort violating groups by ``group_size`` in
314
+ ascending order before displaying them.
315
+
316
+ Sorting may increase computational cost for large datasets
317
+ because it can require an additional distributed ordering
318
+ operation.
319
+
320
+ Returns
321
+ -------
322
+ None
323
+ The selected violating groups are displayed using Spark's
324
+ ``show`` method.
325
+
326
+ Raises
327
+ ------
328
+ ValueError
329
+ If ``n`` is not a positive integer or ``sort`` is not a
330
+ boolean.
331
+
332
+ Notes
333
+ -----
334
+ Calling this method triggers Spark execution.
335
+ """
336
+ if (
337
+ not isinstance(n, int)
338
+ or isinstance(n, bool)
339
+ or n <= 0
340
+ ):
341
+ raise ValueError(
342
+ "'n' must be a positive integer."
343
+ )
344
+
345
+ if not isinstance(sort, bool):
346
+ raise ValueError(
347
+ "'sort' must be a boolean."
348
+ )
349
+
350
+ df = self.violating_groups
351
+
352
+ if sort:
353
+ df = df.orderBy(
354
+ F.col("group_size").asc()
355
+ )
356
+
357
+ df.show(
358
+ n,
359
+ truncate=False
360
+ )
361
+
362
+ def get_summary_df(self) -> DataFrame:
363
+ """
364
+ Returns the aggregated k-anonymity summary.
365
+
366
+ Returns
367
+ -------
368
+ pyspark.sql.DataFrame
369
+ DataFrame containing the aggregated k-anonymity statistics.
370
+
371
+ Notes
372
+ -----
373
+ Returning the DataFrame does not trigger Spark execution.
374
+ """
375
+ return self.summary_df
376
+
377
+ def get_violating_groups(self) -> DataFrame:
378
+ """
379
+ Returns the equivalence groups that violate k-anonymity.
380
+
381
+ Returns
382
+ -------
383
+ pyspark.sql.DataFrame
384
+ Equivalence groups whose size is below the configured
385
+ k-threshold.
386
+
387
+ Notes
388
+ -----
389
+ Returning the DataFrame does not trigger Spark execution.
390
+ """
391
+ return self.violating_groups