rdt 1.16.0.dev0__tar.gz → 1.17.0.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.
Files changed (45) hide show
  1. {rdt-1.16.0.dev0/rdt.egg-info → rdt-1.17.0.dev0}/PKG-INFO +2 -1
  2. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/pyproject.toml +2 -1
  3. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/__init__.py +1 -1
  4. rdt-1.17.0.dev0/rdt/_utils.py +47 -0
  5. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/hyper_transformer.py +33 -0
  6. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/datetime.py +111 -50
  7. rdt-1.17.0.dev0/rdt/transformers/id.py +446 -0
  8. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/pii/anonymizer.py +115 -51
  9. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/utils.py +133 -3
  10. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0/rdt.egg-info}/PKG-INFO +2 -1
  11. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt.egg-info/SOURCES.txt +1 -0
  12. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt.egg-info/requires.txt +1 -0
  13. rdt-1.16.0.dev0/rdt/transformers/id.py +0 -285
  14. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/LICENSE +0 -0
  15. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/README.md +0 -0
  16. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/errors.py +0 -0
  17. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/__init__.py +0 -0
  18. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/__init__.py +0 -0
  19. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/base.py +0 -0
  20. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/boolean.py +0 -0
  21. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/categorical.py +0 -0
  22. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/datetime.py +0 -0
  23. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/id.py +0 -0
  24. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/numerical.py +0 -0
  25. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/pii.py +0 -0
  26. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/text.py +0 -0
  27. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/datasets/utils.py +0 -0
  28. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/performance.py +0 -0
  29. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/performance/profiling.py +0 -0
  30. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/__init__.py +0 -0
  31. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/_validators.py +0 -0
  32. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/base.py +0 -0
  33. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/boolean.py +0 -0
  34. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/categorical.py +0 -0
  35. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/null.py +0 -0
  36. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/numerical.py +0 -0
  37. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/pii/__init__.py +0 -0
  38. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/pii/anonymization.py +0 -0
  39. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/pii/utils.py +0 -0
  40. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt/transformers/text.py +0 -0
  41. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt.egg-info/dependency_links.txt +0 -0
  42. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt.egg-info/entry_points.txt +0 -0
  43. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/rdt.egg-info/top_level.txt +0 -0
  44. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/setup.cfg +0 -0
  45. {rdt-1.16.0.dev0 → rdt-1.17.0.dev0}/tests/test_tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdt
3
- Version: 1.16.0.dev0
3
+ Version: 1.17.0.dev0
4
4
  Summary: Reversible Data Transforms
5
5
  Author-email: "DataCebo, Inc." <info@sdv.dev>
6
6
  License: BSL-1.1
@@ -43,6 +43,7 @@ Requires-Dist: scikit-learn>=1.1.3; python_version >= "3.11" and python_version
43
43
  Requires-Dist: scikit-learn>=1.3.1; python_version >= "3.12" and python_version < "3.13"
44
44
  Requires-Dist: scikit-learn>=1.5.2; python_version >= "3.13"
45
45
  Requires-Dist: Faker>=17
46
+ Requires-Dist: python-dateutil>=2.9
46
47
  Provides-Extra: copulas
47
48
  Requires-Dist: copulas>=0.12.1; extra == "copulas"
48
49
  Provides-Extra: pyarrow
@@ -40,6 +40,7 @@ dependencies = [
40
40
  "scikit-learn>=1.3.1;python_version>='3.12' and python_version<'3.13'",
41
41
  "scikit-learn>=1.5.2;python_version>='3.13'",
42
42
  'Faker>=17',
43
+ 'python-dateutil>=2.9',
43
44
  ]
44
45
 
45
46
  [project.urls]
@@ -144,7 +145,7 @@ collect_ignore = ['pyproject.toml']
144
145
  exclude_lines = ['NotImplementedError()']
145
146
 
146
147
  [tool.bumpversion]
147
- current_version = "1.16.0.dev0"
148
+ current_version = "1.17.0.dev0"
148
149
  parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<release>[a-z]+)(?P<candidate>\d+))?'
149
150
  serialize = [
150
151
  '{major}.{minor}.{patch}.{release}{candidate}',
@@ -4,7 +4,7 @@
4
4
 
5
5
  __author__ = 'DataCebo, Inc.'
6
6
  __email__ = 'info@sdv.dev'
7
- __version__ = '1.16.0.dev0'
7
+ __version__ = '1.17.0.dev0'
8
8
 
9
9
 
10
10
  import sys
@@ -0,0 +1,47 @@
1
+ from collections import defaultdict
2
+
3
+ from rdt.errors import InvalidConfigError
4
+
5
+
6
+ def _validate_unique_transformer_instances(column_name_to_transformer):
7
+ """Validate that the transformer instance for each field is unique.
8
+
9
+ Args:
10
+ column_name_to_transformer (dict):
11
+ A dictionary mapping column names to their current transformer.
12
+
13
+ Raises:
14
+ - ``InvalidConfigError`` if transformers in ``column_name_to_transformer`` are repeated.
15
+ """
16
+ seen_transformers = defaultdict(set)
17
+ for column_name, transformer in column_name_to_transformer.items():
18
+ if transformer is not None:
19
+ seen_transformers[transformer].add(column_name)
20
+
21
+ duplicated_transformers = {
22
+ transformer: columns
23
+ for transformer, columns in seen_transformers.items()
24
+ if len(columns) > 1
25
+ }
26
+ if duplicated_transformers:
27
+ duplicated_column_messages = []
28
+ for duplicated_columns in duplicated_transformers.values():
29
+ columns = ', '.join(
30
+ sorted([
31
+ str(columns) if not isinstance(columns, str) else f"'{columns}'"
32
+ for columns in duplicated_columns
33
+ ])
34
+ )
35
+ duplicated_column_messages.append(f'columns ({columns})')
36
+
37
+ if len(duplicated_column_messages) > 1:
38
+ plurality = 'instances are'
39
+ else:
40
+ plurality = 'instance is'
41
+
42
+ error_message = (
43
+ f'The same transformer {plurality} being assigned to '
44
+ f'{", ".join(duplicated_column_messages)}. Please create different transformer objects '
45
+ 'for each assignment.'
46
+ )
47
+ raise InvalidConfigError(error_message)
@@ -8,6 +8,7 @@ from copy import deepcopy
8
8
 
9
9
  import pandas as pd
10
10
 
11
+ from rdt._utils import _validate_unique_transformer_instances
11
12
  from rdt.errors import (
12
13
  ConfigNotSetError,
13
14
  InvalidConfigError,
@@ -181,6 +182,8 @@ class HyperTransformer:
181
182
  'Please assign an rdt transformer instance to each column name.'
182
183
  )
183
184
 
185
+ _validate_unique_transformer_instances(column_name_to_transformer)
186
+
184
187
  @staticmethod
185
188
  def _validate_sdtypes(sdtypes):
186
189
  """Validate the given sdtypes are valid.
@@ -496,6 +499,33 @@ class HyperTransformer:
496
499
  if self._fitted:
497
500
  warnings.warn(self._REFIT_MESSAGE)
498
501
 
502
+ def _validate_updated_transformer_unique(self, column_name, transformer):
503
+ """Validate the transformer being updated is not reused in the config."""
504
+ if (
505
+ transformer in self.field_transformers.values()
506
+ and self.field_transformers.get(column_name) != transformer
507
+ ):
508
+ existing_column = [
509
+ column
510
+ for column, existing_transformer in self.field_transformers.items()
511
+ if transformer == existing_transformer
512
+ ][0]
513
+ column_name = (
514
+ f"column ('{column_name}')"
515
+ if isinstance(column_name, str)
516
+ else f'columns {str(column_name)}'
517
+ )
518
+ existing_column = (
519
+ f"column ('{existing_column}')"
520
+ if isinstance(existing_column, str)
521
+ else f'columns {str(existing_column)}'
522
+ )
523
+ raise InvalidConfigError(
524
+ f'The transformer for {column_name} is already assigned to '
525
+ f'{existing_column}. Please create different transformer objects for '
526
+ 'each assignment.'
527
+ )
528
+
499
529
  def update_transformers(self, column_name_to_transformer):
500
530
  """Update any of the transformers assigned to each of the column names.
501
531
 
@@ -514,6 +544,9 @@ class HyperTransformer:
514
544
  self._validate_transformers(column_name_to_transformer)
515
545
 
516
546
  for column_name, transformer in column_name_to_transformer.items():
547
+ if transformer is not None:
548
+ self._validate_updated_transformer_unique(column_name, transformer)
549
+
517
550
  columns = column_name if isinstance(column_name, tuple) else (column_name,)
518
551
  for column in columns:
519
552
  if transformer is not None:
@@ -1,5 +1,7 @@
1
1
  """Transformer for datetime data."""
2
2
 
3
+ import warnings
4
+
3
5
  import numpy as np
4
6
  import pandas as pd
5
7
  from pandas.api.types import is_datetime64_dtype, is_numeric_dtype
@@ -8,6 +10,7 @@ from pandas.core.tools.datetimes import _guess_datetime_format_for_array
8
10
  from rdt.errors import TransformerInputError
9
11
  from rdt.transformers.base import BaseTransformer
10
12
  from rdt.transformers.null import NullTransformer
13
+ from rdt.transformers.utils import _safe_parse_datetime, data_has_multiple_timezones
11
14
 
12
15
 
13
16
  class UnixTimestampEncoder(BaseTransformer):
@@ -68,41 +71,75 @@ class UnixTimestampEncoder(BaseTransformer):
68
71
 
69
72
  self.datetime_format = datetime_format
70
73
  self._dtype = None
74
+ self._has_multiple_timezones = False
75
+ self._timezone_offset = None
76
+
77
+ def _warn_if_mixed_timezones(self):
78
+ if self._has_multiple_timezones:
79
+ warnings.warn(
80
+ 'Mixed timezones are not supported in SDV Community. Data will be converted to UTC.'
81
+ )
82
+
83
+ def _raise_appropriate_conversion_error(self, error):
84
+ message = str(error)
85
+ if 'Unknown string' in message or 'Unknown datetime string' in message:
86
+ raise TypeError('Data must be of dtype datetime, or castable to datetime.') from None
87
+
88
+ raise ValueError('Data does not match specified datetime format.') from None
89
+
90
+ def _learn_timezone_offest(self, data):
91
+ """Extracts and stores the timezone offset from the first valid datetime in the data."""
92
+ has_timezone = self.datetime_format and '%z' in self.datetime_format.lower()
93
+ if has_timezone and not self._has_multiple_timezones:
94
+ for val in data:
95
+ if pd.notna(val):
96
+ try:
97
+ dt = _safe_parse_datetime(
98
+ str(val), warn=True, datetime_format=self.datetime_format
99
+ )
100
+ self._timezone_offset = dt.tzinfo
101
+ break
102
+
103
+ except (ValueError, TypeError, AttributeError):
104
+ self._timezone_offset = None
105
+
106
+ def _learn_has_multiple_timezones(self, data):
107
+ """Determines if the data contains multiple timezones and stores the result."""
108
+ if self.datetime_format and '%z' not in self.datetime_format.lower():
109
+ return
110
+
111
+ if not isinstance(data, pd.Series):
112
+ data = data.to_series()
113
+
114
+ self._has_multiple_timezones = data_has_multiple_timezones(
115
+ data, datetime_format=self.datetime_format
116
+ )
71
117
 
72
- def _convert_to_datetime(self, data):
73
- """Convert datetime column into datetime dtype.
118
+ def _needs_datetime_conversion(self, data):
119
+ """Determines if the data requires datetime conversion."""
120
+ return self.datetime_format is not None or not is_numeric_dtype(data)
74
121
 
75
- Convert the datetime column to datetime dtype using the ``datetime_format``.
76
- All non-numeric columns will automatically be cast to datetimes. Numeric columns
77
- with a ``datetime_format`` will be treated as strings and cast to datetime. Numeric
78
- columns without a ``datetime_format`` will be treated as already converted datetimes.
122
+ def _get_pandas_datetime_format(self):
123
+ """Converts the instance's datetime format to a pandas-compatible format."""
124
+ if self.datetime_format:
125
+ return self.datetime_format.replace('%#', '%').replace('%-', '%')
79
126
 
80
- Args:
81
- data (pandas.Series):
82
- The datetime column.
127
+ return None
83
128
 
84
- Raises:
85
- - ``TypeError`` if data cannot be converted to datetime.
86
- - ``ValueError`` if data does not match the specified datetime format
129
+ def _to_datetime(self, data):
130
+ """Converts data to datetime using the instance's datetime format."""
131
+ return pd.to_datetime(
132
+ data,
133
+ format=self._get_pandas_datetime_format(),
134
+ utc=getattr(self, '_has_multiple_timezones', False), # Backward compatibility
135
+ )
87
136
 
88
- Returns:
89
- pandas.Series:
90
- The datetime column converted to the datetime dtype.
91
- """
92
- if self.datetime_format or not is_numeric_dtype(data):
137
+ def _convert_to_datetime(self, data):
138
+ if self._needs_datetime_conversion(data):
93
139
  try:
94
- pandas_datetime_format = None
95
- if self.datetime_format:
96
- pandas_datetime_format = self.datetime_format.replace('%-', '%')
97
-
98
- data = pd.to_datetime(data, format=pandas_datetime_format)
99
-
140
+ return self._to_datetime(data)
100
141
  except ValueError as error:
101
- if 'Unknown string' in str(error) or 'Unknown datetime string' in str(error):
102
- message = 'Data must be of dtype datetime, or castable to datetime.'
103
- raise TypeError(message) from None
104
-
105
- raise ValueError('Data does not match specified datetime format.') from None
142
+ self._raise_appropriate_conversion_error(error)
106
143
 
107
144
  return data
108
145
 
@@ -116,15 +153,6 @@ class UnixTimestampEncoder(BaseTransformer):
116
153
 
117
154
  return transformed
118
155
 
119
- def _reverse_transform_helper(self, data):
120
- """Transform integer values back into datetimes."""
121
- if not isinstance(data, np.ndarray):
122
- data = data.to_numpy()
123
-
124
- data = self.null_transformer.reverse_transform(data)
125
- data = np.round(data.astype(np.float64))
126
- return data
127
-
128
156
  def _fit(self, data):
129
157
  """Fit the transformer to the data.
130
158
 
@@ -137,7 +165,11 @@ class UnixTimestampEncoder(BaseTransformer):
137
165
  datetime_array = data[data.notna()].astype(str).to_numpy()
138
166
  self.datetime_format = _guess_datetime_format_for_array(datetime_array)
139
167
 
168
+ self._learn_has_multiple_timezones(data)
140
169
  transformed = self._transform_helper(data)
170
+ self._learn_timezone_offest(data)
171
+ self._warn_if_mixed_timezones()
172
+
141
173
  if self.enforce_min_max_values:
142
174
  self._min_value = transformed.min()
143
175
  self._max_value = transformed.max()
@@ -200,6 +232,46 @@ class UnixTimestampEncoder(BaseTransformer):
200
232
  data = self._transform_helper(data)
201
233
  return self.null_transformer.transform(data)
202
234
 
235
+ def _convert_data_to_pandas_datetime(self, data):
236
+ """Converts data to pandas datetime, applying UTC or specific timezone offset if set."""
237
+ if hasattr(self, '_has_multiple_timezones') and self._has_multiple_timezones:
238
+ return pd.to_datetime(data, utc=True)
239
+
240
+ elif hasattr(self, '_timezone_offset') and self._timezone_offset is not None:
241
+ datime_data = pd.to_datetime(data, utc=True)
242
+ return datime_data.dt.tz_convert(self._timezone_offset)
243
+
244
+ return pd.to_datetime(data)
245
+
246
+ def _handle_datetime_formatting(self, datetime_data):
247
+ """Formats datetime data based on datetime format or converts to numeric if needed."""
248
+ if self.datetime_format:
249
+ return self._format_datetime(datetime_data)
250
+
251
+ elif is_numeric_dtype(self._dtype):
252
+ datetime_data = pd.to_numeric(datetime_data.astype('object'), errors='coerce')
253
+ return datetime_data.astype(self._dtype)
254
+
255
+ return datetime_data
256
+
257
+ def _format_datetime(self, datetime_data):
258
+ """Formats datetime data to string or datetime using learned datatime format and dtype."""
259
+ if is_datetime64_dtype(self._dtype) and '.%f' not in self.datetime_format:
260
+ return pd.to_datetime(
261
+ datetime_data.dt.strftime(self.datetime_format), format=self.datetime_format
262
+ )
263
+
264
+ return datetime_data.dt.strftime(self.datetime_format).astype(self._dtype)
265
+
266
+ def _reverse_transform_helper(self, data):
267
+ """Transform integer values back into datetimes."""
268
+ if not isinstance(data, np.ndarray):
269
+ data = data.to_numpy()
270
+
271
+ data = self.null_transformer.reverse_transform(data)
272
+ data = np.round(data.astype(np.float64))
273
+ return data
274
+
203
275
  def _reverse_transform(self, data):
204
276
  """Convert float values back to datetimes.
205
277
 
@@ -214,19 +286,8 @@ class UnixTimestampEncoder(BaseTransformer):
214
286
  if self.enforce_min_max_values:
215
287
  data = data.clip(self._min_value, self._max_value)
216
288
 
217
- datetime_data = pd.to_datetime(data)
218
- if self.datetime_format:
219
- if is_datetime64_dtype(self._dtype) and '.%f' not in self.datetime_format:
220
- datetime_data = pd.to_datetime(
221
- datetime_data.dt.strftime(self.datetime_format),
222
- format=self.datetime_format,
223
- )
224
- else:
225
- datetime_data = datetime_data.dt.strftime(self.datetime_format).astype(self._dtype)
226
- elif is_numeric_dtype(self._dtype):
227
- datetime_data = pd.to_numeric(datetime_data.astype('object'), errors='coerce')
228
- datetime_data = datetime_data.astype(self._dtype)
229
-
289
+ datetime_data = self._convert_data_to_pandas_datetime(data)
290
+ datetime_data = self._handle_datetime_formatting(datetime_data)
230
291
  return datetime_data
231
292
 
232
293