Skip to content

Commit 70be697

Browse files
committed
Update test_quality.py
1 parent 58b4c7a commit 70be697

1 file changed

Lines changed: 63 additions & 68 deletions

File tree

tests/test_quality.py

Lines changed: 63 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import numpy as np
22
import pandas as pd
33
from pytest import approx
4-
from scipy.stats import pearsonr, chi2_contingency, ks_2samp
4+
from scipy.stats import chi2_contingency, ks_2samp, pearsonr
55
from sklearn.metrics import mutual_info_score
66

77
from syndiffix import Synthesizer
@@ -12,23 +12,20 @@
1212
def test_quality_float_str() -> None:
1313
"""Test quality of synthetic data for float-string columns with dependency."""
1414
np.random.seed(42)
15-
15+
1616
num_rows = 1000
1717
# Create float column with normal distribution
1818
float_col = np.random.normal(50, 15, num_rows)
1919

2020
# Create categorical string column with dependency on float values
2121
# Higher float values more likely to be in categories 6-9
2222
str_categories = [f"cat_{i}" for i in range(10)]
23-
23+
2424
# Create dependency: map float values to categories with some noise
2525
normalized_float = (float_col - float_col.min()) / (float_col.max() - float_col.min())
26-
category_indices = np.clip(
27-
(normalized_float * 8 + np.random.normal(0, 1, num_rows)).astype(int),
28-
0, 9
29-
)
26+
category_indices = np.clip((normalized_float * 8 + np.random.normal(0, 1, num_rows)).astype(int), 0, 9)
3027
str_col = [str_categories[i] for i in category_indices]
31-
28+
3229
df = pd.DataFrame({"float_col": float_col, "str_col": str_col})
3330
# Ensure at least 20 instances of every category in str_col
3431
min_count = 20
@@ -44,103 +41,102 @@ def test_quality_float_str() -> None:
4441
df = pd.concat([df, replicated], ignore_index=True)
4542
else:
4643
# If no rows exist, create new rows with random float_col
47-
new_rows = pd.DataFrame({
48-
"float_col": np.random.normal(50, 15, min_count),
49-
"str_col": [cat]*min_count
50-
})
44+
new_rows = pd.DataFrame(
45+
{"float_col": np.random.normal(50, 15, min_count), "str_col": [cat] * min_count}
46+
)
5147
df = pd.concat([df, new_rows], ignore_index=True)
52-
48+
5349
# Generate synthetic data
5450
df_syn = Synthesizer(df, anonymization_params=NOISELESS_PARAMS).sample()
55-
51+
5652
# 1. Single-column similarity tests
57-
53+
5854
# Float column: test distribution similarity using KS test
5955
ks_stat, ks_pvalue = ks_2samp(df["float_col"], df_syn["float_col"])
6056
assert ks_stat < 0.15, f"Float column distributions too different (KS statistic: {ks_stat})"
61-
57+
6258
# Float column: test mean and std similarity
6359
assert df_syn["float_col"].mean() == approx(df["float_col"].mean(), abs=1.0)
6460
assert df_syn["float_col"].std() == approx(df["float_col"].std(), abs=0.5)
65-
61+
6662
# String column: test category distribution similarity
6763
orig_counts = df["str_col"].value_counts().sort_index()
6864
syn_counts = df_syn["str_col"].value_counts().sort_index()
69-
65+
7066
# Ensure all original categories are present in synthetic data
7167
for cat in orig_counts.index:
7268
assert cat in syn_counts.index, f"Category {cat} missing from synthetic data"
73-
69+
7470
# Test count similarity (allowing for some variation)
7571
for cat in orig_counts.index:
7672
orig_pct = orig_counts[cat] / len(df)
7773
syn_pct = syn_counts[cat] / len(df_syn)
7874
assert syn_pct == approx(orig_pct, abs=0.01), f"Category {cat} proportion differs too much"
79-
75+
8076
# 2. Dependency/correlation tests
81-
77+
8278
# For float-string dependency, use mutual information
8379
# Discretize float column for mutual information calculation
8480
orig_float_binned = pd.cut(df["float_col"], bins=10, labels=False)
8581
syn_float_binned = pd.cut(df_syn["float_col"], bins=10, labels=False)
86-
82+
8783
orig_str_encoded = pd.Categorical(df["str_col"]).codes
8884
syn_str_encoded = pd.Categorical(df_syn["str_col"]).codes
89-
85+
9086
orig_mi = mutual_info_score(orig_float_binned, orig_str_encoded)
9187
syn_mi = mutual_info_score(syn_float_binned, syn_str_encoded)
92-
88+
9389
assert syn_mi == approx(orig_mi, rel=0.4), f"Mutual information differs too much: orig={orig_mi}, syn={syn_mi}"
9490

9591

9692
def test_quality_float_float() -> None:
9793
"""Test quality of synthetic data for float-float columns with correlation."""
9894
np.random.seed(42)
99-
95+
10096
# Create correlated float columns
10197
x = np.random.normal(0, 1, 1000)
10298
y = 0.7 * x + np.random.normal(0, 0.5, 1000) # Correlation ~0.8
103-
99+
104100
df = pd.DataFrame({"x": x, "y": y})
105-
101+
106102
# Generate synthetic data
107103
df_syn = Synthesizer(df, anonymization_params=NOISELESS_PARAMS).sample()
108-
104+
109105
# 1. Single-column similarity tests
110-
106+
111107
# X column
112108
ks_stat_x, _ = ks_2samp(df["x"], df_syn["x"])
113109
assert ks_stat_x < 0.15, f"X column distributions too different (KS statistic: {ks_stat_x})"
114110
assert df_syn["x"].mean() == approx(df["x"].mean(), abs=0.02)
115111
assert df_syn["x"].std() == approx(df["x"].std(), abs=0.04)
116-
112+
117113
# Y column
118114
ks_stat_y, _ = ks_2samp(df["y"], df_syn["y"])
119115
assert ks_stat_y < 0.15, f"Y column distributions too different (KS statistic: {ks_stat_y})"
120116
assert df_syn["y"].mean() == approx(df["y"].mean(), abs=0.02)
121117
assert df_syn["y"].std() == approx(df["y"].std(), abs=0.04)
122-
118+
123119
# 2. Correlation tests
124-
120+
125121
orig_corr, _ = pearsonr(df["x"], df["y"])
126122
syn_corr, _ = pearsonr(df_syn["x"], df_syn["y"])
127-
123+
128124
assert syn_corr == approx(orig_corr, abs=0.01), f"Correlation differs too much: orig={orig_corr}, syn={syn_corr}"
129125

130126

131127
def test_quality_str_str() -> None:
132128
"""Test quality of synthetic data for string-string columns with dependency."""
133129
np.random.seed(42)
134-
130+
135131
# Create two categorical columns with dependency
136132
categories_a = [f"group_{i}" for i in range(10)]
137133
categories_b = [f"type_{i}" for i in range(10)]
138-
134+
139135
# Create dependency: certain groups prefer certain types
140136
group_prefs = {
141-
0: [0, 1, 2], # group_0 prefers type_0, type_1, type_2
142-
1: [1, 2, 3], # group_1 prefers type_1, type_2, type_3
143-
2: [2, 3, 4], # etc.
137+
0: [0, 1, 2], # group_0 prefers type_0, type_1, type_2
138+
1: [1, 2, 3], # group_1 prefers type_1, type_2, type_3
139+
2: [2, 3, 4], # etc.
144140
3: [3, 4, 5],
145141
4: [4, 5, 6],
146142
5: [5, 6, 7],
@@ -149,64 +145,64 @@ def test_quality_str_str() -> None:
149145
8: [8, 9, 0],
150146
9: [9, 0, 1],
151147
}
152-
148+
153149
# Generate data with dependency
154150
col_a = []
155151
col_b = []
156-
152+
157153
for _ in range(1000):
158154
# Choose group randomly
159155
group_idx = np.random.randint(0, 10)
160156
group = categories_a[group_idx]
161-
157+
162158
# Choose type based on group preference (80% of time) or random (20% of time)
163159
if np.random.random() < 0.8:
164160
type_idx = np.random.choice(group_prefs[group_idx])
165161
else:
166162
type_idx = np.random.randint(0, 10)
167-
163+
168164
type_val = categories_b[type_idx]
169-
165+
170166
col_a.append(group)
171167
col_b.append(type_val)
172-
168+
173169
df = pd.DataFrame({"group": col_a, "type": col_b})
174-
170+
175171
# Generate synthetic data
176172
df_syn = Synthesizer(df, anonymization_params=NOISELESS_PARAMS).sample()
177-
173+
178174
# 1. Single-column similarity tests
179-
175+
180176
# Group column
181177
orig_group_counts = df["group"].value_counts().sort_index()
182178
syn_group_counts = df_syn["group"].value_counts().sort_index()
183-
179+
184180
for group in orig_group_counts.index:
185181
assert group in syn_group_counts.index, f"Group {group} missing from synthetic data"
186182
orig_pct = orig_group_counts[group] / len(df)
187183
syn_pct = syn_group_counts[group] / len(df_syn)
188184
assert syn_pct == approx(orig_pct, abs=0.01), f"Group {group} proportion differs too much"
189-
185+
190186
# Type column
191187
orig_type_counts = df["type"].value_counts().sort_index()
192188
syn_type_counts = df_syn["type"].value_counts().sort_index()
193-
189+
194190
for type_val in orig_type_counts.index:
195191
assert type_val in syn_type_counts.index, f"Type {type_val} missing from synthetic data"
196192
orig_pct = orig_type_counts[type_val] / len(df)
197193
syn_pct = syn_type_counts[type_val] / len(df_syn)
198194
assert syn_pct == approx(orig_pct, abs=0.005), f"Type {type_val} proportion differs too much"
199-
195+
200196
# 2. Dependency tests using contingency table analysis
201-
197+
202198
# Create contingency tables
203199
orig_contingency = pd.crosstab(df["group"], df["type"])
204200
syn_contingency = pd.crosstab(df_syn["group"], df_syn["type"])
205-
201+
206202
# Ensure synthetic contingency table has same shape as original
207203
assert orig_contingency.shape[0] <= syn_contingency.shape[0], "Missing groups in synthetic data"
208204
assert orig_contingency.shape[1] <= syn_contingency.shape[1], "Missing types in synthetic data"
209-
205+
210206
# Calculate Cramér's V (measure of association between categorical variables)
211207
def cramers_v(contingency_table):
212208
chi2, _, _, _ = chi2_contingency(contingency_table)
@@ -215,38 +211,37 @@ def cramers_v(contingency_table):
215211
if min_dim == 0:
216212
return 0
217213
return np.sqrt(chi2 / (n * min_dim))
218-
214+
219215
# Align contingency tables for comparison
220216
common_groups = set(orig_contingency.index) & set(syn_contingency.index)
221217
common_types = set(orig_contingency.columns) & set(syn_contingency.columns)
222-
218+
223219
orig_aligned = orig_contingency.loc[list(common_groups), list(common_types)]
224220
syn_aligned = syn_contingency.loc[list(common_groups), list(common_types)]
225-
221+
226222
orig_cramers_v = cramers_v(orig_aligned)
227223
syn_cramers_v = cramers_v(syn_aligned)
228-
229-
assert syn_cramers_v == approx(orig_cramers_v, rel=0.4), f"Cramér's V differs too much: orig={orig_cramers_v}, syn={syn_cramers_v}"
224+
225+
assert syn_cramers_v == approx(
226+
orig_cramers_v, rel=0.4
227+
), f"Cramér's V differs too much: orig={orig_cramers_v}, syn={syn_cramers_v}"
230228

231229

232230
def test_synthetic_data_size_consistency() -> None:
233231
"""Test that synthetic data has reasonable size compared to original."""
234232
np.random.seed(42)
235-
233+
236234
# Create simple test data
237-
df = pd.DataFrame({
238-
"col1": np.random.normal(0, 1, 1000),
239-
"col2": np.random.choice(["A", "B", "C"], 1000)
240-
})
241-
235+
df = pd.DataFrame({"col1": np.random.normal(0, 1, 1000), "col2": np.random.choice(["A", "B", "C"], 1000)})
236+
242237
df_syn = Synthesizer(df, anonymization_params=NOISELESS_PARAMS).sample()
243-
238+
244239
# Synthetic data should have reasonable size (within 50% of original)
245240
assert len(df_syn) > 0.5 * len(df), "Synthetic data too small"
246241
assert len(df_syn) < 2.0 * len(df), "Synthetic data too large"
247-
242+
248243
# Should have same number of columns
249244
assert len(df_syn.columns) == len(df.columns), "Different number of columns"
250-
245+
251246
# Should have same column names
252247
assert list(df_syn.columns) == list(df.columns), "Different column names"

0 commit comments

Comments
 (0)