sdv 1.37.5.dev0__tar.gz → 1.38.1.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 (89) hide show
  1. {sdv-1.37.5.dev0/sdv.egg-info → sdv-1.38.1.dev0}/PKG-INFO +1 -1
  2. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/pyproject.toml +1 -1
  3. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/__init__.py +1 -1
  4. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/_utils.py +12 -0
  5. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/datasets/local.py +2 -10
  6. sdv-1.38.1.dev0/sdv/evaluation/utils.py +360 -0
  7. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/metadata.py +30 -1
  8. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/base.py +5 -0
  9. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/utils/utils.py +5 -0
  10. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0/sdv.egg-info}/PKG-INFO +1 -1
  11. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/SOURCES.txt +1 -0
  12. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/LICENSE +0 -0
  13. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/README.md +0 -0
  14. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/__init__.py +0 -0
  15. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/_errors.py +0 -0
  16. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/_utils.py +0 -0
  17. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/base.py +0 -0
  18. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/fixed_combinations.py +0 -0
  19. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/fixed_increments.py +0 -0
  20. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/inequality.py +0 -0
  21. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/one_hot_encoding.py +0 -0
  22. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/programmable_constraint.py +0 -0
  23. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/range.py +0 -0
  24. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/__init__.py +0 -0
  25. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/base.py +0 -0
  26. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/errors.py +0 -0
  27. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/tabular.py +0 -0
  28. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/utils.py +0 -0
  29. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/__init__.py +0 -0
  30. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/data_processor.py +0 -0
  31. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/datetime_formatter.py +0 -0
  32. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/errors.py +0 -0
  33. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/numerical_formatter.py +0 -0
  34. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/utils.py +0 -0
  35. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/datasets/__init__.py +0 -0
  36. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/datasets/demo.py +0 -0
  37. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/errors.py +0 -0
  38. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/__init__.py +0 -0
  39. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/_utils.py +0 -0
  40. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/evaluation.py +0 -0
  41. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/multi_table.py +0 -0
  42. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/single_table.py +0 -0
  43. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/io/__init__.py +0 -0
  44. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/io/local/__init__.py +0 -0
  45. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/io/local/local.py +0 -0
  46. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/lite/__init__.py +0 -0
  47. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/lite/single_table.py +0 -0
  48. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/logging/__init__.py +0 -0
  49. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/logging/logger.py +0 -0
  50. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/logging/sdv_logger_config.yml +0 -0
  51. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/logging/utils.py +0 -0
  52. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/__init__.py +0 -0
  53. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/errors.py +0 -0
  54. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/metadata_upgrader.py +0 -0
  55. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/multi_table.py +0 -0
  56. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/single_table.py +0 -0
  57. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/utils.py +0 -0
  58. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/visualization.py +0 -0
  59. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/__init__.py +0 -0
  60. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/demos.py +0 -0
  61. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/relational.py +0 -0
  62. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/tabular.py +0 -0
  63. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/timeseries.py +0 -0
  64. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/__init__.py +0 -0
  65. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/base.py +0 -0
  66. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/dayz.py +0 -0
  67. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/hma.py +0 -0
  68. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/utils.py +0 -0
  69. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sampling/__init__.py +0 -0
  70. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sampling/hierarchical_sampler.py +0 -0
  71. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sampling/independent_sampler.py +0 -0
  72. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sampling/tabular.py +0 -0
  73. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sequential/__init__.py +0 -0
  74. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sequential/par.py +0 -0
  75. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/__init__.py +0 -0
  76. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/copulagan.py +0 -0
  77. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/copulas.py +0 -0
  78. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/ctgan.py +0 -0
  79. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/dayz.py +0 -0
  80. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/utils.py +0 -0
  81. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/utils/__init__.py +0 -0
  82. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/utils/mixins.py +0 -0
  83. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/utils/poc.py +0 -0
  84. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/version/__init__.py +0 -0
  85. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/dependency_links.txt +0 -0
  86. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/entry_points.txt +0 -0
  87. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/requires.txt +0 -0
  88. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/top_level.txt +0 -0
  89. {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdv
3
- Version: 1.37.5.dev0
3
+ Version: 1.38.1.dev0
4
4
  Summary: Generate synthetic data for single table, multi table and sequential data
5
5
  Author-email: "DataCebo, Inc." <info@sdv.dev>
6
6
  License-Expression: BUSL-1.1
@@ -149,7 +149,7 @@ namespaces = false
149
149
  version = {attr = 'sdv.__version__'}
150
150
 
151
151
  [tool.bumpversion]
152
- current_version = "1.37.5.dev0"
152
+ current_version = "1.38.1.dev0"
153
153
  parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<release>[a-z]+)(?P<candidate>\d+))?'
154
154
  serialize = [
155
155
  '{major}.{minor}.{patch}.{release}{candidate}',
@@ -6,7 +6,7 @@
6
6
 
7
7
  __author__ = 'DataCebo, Inc.'
8
8
  __email__ = 'info@sdv.dev'
9
- __version__ = '1.37.5.dev0'
9
+ __version__ = '1.38.1.dev0'
10
10
 
11
11
 
12
12
  import sys
@@ -562,3 +562,15 @@ def _get_unreferenced_keys(parent_columns, child_columns):
562
562
  def _validate_boolean_parameter(parameter, parameter_name):
563
563
  if not isinstance(parameter, bool):
564
564
  raise ValueError(f"'{parameter_name}' must be a boolean value.")
565
+
566
+
567
+ def _check_is_dict_of_dataframes(data, arg_name='data'):
568
+ error_message_data = (
569
+ f"'{arg_name}' must be a dictionary that maps table names to pandas DataFrames."
570
+ )
571
+ if not isinstance(data, dict):
572
+ raise ValueError(error_message_data)
573
+
574
+ for table_name, table in data.items():
575
+ if not isinstance(table, pd.DataFrame):
576
+ raise ValueError(error_message_data)
@@ -3,9 +3,7 @@
3
3
  import os
4
4
  import warnings
5
5
 
6
- import pandas as pd
7
-
8
- from sdv._utils import _load_data_from_csv
6
+ from sdv._utils import _check_is_dict_of_dataframes, _load_data_from_csv
9
7
 
10
8
 
11
9
  def load_csvs(folder_name, read_csv_parameters=None):
@@ -59,13 +57,7 @@ def save_csvs(data, folder_name, suffix=None, to_csv_parameters=None):
59
57
  A python dictionary of with string and value accepted by ``pandas.DataFrame.to_csv``
60
58
  function. Defaults to ``None``.
61
59
  """
62
- error_message_data = "'data' must be a dictionary that maps table names to pandas DataFrames."
63
- if not isinstance(data, dict):
64
- raise ValueError(error_message_data)
65
-
66
- for table_name, table in data.items():
67
- if not isinstance(table, pd.DataFrame):
68
- raise ValueError(error_message_data)
60
+ _check_is_dict_of_dataframes(data)
69
61
 
70
62
  if not os.path.exists(folder_name):
71
63
  os.makedirs(folder_name)
@@ -0,0 +1,360 @@
1
+ """Utility methods to compare the real and synthetic data."""
2
+
3
+ import sys
4
+ import warnings
5
+
6
+ import pandas as pd
7
+ from pandas.api.types import is_datetime64_any_dtype, is_numeric_dtype
8
+
9
+ from sdv._utils import _cast_to_iterable, _check_is_dict_of_dataframes
10
+ from sdv.metadata import Metadata
11
+
12
+ MISSING_VALUE_PLACEHOLDER = '__sdv_missing_value__'
13
+
14
+
15
+ def _validate_referential_integrity_inputs(
16
+ metadata, synthetic_data, table_name, foreign_key_name, num_rows
17
+ ):
18
+ """Validate the inputs of the ``print_referential_integrity`` method."""
19
+ if not isinstance(metadata, Metadata):
20
+ raise TypeError('metadata must be of Metadata type.')
21
+
22
+ _check_is_dict_of_dataframes(synthetic_data, 'synthetic_data')
23
+
24
+ if not isinstance(table_name, str):
25
+ raise TypeError('table_name must be a string.')
26
+
27
+ foreign_key_names = _cast_to_iterable(foreign_key_name)
28
+ if not all(isinstance(name, str) for name in foreign_key_names):
29
+ raise TypeError('foreign_key_name must be a string or a tuple of strings.')
30
+
31
+ if isinstance(num_rows, bool) or not isinstance(num_rows, int):
32
+ raise TypeError("'num_rows' must be an integer greater than 0.")
33
+
34
+ if num_rows <= 0:
35
+ raise ValueError("'num_rows' must be an integer greater than 0.")
36
+
37
+ if table_name not in metadata.tables:
38
+ raise ValueError(f"table_name: '{table_name}' not found in metadata.")
39
+
40
+ if table_name not in synthetic_data:
41
+ raise ValueError(f"table_name: '{table_name}' not found in synthetic_data.")
42
+
43
+ for name in foreign_key_names:
44
+ if name not in metadata.tables[table_name].columns:
45
+ raise ValueError(
46
+ f"foreign_key_name: '{name}' not in Metadata for table_name: '{table_name}'."
47
+ )
48
+
49
+ if name not in synthetic_data[table_name].columns:
50
+ raise ValueError(f"foreign_key_name: '{name}' not found in synthetic_data.")
51
+
52
+ return foreign_key_names
53
+
54
+
55
+ def _get_parent_relationship(metadata, table_name, foreign_key_names):
56
+ """Get the parent table and primary key linked to the given foreign key.
57
+
58
+ Args:
59
+ metadata (Metadata):
60
+ The metadata object describing the synthetic data.
61
+ table_name (str):
62
+ The name of the table that contains the foreign key.
63
+ foreign_key_names (list[str]):
64
+ The columns making up the foreign key to look up.
65
+
66
+ Returns:
67
+ tuple[str, list[str], list[str]]:
68
+ The parent table name, the columns making up its primary key, and the foreign key
69
+ columns in the order the relationship defines them.
70
+ """
71
+ for relationship in metadata.relationships:
72
+ child_foreign_key = _cast_to_iterable(relationship['child_foreign_key'])
73
+ if table_name == relationship['child_table_name'] and set(child_foreign_key) == set(
74
+ foreign_key_names
75
+ ):
76
+ return (
77
+ relationship['parent_table_name'],
78
+ _cast_to_iterable(relationship['parent_primary_key']),
79
+ child_foreign_key,
80
+ )
81
+
82
+ foreign_key = "', '".join(foreign_key_names)
83
+ raise ValueError(
84
+ f"Unable to find a relationship in metadata given table_name: '{table_name}' "
85
+ f"and foreign_key_name: '{foreign_key}'."
86
+ )
87
+
88
+
89
+ def _format_key(key_names, key_values):
90
+ """Format a set of key columns and their values as ``name: value`` pairs."""
91
+ return ', '.join(f'{name}: {value}' for name, value in zip(key_names, key_values))
92
+
93
+
94
+ def print_referential_integrity(
95
+ metadata, synthetic_data, table_name, foreign_key_name, num_rows=10
96
+ ):
97
+ """Check that referential integrity is met by looking up a few rows.
98
+
99
+ A random selection of rows is taken from the table containing the foreign key. For each
100
+ one, the linked row is looked up in the parent table and the outcome is printed.
101
+
102
+ Args:
103
+ metadata (Metadata):
104
+ The metadata object describing the synthetic data.
105
+ synthetic_data (dict):
106
+ A dictionary mapping each table name to a pandas DataFrame containing the
107
+ synthetic data for it.
108
+ table_name (str):
109
+ The name of the table that contains the foreign key to check.
110
+ foreign_key_name (str or tuple[str]):
111
+ The column of the foreign key to check. For composite keys, this is a tuple of
112
+ strings.
113
+ num_rows (int):
114
+ The number of rows to check. Defaults to 10.
115
+
116
+ Raises:
117
+ TypeError:
118
+ If any of the inputs is not of the expected type.
119
+ ValueError:
120
+ If the table, the columns or the relationship is missing, or if ``num_rows`` is
121
+ not greater than 0.
122
+ """
123
+ foreign_key_names = _validate_referential_integrity_inputs(
124
+ metadata, synthetic_data, table_name, foreign_key_name, num_rows
125
+ )
126
+ parent_table_name, parent_primary_keys, foreign_key_names = _get_parent_relationship(
127
+ metadata, table_name, foreign_key_names
128
+ )
129
+
130
+ child_data = synthetic_data[table_name]
131
+ if len(child_data) < num_rows:
132
+ warnings.warn(
133
+ f"The synthetic data contains '{len(child_data)}' rows which is less than "
134
+ f"num_rows: '{num_rows}'. Changing num_rows to '{len(child_data)}'."
135
+ )
136
+ num_rows = len(child_data)
137
+
138
+ parent_data = synthetic_data[parent_table_name]
139
+ parent_keys = set(parent_data[parent_primary_keys].itertuples(index=False, name=None))
140
+ child_primary_keys = _cast_to_iterable(metadata.tables[table_name].primary_key or [])
141
+
142
+ for _, child_row in child_data.sample(n=num_rows, replace=False).iterrows():
143
+ heading = f'Picking random {table_name} row'
144
+ if child_primary_keys:
145
+ key_values = ', '.join(str(child_row[name]) for name in child_primary_keys)
146
+ heading += f': {key_values}'
147
+
148
+ foreign_key_values = tuple(child_row[name] for name in foreign_key_names)
149
+ if any(pd.isna(value) for value in foreign_key_values):
150
+ result = '✅ Foreign key is null; no linked parent row expected'
151
+ elif foreign_key_values in parent_keys:
152
+ found = _format_key(parent_primary_keys, foreign_key_values)
153
+ result = f'✅ Found {parent_table_name} row! {found}'
154
+ else:
155
+ result = f'❌ Unable to find the linked {parent_table_name} row'
156
+
157
+ sys.stdout.write(f'{heading}\n{result}\n\n')
158
+
159
+
160
+ def _validate_data(real_data, synthetic_data, table_name, column_names):
161
+ """Validate that both datasets contain the table and columns to check."""
162
+ if not isinstance(table_name, str):
163
+ raise TypeError(f"'table_name' must be a string, got {type(table_name).__name__}.")
164
+
165
+ if not isinstance(column_names, list) or not all(
166
+ isinstance(column_name, str) for column_name in column_names
167
+ ):
168
+ raise TypeError("'column_names' must be a list of strings.")
169
+
170
+ if not column_names:
171
+ raise ValueError("'column_names' must contain at least one column name.")
172
+
173
+ for argument_name, data in [('real_data', real_data), ('synthetic_data', synthetic_data)]:
174
+ if table_name not in data:
175
+ raise ValueError(f"Table '{table_name}' is not present in '{argument_name}'.")
176
+
177
+ missing = [column for column in column_names if column not in data[table_name].columns]
178
+ if missing:
179
+ missing_columns = "', '".join(missing)
180
+ raise ValueError(
181
+ f"The columns '{missing_columns}' are not present in table '{table_name}' "
182
+ f"of '{argument_name}'."
183
+ )
184
+
185
+
186
+ def _align_dtypes(real_column, synthetic_column):
187
+ """Make sure data types of columns being evaluated match.
188
+
189
+ Args:
190
+ real_column (pd.Series):
191
+ The column of real data.
192
+ synthetic_column (pd.Series):
193
+ The column of synthetic data.
194
+
195
+ Returns:
196
+ tuple[pd.Series, pd.Series]:
197
+ The real and synthetic column, cast to a comparable dtype.
198
+ """
199
+ if real_column.dtype == synthetic_column.dtype:
200
+ return real_column, synthetic_column
201
+
202
+ if is_numeric_dtype(real_column) and is_numeric_dtype(synthetic_column):
203
+ return real_column.astype('float64'), synthetic_column.astype('float64')
204
+
205
+ if is_datetime64_any_dtype(real_column) or is_datetime64_any_dtype(synthetic_column):
206
+ return (
207
+ pd.to_datetime(real_column, errors='coerce'),
208
+ pd.to_datetime(synthetic_column, errors='coerce'),
209
+ )
210
+
211
+ return real_column.astype(str), synthetic_column.astype(str)
212
+
213
+
214
+ def _get_combinations(data):
215
+ """Get the set of unique combinations of values in the data."""
216
+ combinations = data.astype('object')
217
+ combinations = combinations.where(combinations.notna(), MISSING_VALUE_PLACEHOLDER)
218
+
219
+ return set(combinations.itertuples(index=False, name=None))
220
+
221
+
222
+ def _compute_overlap(real_data, synthetic_data, table_name, column_names):
223
+ """Get the number of combinations shared by both datasets and the percentage they represent.
224
+
225
+ Args:
226
+ real_data (dict):
227
+ A dictionary mapping a table name to a pandas DataFrame containing real data.
228
+ synthetic_data (dict):
229
+ A dictionary mapping a table name to a pandas DataFrame containing synthetic data.
230
+ table_name (str):
231
+ The name of the table that contains the columns to check.
232
+ column_names (list[str]):
233
+ The column names to combine.
234
+
235
+ Returns:
236
+ tuple[int, float]:
237
+ The number of shared combinations and their percentage of all combinations.
238
+ """
239
+ real_values = real_data[table_name][column_names].copy()
240
+ synthetic_values = synthetic_data[table_name][column_names].copy()
241
+ for column_name in column_names:
242
+ real_values[column_name], synthetic_values[column_name] = _align_dtypes(
243
+ real_values[column_name], synthetic_values[column_name]
244
+ )
245
+
246
+ real_combinations = _get_combinations(real_values)
247
+ synthetic_combinations = _get_combinations(synthetic_values)
248
+
249
+ num_common = len(real_combinations & synthetic_combinations)
250
+ num_total = len(real_combinations | synthetic_combinations)
251
+ percent = round(num_common / num_total * 100, 2) if num_total else 0.0
252
+
253
+ return num_common, percent
254
+
255
+
256
+ def get_combination_overlap(real_data, synthetic_data, table_name, column_names, verbose=True):
257
+ """Calculate the overlap of combinations of column values between real and synthetic data.
258
+
259
+ Args:
260
+ real_data (dict):
261
+ A dictionary mapping a table name to a pandas DataFrame containing real data.
262
+ synthetic_data (dict):
263
+ A dictionary mapping a table name to a pandas DataFrame containing synthetic data.
264
+ table_name (str):
265
+ The name of the table that contains the columns to check.
266
+ column_names (list[str]):
267
+ A list of strings representing the column names to check. Combinations of these
268
+ columns will be checked.
269
+ verbose (bool):
270
+ Whether to print out the interpretation of the results. Defaults to ``True``.
271
+
272
+ Returns:
273
+ int:
274
+ The number of unique combinations that appear in both the real and synthetic data.
275
+
276
+ Raises:
277
+ TypeError:
278
+ If ``table_name`` is not a string or ``column_names`` is not a list of strings.
279
+ ValueError:
280
+ If the table or any of the columns is missing from the data.
281
+ """
282
+ _validate_data(real_data, synthetic_data, table_name, column_names)
283
+ num_common, percent = _compute_overlap(real_data, synthetic_data, table_name, column_names)
284
+
285
+ if verbose:
286
+ sys.stdout.write(f'Number of common combinations: {num_common} ({percent}%)\n')
287
+ if num_common == 0:
288
+ sys.stdout.write(
289
+ '✅ The synthetic data does not contain any of the same combinations from the '
290
+ 'real data\n'
291
+ )
292
+ elif percent <= 2:
293
+ sys.stdout.write(
294
+ '⚠️ The synthetic data contains a few of the same combinations as the real '
295
+ 'data. This might be due to random chance.\n'
296
+ )
297
+ else:
298
+ sys.stdout.write(
299
+ '❌ The synthetic data contains a significant number of the same combinations '
300
+ 'as the real data. This might be due to a small number of possible '
301
+ 'combinations, a large sample of synthetic data, or a misconfiguration in your '
302
+ 'synthesizer.\n'
303
+ )
304
+
305
+ return num_common
306
+
307
+
308
+ def get_pii_overlap(real_data, synthetic_data, table_name, pii_column_name, verbose=True):
309
+ """Calculate the overlap of PII values between the real and synthetic data.
310
+
311
+ Args:
312
+ real_data (dict):
313
+ A dictionary mapping a table name to a pandas DataFrame containing real data.
314
+ synthetic_data (dict):
315
+ A dictionary mapping a table name to a pandas DataFrame containing synthetic data.
316
+ table_name (str):
317
+ The name of the table that contains the PII column to check.
318
+ pii_column_name (str):
319
+ The name of the column that contains PII values to check.
320
+ verbose (bool):
321
+ Whether to print out the interpretation of the results. Defaults to ``True``.
322
+
323
+ Returns:
324
+ int:
325
+ The number of unique PII values that appear in both the real and synthetic data.
326
+
327
+ Raises:
328
+ TypeError:
329
+ If ``table_name`` or ``pii_column_name`` is not a string.
330
+ ValueError:
331
+ If the table or the column is missing from the data.
332
+ """
333
+ if not isinstance(pii_column_name, str):
334
+ raise TypeError(
335
+ f"'pii_column_name' must be a string, got {type(pii_column_name).__name__}."
336
+ )
337
+
338
+ column_names = [pii_column_name]
339
+ _validate_data(real_data, synthetic_data, table_name, column_names)
340
+ num_common, percent = _compute_overlap(real_data, synthetic_data, table_name, column_names)
341
+
342
+ if verbose:
343
+ sys.stdout.write(f'Number of common data points: {num_common} ({percent}%)\n')
344
+ if num_common == 0:
345
+ sys.stdout.write(
346
+ '✅ The synthetic data does not contain any PII values from the real data\n'
347
+ )
348
+ elif percent <= 2:
349
+ sys.stdout.write(
350
+ '⚠️ The synthetic data contains a few PII values from the real data. '
351
+ 'This might be due to random chance.\n'
352
+ )
353
+ else:
354
+ sys.stdout.write(
355
+ '❌ The synthetic data contains a significant number of the same PII values of '
356
+ 'as the real data. This might be due to a small number of possible PII values, '
357
+ 'a large sample of synthetic data, or a misconfiguration in your synthesizer.\n'
358
+ )
359
+
360
+ return num_common
@@ -4,7 +4,7 @@ import warnings
4
4
 
5
5
  import pandas as pd
6
6
 
7
- from sdv._utils import _validate_boolean_parameter
7
+ from sdv._utils import _is_datetime_type, _is_numerical, _validate_boolean_parameter
8
8
  from sdv.metadata.errors import InvalidMetadataError
9
9
  from sdv.metadata.multi_table import MultiTableMetadata
10
10
  from sdv.metadata.single_table import SingleTableMetadata
@@ -308,6 +308,35 @@ class Metadata(MultiTableMetadata):
308
308
  self._validate_table_exists(table_name)
309
309
  self.tables[table_name].set_sequence_key(column_name)
310
310
 
311
+ def _validate_data(self, data, table_name=None):
312
+ """Validate data and warn about datetime formats that cannot be verified."""
313
+ super()._validate_data(data, table_name)
314
+ for current_table_name, table_data in data.items():
315
+ table_metadata = self.tables.get(current_table_name)
316
+ if table_metadata is None:
317
+ continue
318
+
319
+ for column_name, column_metadata in table_metadata.columns.items():
320
+ datetime_format = column_metadata.get('datetime_format')
321
+ if not datetime_format:
322
+ continue
323
+
324
+ column_data = table_data[column_name]
325
+ has_datetime_objects = any(
326
+ not isinstance(value, str)
327
+ and not _is_numerical(value)
328
+ and _is_datetime_type(value)
329
+ for value in column_data.dropna().head(1000)
330
+ )
331
+ if has_datetime_objects:
332
+ warnings.warn(
333
+ f"The datetime format for column '{column_name}' "
334
+ f"(table '{current_table_name}') could not be verified because the data "
335
+ f"is represented as dtype '{column_data.dtype}'.\n"
336
+ 'Please omit the datetime format string from the metadata or cast the '
337
+ 'data to strings with the right format.'
338
+ )
339
+
311
340
  def validate_table(self, data, table_name=None):
312
341
  """Validate a table against the metadata.
313
342
 
@@ -651,6 +651,11 @@ class BaseSynthesizer:
651
651
  message=r"No 'datetime_format' is present.*",
652
652
  category=UserWarning,
653
653
  )
654
+ warnings.filterwarnings(
655
+ 'ignore',
656
+ message=r'The datetime format for column .* could not be verified.*',
657
+ category=UserWarning,
658
+ )
654
659
  self._original_metadata.validate_data({self._table_name: data})
655
660
  else:
656
661
  self._original_metadata.validate_data({self._table_name: data})
@@ -64,6 +64,11 @@ def drop_unknown_references(data, metadata, drop_missing_values=False, verbose=T
64
64
  message=r"No 'datetime_format' is present.*",
65
65
  category=UserWarning,
66
66
  )
67
+ warnings.filterwarnings(
68
+ 'ignore',
69
+ message=r'The datetime format for column .* could not be verified.*',
70
+ category=UserWarning,
71
+ )
67
72
  metadata.validate_data(data)
68
73
  if drop_missing_values:
69
74
  _validate_foreign_keys_not_null(metadata, data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdv
3
- Version: 1.37.5.dev0
3
+ Version: 1.38.1.dev0
4
4
  Summary: Generate synthetic data for single table, multi table and sequential data
5
5
  Author-email: "DataCebo, Inc." <info@sdv.dev>
6
6
  License-Expression: BUSL-1.1
@@ -39,6 +39,7 @@ sdv/evaluation/_utils.py
39
39
  sdv/evaluation/evaluation.py
40
40
  sdv/evaluation/multi_table.py
41
41
  sdv/evaluation/single_table.py
42
+ sdv/evaluation/utils.py
42
43
  sdv/io/__init__.py
43
44
  sdv/io/local/__init__.py
44
45
  sdv/io/local/local.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes