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,907 @@
1
+ import json
2
+ import os
3
+ import warnings
4
+
5
+ from pyspark.sql import DataFrame
6
+ from pyspark.sql import functions as F
7
+
8
+ from ..utils import infer_semantic_type
9
+
10
+
11
+ class Generalization:
12
+ """
13
+ Applies generalization to a column in a Spark DataFrame.
14
+
15
+ The transformation supports:
16
+
17
+ - Categorical generalization using mapping rules.
18
+ - Numerical generalization using interval rules.
19
+ - Temporal generalization using year, month, quarter or semester.
20
+
21
+ Generalization rules can be loaded from JSON files stored in locations
22
+ accessible either by the local Python process or by Spark. This allows
23
+ the transformation to work with local files as well as distributed or
24
+ remote storage systems supported by the active Spark environment.
25
+
26
+ Examples of supported locations may include:
27
+
28
+ - Local filesystem paths.
29
+ - Databricks Workspace files.
30
+ - Databricks Volumes.
31
+ - DBFS paths.
32
+ - Cloud object storage such as S3 or ABFS, when configured in Spark.
33
+
34
+ Parameters
35
+ ----------
36
+ column : str
37
+ Name of the column to be generalized.
38
+
39
+ rules_path : str, optional
40
+ Path to a JSON file containing the generalization configuration
41
+ and rules.
42
+
43
+ The path may refer to a local file or to a location readable by
44
+ Spark.
45
+
46
+ Expected JSON structure for categorical generalization::
47
+
48
+ {
49
+ "type": "categorical",
50
+ "rules": [
51
+ {"from": ["A", "B"], "to": "Group 1"},
52
+ {"from": "C", "to": "Group 2"}
53
+ ]
54
+ }
55
+
56
+ Expected JSON structure for numerical generalization::
57
+
58
+ {
59
+ "type": "numeric",
60
+ "rules": [
61
+ {"min": 0, "max": 18, "value": "0-18"},
62
+ {"min": 19, "max": 35, "value": "19-35"}
63
+ ]
64
+ }
65
+
66
+ For temporal generalization, the JSON configuration may contain::
67
+
68
+ {
69
+ "type": "date"
70
+ }
71
+
72
+ A rules file is required for categorical and numerical
73
+ generalization. Temporal generalization can also be inferred
74
+ directly from the column semantic type when no rules file is
75
+ provided.
76
+
77
+ mode : str, optional
78
+ Temporal generalization mode.
79
+
80
+ Supported values are:
81
+
82
+ - ``"year"``
83
+ - ``"month"``
84
+ - ``"quarter"``
85
+ - ``"semester"``
86
+
87
+ This parameter is required when temporal generalization is applied.
88
+
89
+ default_value : str, optional
90
+ Value assigned when no categorical rule matches or when a numeric
91
+ value falls outside all configured intervals.
92
+
93
+ If not provided, the original value is preserved.
94
+
95
+ include_year : bool, default=True
96
+ Controls whether temporal generalization preserves the year.
97
+
98
+ Examples with ``include_year=True``:
99
+
100
+ - month: ``2024-05``
101
+ - quarter: ``2024-Q2``
102
+ - semester: ``2024-S1``
103
+
104
+ Examples with ``include_year=False``:
105
+
106
+ - month: ``05``
107
+ - quarter: ``Q2``
108
+ - semester: ``S1``
109
+
110
+ The option has no effect when ``mode="year"``.
111
+
112
+ output_column : str, optional
113
+ Name assigned to the generalized column.
114
+
115
+ If not provided, the original column is overwritten.
116
+
117
+ If provided and different from ``column``, the original column is
118
+ generalized and then renamed to ``output_column``. Therefore, the
119
+ original column is not preserved in the resulting DataFrame.
120
+
121
+ Notes
122
+ -----
123
+ The transformation itself is performed using Spark expressions and is
124
+ therefore distributed.
125
+
126
+ Generalization rules are small configuration objects and are loaded on
127
+ the driver. Dataset records are not collected to the driver.
128
+
129
+ Spark-compatible paths are read through Spark, while ordinary local
130
+ paths can be read directly through Python.
131
+ """
132
+
133
+ VALID_TEMPORAL_MODES = {
134
+ "year",
135
+ "month",
136
+ "quarter",
137
+ "semester"
138
+ }
139
+
140
+ VALID_RULE_TYPES = {
141
+ "categorical",
142
+ "numeric",
143
+ "date"
144
+ }
145
+
146
+ def __init__(
147
+ self,
148
+ column: str,
149
+ rules_path: str = None,
150
+ mode: str = None,
151
+ default_value: str = None,
152
+ include_year: bool = True,
153
+ output_column: str = None
154
+ ):
155
+ """
156
+ Initializes the generalization transformation.
157
+
158
+ Parameters
159
+ ----------
160
+ column : str
161
+ Name of the column to be generalized.
162
+
163
+ rules_path : str, optional
164
+ Path to a JSON configuration containing the generalization
165
+ rules. The path may refer to a local file or to a location
166
+ readable by Spark.
167
+
168
+ mode : str, optional
169
+ Temporal generalization mode. Supported values are ``year``,
170
+ ``month``, ``quarter`` and ``semester``.
171
+
172
+ default_value : str, optional
173
+ Value assigned when no categorical or numerical rule applies.
174
+ If omitted, the original value is preserved.
175
+
176
+ include_year : bool, default=True
177
+ Whether temporal generalization should preserve the year for
178
+ month, quarter and semester modes.
179
+
180
+ output_column : str, optional
181
+ Name of the generalized output column. If omitted, the
182
+ original column is overwritten.
183
+
184
+ Raises
185
+ ------
186
+ ValueError
187
+ If any configuration parameter has an invalid value or type.
188
+ """
189
+ if not isinstance(column, str) or not column.strip():
190
+ raise ValueError(
191
+ "'column' must be a non-empty string."
192
+ )
193
+
194
+ if rules_path is not None:
195
+ if not isinstance(rules_path, str) or not rules_path.strip():
196
+ raise ValueError(
197
+ "'rules_path' must be a non-empty string."
198
+ )
199
+
200
+ if mode is not None:
201
+ if not isinstance(mode, str):
202
+ raise ValueError(
203
+ "'mode' must be a string."
204
+ )
205
+
206
+ if mode not in self.VALID_TEMPORAL_MODES:
207
+ raise ValueError(
208
+ f"Unsupported mode '{mode}'. "
209
+ f"Use {sorted(self.VALID_TEMPORAL_MODES)}."
210
+ )
211
+
212
+ if not isinstance(include_year, bool):
213
+ raise ValueError(
214
+ "'include_year' must be a boolean."
215
+ )
216
+
217
+ if output_column is not None:
218
+ if (
219
+ not isinstance(output_column, str)
220
+ or not output_column.strip()
221
+ ):
222
+ raise ValueError(
223
+ "'output_column' must be a non-empty string."
224
+ )
225
+
226
+ self.column = column
227
+ self.rules_path = rules_path
228
+ self.mode = mode
229
+ self.default_value = default_value
230
+ self.include_year = include_year
231
+ self.output_column = output_column or column
232
+
233
+ def _rename_output_column(
234
+ self,
235
+ df: DataFrame
236
+ ) -> DataFrame:
237
+ """
238
+ Renames the generalized column when an output column is configured.
239
+
240
+ Parameters
241
+ ----------
242
+ df : pyspark.sql.DataFrame
243
+ DataFrame containing the generalized column.
244
+
245
+ Returns
246
+ -------
247
+ pyspark.sql.DataFrame
248
+ DataFrame with the generalized column renamed when
249
+ ``output_column`` differs from the original column name.
250
+ """
251
+ if self.output_column != self.column:
252
+ return df.withColumnRenamed(
253
+ self.column,
254
+ self.output_column
255
+ )
256
+
257
+ return df
258
+
259
+ def _load_rules_config(
260
+ self,
261
+ df: DataFrame
262
+ ) -> dict:
263
+ """
264
+ Loads the generalization configuration from a JSON file.
265
+
266
+ The method supports both local files and paths accessible through
267
+ Spark.
268
+
269
+ Local files are read directly using Python. Other paths are read
270
+ through Spark's text reader, allowing the active Spark environment
271
+ to resolve distributed filesystems and cloud storage locations.
272
+
273
+ Parameters
274
+ ----------
275
+ df : pyspark.sql.DataFrame
276
+ Spark DataFrame used to access the active Spark session.
277
+
278
+ Returns
279
+ -------
280
+ dict
281
+ Parsed JSON configuration.
282
+
283
+ Raises
284
+ ------
285
+ ValueError
286
+ If no rules path is configured, the file cannot be read, the
287
+ file is empty, the JSON is invalid, or the root JSON value is
288
+ not an object.
289
+
290
+ Notes
291
+ -----
292
+ Only the rules configuration is transferred to the driver. These
293
+ files are expected to be small configuration files rather than
294
+ large datasets.
295
+ """
296
+ if self.rules_path is None:
297
+ raise ValueError(
298
+ "'rules_path' must be provided."
299
+ )
300
+
301
+ content = None
302
+
303
+ # ------------------------------------------------------
304
+ # LOCAL FILE
305
+ # ------------------------------------------------------
306
+
307
+ if os.path.isfile(self.rules_path):
308
+ try:
309
+ with open(
310
+ self.rules_path,
311
+ "r",
312
+ encoding="utf-8"
313
+ ) as file:
314
+ content = file.read()
315
+
316
+ except OSError as exc:
317
+ raise ValueError(
318
+ f"Unable to read rules file "
319
+ f"'{self.rules_path}'."
320
+ ) from exc
321
+
322
+ # ------------------------------------------------------
323
+ # SPARK-COMPATIBLE PATH
324
+ # ------------------------------------------------------
325
+
326
+ else:
327
+ try:
328
+ rows = (
329
+ df.sparkSession
330
+ .read
331
+ .text(self.rules_path)
332
+ .select("value")
333
+ .collect()
334
+ )
335
+
336
+ if rows:
337
+ content = "\n".join(
338
+ row["value"]
339
+ for row in rows
340
+ )
341
+
342
+ except Exception as exc:
343
+ raise ValueError(
344
+ f"Unable to read rules file "
345
+ f"'{self.rules_path}' using either the local "
346
+ f"filesystem or Spark."
347
+ ) from exc
348
+
349
+ if not content or not content.strip():
350
+ raise ValueError(
351
+ f"Rules file '{self.rules_path}' is empty."
352
+ )
353
+
354
+ try:
355
+ config = json.loads(content)
356
+
357
+ except json.JSONDecodeError as exc:
358
+ raise ValueError(
359
+ f"Invalid JSON in rules file "
360
+ f"'{self.rules_path}'."
361
+ ) from exc
362
+
363
+ if not isinstance(config, dict):
364
+ raise ValueError(
365
+ "The rules JSON root must be an object."
366
+ )
367
+
368
+ return config
369
+
370
+ def _get_rules_type(
371
+ self,
372
+ config: dict
373
+ ) -> str:
374
+ """
375
+ Returns and validates the generalization type defined in a rules
376
+ configuration.
377
+
378
+ Parameters
379
+ ----------
380
+ config : dict
381
+ Parsed generalization configuration.
382
+
383
+ Returns
384
+ -------
385
+ str
386
+ Generalization type defined by the ``type`` field.
387
+
388
+ Raises
389
+ ------
390
+ ValueError
391
+ If the configuration does not contain a supported
392
+ generalization type.
393
+ """
394
+ rules_type = config.get("type")
395
+
396
+ if rules_type not in self.VALID_RULE_TYPES:
397
+ raise ValueError(
398
+ f"Unsupported generalization type in JSON: "
399
+ f"{rules_type}. "
400
+ f"Use {sorted(self.VALID_RULE_TYPES)}."
401
+ )
402
+
403
+ return rules_type
404
+
405
+ def _apply_temporal_with_mode(
406
+ self,
407
+ df: DataFrame
408
+ ) -> DataFrame:
409
+ """
410
+ Applies temporal generalization after validating that a temporal
411
+ mode has been configured.
412
+
413
+ Parameters
414
+ ----------
415
+ df : pyspark.sql.DataFrame
416
+ Input dataset.
417
+
418
+ Returns
419
+ -------
420
+ pyspark.sql.DataFrame
421
+ DataFrame with the temporal column generalized.
422
+
423
+ Raises
424
+ ------
425
+ ValueError
426
+ If no temporal generalization mode has been provided.
427
+ """
428
+ if self.mode is None:
429
+ raise ValueError(
430
+ "'mode' must be provided for temporal generalization."
431
+ )
432
+
433
+ return self._apply_temporal_generalization(df)
434
+
435
+ def _apply_categorical(
436
+ self,
437
+ df: DataFrame,
438
+ config: dict
439
+ ) -> DataFrame:
440
+ """
441
+ Applies categorical generalization using mapping rules.
442
+
443
+ Each categorical rule maps one or more original values to a
444
+ generalized value.
445
+
446
+ A rule can contain either a single value or a list of values in
447
+ the ``from`` field.
448
+
449
+ Example
450
+ -------
451
+ A rule such as::
452
+
453
+ {"from": ["Single", "Divorced"], "to": "Not married"}
454
+
455
+ maps both original values to ``"Not married"``.
456
+
457
+ Parameters
458
+ ----------
459
+ df : pyspark.sql.DataFrame
460
+ Input dataset.
461
+
462
+ config : dict
463
+ Parsed JSON configuration containing categorical rules.
464
+
465
+ Returns
466
+ -------
467
+ pyspark.sql.DataFrame
468
+ DataFrame with the selected column generalized.
469
+
470
+ Raises
471
+ ------
472
+ ValueError
473
+ If no rules or no valid categorical rules are found.
474
+
475
+ Notes
476
+ -----
477
+ The mapping is converted into a Spark map expression. The
478
+ transformation is therefore evaluated by Spark workers without
479
+ requiring dataset records to be collected to the driver.
480
+ """
481
+ mapping = {}
482
+
483
+ rules = config.get("rules", [])
484
+
485
+ if not rules:
486
+ raise ValueError(
487
+ "No rules found in JSON configuration."
488
+ )
489
+
490
+ for rule in rules:
491
+ if not isinstance(rule, dict):
492
+ warnings.warn(
493
+ f"Invalid categorical rule ignored: {rule}"
494
+ )
495
+ continue
496
+
497
+ try:
498
+ original = rule["from"]
499
+ general = str(rule["to"])
500
+
501
+ except KeyError as exc:
502
+ warnings.warn(
503
+ f"Invalid rule ignored. "
504
+ f"Missing key {exc}: {rule}"
505
+ )
506
+ continue
507
+
508
+ except (TypeError, ValueError):
509
+ warnings.warn(
510
+ f"Invalid categorical rule ignored: {rule}"
511
+ )
512
+ continue
513
+
514
+ # Allow a single value or a list of values in "from"
515
+ originals = original if isinstance(original, list) else [original]
516
+
517
+ for value in originals:
518
+ mapping[str(value)] = general
519
+
520
+ if not mapping:
521
+ raise ValueError(
522
+ "No valid categorical rules found."
523
+ )
524
+
525
+ mapping_expr = F.create_map(
526
+ *[
527
+ item
528
+ for key, value in mapping.items()
529
+ for item in (
530
+ F.lit(key),
531
+ F.lit(value)
532
+ )
533
+ ]
534
+ )
535
+
536
+ source_col = (
537
+ F.col(self.column)
538
+ .cast("string")
539
+ )
540
+
541
+ new_col = mapping_expr[source_col]
542
+
543
+ if self.default_value is not None:
544
+ new_col = F.coalesce(
545
+ new_col,
546
+ F.lit(str(self.default_value))
547
+ )
548
+ else:
549
+ new_col = F.coalesce(
550
+ new_col,
551
+ source_col
552
+ )
553
+
554
+ result = df.withColumn(
555
+ self.column,
556
+ new_col.cast("string")
557
+ )
558
+
559
+ return self._rename_output_column(result)
560
+
561
+ def _apply_numeric(
562
+ self,
563
+ df: DataFrame,
564
+ config: dict
565
+ ) -> DataFrame:
566
+ """
567
+ Applies numerical generalization using interval rules.
568
+
569
+ Each rule defines a closed numerical interval using ``min`` and
570
+ ``max`` together with the generalized label assigned to values
571
+ inside that interval.
572
+
573
+ Example
574
+ -------
575
+ A rule such as::
576
+
577
+ {"min": 18, "max": 30, "value": "18-30"}
578
+
579
+ assigns the value ``"18-30"`` to numeric values satisfying:
580
+
581
+ 18 <= value <= 30
582
+
583
+ Parameters
584
+ ----------
585
+ df : pyspark.sql.DataFrame
586
+ Input dataset.
587
+
588
+ config : dict
589
+ Parsed JSON configuration containing numerical interval rules.
590
+
591
+ Returns
592
+ -------
593
+ pyspark.sql.DataFrame
594
+ DataFrame with the selected column generalized.
595
+
596
+ Raises
597
+ ------
598
+ ValueError
599
+ If no rules or no valid numerical rules are found.
600
+
601
+ Notes
602
+ -----
603
+ Interval rules are converted into Spark ``when`` expressions and
604
+ are evaluated in a distributed manner.
605
+
606
+ Intervals are evaluated in the order in which they appear in the
607
+ configuration. If intervals overlap, the first matching rule takes
608
+ precedence.
609
+ """
610
+ rules = config.get("rules", [])
611
+
612
+ if not rules:
613
+ raise ValueError(
614
+ "No rules found in JSON configuration."
615
+ )
616
+
617
+ expr = None
618
+
619
+ numeric_col = (
620
+ F.col(self.column)
621
+ .cast("double")
622
+ )
623
+
624
+ for rule in rules:
625
+ if not isinstance(rule, dict):
626
+ warnings.warn(
627
+ f"Invalid numeric rule ignored: {rule}"
628
+ )
629
+ continue
630
+
631
+ try:
632
+ start = float(rule["min"])
633
+ end = float(rule["max"])
634
+ label = str(rule["value"])
635
+
636
+ except KeyError as exc:
637
+ warnings.warn(
638
+ f"Invalid rule ignored. "
639
+ f"Missing key {exc}: {rule}"
640
+ )
641
+ continue
642
+
643
+ except (TypeError, ValueError):
644
+ warnings.warn(
645
+ f"Invalid numeric values in rule: {rule}"
646
+ )
647
+ continue
648
+
649
+ if start > end:
650
+ warnings.warn(
651
+ f"Invalid interval ignored: {rule}"
652
+ )
653
+ continue
654
+
655
+ condition = (
656
+ (numeric_col >= F.lit(start))
657
+ & (numeric_col <= F.lit(end))
658
+ )
659
+
660
+ if expr is None:
661
+ expr = F.when(
662
+ condition,
663
+ F.lit(label)
664
+ )
665
+ else:
666
+ expr = expr.when(
667
+ condition,
668
+ F.lit(label)
669
+ )
670
+
671
+ if expr is None:
672
+ raise ValueError(
673
+ "No valid numerical rules found."
674
+ )
675
+
676
+ if self.default_value is not None:
677
+ expr = expr.otherwise(
678
+ F.lit(str(self.default_value))
679
+ )
680
+ else:
681
+ expr = expr.otherwise(
682
+ F.col(self.column).cast("string")
683
+ )
684
+
685
+ result = df.withColumn(
686
+ self.column,
687
+ expr.cast("string")
688
+ )
689
+
690
+ return self._rename_output_column(result)
691
+
692
+ def _apply_temporal_generalization(
693
+ self,
694
+ df: DataFrame
695
+ ) -> DataFrame:
696
+ """
697
+ Applies temporal generalization based on the selected aggregation
698
+ level.
699
+
700
+ Supported modes are year, month, quarter and semester.
701
+
702
+ By default, ``include_year`` is True, meaning that generalized
703
+ values preserve the year whenever applicable.
704
+
705
+ Examples
706
+ --------
707
+ With ``include_year=True``:
708
+
709
+ - year: ``2024``
710
+ - month: ``2024-05``
711
+ - quarter: ``2024-Q2``
712
+ - semester: ``2024-S1``
713
+
714
+ With ``include_year=False``:
715
+
716
+ - year: ``2024``
717
+ - month: ``05``
718
+ - quarter: ``Q2``
719
+ - semester: ``S1``
720
+
721
+ Parameters
722
+ ----------
723
+ df : pyspark.sql.DataFrame
724
+ Input dataset.
725
+
726
+ Returns
727
+ -------
728
+ pyspark.sql.DataFrame
729
+ DataFrame with the temporal column generalized.
730
+
731
+ Raises
732
+ ------
733
+ ValueError
734
+ If the configured temporal mode is unsupported.
735
+ """
736
+ col = F.col(self.column)
737
+
738
+ if self.mode == "year":
739
+ new_col = (
740
+ F.year(col)
741
+ .cast("string")
742
+ )
743
+
744
+ elif self.mode == "month":
745
+ if self.include_year:
746
+ new_col = F.date_format(
747
+ col,
748
+ "yyyy-MM"
749
+ )
750
+ else:
751
+ new_col = F.date_format(
752
+ col,
753
+ "MM"
754
+ )
755
+
756
+ elif self.mode == "quarter":
757
+ quarter = F.concat(
758
+ F.lit("Q"),
759
+ F.quarter(col)
760
+ )
761
+
762
+ if self.include_year:
763
+ new_col = F.concat(
764
+ F.year(col),
765
+ F.lit("-"),
766
+ quarter
767
+ )
768
+ else:
769
+ new_col = quarter
770
+
771
+ elif self.mode == "semester":
772
+ semester = F.concat(
773
+ F.lit("S"),
774
+ F.when(
775
+ F.month(col) <= 6,
776
+ 1
777
+ ).otherwise(2)
778
+ )
779
+
780
+ if self.include_year:
781
+ new_col = F.concat(
782
+ F.year(col),
783
+ F.lit("-"),
784
+ semester
785
+ )
786
+ else:
787
+ new_col = semester
788
+
789
+ else:
790
+ raise ValueError(
791
+ f"Unsupported mode '{self.mode}'."
792
+ )
793
+
794
+ result = df.withColumn(
795
+ self.column,
796
+ new_col.cast("string")
797
+ )
798
+
799
+ return self._rename_output_column(result)
800
+
801
+ def transform(
802
+ self,
803
+ df: DataFrame
804
+ ) -> DataFrame:
805
+ """
806
+ Applies generalization to the configured column.
807
+
808
+ The transformation strategy is selected from either the external
809
+ rules configuration or the semantic type of the target column.
810
+
811
+ When ``rules_path`` is provided, the JSON configuration is loaded
812
+ once and its ``type`` field determines the transformation:
813
+
814
+ - ``categorical``:
815
+ Categorical mapping rules are applied.
816
+
817
+ - ``numeric``:
818
+ Numerical interval rules are applied.
819
+
820
+ - ``date``:
821
+ Temporal generalization is applied using the configured mode.
822
+
823
+ When no rules file is provided, temporal columns can be generalized
824
+ directly using their inferred semantic type. Numerical and
825
+ categorical generalization require external rules.
826
+
827
+ Parameters
828
+ ----------
829
+ df : pyspark.sql.DataFrame
830
+ Input dataset.
831
+
832
+ Returns
833
+ -------
834
+ pyspark.sql.DataFrame
835
+ DataFrame with the selected column generalized.
836
+
837
+ Raises
838
+ ------
839
+ ValueError
840
+ If the input DataFrame is None, is not Spark-compatible, does
841
+ not contain the configured column, contains an unsupported
842
+ rules type, or requires rules that were not provided.
843
+
844
+ Notes
845
+ -----
846
+ The method only constructs Spark transformations. Dataset records
847
+ are not collected to the driver.
848
+
849
+ When external rules are used, only the small JSON configuration
850
+ itself is loaded on the driver.
851
+ """
852
+ if df is None:
853
+ raise ValueError(
854
+ "Input DataFrame cannot be None."
855
+ )
856
+
857
+ # Compatible with standard Spark and Spark Connect.
858
+ if not hasattr(df, "withColumn"):
859
+ raise ValueError(
860
+ "Input must be a Spark-compatible DataFrame."
861
+ )
862
+
863
+ if self.column not in df.columns:
864
+ raise ValueError(
865
+ f"Column '{self.column}' does not exist "
866
+ f"in the DataFrame."
867
+ )
868
+
869
+ # ------------------------------------------------------
870
+ # EXTERNAL RULES
871
+ # ------------------------------------------------------
872
+
873
+ if self.rules_path is not None:
874
+ config = self._load_rules_config(df)
875
+ rules_type = self._get_rules_type(config)
876
+
877
+ if rules_type == "numeric":
878
+ return self._apply_numeric(
879
+ df,
880
+ config
881
+ )
882
+
883
+ if rules_type == "categorical":
884
+ return self._apply_categorical(
885
+ df,
886
+ config
887
+ )
888
+
889
+ if rules_type == "date":
890
+ return self._apply_temporal_with_mode(df)
891
+
892
+ # ------------------------------------------------------
893
+ # AUTOMATIC TEMPORAL GENERALIZATION
894
+ # ------------------------------------------------------
895
+
896
+ semantic_type = infer_semantic_type(
897
+ df,
898
+ self.column
899
+ )
900
+
901
+ if semantic_type == "date":
902
+ return self._apply_temporal_with_mode(df)
903
+
904
+ raise ValueError(
905
+ "For numeric or categorical generalization, "
906
+ "'rules_path' must be provided."
907
+ )