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.
- {sdv-1.37.5.dev0/sdv.egg-info → sdv-1.38.1.dev0}/PKG-INFO +1 -1
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/pyproject.toml +1 -1
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/__init__.py +1 -1
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/_utils.py +12 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/datasets/local.py +2 -10
- sdv-1.38.1.dev0/sdv/evaluation/utils.py +360 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/metadata.py +30 -1
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/base.py +5 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/utils/utils.py +5 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0/sdv.egg-info}/PKG-INFO +1 -1
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/SOURCES.txt +1 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/LICENSE +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/README.md +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/_errors.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/_utils.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/base.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/fixed_combinations.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/fixed_increments.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/inequality.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/one_hot_encoding.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/programmable_constraint.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/cag/range.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/base.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/errors.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/tabular.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/constraints/utils.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/data_processor.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/datetime_formatter.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/errors.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/numerical_formatter.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/data_processing/utils.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/datasets/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/datasets/demo.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/errors.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/_utils.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/evaluation.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/multi_table.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/evaluation/single_table.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/io/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/io/local/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/io/local/local.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/lite/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/lite/single_table.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/logging/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/logging/logger.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/logging/sdv_logger_config.yml +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/logging/utils.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/errors.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/metadata_upgrader.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/multi_table.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/single_table.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/utils.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metadata/visualization.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/demos.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/relational.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/tabular.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/metrics/timeseries.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/base.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/dayz.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/hma.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/multi_table/utils.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sampling/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sampling/hierarchical_sampler.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sampling/independent_sampler.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sampling/tabular.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sequential/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/sequential/par.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/copulagan.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/copulas.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/ctgan.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/dayz.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/single_table/utils.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/utils/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/utils/mixins.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/utils/poc.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv/version/__init__.py +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/dependency_links.txt +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/entry_points.txt +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/requires.txt +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/sdv.egg-info/top_level.txt +0 -0
- {sdv-1.37.5.dev0 → sdv-1.38.1.dev0}/setup.cfg +0 -0
|
@@ -149,7 +149,7 @@ namespaces = false
|
|
|
149
149
|
version = {attr = 'sdv.__version__'}
|
|
150
150
|
|
|
151
151
|
[tool.bumpversion]
|
|
152
|
-
current_version = "1.
|
|
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}',
|
|
@@ -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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|