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,134 @@
|
|
|
1
|
+
from pyspark.sql import DataFrame
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def transformation_pipeline(
|
|
5
|
+
df: DataFrame,
|
|
6
|
+
transformations: list = None
|
|
7
|
+
) -> DataFrame:
|
|
8
|
+
"""
|
|
9
|
+
Applies a sequence of anonymization transformations to a Spark DataFrame.
|
|
10
|
+
|
|
11
|
+
Each transformation is applied sequentially, where the output of one
|
|
12
|
+
transformation becomes the input of the next transformation.
|
|
13
|
+
|
|
14
|
+
All transformation objects must implement a callable
|
|
15
|
+
``transform(df: DataFrame) -> DataFrame`` method.
|
|
16
|
+
|
|
17
|
+
This allows different anonymization techniques, such as suppression,
|
|
18
|
+
substitution and generalization, to be combined without coupling the
|
|
19
|
+
pipeline to specific transformation classes.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
df : pyspark.sql.DataFrame
|
|
24
|
+
Input dataset to be anonymized.
|
|
25
|
+
|
|
26
|
+
transformations : list
|
|
27
|
+
Ordered list of transformation objects to apply.
|
|
28
|
+
|
|
29
|
+
Each object must provide a callable ``transform`` method that
|
|
30
|
+
receives a Spark DataFrame and returns another Spark DataFrame.
|
|
31
|
+
|
|
32
|
+
Examples of compatible transformations include:
|
|
33
|
+
|
|
34
|
+
- ``Suppression``
|
|
35
|
+
- ``Substitution``
|
|
36
|
+
- ``Generalization``
|
|
37
|
+
|
|
38
|
+
The order of the list determines the order in which transformations
|
|
39
|
+
are applied.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
pyspark.sql.DataFrame
|
|
44
|
+
DataFrame resulting from applying all configured transformations
|
|
45
|
+
sequentially.
|
|
46
|
+
|
|
47
|
+
Raises
|
|
48
|
+
------
|
|
49
|
+
ValueError
|
|
50
|
+
If the input DataFrame is None or is not Spark-compatible.
|
|
51
|
+
|
|
52
|
+
If ``transformations`` is None, is not a list, or is empty.
|
|
53
|
+
|
|
54
|
+
If any element in ``transformations`` is None or does not implement
|
|
55
|
+
a callable ``transform`` method.
|
|
56
|
+
|
|
57
|
+
Notes
|
|
58
|
+
-----
|
|
59
|
+
The pipeline does not trigger Spark execution by itself. Transformations
|
|
60
|
+
based on Spark DataFrame operations are lazy, so the resulting logical
|
|
61
|
+
plan is evaluated only when a Spark action is executed.
|
|
62
|
+
|
|
63
|
+
Spark's query optimizer can optimize the combined logical plan generated
|
|
64
|
+
by consecutive transformations.
|
|
65
|
+
|
|
66
|
+
The pipeline intentionally relies on the common ``transform`` interface
|
|
67
|
+
instead of checking for specific transformation classes. This allows
|
|
68
|
+
custom transformations to be used as long as they implement the expected
|
|
69
|
+
interface.
|
|
70
|
+
|
|
71
|
+
The order of transformations may affect the resulting dataset. For
|
|
72
|
+
example, dropping a column before another transformation attempts to use
|
|
73
|
+
it will cause that subsequent transformation to fail.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------
|
|
77
|
+
# VALIDATE INPUT DATAFRAME
|
|
78
|
+
# ------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
if df is None:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"Input DataFrame cannot be None."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Compatible with standard Spark and Spark Connect.
|
|
86
|
+
if not hasattr(df, "select"):
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"Input must be a Spark-compatible DataFrame."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------
|
|
92
|
+
# VALIDATE TRANSFORMATIONS
|
|
93
|
+
# ------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
if transformations is None:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"'transformations' must be provided and cannot be None."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not isinstance(transformations, list):
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"'transformations' must be a list."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not transformations:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
"'transformations' cannot be an empty list."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
for index, transformation in enumerate(transformations):
|
|
111
|
+
if transformation is None:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"Transformation at index {index} cannot be None."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if not callable(
|
|
117
|
+
getattr(transformation, "transform", None)
|
|
118
|
+
):
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"Transformation at index {index} "
|
|
121
|
+
f"('{type(transformation).__name__}') must implement "
|
|
122
|
+
"a callable 'transform' method."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------
|
|
126
|
+
# APPLY TRANSFORMATIONS
|
|
127
|
+
# ------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
final_df = df
|
|
130
|
+
|
|
131
|
+
for transformation in transformations:
|
|
132
|
+
final_df = transformation.transform(final_df)
|
|
133
|
+
|
|
134
|
+
return final_df
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from pyspark.sql import DataFrame
|
|
2
|
+
from pyspark.sql import functions as F
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Substitution:
|
|
6
|
+
"""
|
|
7
|
+
Applies masking to a column by replacing its values fully or partially
|
|
8
|
+
with a specified character.
|
|
9
|
+
|
|
10
|
+
The transformation supports two substitution modes:
|
|
11
|
+
|
|
12
|
+
- Full substitution:
|
|
13
|
+
Replaces every character of the original value while preserving
|
|
14
|
+
its original length.
|
|
15
|
+
|
|
16
|
+
- Partial substitution:
|
|
17
|
+
Replaces a specific portion of the original value, preserving the
|
|
18
|
+
characters outside the configured substitution range.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
column : str
|
|
23
|
+
Name of the column to be masked.
|
|
24
|
+
|
|
25
|
+
replacement_char : str, default="*"
|
|
26
|
+
Single character used to replace characters from the original
|
|
27
|
+
values.
|
|
28
|
+
|
|
29
|
+
mode : str, default="full"
|
|
30
|
+
Substitution mode.
|
|
31
|
+
|
|
32
|
+
Supported values are:
|
|
33
|
+
|
|
34
|
+
- ``"full"``:
|
|
35
|
+
Replaces the complete value.
|
|
36
|
+
|
|
37
|
+
- ``"partial"``:
|
|
38
|
+
Replaces only a configured portion of the value.
|
|
39
|
+
|
|
40
|
+
start : int, optional
|
|
41
|
+
Starting position, using a 0-based index, for partial substitution.
|
|
42
|
+
|
|
43
|
+
Required when ``mode="partial"``.
|
|
44
|
+
|
|
45
|
+
length : int, optional
|
|
46
|
+
Maximum number of characters to replace starting from ``start``.
|
|
47
|
+
|
|
48
|
+
Required when ``mode="partial"``.
|
|
49
|
+
|
|
50
|
+
Notes
|
|
51
|
+
-----
|
|
52
|
+
The transformation is implemented entirely using Spark SQL
|
|
53
|
+
expressions. No records are collected to the driver and no Python UDF
|
|
54
|
+
is used, allowing the operation to be executed in a distributed manner.
|
|
55
|
+
|
|
56
|
+
Null values are preserved.
|
|
57
|
+
|
|
58
|
+
Values are converted to strings before masking. Therefore, non-null
|
|
59
|
+
values in the transformed column are returned as strings.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
VALID_MODES = {
|
|
63
|
+
"full",
|
|
64
|
+
"partial"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
column: str,
|
|
70
|
+
replacement_char: str = "*",
|
|
71
|
+
mode: str = "full",
|
|
72
|
+
start: int = None,
|
|
73
|
+
length: int = None
|
|
74
|
+
):
|
|
75
|
+
"""
|
|
76
|
+
Initializes the substitution transformation.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
column : str
|
|
81
|
+
Name of the column to be masked.
|
|
82
|
+
|
|
83
|
+
replacement_char : str, default="*"
|
|
84
|
+
Single character used to replace the original values.
|
|
85
|
+
|
|
86
|
+
mode : str, default="full"
|
|
87
|
+
Substitution mode.
|
|
88
|
+
|
|
89
|
+
Supported values are:
|
|
90
|
+
|
|
91
|
+
- ``"full"``:
|
|
92
|
+
Replaces the entire value.
|
|
93
|
+
|
|
94
|
+
- ``"partial"``:
|
|
95
|
+
Replaces only a portion of the value.
|
|
96
|
+
|
|
97
|
+
start : int, optional
|
|
98
|
+
Starting position, using a 0-based index, for partial
|
|
99
|
+
substitution.
|
|
100
|
+
|
|
101
|
+
Required when ``mode="partial"``.
|
|
102
|
+
|
|
103
|
+
length : int, optional
|
|
104
|
+
Number of characters to replace starting from ``start``.
|
|
105
|
+
|
|
106
|
+
Required when ``mode="partial"``.
|
|
107
|
+
|
|
108
|
+
Raises
|
|
109
|
+
------
|
|
110
|
+
ValueError
|
|
111
|
+
If the column name is invalid, the replacement character does
|
|
112
|
+
not contain exactly one character, the mode is unsupported, or
|
|
113
|
+
the parameters required by the selected mode are invalid.
|
|
114
|
+
"""
|
|
115
|
+
if not isinstance(column, str) or not column.strip():
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"'column' must be a non-empty string."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
not isinstance(replacement_char, str)
|
|
122
|
+
or len(replacement_char) != 1
|
|
123
|
+
):
|
|
124
|
+
raise ValueError(
|
|
125
|
+
"'replacement_char' must be a single character."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if mode not in self.VALID_MODES:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Unsupported mode '{mode}'. "
|
|
131
|
+
f"Use {sorted(self.VALID_MODES)}."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self._validate_mode_parameters(
|
|
135
|
+
mode,
|
|
136
|
+
start,
|
|
137
|
+
length
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self.column = column
|
|
141
|
+
self.replacement_char = replacement_char
|
|
142
|
+
self.mode = mode
|
|
143
|
+
self.start = start
|
|
144
|
+
self.length = length
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _validate_mode_parameters(
|
|
148
|
+
mode: str,
|
|
149
|
+
start: int | None,
|
|
150
|
+
length: int | None
|
|
151
|
+
) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Validates parameters specific to the selected substitution mode.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
mode : str
|
|
158
|
+
Substitution mode. Expected values are ``"full"`` or
|
|
159
|
+
``"partial"``.
|
|
160
|
+
|
|
161
|
+
start : int or None
|
|
162
|
+
Starting position for partial substitution.
|
|
163
|
+
|
|
164
|
+
length : int or None
|
|
165
|
+
Number of characters to replace in partial substitution.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
None
|
|
170
|
+
The method only validates the provided parameters.
|
|
171
|
+
|
|
172
|
+
Raises
|
|
173
|
+
------
|
|
174
|
+
ValueError
|
|
175
|
+
If ``start`` or ``length`` are provided in full mode, if they
|
|
176
|
+
are missing in partial mode, if they are not integers, or if
|
|
177
|
+
their values are outside the valid range.
|
|
178
|
+
"""
|
|
179
|
+
if mode == "full":
|
|
180
|
+
if start is not None or length is not None:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
"'start' and 'length' must not be provided "
|
|
183
|
+
"in 'full' mode."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if start is None or length is None:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
"In 'partial' mode, 'start' and 'length' "
|
|
191
|
+
"must be provided."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if (
|
|
195
|
+
not isinstance(start, int)
|
|
196
|
+
or isinstance(start, bool)
|
|
197
|
+
or not isinstance(length, int)
|
|
198
|
+
or isinstance(length, bool)
|
|
199
|
+
):
|
|
200
|
+
raise ValueError(
|
|
201
|
+
"'start' and 'length' must be integers."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if start < 0:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
"'start' must be greater than or equal to 0."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if length <= 0:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
"'length' must be greater than 0."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def transform(
|
|
215
|
+
self,
|
|
216
|
+
df: DataFrame
|
|
217
|
+
) -> DataFrame:
|
|
218
|
+
"""
|
|
219
|
+
Applies the configured substitution to the selected column.
|
|
220
|
+
|
|
221
|
+
In full mode, every character of each non-null value is replaced
|
|
222
|
+
with ``replacement_char`` while preserving the original value
|
|
223
|
+
length.
|
|
224
|
+
|
|
225
|
+
In partial mode, substitution starts at the configured 0-based
|
|
226
|
+
position and replaces at most ``length`` characters. Characters
|
|
227
|
+
before and after the selected region are preserved.
|
|
228
|
+
|
|
229
|
+
If the requested substitution range extends beyond the end of the
|
|
230
|
+
value, only the available characters are replaced.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
df : pyspark.sql.DataFrame
|
|
235
|
+
Input dataset containing the column to be masked.
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
pyspark.sql.DataFrame
|
|
240
|
+
New DataFrame with the selected column masked.
|
|
241
|
+
|
|
242
|
+
Raises
|
|
243
|
+
------
|
|
244
|
+
ValueError
|
|
245
|
+
If the input DataFrame is None, is not Spark-compatible, or
|
|
246
|
+
does not contain the configured column.
|
|
247
|
+
|
|
248
|
+
Notes
|
|
249
|
+
-----
|
|
250
|
+
Null values are preserved.
|
|
251
|
+
|
|
252
|
+
Non-null values are converted to strings before substitution, so
|
|
253
|
+
the resulting transformed values are strings.
|
|
254
|
+
|
|
255
|
+
The transformation uses only native Spark SQL expressions and is
|
|
256
|
+
therefore evaluated in a distributed manner without collecting
|
|
257
|
+
dataset records to the driver.
|
|
258
|
+
"""
|
|
259
|
+
if df is None:
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"Input DataFrame cannot be None."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Compatible with standard Spark and Spark Connect.
|
|
265
|
+
if not hasattr(df, "withColumn"):
|
|
266
|
+
raise ValueError(
|
|
267
|
+
"Input must be a Spark-compatible DataFrame."
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if self.column not in df.columns:
|
|
271
|
+
raise ValueError(
|
|
272
|
+
f"The column '{self.column}' does not exist "
|
|
273
|
+
f"in the DataFrame."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
source_col = F.col(self.column)
|
|
277
|
+
string_col = source_col.cast("string")
|
|
278
|
+
value_length = F.length(string_col)
|
|
279
|
+
|
|
280
|
+
# ------------------------------------------------------
|
|
281
|
+
# FULL SUBSTITUTION
|
|
282
|
+
# ------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
if self.mode == "full":
|
|
285
|
+
masked_value = F.repeat(
|
|
286
|
+
F.lit(self.replacement_char),
|
|
287
|
+
value_length
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return df.withColumn(
|
|
291
|
+
self.column,
|
|
292
|
+
F.when(
|
|
293
|
+
source_col.isNull(),
|
|
294
|
+
F.lit(None).cast("string")
|
|
295
|
+
).otherwise(
|
|
296
|
+
masked_value
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# ------------------------------------------------------
|
|
301
|
+
# PARTIAL SUBSTITUTION
|
|
302
|
+
# ------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
replacement_length = F.greatest(
|
|
305
|
+
F.lit(0),
|
|
306
|
+
F.least(
|
|
307
|
+
F.lit(self.length),
|
|
308
|
+
value_length - F.lit(self.start)
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
prefix = F.substring(
|
|
313
|
+
string_col,
|
|
314
|
+
1,
|
|
315
|
+
self.start
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
masked_part = F.repeat(
|
|
319
|
+
F.lit(self.replacement_char),
|
|
320
|
+
replacement_length
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
suffix = F.substring(
|
|
324
|
+
string_col,
|
|
325
|
+
self.start + self.length + 1,
|
|
326
|
+
value_length
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
masked_value = F.concat(
|
|
330
|
+
prefix,
|
|
331
|
+
masked_part,
|
|
332
|
+
suffix
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return df.withColumn(
|
|
336
|
+
self.column,
|
|
337
|
+
F.when(
|
|
338
|
+
source_col.isNull(),
|
|
339
|
+
F.lit(None).cast("string")
|
|
340
|
+
).otherwise(
|
|
341
|
+
masked_value
|
|
342
|
+
)
|
|
343
|
+
)
|