tabeval 2025.8.8__py3-none-any.whl
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.
- tabeval/__init__.py +26 -0
- tabeval/benchmark/__init__.py +394 -0
- tabeval/benchmark/utils.py +206 -0
- tabeval/logger.py +103 -0
- tabeval/metrics/__init__.py +3 -0
- tabeval/metrics/_utils.py +559 -0
- tabeval/metrics/core/__init__.py +2 -0
- tabeval/metrics/core/metric.py +131 -0
- tabeval/metrics/eval.py +255 -0
- tabeval/metrics/eval_attacks.py +218 -0
- tabeval/metrics/eval_causality.py +165 -0
- tabeval/metrics/eval_detection.py +338 -0
- tabeval/metrics/eval_performance.py +1145 -0
- tabeval/metrics/eval_privacy.py +580 -0
- tabeval/metrics/eval_sanity.py +243 -0
- tabeval/metrics/eval_statistical.py +851 -0
- tabeval/metrics/eval_structure.py +431 -0
- tabeval/metrics/plots.py +100 -0
- tabeval/metrics/representations/OneClass.py +202 -0
- tabeval/metrics/representations/__init__.py +0 -0
- tabeval/metrics/representations/networks.py +98 -0
- tabeval/metrics/scores.py +143 -0
- tabeval/metrics/weighted_metrics.py +107 -0
- tabeval/plugins/__init__.py +39 -0
- tabeval/plugins/core/__init__.py +0 -0
- tabeval/plugins/core/constraints.py +312 -0
- tabeval/plugins/core/dataloader.py +1820 -0
- tabeval/plugins/core/dataset.py +188 -0
- tabeval/plugins/core/distribution.py +857 -0
- tabeval/plugins/core/models/RGCNConv.py +281 -0
- tabeval/plugins/core/models/__init__.py +0 -0
- tabeval/plugins/core/models/aim.py +365 -0
- tabeval/plugins/core/models/bnaf.py +468 -0
- tabeval/plugins/core/models/convnet.py +584 -0
- tabeval/plugins/core/models/dag/__init__.py +0 -0
- tabeval/plugins/core/models/dag/data.py +77 -0
- tabeval/plugins/core/models/dag/dsl.py +203 -0
- tabeval/plugins/core/models/dag/dstruct.py +282 -0
- tabeval/plugins/core/models/dag/utils.py +258 -0
- tabeval/plugins/core/models/factory.py +141 -0
- tabeval/plugins/core/models/feature_encoder.py +283 -0
- tabeval/plugins/core/models/flows.py +406 -0
- tabeval/plugins/core/models/functions.py +152 -0
- tabeval/plugins/core/models/gan.py +719 -0
- tabeval/plugins/core/models/goggle.py +730 -0
- tabeval/plugins/core/models/image_gan.py +687 -0
- tabeval/plugins/core/models/layers.py +157 -0
- tabeval/plugins/core/models/mbi/__init__.py +12 -0
- tabeval/plugins/core/models/mbi/callbacks.py +113 -0
- tabeval/plugins/core/models/mbi/clique_vector.py +107 -0
- tabeval/plugins/core/models/mbi/dataset.py +72 -0
- tabeval/plugins/core/models/mbi/domain.py +121 -0
- tabeval/plugins/core/models/mbi/factor.py +208 -0
- tabeval/plugins/core/models/mbi/factor_graph.py +287 -0
- tabeval/plugins/core/models/mbi/graphical_model.py +332 -0
- tabeval/plugins/core/models/mbi/identity.py +346 -0
- tabeval/plugins/core/models/mbi/inference.py +419 -0
- tabeval/plugins/core/models/mbi/junction_tree.py +131 -0
- tabeval/plugins/core/models/mbi/local_inference.py +293 -0
- tabeval/plugins/core/models/mbi/mechanism.py +96 -0
- tabeval/plugins/core/models/mbi/mixture_inference.py +206 -0
- tabeval/plugins/core/models/mbi/public_inference.py +128 -0
- tabeval/plugins/core/models/mbi/region_graph.py +544 -0
- tabeval/plugins/core/models/mbi/torch_factor.py +206 -0
- tabeval/plugins/core/models/mlp.py +356 -0
- tabeval/plugins/core/models/survival_analysis/__init__.py +8 -0
- tabeval/plugins/core/models/survival_analysis/_base.py +52 -0
- tabeval/plugins/core/models/survival_analysis/benchmarks.py +268 -0
- tabeval/plugins/core/models/survival_analysis/loader.py +63 -0
- tabeval/plugins/core/models/survival_analysis/metrics.py +153 -0
- tabeval/plugins/core/models/survival_analysis/surv_aft.py +71 -0
- tabeval/plugins/core/models/survival_analysis/surv_coxph.py +85 -0
- tabeval/plugins/core/models/survival_analysis/surv_deephit.py +158 -0
- tabeval/plugins/core/models/survival_analysis/surv_xgb.py +165 -0
- tabeval/plugins/core/models/survival_analysis/third_party/__init__.py +0 -0
- tabeval/plugins/core/models/survival_analysis/third_party/metrics.py +427 -0
- tabeval/plugins/core/models/survival_analysis/third_party/nonparametric.py +393 -0
- tabeval/plugins/core/models/survival_analysis/third_party/util.py +280 -0
- tabeval/plugins/core/models/tabnet.py +516 -0
- tabeval/plugins/core/models/tabular_aim.py +133 -0
- tabeval/plugins/core/models/tabular_arf.py +150 -0
- tabeval/plugins/core/models/tabular_ddpm/__init__.py +218 -0
- tabeval/plugins/core/models/tabular_ddpm/gaussian_multinomial_diffsuion.py +950 -0
- tabeval/plugins/core/models/tabular_ddpm/modules.py +117 -0
- tabeval/plugins/core/models/tabular_ddpm/utils.py +177 -0
- tabeval/plugins/core/models/tabular_encoder.py +575 -0
- tabeval/plugins/core/models/tabular_flows.py +163 -0
- tabeval/plugins/core/models/tabular_gan.py +460 -0
- tabeval/plugins/core/models/tabular_goggle.py +241 -0
- tabeval/plugins/core/models/tabular_great.py +160 -0
- tabeval/plugins/core/models/tabular_scm.py +106 -0
- tabeval/plugins/core/models/tabular_vae.py +301 -0
- tabeval/plugins/core/models/time_series_survival/__init__.py +8 -0
- tabeval/plugins/core/models/time_series_survival/_base.py +65 -0
- tabeval/plugins/core/models/time_series_survival/benchmarks.py +442 -0
- tabeval/plugins/core/models/time_series_survival/loader.py +20 -0
- tabeval/plugins/core/models/time_series_survival/ts_surv_coxph.py +114 -0
- tabeval/plugins/core/models/time_series_survival/ts_surv_dynamic_deephit.py +761 -0
- tabeval/plugins/core/models/time_series_survival/ts_surv_xgb.py +134 -0
- tabeval/plugins/core/models/time_series_survival/utils.py +173 -0
- tabeval/plugins/core/models/time_to_event/__init__.py +4 -0
- tabeval/plugins/core/models/time_to_event/_base.py +76 -0
- tabeval/plugins/core/models/time_to_event/benchmarks.py +207 -0
- tabeval/plugins/core/models/time_to_event/loader.py +28 -0
- tabeval/plugins/core/models/time_to_event/metrics.py +127 -0
- tabeval/plugins/core/models/time_to_event/tte_aft.py +78 -0
- tabeval/plugins/core/models/time_to_event/tte_coxph.py +76 -0
- tabeval/plugins/core/models/time_to_event/tte_date.py +471 -0
- tabeval/plugins/core/models/time_to_event/tte_deephit.py +157 -0
- tabeval/plugins/core/models/time_to_event/tte_survival_function_regression.py +94 -0
- tabeval/plugins/core/models/time_to_event/tte_survival_time_series.py +153 -0
- tabeval/plugins/core/models/time_to_event/tte_tenn.py +498 -0
- tabeval/plugins/core/models/time_to_event/tte_xgb.py +151 -0
- tabeval/plugins/core/models/transformer.py +62 -0
- tabeval/plugins/core/models/ts_gan.py +845 -0
- tabeval/plugins/core/models/ts_model.py +734 -0
- tabeval/plugins/core/models/ts_tabular_gan.py +323 -0
- tabeval/plugins/core/models/ts_tabular_vae.py +271 -0
- tabeval/plugins/core/models/ts_vae.py +603 -0
- tabeval/plugins/core/models/vae.py +601 -0
- tabeval/plugins/core/plugin.py +733 -0
- tabeval/plugins/core/schema.py +336 -0
- tabeval/plugins/core/serializable.py +150 -0
- tabeval/plugins/domain_adaptation/__init__.py +0 -0
- tabeval/plugins/domain_adaptation/plugin_radialgan.py +917 -0
- tabeval/plugins/generic/__init__.py +18 -0
- tabeval/plugins/generic/plugin_arf.py +170 -0
- tabeval/plugins/generic/plugin_bayesian_network.py +187 -0
- tabeval/plugins/generic/plugin_ctgan.py +285 -0
- tabeval/plugins/generic/plugin_ddpm.py +249 -0
- tabeval/plugins/generic/plugin_dummy_sampler.py +72 -0
- tabeval/plugins/generic/plugin_goggle.py +253 -0
- tabeval/plugins/generic/plugin_great.py +206 -0
- tabeval/plugins/generic/plugin_marginal_distributions.py +71 -0
- tabeval/plugins/generic/plugin_nflow.py +292 -0
- tabeval/plugins/generic/plugin_rtvae.py +230 -0
- tabeval/plugins/generic/plugin_scm.py +96 -0
- tabeval/plugins/generic/plugin_tvae.py +233 -0
- tabeval/plugins/generic/plugin_uniform_sampler.py +70 -0
- tabeval/plugins/images/__init__.py +18 -0
- tabeval/plugins/images/plugin_image_adsgan.py +331 -0
- tabeval/plugins/images/plugin_image_cgan.py +324 -0
- tabeval/plugins/privacy/__init__.py +18 -0
- tabeval/plugins/privacy/plugin_adsgan.py +282 -0
- tabeval/plugins/privacy/plugin_aim.py +160 -0
- tabeval/plugins/privacy/plugin_decaf.py +471 -0
- tabeval/plugins/privacy/plugin_dpgan.py +294 -0
- tabeval/plugins/privacy/plugin_pategan.py +531 -0
- tabeval/plugins/privacy/plugin_privbayes.py +641 -0
- tabeval/plugins/survival_analysis/__init__.py +18 -0
- tabeval/plugins/survival_analysis/_survival_pipeline.py +216 -0
- tabeval/plugins/survival_analysis/plugin_survae.py +172 -0
- tabeval/plugins/survival_analysis/plugin_survival_ctgan.py +173 -0
- tabeval/plugins/survival_analysis/plugin_survival_gan.py +221 -0
- tabeval/plugins/survival_analysis/plugin_survival_nflow.py +159 -0
- tabeval/plugins/time_series/__init__.py +18 -0
- tabeval/plugins/time_series/plugin_fflows.py +285 -0
- tabeval/plugins/time_series/plugin_timegan.py +436 -0
- tabeval/plugins/time_series/plugin_timevae.py +343 -0
- tabeval/utils/anonymization.py +323 -0
- tabeval/utils/callbacks.py +173 -0
- tabeval/utils/compression.py +192 -0
- tabeval/utils/constants.py +4 -0
- tabeval/utils/dataframe.py +23 -0
- tabeval/utils/datasets/__init__.py +0 -0
- tabeval/utils/datasets/categorical/__init__.py +0 -0
- tabeval/utils/datasets/categorical/categorical_adult.py +33 -0
- tabeval/utils/datasets/categorical/data/__init__ .py +0 -0
- tabeval/utils/datasets/time_series/__init__.py +0 -0
- tabeval/utils/datasets/time_series/data/__init__.py +0 -0
- tabeval/utils/datasets/time_series/google_stocks.py +82 -0
- tabeval/utils/datasets/time_series/pbc.py +198 -0
- tabeval/utils/datasets/time_series/sine.py +96 -0
- tabeval/utils/evaluation.py +123 -0
- tabeval/utils/optimizer.py +228 -0
- tabeval/utils/optuna_sample.py +27 -0
- tabeval/utils/redis_wrapper.py +27 -0
- tabeval/utils/reproducibility.py +24 -0
- tabeval/utils/samplers.py +321 -0
- tabeval/utils/serialization.py +172 -0
- tabeval/version.py +4 -0
- tabeval-2025.8.8.dist-info/METADATA +297 -0
- tabeval-2025.8.8.dist-info/RECORD +185 -0
- tabeval-2025.8.8.dist-info/WHEEL +4 -0
- tabeval-2025.8.8.dist-info/licenses/LICENSE +201 -0
tabeval/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# stdlib
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
# third party
|
|
8
|
+
import optuna
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
# tabeval relative
|
|
12
|
+
from . import logger # noqa: F401
|
|
13
|
+
|
|
14
|
+
optuna.logging.set_verbosity(optuna.logging.FATAL)
|
|
15
|
+
optuna.logging.disable_propagation()
|
|
16
|
+
optuna.logging.disable_default_handler() # Stop showing logs in sys.stderr.
|
|
17
|
+
|
|
18
|
+
pd.options.mode.chained_assignment = None
|
|
19
|
+
|
|
20
|
+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
|
|
21
|
+
logging.getLogger("tensorflow").setLevel(logging.CRITICAL)
|
|
22
|
+
logging.getLogger("tensorflow_hub").setLevel(logging.CRITICAL)
|
|
23
|
+
|
|
24
|
+
warnings.simplefilter(action="ignore")
|
|
25
|
+
|
|
26
|
+
logger.add(sink=sys.stderr, level="CRITICAL")
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# stdlib
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import platform
|
|
5
|
+
import random
|
|
6
|
+
from copy import copy
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
# third party
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from IPython.display import display
|
|
14
|
+
from pydantic import validate_arguments
|
|
15
|
+
|
|
16
|
+
# tabeval absolute
|
|
17
|
+
import tabeval.logger as log
|
|
18
|
+
from tabeval.benchmark.utils import augment_data, get_json_serializable_kwargs
|
|
19
|
+
from tabeval.metrics import Metrics
|
|
20
|
+
from tabeval.metrics.scores import ScoreEvaluator
|
|
21
|
+
from tabeval.plugins import Plugins
|
|
22
|
+
from tabeval.plugins.core.constraints import Constraints
|
|
23
|
+
from tabeval.plugins.core.dataloader import DataLoader
|
|
24
|
+
from tabeval.utils.reproducibility import clear_cache, enable_reproducible_results
|
|
25
|
+
from tabeval.utils.serialization import load_from_file, save_to_file
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_score(mean: pd.Series, std: pd.Series) -> pd.Series:
|
|
29
|
+
pd.options.mode.chained_assignment = None
|
|
30
|
+
|
|
31
|
+
mean.loc[(mean < 1e-3) & (mean != 0)] = 1e-3
|
|
32
|
+
std.loc[(std < 1e-3) & (std != 0)] = 1e-3
|
|
33
|
+
|
|
34
|
+
mean = mean.round(3).astype(str)
|
|
35
|
+
std = std.round(3).astype(str)
|
|
36
|
+
|
|
37
|
+
return mean + " +/- " + std
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Benchmarks:
|
|
41
|
+
@staticmethod
|
|
42
|
+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
|
|
43
|
+
def evaluate(
|
|
44
|
+
tests: List[Tuple[str, str, dict]], # test name, plugin name, plugin args
|
|
45
|
+
X: DataLoader,
|
|
46
|
+
X_test: Optional[DataLoader] = None,
|
|
47
|
+
metrics: Optional[Dict] = None,
|
|
48
|
+
repeats: int = 3,
|
|
49
|
+
synthetic_size: Optional[int] = None,
|
|
50
|
+
synthetic_constraints: Optional[Constraints] = None,
|
|
51
|
+
synthetic_cache: bool = True,
|
|
52
|
+
synthetic_reuse_if_exists: bool = True,
|
|
53
|
+
augmented_reuse_if_exists: bool = True,
|
|
54
|
+
task_type: str = "classification", # classification, regression, survival_analysis, time_series
|
|
55
|
+
workspace: Path = Path("logs/tabeval_workspace"),
|
|
56
|
+
augmentation_rule: str = "equal",
|
|
57
|
+
strict_augmentation: bool = False,
|
|
58
|
+
ad_hoc_augment_vals: Optional[Dict] = None,
|
|
59
|
+
use_metric_cache: bool = True,
|
|
60
|
+
n_eval_folds: int = 5,
|
|
61
|
+
**generate_kwargs: Any,
|
|
62
|
+
) -> pd.DataFrame:
|
|
63
|
+
"""Benchmark the performance of several algorithms.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
tests: List[Tuple[str, str, dict]]
|
|
67
|
+
Tuples of form (testname: str, plugin_name, str, plugin_args: dict)
|
|
68
|
+
X: DataLoader
|
|
69
|
+
The baseline dataset to learn
|
|
70
|
+
X_test: Optional[DataLoader]
|
|
71
|
+
Optional test dataset for evaluation. If None, X will be split in train/test datasets.
|
|
72
|
+
metrics:
|
|
73
|
+
List of metrics to test. By default, all metrics are evaluated.
|
|
74
|
+
Full dictionary of metrics is:
|
|
75
|
+
{
|
|
76
|
+
'sanity': ['data_mismatch', 'common_rows_proportion', 'nearest_syn_neighbor_distance', 'close_values_probability', 'distant_values_probability'],
|
|
77
|
+
'stats': ['jensenshannon_dist', 'chi_squared_test', 'feature_corr', 'inv_kl_divergence', 'ks_test', 'max_mean_discrepancy', 'wasserstein_dist', 'prdc', 'alpha_precision', 'survival_km_distance'],
|
|
78
|
+
'performance': ['linear_model', 'mlp', 'xgb', 'feat_rank_distance'],
|
|
79
|
+
'detection': ['detection_xgb', 'detection_mlp', 'detection_gmm', 'detection_linear'],
|
|
80
|
+
'privacy': ['delta-presence', 'k-anonymization', 'k-map', 'distinct l-diversity', 'identifiability_score', 'DomiasMIA_BNAF', 'DomiasMIA_KDE', 'DomiasMIA_prior']
|
|
81
|
+
}
|
|
82
|
+
repeats:
|
|
83
|
+
Number of test repeats
|
|
84
|
+
synthetic_size: int
|
|
85
|
+
The size of the synthetic dataset. By default, it is len(X).
|
|
86
|
+
synthetic_constraints:
|
|
87
|
+
Optional constraints on the synthetic data. By default, it inherits the constraints from X.
|
|
88
|
+
synthetic_cache: bool
|
|
89
|
+
Enable experiment caching
|
|
90
|
+
synthetic_reuse_if_exists: bool
|
|
91
|
+
If the current synthetic dataset is cached, it will be reused for the experiments. Defaults to True.
|
|
92
|
+
augmented_reuse_if_exists: bool
|
|
93
|
+
If the current augmented dataset is cached, it will be reused for the experiments. Defaults to True.
|
|
94
|
+
task_type: str
|
|
95
|
+
The type of problem. Relevant for evaluating the downstream models with the correct metrics. Valid tasks are: "classification", "regression", "survival_analysis", "time_series", "time_series_survival".
|
|
96
|
+
workspace: Path
|
|
97
|
+
Path for caching experiments. Default: "workspace".
|
|
98
|
+
augmentation_rule: str
|
|
99
|
+
The rule used to achieve the desired proportion records with each value in the fairness column. Possible values are: 'equal', 'log', and 'ad-hoc'. Defaults to "equal".
|
|
100
|
+
strict_augmentation: bool
|
|
101
|
+
Flag to ensure that the condition for generating synthetic data is strictly met. Defaults to False.
|
|
102
|
+
ad_hoc_augment_vals: Dict
|
|
103
|
+
A dictionary containing the number of each class to augment the real data with. This is only required if using the rule="ad-hoc" option. Defaults to None.
|
|
104
|
+
use_metric_cache: bool
|
|
105
|
+
If the current metric has been previously run and is cached, it will be reused for the experiments. Defaults to True.
|
|
106
|
+
n_eval_folds: int
|
|
107
|
+
the KFolds used by MetricEvaluators in the benchmarks. Defaults to 5.
|
|
108
|
+
plugin_kwargs:
|
|
109
|
+
Optional kwargs for each algorithm. Example {"adsgan": {"n_iter": 10}},
|
|
110
|
+
"""
|
|
111
|
+
out = {}
|
|
112
|
+
|
|
113
|
+
experiment_name = X.hash()
|
|
114
|
+
|
|
115
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
|
|
117
|
+
plugin_cats = ["generic", "privacy", "domain_adaptation"]
|
|
118
|
+
if X.type() == "images":
|
|
119
|
+
plugin_cats.append("images")
|
|
120
|
+
elif task_type == "survival_analysis":
|
|
121
|
+
plugin_cats.append("survival_analysis")
|
|
122
|
+
elif task_type == "time_series" or task_type == "time_series_survival":
|
|
123
|
+
plugin_cats.append("time_series")
|
|
124
|
+
|
|
125
|
+
for testcase, plugin, kwargs in tests:
|
|
126
|
+
log.info(f"Testcase : {testcase}")
|
|
127
|
+
if not isinstance(kwargs, dict):
|
|
128
|
+
raise ValueError(f"'kwargs' must be a dict for {testcase}:{plugin}")
|
|
129
|
+
|
|
130
|
+
scores = ScoreEvaluator()
|
|
131
|
+
|
|
132
|
+
kwargs_hash = ""
|
|
133
|
+
if len(kwargs) > 0:
|
|
134
|
+
serializable_kwargs = get_json_serializable_kwargs(kwargs)
|
|
135
|
+
kwargs_hash_raw = json.dumps(
|
|
136
|
+
serializable_kwargs, sort_keys=True
|
|
137
|
+
).encode()
|
|
138
|
+
hash_object = hashlib.sha256(kwargs_hash_raw)
|
|
139
|
+
kwargs_hash = hash_object.hexdigest()
|
|
140
|
+
|
|
141
|
+
augmentation_arguments = {
|
|
142
|
+
"augmentation_rule": augmentation_rule,
|
|
143
|
+
"strict_augmentation": strict_augmentation,
|
|
144
|
+
"ad_hoc_augment_vals": ad_hoc_augment_vals,
|
|
145
|
+
}
|
|
146
|
+
augmentation_arguments_hash_raw = json.dumps(
|
|
147
|
+
copy(augmentation_arguments), sort_keys=True
|
|
148
|
+
).encode()
|
|
149
|
+
augmentation_hash_object = hashlib.sha256(augmentation_arguments_hash_raw)
|
|
150
|
+
augmentation_hash = augmentation_hash_object.hexdigest()
|
|
151
|
+
|
|
152
|
+
repeats_list = list(range(repeats))
|
|
153
|
+
random.shuffle(repeats_list)
|
|
154
|
+
|
|
155
|
+
for repeat in repeats_list:
|
|
156
|
+
enable_reproducible_results(repeat)
|
|
157
|
+
|
|
158
|
+
kwargs["workspace"] = workspace
|
|
159
|
+
kwargs["random_state"] = repeat
|
|
160
|
+
|
|
161
|
+
clear_cache()
|
|
162
|
+
|
|
163
|
+
X_syn_cache_file = (
|
|
164
|
+
workspace
|
|
165
|
+
/ f"{experiment_name}_{testcase}_{plugin}_{kwargs_hash}_{platform.python_version()}_{repeat}.bkp"
|
|
166
|
+
)
|
|
167
|
+
X_ref_syn_cache_file = (
|
|
168
|
+
workspace
|
|
169
|
+
/ f"{experiment_name}_{testcase}_{plugin}_{kwargs_hash}_{platform.python_version()}_{repeat}_reference.bkp"
|
|
170
|
+
)
|
|
171
|
+
generator_file = (
|
|
172
|
+
workspace
|
|
173
|
+
/ f"{experiment_name}_{testcase}_{plugin}_{kwargs_hash}_{platform.python_version()}_generator_{repeat}.bkp"
|
|
174
|
+
)
|
|
175
|
+
X_augment_cache_file = (
|
|
176
|
+
workspace
|
|
177
|
+
/ f"{experiment_name}_{testcase}_{plugin}_augmentation_{augmentation_hash}_{kwargs_hash}_{platform.python_version()}_{repeat}.bkp"
|
|
178
|
+
)
|
|
179
|
+
augment_generator_file = (
|
|
180
|
+
workspace
|
|
181
|
+
/ f"{experiment_name}_{testcase}_{plugin}_augmentation_{augmentation_hash}_{kwargs_hash}_{platform.python_version()}_generator_{repeat}.bkp"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
log.info(
|
|
185
|
+
f"[testcase] Experiment repeat: {repeat} task type: {task_type} Train df hash = {experiment_name}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# TODO: caches should be from the same version of tabeval. Different APIs will crash.
|
|
189
|
+
if generator_file.exists() and synthetic_reuse_if_exists:
|
|
190
|
+
generator = load_from_file(generator_file)
|
|
191
|
+
else:
|
|
192
|
+
generator = Plugins(categories=plugin_cats).get(
|
|
193
|
+
plugin,
|
|
194
|
+
**kwargs,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
generator.fit(X.train())
|
|
198
|
+
|
|
199
|
+
if synthetic_cache:
|
|
200
|
+
save_to_file(generator_file, generator)
|
|
201
|
+
|
|
202
|
+
if X_syn_cache_file.exists() and synthetic_reuse_if_exists:
|
|
203
|
+
X_syn = load_from_file(X_syn_cache_file)
|
|
204
|
+
else:
|
|
205
|
+
try:
|
|
206
|
+
X_syn = generator.generate(
|
|
207
|
+
count=synthetic_size,
|
|
208
|
+
constraints=synthetic_constraints,
|
|
209
|
+
**generate_kwargs,
|
|
210
|
+
)
|
|
211
|
+
if len(X_syn) == 0:
|
|
212
|
+
raise RuntimeError("Plugin failed to generate data")
|
|
213
|
+
except BaseException as e:
|
|
214
|
+
log.critical(f"[{plugin}][take {repeat}] failed: {e}")
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
if synthetic_cache:
|
|
218
|
+
save_to_file(X_syn_cache_file, X_syn)
|
|
219
|
+
|
|
220
|
+
# X_ref_syn is the reference synthetic data used for DomiasMIA metrics
|
|
221
|
+
if X_ref_syn_cache_file.exists() and synthetic_reuse_if_exists:
|
|
222
|
+
X_ref_syn = load_from_file(X_ref_syn_cache_file)
|
|
223
|
+
else:
|
|
224
|
+
try:
|
|
225
|
+
X_ref_syn = generator.generate(
|
|
226
|
+
count=synthetic_size,
|
|
227
|
+
constraints=synthetic_constraints,
|
|
228
|
+
**generate_kwargs,
|
|
229
|
+
)
|
|
230
|
+
if len(X_syn) == 0:
|
|
231
|
+
raise RuntimeError("Plugin failed to generate data")
|
|
232
|
+
except BaseException as e:
|
|
233
|
+
log.critical(f"[{plugin}][take {repeat}] failed: {e}")
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if synthetic_cache:
|
|
237
|
+
save_to_file(X_ref_syn_cache_file, X_ref_syn)
|
|
238
|
+
|
|
239
|
+
# Augmentation
|
|
240
|
+
if metrics and any(
|
|
241
|
+
"augmentation" in metric
|
|
242
|
+
for metric in [x for v in metrics.values() for x in v]
|
|
243
|
+
):
|
|
244
|
+
if augment_generator_file.exists() and augmented_reuse_if_exists:
|
|
245
|
+
augment_generator = load_from_file(augment_generator_file)
|
|
246
|
+
else:
|
|
247
|
+
augment_generator = Plugins(categories=plugin_cats).get(
|
|
248
|
+
plugin,
|
|
249
|
+
**kwargs,
|
|
250
|
+
)
|
|
251
|
+
try:
|
|
252
|
+
if not X.get_fairness_column():
|
|
253
|
+
raise ValueError(
|
|
254
|
+
"To use the augmentation metrics, `fairness_column` must be set to a string representing the name of a column in the DataLoader."
|
|
255
|
+
)
|
|
256
|
+
augment_generator.fit(
|
|
257
|
+
X.train(),
|
|
258
|
+
cond=X.train()[X.get_fairness_column()],
|
|
259
|
+
)
|
|
260
|
+
except BaseException as e:
|
|
261
|
+
log.critical(
|
|
262
|
+
f"[{plugin}][take {repeat}] failed to fit augmentation generator: {e}"
|
|
263
|
+
)
|
|
264
|
+
continue
|
|
265
|
+
if synthetic_cache:
|
|
266
|
+
save_to_file(augment_generator_file, augment_generator)
|
|
267
|
+
|
|
268
|
+
if X_augment_cache_file.exists() and augmented_reuse_if_exists:
|
|
269
|
+
X_augmented = load_from_file(X_augment_cache_file)
|
|
270
|
+
else:
|
|
271
|
+
try:
|
|
272
|
+
X_augmented = augment_data(
|
|
273
|
+
X.train(),
|
|
274
|
+
augment_generator,
|
|
275
|
+
rule=augmentation_rule,
|
|
276
|
+
strict=strict_augmentation,
|
|
277
|
+
ad_hoc_augment_vals=ad_hoc_augment_vals,
|
|
278
|
+
**generate_kwargs,
|
|
279
|
+
)
|
|
280
|
+
if len(X_augmented) == 0:
|
|
281
|
+
raise RuntimeError("Plugin failed to generate data")
|
|
282
|
+
except BaseException as e:
|
|
283
|
+
log.critical(
|
|
284
|
+
f"[{plugin}][take {repeat}] failed to generate augmentation data: {e}"
|
|
285
|
+
)
|
|
286
|
+
continue
|
|
287
|
+
if synthetic_cache:
|
|
288
|
+
save_to_file(X_augment_cache_file, X_augmented)
|
|
289
|
+
else:
|
|
290
|
+
X_augmented = None
|
|
291
|
+
evaluation = Metrics.evaluate(
|
|
292
|
+
X_test if X_test is not None else X.test(),
|
|
293
|
+
X_syn,
|
|
294
|
+
X.train(),
|
|
295
|
+
X_ref_syn,
|
|
296
|
+
X_augmented,
|
|
297
|
+
metrics=metrics,
|
|
298
|
+
task_type=task_type,
|
|
299
|
+
workspace=workspace,
|
|
300
|
+
use_cache=use_metric_cache,
|
|
301
|
+
n_folds=n_eval_folds,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
mean_score = evaluation["mean"].to_dict()
|
|
305
|
+
errors = evaluation["errors"].to_dict()
|
|
306
|
+
duration = evaluation["durations"].to_dict()
|
|
307
|
+
direction = evaluation["direction"].to_dict()
|
|
308
|
+
|
|
309
|
+
for key in mean_score:
|
|
310
|
+
scores.add(
|
|
311
|
+
key,
|
|
312
|
+
mean_score[key],
|
|
313
|
+
errors[key],
|
|
314
|
+
duration[key],
|
|
315
|
+
direction[key],
|
|
316
|
+
)
|
|
317
|
+
out[testcase] = scores.to_dataframe()
|
|
318
|
+
|
|
319
|
+
return out
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
|
|
323
|
+
def print(
|
|
324
|
+
results: Dict,
|
|
325
|
+
only_comparatives: bool = True,
|
|
326
|
+
) -> None:
|
|
327
|
+
pd.set_option("display.max_rows", None, "display.max_columns", None)
|
|
328
|
+
|
|
329
|
+
means = []
|
|
330
|
+
for plugin in results:
|
|
331
|
+
mean = results[plugin]["mean"]
|
|
332
|
+
stddev = results[plugin]["stddev"]
|
|
333
|
+
means.append(print_score(mean, stddev))
|
|
334
|
+
|
|
335
|
+
avg = pd.concat(means, axis=1)
|
|
336
|
+
avg = avg.set_axis(results.keys(), axis=1)
|
|
337
|
+
|
|
338
|
+
if len(means) > 1:
|
|
339
|
+
print()
|
|
340
|
+
print("\033[4m" + "\033[1m" + "Comparatives" + "\033[0m" + "\033[0m")
|
|
341
|
+
display(avg)
|
|
342
|
+
|
|
343
|
+
if only_comparatives:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
for plugin in results:
|
|
347
|
+
print()
|
|
348
|
+
print("\033[4m" + "\033[1m" + f"Plugin : {plugin}" + "\033[0m" + "\033[0m")
|
|
349
|
+
|
|
350
|
+
display(results[plugin].drop(columns=["direction"]))
|
|
351
|
+
print()
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
|
|
355
|
+
def highlight(
|
|
356
|
+
results: Dict,
|
|
357
|
+
) -> None:
|
|
358
|
+
pd.set_option("display.max_rows", None, "display.max_columns", None)
|
|
359
|
+
means = []
|
|
360
|
+
for plugin in results:
|
|
361
|
+
data = results[plugin]["mean"]
|
|
362
|
+
directions = results[plugin]["direction"].to_dict()
|
|
363
|
+
means.append(data)
|
|
364
|
+
|
|
365
|
+
out = pd.concat(means, axis=1)
|
|
366
|
+
out.set_axis(results.keys(), axis=1, inplace=True)
|
|
367
|
+
|
|
368
|
+
bad_highlight = "background-color: lightcoral;"
|
|
369
|
+
ok_highlight = "background-color: green;"
|
|
370
|
+
default = ""
|
|
371
|
+
|
|
372
|
+
def highlights(row: pd.Series) -> Any:
|
|
373
|
+
metric = row.name
|
|
374
|
+
if directions[metric] == "minimize":
|
|
375
|
+
best_val = np.min(row.values)
|
|
376
|
+
worst_val = np.max(row)
|
|
377
|
+
else:
|
|
378
|
+
best_val = np.max(row.values)
|
|
379
|
+
worst_val = np.min(row)
|
|
380
|
+
|
|
381
|
+
styles = []
|
|
382
|
+
for val in row.values:
|
|
383
|
+
if val == best_val:
|
|
384
|
+
styles.append(ok_highlight)
|
|
385
|
+
elif val == worst_val:
|
|
386
|
+
styles.append(bad_highlight)
|
|
387
|
+
else:
|
|
388
|
+
styles.append(default)
|
|
389
|
+
|
|
390
|
+
return styles
|
|
391
|
+
|
|
392
|
+
out.style.apply(highlights, axis=1)
|
|
393
|
+
|
|
394
|
+
return out
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# stdlib
|
|
2
|
+
import math
|
|
3
|
+
from copy import copy, deepcopy
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
# third party
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from pydantic import validate_arguments
|
|
11
|
+
from typing_extensions import Literal
|
|
12
|
+
|
|
13
|
+
# tabeval absolute
|
|
14
|
+
from tabeval.plugins.core.constraints import Constraints
|
|
15
|
+
from tabeval.plugins.core.dataloader import DataLoader
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_json_serializable_kwargs(kwargs: Dict) -> Dict:
|
|
19
|
+
"""
|
|
20
|
+
This function should take the kwargs for Benchmarks.evaluate and makes them serializable with json.dumps.
|
|
21
|
+
Currently it only handles pathlib.Path -> str.
|
|
22
|
+
"""
|
|
23
|
+
serializable_kwargs = deepcopy(kwargs)
|
|
24
|
+
for k, v in serializable_kwargs.items():
|
|
25
|
+
if isinstance(v, Path):
|
|
26
|
+
serializable_kwargs[k] = str(serializable_kwargs[k])
|
|
27
|
+
return serializable_kwargs
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def calculate_fair_aug_sample_size(
|
|
31
|
+
X_train: pd.DataFrame,
|
|
32
|
+
fairness_column: Optional[str], # a categorical column of K levels
|
|
33
|
+
rule: Literal[
|
|
34
|
+
"equal", "log", "ad-hoc"
|
|
35
|
+
], # TODO: Confirm are there any more methods to include
|
|
36
|
+
ad_hoc_augment_vals: Optional[
|
|
37
|
+
Dict[Any, int]
|
|
38
|
+
] = None, # Only required for rule == "ad-hoc"
|
|
39
|
+
) -> Dict:
|
|
40
|
+
"""Calculate how many samples to augment.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
X_train (pd.DataFrame): The real dataset to be augmented.
|
|
44
|
+
fairness_column (str): The column name of the column to test the fairness of a downstream model with respect to.
|
|
45
|
+
rule (Literal["equal", "log", "ad-hoc"]): The rule used to achieve the desired proportion records with each value in the fairness column. Defaults to "equal".
|
|
46
|
+
ad_hoc_augment_vals (Dict[ Union[int, str], int ], optional): A dictionary containing the number of each class to augment the real data with. If using rule="ad-hoc" this function returns ad_hoc_augment_vals, otherwise this parameter is ignored. Defaults to {}.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dict: A dictionary containing the number of each class to augment the real data with.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# the majority class is unchanged
|
|
53
|
+
if rule == "equal":
|
|
54
|
+
# number of sample will be the same for each value in the fairness column after augmentation
|
|
55
|
+
# N_aug(i) = N_ang(j) for all i and j in value in the fairness column
|
|
56
|
+
fairness_col_counts = X_train[fairness_column].value_counts()
|
|
57
|
+
majority_size = fairness_col_counts.max()
|
|
58
|
+
augmentation_counts = {
|
|
59
|
+
fair_col_val: (majority_size - fairness_col_counts.loc[fair_col_val])
|
|
60
|
+
for fair_col_val in fairness_col_counts.index
|
|
61
|
+
}
|
|
62
|
+
elif rule == "log":
|
|
63
|
+
# number of samples in aug data will be proportional to the log frequency in the real data.
|
|
64
|
+
# Note: taking the log makes the distribution more even.
|
|
65
|
+
# N_aug(i) is proportional to log(N_real(i))
|
|
66
|
+
fairness_col_counts = X_train[fairness_column].value_counts()
|
|
67
|
+
majority_size = fairness_col_counts.max()
|
|
68
|
+
log_coefficient = majority_size / math.log(majority_size)
|
|
69
|
+
|
|
70
|
+
augmentation_counts = {
|
|
71
|
+
fair_col_val: (
|
|
72
|
+
majority_size - round(math.log(fair_col_count) * log_coefficient)
|
|
73
|
+
)
|
|
74
|
+
for fair_col_val, fair_col_count in fairness_col_counts.items()
|
|
75
|
+
}
|
|
76
|
+
elif rule == "ad-hoc":
|
|
77
|
+
# use user-specified values to augment
|
|
78
|
+
if not ad_hoc_augment_vals:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"When augmenting with an `ad-hoc` method, ad_hoc_augment_vals must be a dictionary, where the dictionary keys are the values of the fairness_column and the dictionary values are the number of records to augment."
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
if not set(ad_hoc_augment_vals.keys()).issubset(
|
|
84
|
+
set(X_train[fairness_column].values)
|
|
85
|
+
):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"ad_hoc_augment_vals must be a dictionary, where the dictionary keys are the values of the fairness_column and the dictionary values are the number of records to augment."
|
|
88
|
+
)
|
|
89
|
+
elif set(X_train[fairness_column].values) != set(
|
|
90
|
+
ad_hoc_augment_vals.keys()
|
|
91
|
+
):
|
|
92
|
+
ad_hoc_augment_vals = {
|
|
93
|
+
k: v
|
|
94
|
+
for k, v in ad_hoc_augment_vals.items()
|
|
95
|
+
if k in set(X_train[fairness_column].values)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
augmentation_counts = ad_hoc_augment_vals
|
|
99
|
+
|
|
100
|
+
return augmentation_counts
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
|
|
104
|
+
def _generate_synthetic_data(
|
|
105
|
+
X_train: DataLoader,
|
|
106
|
+
augment_generator: Any,
|
|
107
|
+
strict: bool = True,
|
|
108
|
+
rule: Literal["equal", "log", "ad-hoc"] = "equal",
|
|
109
|
+
ad_hoc_augment_vals: Optional[
|
|
110
|
+
Dict[Any, int]
|
|
111
|
+
] = None, # Only required for rule == "ad-hoc"
|
|
112
|
+
synthetic_constraints: Optional[Constraints] = None,
|
|
113
|
+
**generate_kwargs: Any,
|
|
114
|
+
) -> pd.DataFrame:
|
|
115
|
+
"""Generates synthetic data
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
X_train (DataLoader): The dataset used to train the downstream model.
|
|
119
|
+
augment_generator (Any): The synthetic model to be used to generate the synthetic portion of the augmented dataset.
|
|
120
|
+
strict (bool, optional): Flag to ensure that the condition for generating synthetic data is strictly met. Defaults to False.
|
|
121
|
+
rule (Literal["equal", "log", "ad-hoc"): The rule used to achieve the desired proportion records with each value in the fairness column. Defaults to "equal".
|
|
122
|
+
ad_hoc_augment_vals (Dict[ Union[int, str], int ], optional): A dictionary containing the number of each class to augment the real data with. This is only required if using the rule="ad-hoc" option. Defaults to {}.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
pd.DataFrame: The generated synthetic data.
|
|
126
|
+
"""
|
|
127
|
+
augmentation_counts = calculate_fair_aug_sample_size(
|
|
128
|
+
X_train.dataframe(),
|
|
129
|
+
X_train.get_fairness_column(),
|
|
130
|
+
rule,
|
|
131
|
+
ad_hoc_augment_vals=ad_hoc_augment_vals,
|
|
132
|
+
)
|
|
133
|
+
if not strict:
|
|
134
|
+
# set count equal to the total number of records required according to calculate_fair_aug_sample_size
|
|
135
|
+
count = sum(augmentation_counts.values())
|
|
136
|
+
cond = pd.Series(
|
|
137
|
+
np.repeat(
|
|
138
|
+
list(augmentation_counts.keys()), list(augmentation_counts.values())
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
syn_data = augment_generator.generate(
|
|
142
|
+
count=count,
|
|
143
|
+
cond=cond,
|
|
144
|
+
constraints=synthetic_constraints,
|
|
145
|
+
**generate_kwargs,
|
|
146
|
+
).dataframe()
|
|
147
|
+
else:
|
|
148
|
+
syn_data_list = []
|
|
149
|
+
for fairness_value, count in augmentation_counts.items():
|
|
150
|
+
if count > 0:
|
|
151
|
+
constraints = Constraints(
|
|
152
|
+
rules=[(X_train.get_fairness_column(), "==", fairness_value)]
|
|
153
|
+
)
|
|
154
|
+
syn_data_list.append(
|
|
155
|
+
augment_generator.generate(
|
|
156
|
+
count=count, constraints=constraints
|
|
157
|
+
).dataframe()
|
|
158
|
+
)
|
|
159
|
+
syn_data = pd.concat(syn_data_list)
|
|
160
|
+
return syn_data
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
|
|
164
|
+
def augment_data(
|
|
165
|
+
X_train: DataLoader,
|
|
166
|
+
augment_generator: Any,
|
|
167
|
+
strict: bool = False,
|
|
168
|
+
rule: Literal["equal", "log", "ad-hoc"] = "equal",
|
|
169
|
+
ad_hoc_augment_vals: Optional[
|
|
170
|
+
Dict[Any, int]
|
|
171
|
+
] = None, # Only required for rule == "ad-hoc"
|
|
172
|
+
synthetic_constraints: Optional[Constraints] = None,
|
|
173
|
+
**generate_kwargs: Any,
|
|
174
|
+
) -> DataLoader:
|
|
175
|
+
"""Augment the real data with generated synthetic data
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
X_train (DataLoader): The ground truth DataLoader to augment with synthetic data.
|
|
179
|
+
augment_generator (Any): The synthetic model to be used to generate the synthetic portion of the augmented dataset.
|
|
180
|
+
strict (bool, optional): Flag to ensure that the condition for generating synthetic data is strictly met. Defaults to False.
|
|
181
|
+
rule (Literal["equal", "log", "ad-hoc"): The rule used to achieve the desired proportion records with each value in the fairness column. Defaults to "equal".
|
|
182
|
+
ad_hoc_augment_vals (Dict[Union[int, str], int], optional): A dictionary containing the number of each class to augment the real data with. This is only required if using the rule="ad-hoc" option. Defaults to None.
|
|
183
|
+
synthetic_constraints (Optional[Constraints]): Constraints placed on the generation of the synthetic data. Defaults to None.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
DataLoader: The augmented dataset and labels.
|
|
187
|
+
"""
|
|
188
|
+
syn_data = _generate_synthetic_data(
|
|
189
|
+
X_train,
|
|
190
|
+
augment_generator,
|
|
191
|
+
strict=strict,
|
|
192
|
+
rule=rule,
|
|
193
|
+
ad_hoc_augment_vals=ad_hoc_augment_vals,
|
|
194
|
+
synthetic_constraints=synthetic_constraints,
|
|
195
|
+
**generate_kwargs,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
augmented_data_loader = copy(X_train)
|
|
199
|
+
augmented_data_loader.data = pd.concat(
|
|
200
|
+
[
|
|
201
|
+
X_train.data,
|
|
202
|
+
syn_data,
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return augmented_data_loader
|