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.
Files changed (185) hide show
  1. tabeval/__init__.py +26 -0
  2. tabeval/benchmark/__init__.py +394 -0
  3. tabeval/benchmark/utils.py +206 -0
  4. tabeval/logger.py +103 -0
  5. tabeval/metrics/__init__.py +3 -0
  6. tabeval/metrics/_utils.py +559 -0
  7. tabeval/metrics/core/__init__.py +2 -0
  8. tabeval/metrics/core/metric.py +131 -0
  9. tabeval/metrics/eval.py +255 -0
  10. tabeval/metrics/eval_attacks.py +218 -0
  11. tabeval/metrics/eval_causality.py +165 -0
  12. tabeval/metrics/eval_detection.py +338 -0
  13. tabeval/metrics/eval_performance.py +1145 -0
  14. tabeval/metrics/eval_privacy.py +580 -0
  15. tabeval/metrics/eval_sanity.py +243 -0
  16. tabeval/metrics/eval_statistical.py +851 -0
  17. tabeval/metrics/eval_structure.py +431 -0
  18. tabeval/metrics/plots.py +100 -0
  19. tabeval/metrics/representations/OneClass.py +202 -0
  20. tabeval/metrics/representations/__init__.py +0 -0
  21. tabeval/metrics/representations/networks.py +98 -0
  22. tabeval/metrics/scores.py +143 -0
  23. tabeval/metrics/weighted_metrics.py +107 -0
  24. tabeval/plugins/__init__.py +39 -0
  25. tabeval/plugins/core/__init__.py +0 -0
  26. tabeval/plugins/core/constraints.py +312 -0
  27. tabeval/plugins/core/dataloader.py +1820 -0
  28. tabeval/plugins/core/dataset.py +188 -0
  29. tabeval/plugins/core/distribution.py +857 -0
  30. tabeval/plugins/core/models/RGCNConv.py +281 -0
  31. tabeval/plugins/core/models/__init__.py +0 -0
  32. tabeval/plugins/core/models/aim.py +365 -0
  33. tabeval/plugins/core/models/bnaf.py +468 -0
  34. tabeval/plugins/core/models/convnet.py +584 -0
  35. tabeval/plugins/core/models/dag/__init__.py +0 -0
  36. tabeval/plugins/core/models/dag/data.py +77 -0
  37. tabeval/plugins/core/models/dag/dsl.py +203 -0
  38. tabeval/plugins/core/models/dag/dstruct.py +282 -0
  39. tabeval/plugins/core/models/dag/utils.py +258 -0
  40. tabeval/plugins/core/models/factory.py +141 -0
  41. tabeval/plugins/core/models/feature_encoder.py +283 -0
  42. tabeval/plugins/core/models/flows.py +406 -0
  43. tabeval/plugins/core/models/functions.py +152 -0
  44. tabeval/plugins/core/models/gan.py +719 -0
  45. tabeval/plugins/core/models/goggle.py +730 -0
  46. tabeval/plugins/core/models/image_gan.py +687 -0
  47. tabeval/plugins/core/models/layers.py +157 -0
  48. tabeval/plugins/core/models/mbi/__init__.py +12 -0
  49. tabeval/plugins/core/models/mbi/callbacks.py +113 -0
  50. tabeval/plugins/core/models/mbi/clique_vector.py +107 -0
  51. tabeval/plugins/core/models/mbi/dataset.py +72 -0
  52. tabeval/plugins/core/models/mbi/domain.py +121 -0
  53. tabeval/plugins/core/models/mbi/factor.py +208 -0
  54. tabeval/plugins/core/models/mbi/factor_graph.py +287 -0
  55. tabeval/plugins/core/models/mbi/graphical_model.py +332 -0
  56. tabeval/plugins/core/models/mbi/identity.py +346 -0
  57. tabeval/plugins/core/models/mbi/inference.py +419 -0
  58. tabeval/plugins/core/models/mbi/junction_tree.py +131 -0
  59. tabeval/plugins/core/models/mbi/local_inference.py +293 -0
  60. tabeval/plugins/core/models/mbi/mechanism.py +96 -0
  61. tabeval/plugins/core/models/mbi/mixture_inference.py +206 -0
  62. tabeval/plugins/core/models/mbi/public_inference.py +128 -0
  63. tabeval/plugins/core/models/mbi/region_graph.py +544 -0
  64. tabeval/plugins/core/models/mbi/torch_factor.py +206 -0
  65. tabeval/plugins/core/models/mlp.py +356 -0
  66. tabeval/plugins/core/models/survival_analysis/__init__.py +8 -0
  67. tabeval/plugins/core/models/survival_analysis/_base.py +52 -0
  68. tabeval/plugins/core/models/survival_analysis/benchmarks.py +268 -0
  69. tabeval/plugins/core/models/survival_analysis/loader.py +63 -0
  70. tabeval/plugins/core/models/survival_analysis/metrics.py +153 -0
  71. tabeval/plugins/core/models/survival_analysis/surv_aft.py +71 -0
  72. tabeval/plugins/core/models/survival_analysis/surv_coxph.py +85 -0
  73. tabeval/plugins/core/models/survival_analysis/surv_deephit.py +158 -0
  74. tabeval/plugins/core/models/survival_analysis/surv_xgb.py +165 -0
  75. tabeval/plugins/core/models/survival_analysis/third_party/__init__.py +0 -0
  76. tabeval/plugins/core/models/survival_analysis/third_party/metrics.py +427 -0
  77. tabeval/plugins/core/models/survival_analysis/third_party/nonparametric.py +393 -0
  78. tabeval/plugins/core/models/survival_analysis/third_party/util.py +280 -0
  79. tabeval/plugins/core/models/tabnet.py +516 -0
  80. tabeval/plugins/core/models/tabular_aim.py +133 -0
  81. tabeval/plugins/core/models/tabular_arf.py +150 -0
  82. tabeval/plugins/core/models/tabular_ddpm/__init__.py +218 -0
  83. tabeval/plugins/core/models/tabular_ddpm/gaussian_multinomial_diffsuion.py +950 -0
  84. tabeval/plugins/core/models/tabular_ddpm/modules.py +117 -0
  85. tabeval/plugins/core/models/tabular_ddpm/utils.py +177 -0
  86. tabeval/plugins/core/models/tabular_encoder.py +575 -0
  87. tabeval/plugins/core/models/tabular_flows.py +163 -0
  88. tabeval/plugins/core/models/tabular_gan.py +460 -0
  89. tabeval/plugins/core/models/tabular_goggle.py +241 -0
  90. tabeval/plugins/core/models/tabular_great.py +160 -0
  91. tabeval/plugins/core/models/tabular_scm.py +106 -0
  92. tabeval/plugins/core/models/tabular_vae.py +301 -0
  93. tabeval/plugins/core/models/time_series_survival/__init__.py +8 -0
  94. tabeval/plugins/core/models/time_series_survival/_base.py +65 -0
  95. tabeval/plugins/core/models/time_series_survival/benchmarks.py +442 -0
  96. tabeval/plugins/core/models/time_series_survival/loader.py +20 -0
  97. tabeval/plugins/core/models/time_series_survival/ts_surv_coxph.py +114 -0
  98. tabeval/plugins/core/models/time_series_survival/ts_surv_dynamic_deephit.py +761 -0
  99. tabeval/plugins/core/models/time_series_survival/ts_surv_xgb.py +134 -0
  100. tabeval/plugins/core/models/time_series_survival/utils.py +173 -0
  101. tabeval/plugins/core/models/time_to_event/__init__.py +4 -0
  102. tabeval/plugins/core/models/time_to_event/_base.py +76 -0
  103. tabeval/plugins/core/models/time_to_event/benchmarks.py +207 -0
  104. tabeval/plugins/core/models/time_to_event/loader.py +28 -0
  105. tabeval/plugins/core/models/time_to_event/metrics.py +127 -0
  106. tabeval/plugins/core/models/time_to_event/tte_aft.py +78 -0
  107. tabeval/plugins/core/models/time_to_event/tte_coxph.py +76 -0
  108. tabeval/plugins/core/models/time_to_event/tte_date.py +471 -0
  109. tabeval/plugins/core/models/time_to_event/tte_deephit.py +157 -0
  110. tabeval/plugins/core/models/time_to_event/tte_survival_function_regression.py +94 -0
  111. tabeval/plugins/core/models/time_to_event/tte_survival_time_series.py +153 -0
  112. tabeval/plugins/core/models/time_to_event/tte_tenn.py +498 -0
  113. tabeval/plugins/core/models/time_to_event/tte_xgb.py +151 -0
  114. tabeval/plugins/core/models/transformer.py +62 -0
  115. tabeval/plugins/core/models/ts_gan.py +845 -0
  116. tabeval/plugins/core/models/ts_model.py +734 -0
  117. tabeval/plugins/core/models/ts_tabular_gan.py +323 -0
  118. tabeval/plugins/core/models/ts_tabular_vae.py +271 -0
  119. tabeval/plugins/core/models/ts_vae.py +603 -0
  120. tabeval/plugins/core/models/vae.py +601 -0
  121. tabeval/plugins/core/plugin.py +733 -0
  122. tabeval/plugins/core/schema.py +336 -0
  123. tabeval/plugins/core/serializable.py +150 -0
  124. tabeval/plugins/domain_adaptation/__init__.py +0 -0
  125. tabeval/plugins/domain_adaptation/plugin_radialgan.py +917 -0
  126. tabeval/plugins/generic/__init__.py +18 -0
  127. tabeval/plugins/generic/plugin_arf.py +170 -0
  128. tabeval/plugins/generic/plugin_bayesian_network.py +187 -0
  129. tabeval/plugins/generic/plugin_ctgan.py +285 -0
  130. tabeval/plugins/generic/plugin_ddpm.py +249 -0
  131. tabeval/plugins/generic/plugin_dummy_sampler.py +72 -0
  132. tabeval/plugins/generic/plugin_goggle.py +253 -0
  133. tabeval/plugins/generic/plugin_great.py +206 -0
  134. tabeval/plugins/generic/plugin_marginal_distributions.py +71 -0
  135. tabeval/plugins/generic/plugin_nflow.py +292 -0
  136. tabeval/plugins/generic/plugin_rtvae.py +230 -0
  137. tabeval/plugins/generic/plugin_scm.py +96 -0
  138. tabeval/plugins/generic/plugin_tvae.py +233 -0
  139. tabeval/plugins/generic/plugin_uniform_sampler.py +70 -0
  140. tabeval/plugins/images/__init__.py +18 -0
  141. tabeval/plugins/images/plugin_image_adsgan.py +331 -0
  142. tabeval/plugins/images/plugin_image_cgan.py +324 -0
  143. tabeval/plugins/privacy/__init__.py +18 -0
  144. tabeval/plugins/privacy/plugin_adsgan.py +282 -0
  145. tabeval/plugins/privacy/plugin_aim.py +160 -0
  146. tabeval/plugins/privacy/plugin_decaf.py +471 -0
  147. tabeval/plugins/privacy/plugin_dpgan.py +294 -0
  148. tabeval/plugins/privacy/plugin_pategan.py +531 -0
  149. tabeval/plugins/privacy/plugin_privbayes.py +641 -0
  150. tabeval/plugins/survival_analysis/__init__.py +18 -0
  151. tabeval/plugins/survival_analysis/_survival_pipeline.py +216 -0
  152. tabeval/plugins/survival_analysis/plugin_survae.py +172 -0
  153. tabeval/plugins/survival_analysis/plugin_survival_ctgan.py +173 -0
  154. tabeval/plugins/survival_analysis/plugin_survival_gan.py +221 -0
  155. tabeval/plugins/survival_analysis/plugin_survival_nflow.py +159 -0
  156. tabeval/plugins/time_series/__init__.py +18 -0
  157. tabeval/plugins/time_series/plugin_fflows.py +285 -0
  158. tabeval/plugins/time_series/plugin_timegan.py +436 -0
  159. tabeval/plugins/time_series/plugin_timevae.py +343 -0
  160. tabeval/utils/anonymization.py +323 -0
  161. tabeval/utils/callbacks.py +173 -0
  162. tabeval/utils/compression.py +192 -0
  163. tabeval/utils/constants.py +4 -0
  164. tabeval/utils/dataframe.py +23 -0
  165. tabeval/utils/datasets/__init__.py +0 -0
  166. tabeval/utils/datasets/categorical/__init__.py +0 -0
  167. tabeval/utils/datasets/categorical/categorical_adult.py +33 -0
  168. tabeval/utils/datasets/categorical/data/__init__ .py +0 -0
  169. tabeval/utils/datasets/time_series/__init__.py +0 -0
  170. tabeval/utils/datasets/time_series/data/__init__.py +0 -0
  171. tabeval/utils/datasets/time_series/google_stocks.py +82 -0
  172. tabeval/utils/datasets/time_series/pbc.py +198 -0
  173. tabeval/utils/datasets/time_series/sine.py +96 -0
  174. tabeval/utils/evaluation.py +123 -0
  175. tabeval/utils/optimizer.py +228 -0
  176. tabeval/utils/optuna_sample.py +27 -0
  177. tabeval/utils/redis_wrapper.py +27 -0
  178. tabeval/utils/reproducibility.py +24 -0
  179. tabeval/utils/samplers.py +321 -0
  180. tabeval/utils/serialization.py +172 -0
  181. tabeval/version.py +4 -0
  182. tabeval-2025.8.8.dist-info/METADATA +297 -0
  183. tabeval-2025.8.8.dist-info/RECORD +185 -0
  184. tabeval-2025.8.8.dist-info/WHEEL +4 -0
  185. 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