rdt 1.9.1__tar.gz → 1.9.2.dev0__tar.gz
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.
- {rdt-1.9.1 → rdt-1.9.2.dev0}/PKG-INFO +1 -1
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/__init__.py +1 -1
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/hyper_transformer.py +31 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/base.py +4 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/categorical.py +19 -5
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/numerical.py +2 -31
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/text.py +60 -35
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/utils.py +92 -1
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt.egg-info/PKG-INFO +1 -1
- {rdt-1.9.1 → rdt-1.9.2.dev0}/setup.cfg +1 -1
- {rdt-1.9.1 → rdt-1.9.2.dev0}/setup.py +1 -1
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/test_hyper_transformer.py +103 -1
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/test_base.py +2 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/test_categorical.py +30 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/test_text.py +34 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/test_hyper_transformer.py +121 -10
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/test_base.py +6 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/test_categorical.py +78 -5
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/test_numerical.py +0 -80
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/test_text.py +87 -33
- rdt-1.9.2.dev0/tests/unit/transformers/test_utils.py +215 -0
- rdt-1.9.1/tests/unit/transformers/test_utils.py +0 -68
- {rdt-1.9.1 → rdt-1.9.2.dev0}/AUTHORS.rst +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/CONTRIBUTING.rst +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/HISTORY.md +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/LICENSE +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/MANIFEST.in +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/README.md +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/RELEASE.md +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/errors.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/datasets/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/datasets/base.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/datasets/boolean.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/datasets/categorical.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/datasets/datetime.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/datasets/numerical.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/datasets/pii.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/datasets/text.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/datasets/utils.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/performance.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/performance/profiling.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/addons/addons_setup.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/boolean.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/datetime.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/null.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/pii/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/pii/anonymization.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/pii/anonymizer.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt/transformers/pii/utils.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt.egg-info/SOURCES.txt +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt.egg-info/dependency_links.txt +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt.egg-info/not-zip-safe +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt.egg-info/requires.txt +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/rdt.egg-info/top_level.txt +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/code_style.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/contributing.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/datasets/tests/test_boolean.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/datasets/tests/test_categorical.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/datasets/tests/test_datetime.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/datasets/tests/test_numerical.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/datasets/tests/test_utils.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/test_transformers.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/pii/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/pii/test_anonymization.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/pii/test_anonymizer.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/test_boolean.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/test_datetime.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/integration/transformers/test_numerical.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/performance/README.md +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/performance/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/performance/test_performance.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/performance/tests/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/performance/tests/test_profiling.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/test___init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/addons/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/addons/identity/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/addons/identity/test_identity.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/pii/__init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/pii/test_anonymization.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/pii/test_anonymizer.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/pii/test_utils.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/test___init__.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/test_boolean.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/test_datetime.py +0 -0
- {rdt-1.9.1 → rdt-1.9.2.dev0}/tests/unit/transformers/test_null.py +0 -0
|
@@ -357,6 +357,32 @@ class HyperTransformer:
|
|
|
357
357
|
|
|
358
358
|
self.field_transformers[new_tuple] = self.field_transformers.pop(old_tuple)
|
|
359
359
|
|
|
360
|
+
def _update_multi_column_transformer(self):
|
|
361
|
+
"""Check that multi-columns mappings are valid and update them otherwise."""
|
|
362
|
+
all_fields_multi_column = set()
|
|
363
|
+
for columns, transformer in self.field_transformers.items():
|
|
364
|
+
if isinstance(transformer, BaseMultiColumnTransformer):
|
|
365
|
+
all_fields_multi_column.add(columns)
|
|
366
|
+
|
|
367
|
+
for field in all_fields_multi_column:
|
|
368
|
+
transformer = self.field_transformers[field]
|
|
369
|
+
|
|
370
|
+
columns_to_sdtypes = self._get_columns_to_sdtypes(field)
|
|
371
|
+
try:
|
|
372
|
+
transformer._validate_sdtypes( # pylint: disable=protected-access
|
|
373
|
+
columns_to_sdtypes
|
|
374
|
+
)
|
|
375
|
+
except TransformerInputError:
|
|
376
|
+
warnings.warn(
|
|
377
|
+
f"Transformer '{transformer.get_name()}' is incompatible with the "
|
|
378
|
+
f"multi-column field '{field}'. Assigning default transformer to the columns."
|
|
379
|
+
)
|
|
380
|
+
del self.field_transformers[field]
|
|
381
|
+
for column, sdtype in columns_to_sdtypes.items():
|
|
382
|
+
self.field_transformers[column] = deepcopy(get_default_transformer(sdtype))
|
|
383
|
+
|
|
384
|
+
self._multi_column_fields = self._create_multi_column_fields()
|
|
385
|
+
|
|
360
386
|
def update_transformers_by_sdtype(
|
|
361
387
|
self, sdtype, transformer=None, transformer_name=None, transformer_parameters=None):
|
|
362
388
|
"""Update the transformers for the specified ``sdtype``.
|
|
@@ -397,6 +423,7 @@ class HyperTransformer:
|
|
|
397
423
|
self._remove_column_in_multi_column_fields(field)
|
|
398
424
|
|
|
399
425
|
self._multi_column_fields = self._create_multi_column_fields()
|
|
426
|
+
self._update_multi_column_transformer()
|
|
400
427
|
self._modified_config = True
|
|
401
428
|
|
|
402
429
|
def update_sdtypes(self, column_name_to_sdtype):
|
|
@@ -445,6 +472,7 @@ class HyperTransformer:
|
|
|
445
472
|
)
|
|
446
473
|
|
|
447
474
|
self._multi_column_fields = self._create_multi_column_fields()
|
|
475
|
+
self._update_multi_column_transformer()
|
|
448
476
|
self._modified_config = True
|
|
449
477
|
if self._fitted:
|
|
450
478
|
warnings.warn(self._REFIT_MESSAGE)
|
|
@@ -485,6 +513,7 @@ class HyperTransformer:
|
|
|
485
513
|
self.field_transformers[column_name] = transformer
|
|
486
514
|
|
|
487
515
|
self._multi_column_fields = self._create_multi_column_fields()
|
|
516
|
+
self._update_multi_column_transformer()
|
|
488
517
|
self._modified_config = True
|
|
489
518
|
|
|
490
519
|
def remove_transformers(self, column_names):
|
|
@@ -514,6 +543,7 @@ class HyperTransformer:
|
|
|
514
543
|
|
|
515
544
|
self.field_transformers[column_name] = None
|
|
516
545
|
|
|
546
|
+
self._update_multi_column_transformer()
|
|
517
547
|
if self._fitted:
|
|
518
548
|
warnings.warn(self._REFIT_MESSAGE)
|
|
519
549
|
|
|
@@ -540,6 +570,7 @@ class HyperTransformer:
|
|
|
540
570
|
|
|
541
571
|
self.field_transformers[column_name] = None
|
|
542
572
|
|
|
573
|
+
self._update_multi_column_transformer()
|
|
543
574
|
if self._fitted:
|
|
544
575
|
warnings.warn(self._REFIT_MESSAGE)
|
|
545
576
|
|
|
@@ -547,6 +547,10 @@ class BaseMultiColumnTransformer(BaseTransformer):
|
|
|
547
547
|
missing_to_print = ', '.join(missing)
|
|
548
548
|
raise ValueError(f'Columns ({missing_to_print}) are not present in the data.')
|
|
549
549
|
|
|
550
|
+
@classmethod
|
|
551
|
+
def _validate_sdtypes(cls, columns_to_sdtypes):
|
|
552
|
+
raise NotImplementedError()
|
|
553
|
+
|
|
550
554
|
def _fit(self, data):
|
|
551
555
|
"""Fit the transformer to the data.
|
|
552
556
|
|
|
@@ -9,7 +9,7 @@ from scipy.stats import norm
|
|
|
9
9
|
|
|
10
10
|
from rdt.errors import TransformerInputError
|
|
11
11
|
from rdt.transformers.base import BaseTransformer
|
|
12
|
-
from rdt.transformers.utils import fill_nan_with_none
|
|
12
|
+
from rdt.transformers.utils import check_nan_in_transform, fill_nan_with_none, try_convert_to_dtype
|
|
13
13
|
|
|
14
14
|
LOGGER = logging.getLogger(__name__)
|
|
15
15
|
|
|
@@ -177,6 +177,7 @@ class UniformEncoder(BaseTransformer):
|
|
|
177
177
|
Returns:
|
|
178
178
|
pandas.Series
|
|
179
179
|
"""
|
|
180
|
+
check_nan_in_transform(data, self.dtype)
|
|
180
181
|
data = data.clip(0, 1)
|
|
181
182
|
bins = [0]
|
|
182
183
|
labels = []
|
|
@@ -192,7 +193,10 @@ class UniformEncoder(BaseTransformer):
|
|
|
192
193
|
labels.append(key)
|
|
193
194
|
|
|
194
195
|
result = pd.cut(data, bins=bins, labels=labels, include_lowest=True)
|
|
195
|
-
|
|
196
|
+
result = result.replace(nan_name, np.nan)
|
|
197
|
+
result = try_convert_to_dtype(result, self.dtype)
|
|
198
|
+
|
|
199
|
+
return result
|
|
196
200
|
|
|
197
201
|
|
|
198
202
|
class OrderedUniformEncoder(UniformEncoder):
|
|
@@ -333,6 +337,7 @@ class FrequencyEncoder(BaseTransformer):
|
|
|
333
337
|
)
|
|
334
338
|
super().__init__()
|
|
335
339
|
self.add_noise = add_noise
|
|
340
|
+
self._is_integer = None
|
|
336
341
|
|
|
337
342
|
@staticmethod
|
|
338
343
|
def _get_intervals(data):
|
|
@@ -516,6 +521,7 @@ class FrequencyEncoder(BaseTransformer):
|
|
|
516
521
|
Returns:
|
|
517
522
|
pandas.Series
|
|
518
523
|
"""
|
|
524
|
+
check_nan_in_transform(data, self.dtype)
|
|
519
525
|
data = data.clip(0, 1)
|
|
520
526
|
num_rows = len(data)
|
|
521
527
|
num_categories = len(self.means)
|
|
@@ -545,6 +551,7 @@ class OneHotEncoder(BaseTransformer):
|
|
|
545
551
|
_dummy_encoded = False
|
|
546
552
|
_indexer = None
|
|
547
553
|
_uniques = None
|
|
554
|
+
dtype = None
|
|
548
555
|
|
|
549
556
|
@staticmethod
|
|
550
557
|
def _prepare_data(data):
|
|
@@ -582,6 +589,7 @@ class OneHotEncoder(BaseTransformer):
|
|
|
582
589
|
data (pandas.Series or pandas.DataFrame):
|
|
583
590
|
Data to fit the transformer to.
|
|
584
591
|
"""
|
|
592
|
+
self.dtype = data.dtype
|
|
585
593
|
data = self._prepare_data(data)
|
|
586
594
|
|
|
587
595
|
null = pd.isna(data).to_numpy()
|
|
@@ -591,7 +599,7 @@ class OneHotEncoder(BaseTransformer):
|
|
|
591
599
|
self._indexer = list(range(self._num_dummies))
|
|
592
600
|
self.dummies = self._uniques.copy()
|
|
593
601
|
|
|
594
|
-
if not np.issubdtype(data.dtype, np.number):
|
|
602
|
+
if not np.issubdtype(data.dtype.type, np.number):
|
|
595
603
|
self._dummy_encoded = True
|
|
596
604
|
|
|
597
605
|
if self._dummy_na:
|
|
@@ -657,6 +665,7 @@ class OneHotEncoder(BaseTransformer):
|
|
|
657
665
|
Returns:
|
|
658
666
|
pandas.Series
|
|
659
667
|
"""
|
|
668
|
+
check_nan_in_transform(data, self.dtype)
|
|
660
669
|
if not isinstance(data, np.ndarray):
|
|
661
670
|
data = data.to_numpy()
|
|
662
671
|
|
|
@@ -664,8 +673,10 @@ class OneHotEncoder(BaseTransformer):
|
|
|
664
673
|
data = data.reshape(-1, 1)
|
|
665
674
|
|
|
666
675
|
indices = np.argmax(data, axis=1)
|
|
676
|
+
result = pd.Series(indices).map(self.dummies.__getitem__)
|
|
677
|
+
result = try_convert_to_dtype(result, self.dtype)
|
|
667
678
|
|
|
668
|
-
return
|
|
679
|
+
return result
|
|
669
680
|
|
|
670
681
|
|
|
671
682
|
class LabelEncoder(BaseTransformer):
|
|
@@ -801,13 +812,15 @@ class LabelEncoder(BaseTransformer):
|
|
|
801
812
|
Returns:
|
|
802
813
|
pandas.Series
|
|
803
814
|
"""
|
|
815
|
+
check_nan_in_transform(data, self.dtype)
|
|
804
816
|
if self.add_noise:
|
|
805
817
|
data = np.floor(data)
|
|
806
818
|
|
|
807
819
|
data = data.clip(min(self.values_to_categories), max(self.values_to_categories))
|
|
808
820
|
data = data.round().map(self.values_to_categories)
|
|
821
|
+
data = try_convert_to_dtype(data, self.dtype)
|
|
809
822
|
|
|
810
|
-
return data
|
|
823
|
+
return data
|
|
811
824
|
|
|
812
825
|
|
|
813
826
|
class OrderedLabelEncoder(LabelEncoder):
|
|
@@ -865,6 +878,7 @@ class OrderedLabelEncoder(LabelEncoder):
|
|
|
865
878
|
data (pandas.Series):
|
|
866
879
|
Data to fit the transformer to.
|
|
867
880
|
"""
|
|
881
|
+
self.dtype = data.dtype
|
|
868
882
|
data = data.fillna(np.nan)
|
|
869
883
|
missing = list(data[~data.isin(self.order)].unique())
|
|
870
884
|
if len(missing) > 0:
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Transformers for numerical data."""
|
|
2
2
|
import copy
|
|
3
|
-
import logging
|
|
4
|
-
import sys
|
|
5
3
|
import warnings
|
|
6
4
|
|
|
7
5
|
import numpy as np
|
|
@@ -11,11 +9,9 @@ from sklearn.mixture import BayesianGaussianMixture
|
|
|
11
9
|
|
|
12
10
|
from rdt.transformers.base import BaseTransformer
|
|
13
11
|
from rdt.transformers.null import NullTransformer
|
|
14
|
-
|
|
15
|
-
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
from rdt.transformers.utils import learn_rounding_digits
|
|
16
13
|
|
|
17
14
|
EPSILON = np.finfo(np.float32).eps
|
|
18
|
-
MAX_DECIMALS = sys.float_info.dig - 1
|
|
19
15
|
INTEGER_BOUNDS = {
|
|
20
16
|
'Int8': (-2**7, 2**7 - 1),
|
|
21
17
|
'Int16': (-2**15, 2**15 - 1),
|
|
@@ -91,31 +87,6 @@ class FloatFormatter(BaseTransformer):
|
|
|
91
87
|
self.enforce_min_max_values = enforce_min_max_values
|
|
92
88
|
self.computer_representation = computer_representation
|
|
93
89
|
|
|
94
|
-
@staticmethod
|
|
95
|
-
def _learn_rounding_digits(data):
|
|
96
|
-
# check if data has any decimals
|
|
97
|
-
name = data.name
|
|
98
|
-
data = np.array(data)
|
|
99
|
-
roundable_data = data[~(np.isinf(data) | pd.isna(data))]
|
|
100
|
-
|
|
101
|
-
# Doesn't contain numbers
|
|
102
|
-
if len(roundable_data) == 0:
|
|
103
|
-
return None
|
|
104
|
-
|
|
105
|
-
# Doesn't contain decimal digits
|
|
106
|
-
if ((roundable_data % 1) == 0).all():
|
|
107
|
-
return 0
|
|
108
|
-
|
|
109
|
-
# Try to round to fewer digits
|
|
110
|
-
if (roundable_data == roundable_data.round(MAX_DECIMALS)).all():
|
|
111
|
-
for decimal in range(MAX_DECIMALS + 1):
|
|
112
|
-
if (roundable_data == roundable_data.round(decimal)).all():
|
|
113
|
-
return decimal
|
|
114
|
-
|
|
115
|
-
# Can't round, not equal after MAX_DECIMALS digits of precision
|
|
116
|
-
LOGGER.info("No rounding scheme detected for column '%s'. Data will not be rounded.", name)
|
|
117
|
-
return None
|
|
118
|
-
|
|
119
90
|
def _raise_out_of_bounds_error(self, value, name, bound_type, min_bound, max_bound):
|
|
120
91
|
raise ValueError(
|
|
121
92
|
f"The {bound_type} value in column '{name}' is {value}."
|
|
@@ -158,7 +129,7 @@ class FloatFormatter(BaseTransformer):
|
|
|
158
129
|
self._max_value = data.max()
|
|
159
130
|
|
|
160
131
|
if self.learn_rounding_scheme:
|
|
161
|
-
self._rounding_digits =
|
|
132
|
+
self._rounding_digits = learn_rounding_digits(data)
|
|
162
133
|
|
|
163
134
|
self.null_transformer = NullTransformer(
|
|
164
135
|
self.missing_value_replacement,
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""Transformers for text data."""
|
|
2
|
+
import logging
|
|
2
3
|
import warnings
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
5
6
|
import pandas as pd
|
|
6
7
|
|
|
7
|
-
from rdt.errors import TransformerProcessingError
|
|
8
8
|
from rdt.transformers.base import BaseTransformer
|
|
9
9
|
from rdt.transformers.utils import strings_from_regex
|
|
10
10
|
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
class IDGenerator(BaseTransformer):
|
|
13
15
|
"""Generate an ID column.
|
|
@@ -139,6 +141,37 @@ class RegexGenerator(BaseTransformer):
|
|
|
139
141
|
"""Drop the input column by returning ``None``."""
|
|
140
142
|
return None
|
|
141
143
|
|
|
144
|
+
def _warn_not_enough_unique_values(self, sample_size):
|
|
145
|
+
"""Warn the user that the regex cannot generate enough unique values.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
sample_size (int):
|
|
149
|
+
Number of samples to be generated.
|
|
150
|
+
"""
|
|
151
|
+
warned = False
|
|
152
|
+
if sample_size > self.generator_size:
|
|
153
|
+
if self.enforce_uniqueness:
|
|
154
|
+
warnings.warn(
|
|
155
|
+
f"The regex for '{self.get_input_column()}' can only generate "
|
|
156
|
+
f'{self.generator_size} unique values. Additional values may not exactly '
|
|
157
|
+
'follow the provided regex.'
|
|
158
|
+
)
|
|
159
|
+
warned = True
|
|
160
|
+
else:
|
|
161
|
+
LOGGER.info(
|
|
162
|
+
"The data has %s rows but the regex for '%s' can only create %s unique values."
|
|
163
|
+
" Some values in '%s' may be repeated.",
|
|
164
|
+
sample_size, self.get_input_column(), self.generator_size,
|
|
165
|
+
self.get_input_column()
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
remaining = self.generator_size - self.generated
|
|
169
|
+
if sample_size > remaining and self.enforce_uniqueness and not warned:
|
|
170
|
+
warnings.warn(
|
|
171
|
+
f'The regex generator is not able to generate {sample_size} new unique '
|
|
172
|
+
f'values (only {max(remaining, 0)} unique values left).'
|
|
173
|
+
)
|
|
174
|
+
|
|
142
175
|
def _reverse_transform(self, data):
|
|
143
176
|
"""Generate new data using the provided ``regex_format``.
|
|
144
177
|
|
|
@@ -154,47 +187,39 @@ class RegexGenerator(BaseTransformer):
|
|
|
154
187
|
else:
|
|
155
188
|
sample_size = self.data_length
|
|
156
189
|
|
|
157
|
-
|
|
158
|
-
if self.enforce_uniqueness:
|
|
159
|
-
raise TransformerProcessingError(
|
|
160
|
-
f'The regex is not able to generate {sample_size} unique values. '
|
|
161
|
-
f"Please use a different regex for column ('{self.get_input_column()}')."
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
warnings.warn(
|
|
165
|
-
f"The data has {sample_size} rows but the regex for '{self.get_input_column()}' "
|
|
166
|
-
f'can only create {self.generator_size} unique values. Some values in '
|
|
167
|
-
f"'{self.get_input_column()}' may be repeated."
|
|
168
|
-
)
|
|
190
|
+
self._warn_not_enough_unique_values(sample_size)
|
|
169
191
|
|
|
170
192
|
remaining = self.generator_size - self.generated
|
|
171
|
-
if sample_size >
|
|
172
|
-
if self.enforce_uniqueness:
|
|
173
|
-
raise TransformerProcessingError(
|
|
174
|
-
f'The regex generator is not able to generate {sample_size} new unique '
|
|
175
|
-
f'values (only {remaining} unique value left). Please use '
|
|
176
|
-
"'reset_randomization' in order to restart the generator."
|
|
177
|
-
)
|
|
178
|
-
|
|
193
|
+
if sample_size > remaining:
|
|
179
194
|
self.reset_randomization()
|
|
180
195
|
remaining = self.generator_size
|
|
181
196
|
|
|
182
197
|
if remaining >= sample_size:
|
|
183
|
-
reverse_transformed =
|
|
184
|
-
next(self.generator)
|
|
185
|
-
for _ in range(sample_size)
|
|
186
|
-
], dtype=object)
|
|
187
|
-
|
|
198
|
+
reverse_transformed = [next(self.generator) for _ in range(sample_size)]
|
|
188
199
|
self.generated += sample_size
|
|
189
200
|
|
|
190
201
|
else:
|
|
191
|
-
self.generated = self.generator_size
|
|
192
202
|
generated_values = list(self.generator)
|
|
193
|
-
reverse_transformed = []
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
reverse_transformed = generated_values[:]
|
|
204
|
+
self.generated = self.generator_size
|
|
205
|
+
if self.enforce_uniqueness:
|
|
206
|
+
try:
|
|
207
|
+
remaining_samples = sample_size - len(reverse_transformed)
|
|
208
|
+
start = int(generated_values[-1]) + 1
|
|
209
|
+
reverse_transformed.extend(
|
|
210
|
+
[str(i) for i in range(start, start + remaining_samples)])
|
|
211
|
+
|
|
212
|
+
except ValueError:
|
|
213
|
+
counter = 0
|
|
214
|
+
while len(reverse_transformed) < sample_size:
|
|
215
|
+
remaining_samples = sample_size - len(reverse_transformed)
|
|
216
|
+
reverse_transformed.extend(
|
|
217
|
+
[f'{i}({counter})' for i in generated_values[:remaining_samples]])
|
|
218
|
+
counter += 1
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
while len(reverse_transformed) < sample_size:
|
|
222
|
+
remaining_samples = sample_size - len(reverse_transformed)
|
|
223
|
+
reverse_transformed.extend(generated_values[:remaining_samples])
|
|
224
|
+
|
|
225
|
+
return np.array(reverse_transformed, dtype=object)
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
"""Tools to generate strings from regular expressions."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import re
|
|
4
5
|
import string
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
5
8
|
|
|
6
9
|
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
7
11
|
|
|
8
12
|
import sre_parse # isort:skip
|
|
9
13
|
|
|
14
|
+
LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
MAX_DECIMALS = sys.float_info.dig - 1
|
|
17
|
+
|
|
10
18
|
|
|
11
19
|
def _literal(character, max_repeat):
|
|
12
20
|
del max_repeat
|
|
@@ -149,7 +157,7 @@ def strings_from_regex(regex, max_repeat=16):
|
|
|
149
157
|
generators.append((generator, option, args))
|
|
150
158
|
sizes.append(size)
|
|
151
159
|
|
|
152
|
-
return _from_generators(generators, max_repeat), np.prod(sizes, dtype=np.complex128)
|
|
160
|
+
return _from_generators(generators, max_repeat), np.prod(sizes, dtype=np.complex128).real
|
|
153
161
|
|
|
154
162
|
|
|
155
163
|
def fill_nan_with_none(data):
|
|
@@ -184,3 +192,86 @@ def flatten_column_list(column_list):
|
|
|
184
192
|
flattened.append(column)
|
|
185
193
|
|
|
186
194
|
return flattened
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def check_nan_in_transform(data, dtype):
|
|
198
|
+
"""Check if there are null values in the transformed data.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
data (pd.Series or numpy.ndarray):
|
|
202
|
+
Data that has been transformed.
|
|
203
|
+
dtype (str):
|
|
204
|
+
Data type of the transformed data.
|
|
205
|
+
"""
|
|
206
|
+
if pd.isna(data).any().any():
|
|
207
|
+
message = (
|
|
208
|
+
'There are null values in the transformed data. The reversed '
|
|
209
|
+
'transformed data will contain null values'
|
|
210
|
+
)
|
|
211
|
+
is_integer = pd.api.types.is_integer_dtype(dtype)
|
|
212
|
+
if is_integer:
|
|
213
|
+
message += " of type 'float'."
|
|
214
|
+
else:
|
|
215
|
+
message += '.'
|
|
216
|
+
|
|
217
|
+
warnings.warn(message)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def try_convert_to_dtype(data, dtype):
|
|
221
|
+
"""Try to convert data to a given dtype.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
data (pd.Series or numpy.ndarray):
|
|
225
|
+
Data to convert.
|
|
226
|
+
dtype (str):
|
|
227
|
+
Data type to convert to.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
data:
|
|
231
|
+
Data converted to the given dtype.
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
data = data.astype(dtype)
|
|
235
|
+
except ValueError as error:
|
|
236
|
+
is_integer = pd.api.types.is_integer_dtype(dtype)
|
|
237
|
+
if is_integer:
|
|
238
|
+
data = data.astype(float)
|
|
239
|
+
else:
|
|
240
|
+
raise error
|
|
241
|
+
|
|
242
|
+
return data
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def learn_rounding_digits(data):
|
|
246
|
+
"""Learn the number of digits to round data to.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
data (pd.Series):
|
|
250
|
+
Data to learn the number of digits to round to.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
int or None:
|
|
254
|
+
Number of digits to round to.
|
|
255
|
+
"""
|
|
256
|
+
# check if data has any decimals
|
|
257
|
+
name = data.name
|
|
258
|
+
data = np.array(data)
|
|
259
|
+
roundable_data = data[~(np.isinf(data) | pd.isna(data))]
|
|
260
|
+
|
|
261
|
+
# Doesn't contain numbers
|
|
262
|
+
if len(roundable_data) == 0:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
# Doesn't contain decimal digits
|
|
266
|
+
if ((roundable_data % 1) == 0).all():
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
# Try to round to fewer digits
|
|
270
|
+
if (roundable_data == roundable_data.round(MAX_DECIMALS)).all():
|
|
271
|
+
for decimal in range(MAX_DECIMALS + 1):
|
|
272
|
+
if (roundable_data == roundable_data.round(decimal)).all():
|
|
273
|
+
return decimal
|
|
274
|
+
|
|
275
|
+
# Can't round, not equal after MAX_DECIMALS digits of precision
|
|
276
|
+
LOGGER.info("No rounding scheme detected for column '%s'. Data will not be rounded.", name)
|
|
277
|
+
return None
|
|
@@ -7,7 +7,8 @@ import pandas as pd
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
from rdt import get_demo
|
|
10
|
-
from rdt.errors import
|
|
10
|
+
from rdt.errors import (
|
|
11
|
+
ConfigNotSetError, InvalidConfigError, InvalidDataError, NotFittedError, TransformerInputError)
|
|
11
12
|
from rdt.hyper_transformer import Config, HyperTransformer
|
|
12
13
|
from rdt.transformers import (
|
|
13
14
|
AnonymizedFaker, BaseMultiColumnTransformer, BaseTransformer, BinaryEncoder,
|
|
@@ -67,6 +68,10 @@ class DummyMultiColumnTransformerNumerical(BaseMultiColumnTransformer):
|
|
|
67
68
|
} for column in self.columns
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
@classmethod
|
|
72
|
+
def _validate_sdtypes(cls, columns_to_sdtype):
|
|
73
|
+
return None
|
|
74
|
+
|
|
70
75
|
def _get_prefix(self):
|
|
71
76
|
return None
|
|
72
77
|
|
|
@@ -1846,3 +1851,100 @@ class TestHyperTransformer:
|
|
|
1846
1851
|
]
|
|
1847
1852
|
})
|
|
1848
1853
|
pd.testing.assert_frame_equal(result, expected_results)
|
|
1854
|
+
|
|
1855
|
+
expected_sdtype = {
|
|
1856
|
+
'sdtypes': {
|
|
1857
|
+
'A': 'categorical',
|
|
1858
|
+
'B': 'categorical',
|
|
1859
|
+
'D': 'categorical',
|
|
1860
|
+
'E': 'categorical',
|
|
1861
|
+
'C': 'boolean'
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
expected_transformer_update = {
|
|
1865
|
+
'transformers': {
|
|
1866
|
+
'A': UniformEncoder(),
|
|
1867
|
+
'E': UniformEncoder(),
|
|
1868
|
+
'C': UniformEncoder(),
|
|
1869
|
+
'B': UniformEncoder(),
|
|
1870
|
+
'D': UniformEncoder()
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
expected_transformer_remove = {
|
|
1874
|
+
'transformers': {
|
|
1875
|
+
'A': UniformEncoder(),
|
|
1876
|
+
'E': UniformEncoder(),
|
|
1877
|
+
'C': None,
|
|
1878
|
+
'B': UniformEncoder(),
|
|
1879
|
+
'D': UniformEncoder()
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
expected_update = {
|
|
1883
|
+
**expected_sdtype,
|
|
1884
|
+
**expected_transformer_update
|
|
1885
|
+
}
|
|
1886
|
+
expected_remove = {
|
|
1887
|
+
**expected_sdtype,
|
|
1888
|
+
**expected_transformer_remove
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
parametrization = [
|
|
1892
|
+
(
|
|
1893
|
+
'update_transformers', {'column_name_to_transformer': {'C': UniformEncoder()}},
|
|
1894
|
+
expected_update
|
|
1895
|
+
),
|
|
1896
|
+
(
|
|
1897
|
+
'update_transformers_by_sdtype',
|
|
1898
|
+
{'sdtype': 'boolean', 'transformer': UniformEncoder()}, expected_update
|
|
1899
|
+
),
|
|
1900
|
+
('remove_transformers', {'column_names': 'C'}, expected_remove),
|
|
1901
|
+
('remove_transformers_by_sdtype', {'sdtype': 'boolean'}, expected_remove),
|
|
1902
|
+
]
|
|
1903
|
+
|
|
1904
|
+
@pytest.mark.parametrize(('method_name', 'method_input', 'expected_result'), parametrization)
|
|
1905
|
+
def test_invalid_multi_column(self, method_name, method_input, expected_result):
|
|
1906
|
+
"""Test the ``update`` and ``remove`` methods with invalid multi column transformer.
|
|
1907
|
+
|
|
1908
|
+
When a multi column is no longer valid, all these methods should raise a warning
|
|
1909
|
+
and assign the default transformer to the columns.
|
|
1910
|
+
"""
|
|
1911
|
+
# Setup
|
|
1912
|
+
class BadDummyMultiColumnTransformer(DummyMultiColumnTransformerNumerical):
|
|
1913
|
+
|
|
1914
|
+
@classmethod
|
|
1915
|
+
def _validate_sdtypes(cls, columns_to_sdtype):
|
|
1916
|
+
raise TransformerInputError('Invalid sdtype')
|
|
1917
|
+
|
|
1918
|
+
dict_config = {
|
|
1919
|
+
'sdtypes': {
|
|
1920
|
+
'A': 'categorical',
|
|
1921
|
+
'B': 'categorical',
|
|
1922
|
+
'D': 'categorical',
|
|
1923
|
+
'E': 'categorical',
|
|
1924
|
+
'C': 'boolean',
|
|
1925
|
+
},
|
|
1926
|
+
'transformers': {
|
|
1927
|
+
'A': UniformEncoder(),
|
|
1928
|
+
('B', 'D', 'C'): BadDummyMultiColumnTransformer(),
|
|
1929
|
+
'E': UniformEncoder()
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
config = Config(dict_config)
|
|
1934
|
+
ht = HyperTransformer()
|
|
1935
|
+
ht.set_config(config)
|
|
1936
|
+
|
|
1937
|
+
# Run
|
|
1938
|
+
expected_warning = re.escape(
|
|
1939
|
+
"Transformer 'BadDummyMultiColumnTransformer' is incompatible with the "
|
|
1940
|
+
"multi-column field '('B', 'D')'. Assigning default transformer to the columns."
|
|
1941
|
+
)
|
|
1942
|
+
with pytest.warns(UserWarning, match=expected_warning):
|
|
1943
|
+
ht.__getattribute__(method_name)(**method_input)
|
|
1944
|
+
|
|
1945
|
+
# Assert
|
|
1946
|
+
new_config = ht.get_config()
|
|
1947
|
+
expected_config = Config(expected_result)
|
|
1948
|
+
expected_multi_columns = {}
|
|
1949
|
+
assert ht._multi_column_fields == expected_multi_columns
|
|
1950
|
+
assert repr(new_config) == repr(expected_config)
|
|
@@ -136,6 +136,7 @@ def test_multi_column_transformer_same_number_of_columns_input_output():
|
|
|
136
136
|
# Setup
|
|
137
137
|
class AdditionTransformer(BaseMultiColumnTransformer):
|
|
138
138
|
"""This transformer takes 3 columns and return the cumulative sum of each row."""
|
|
139
|
+
|
|
139
140
|
def _fit(self, columns_data):
|
|
140
141
|
self.output_properties = {
|
|
141
142
|
f'{self.columns[0]}': {'sdtype': 'numerical'},
|
|
@@ -188,6 +189,7 @@ def test_multi_column_transformer_less_output_than_input_columns():
|
|
|
188
189
|
"""This transformer takes 4 columns and concatenate them into 2 columns.
|
|
189
190
|
The two first and last columns are concatenated together.
|
|
190
191
|
"""
|
|
192
|
+
|
|
191
193
|
def _fit(self, columns_data):
|
|
192
194
|
self.name_1 = self.columns[0] + '#' + self.columns[1]
|
|
193
195
|
self.name_2 = self.columns[2] + '#' + self.columns[3]
|