BenchmarkDPFair 0.1.0__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.
@@ -0,0 +1,4 @@
1
+ from .benchmark import BenchmarkInfo,benchmark
2
+ from .dataconf import BenchmarkDatasetConfig
3
+
4
+ __all__ = ["BenchmarkInfo", "BenchmarkDatasetConfig", "benchmark"]
@@ -0,0 +1,282 @@
1
+ import os
2
+ import warnings
3
+ import pandas as pd
4
+ import numpy as np
5
+ import inspect
6
+
7
+ from typing import Callable, List, Any, Optional, Tuple, Union
8
+ from sklearn.model_selection import train_test_split
9
+ from tabulate import tabulate
10
+
11
+ from .dataconf import BenchmarkDatasetConfig
12
+ from .utils.types import FloatOrTuple, DFTuple
13
+ from .utils.verifiers import check_data_loader, check_splitdata, check_target, read_verification, check_dict
14
+
15
+ from .utils.benchmark import Benchmark
16
+ from .utils.auxiliar import save_experiment
17
+
18
+ DEFAULT_SEEDS : List[float]= [5,42,253,4112,32645,602627,153073,53453,178753,243421,767707,113647,796969,553067,96797,133843,6977,460403,126613,583879]
19
+ DEFAULT_EPS : List[float] = [0.05, 0.1, 0.25, 0.5, 0.75, 1, 2, 3, 5, 10, 15, 20]
20
+ DP_ALGORITHM : str = ""
21
+
22
+ class BenchmarkInfo:
23
+ def __init__(self, dp_method:str, output_dir: str, data_loader: Optional[Callable[..., DFTuple]] = None, dlkwargs: Union[dict, set] = {},
24
+ split_data: Optional[FloatOrTuple] = None, normalize: bool = True, seeds: List[float] = DEFAULT_SEEDS,
25
+ eps: List[Union[float,int]] = DEFAULT_EPS, classifier: Any = None, classifier_kwargs: Optional[Union[dict,set]] = None):
26
+ """
27
+ Set of possible confiigurations for the Benchmark experiments.
28
+
29
+ **In case you do not use our own generator, read the documentation first to understand how the benchmark expects the data to be organized.**
30
+
31
+ Parameters
32
+ ----------
33
+ dp_method : str
34
+ Which DP symthetic data generator was used
35
+ output_dir : str
36
+ Directory to save the experiment logs and metrics.
37
+ data_loader : Callable, optional
38
+ In case a new data loader needs to be used, refer to the documentation to understand the default data loader's behaviour. data_loader must accept seed as an argument and also kwargs.
39
+ dlkwargs : dict | set, optional
40
+ Custom parameters for the data loader.
41
+ split_data : FloatOrTuple, optional
42
+ Split distributions used while loading data. If not provided, the final distributions are **0.6, 0.2 and 0.2**, which is `split_data = (0.4, 0.5)`.
43
+ normalize : bool, optional
44
+ Allow MinMax normalization of the data. Default is **True**.
45
+ seeds : List[int], optional
46
+ List of seeds for the benchmark. Used to increase reproducibility.
47
+ eps : List[float|int], optional
48
+ List of DP epsilons (privacy budget) analysed during the benchmark.
49
+ classifier : Any, optional
50
+ Custom classifier. **Must implement fit, predict and predict_proba**. Default is [XGBoost](https://xgboost.readthedocs.io/en/stable/).
51
+ classifier_kwargs : dict | set, optional
52
+ Custom parameters for the classifier.
53
+ """
54
+
55
+ self.dp_method = dp_method
56
+ self.output_dir = output_dir
57
+ self.normalize = normalize
58
+ self.seeds = seeds
59
+ self.eps = eps
60
+
61
+ global DP_ALGORITHM
62
+ DP_ALGORITHM = self.dp_method
63
+
64
+ check_splitdata(split_data)
65
+ self.split = split_data
66
+
67
+ # Wrap user-supplied function with enforcement
68
+ self.data_loader = check_data_loader(data_loader) if data_loader is not None else self.__data_loader
69
+ self.custom_loader = False if data_loader is None else True
70
+ self.dlkwargs = dlkwargs
71
+
72
+ self.classifier = classifier
73
+ self.classifier_kwargs = classifier_kwargs
74
+
75
+ def dataloader(self, **kwargs) -> DFTuple:
76
+ """
77
+ Data loader, by default assumes that within the `baseline_dir` there exists a CSV file with the name set in `filename` parameter.
78
+
79
+ If the `split_data` has been set before, it will look for the file mentioned and split it into three sets following the provided distribution.
80
+
81
+ The split happens sequentially, if two values has been provided to split, the first split (train+test) happens normally, and then the test set is split following the second distribution.
82
+
83
+ If only one number has been provided and no test directory found, the split happens sequentially following the distribution of the test set.
84
+
85
+ **Please refer to the documentation to understand how the default dataloader expects the directory structure to be like.**
86
+
87
+ Parameters
88
+ ----------
89
+ data_conf : DatasetConf
90
+ Configuration of the desired dataset.
91
+ filename : str
92
+ The name of the CSV file to load.
93
+ seed : int
94
+ The current seed used to load the file and split the data.
95
+ verbose : bool, optional
96
+ If `true` prints information on the laoded dataset.
97
+ extra_processing : Callable, optional
98
+ Custom (users) porcessing function applied to loaded data. Will be called using kwargs and the loaded data as arguments.
99
+ kwargs : Any, optional,
100
+ If an extra processing function is provided, will be forwarded while calling, with the loaded dataset.
101
+
102
+ Returns
103
+ ----------
104
+ Three tuple[pd.DataFrame, pd.DataFrame]
105
+ - A 2-tuple of pandas DataFrames `(X, y)`.
106
+ """
107
+ return self.data_loader(**kwargs)
108
+
109
+
110
+ @check_data_loader
111
+ def __data_loader(self, data_conf: BenchmarkDatasetConfig, filename: str, seed: int, **kwargs) -> DFTuple:
112
+ return _load_data(data_conf, filename, seed, split=self.split, **kwargs)
113
+
114
+
115
+ def _load_data(data_conf: BenchmarkDatasetConfig, filename: str, seed: int, epsilon: Optional[float] = None,
116
+ verbose: bool=True, split: Optional[FloatOrTuple] = None, extra_processing: Optional[Callable] = None, **kwargs) -> DFTuple:
117
+
118
+ if verbose:
119
+ print(f"** Loading dataset {data_conf.name.upper()} **")
120
+
121
+ if split is None:
122
+ split = (0.4, 0.5)
123
+
124
+ base, ext = os.path.splitext(filename)
125
+ base_pattern = base.rsplit("_", 1)
126
+
127
+ if (os.path.dirname(filename)):
128
+ test_path = os.path.dirname(os.path.dirname(filename)) + "DP-dataset-test/"
129
+ else:
130
+ test_path = f"{data_conf.dir}/{data_conf.name}/{DP_ALGORITHM}/DP-dataset-test/"
131
+ filename = f"{data_conf.dir}/{data_conf.name}/{DP_ALGORITHM}/DP-dataset-{f'epsilon-{epsilon}' if epsilon is not None else 'train'}/{filename}"
132
+
133
+ test_filename = f"{base_pattern[0]}_test{ext}"
134
+
135
+ cols = list(dict.fromkeys(data_conf.usecols + [data_conf.index_col] if data_conf.index_col else data_conf.usecols))
136
+ ds = pd.read_csv(filename, usecols=lambda col: col in cols)
137
+
138
+ if data_conf.index_col:
139
+ ds.set_index(data_conf.index_col, inplace=True)
140
+
141
+ # Verify if data was read successfully
142
+ read_verification(ds, data_conf.usecols)
143
+
144
+ # Apply extra processing to dataset if the user wants it
145
+ if extra_processing is not None:
146
+ extra_processing(ds, **kwargs)
147
+
148
+ # Ensure all dataset is numerical
149
+ for col in data_conf.categorical_cols:
150
+ if not pd.api.types.is_numeric_dtype(ds[col]):
151
+ ds[col] = ds[col].astype('category').cat.codes # Int encode
152
+
153
+ X = ds.drop(columns=[data_conf.target])
154
+ y = ds[data_conf.target]
155
+
156
+ # Split data
157
+ if not os.path.exists(test_path) or not os.path.exists(test_path + "/" + test_filename):
158
+ if verbose:
159
+ train_split_distrib = 1 - split[0] if isinstance(split, Tuple) else split
160
+ val_split_distrib = split[0] * (1 - split[1]) if isinstance(split, Tuple) else split * (1 - split)
161
+ test_split_distrib = split[0] * split[1] if isinstance(split, Tuple) else split * split
162
+ print(f"[WARN] Test directory and/or file with test set not found, the provided {filename} will be split into three sets with distributions {(train_split_distrib, val_split_distrib, test_split_distrib)}.")
163
+ print(f" This is the path we are looking for: {test_path + '/' + test_filename}.\n")
164
+
165
+ # No test path found, so split the data from filename
166
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=split[0] if isinstance(split, Tuple) else split, random_state=seed)
167
+ X_val, X_test, y_val, y_test = train_test_split(X_test, y_test, test_size=split[1] if isinstance(split, Tuple) else split, random_state=seed)
168
+
169
+ else:
170
+ X_train = X
171
+ y_train = y
172
+
173
+ test_ds = pd.read_csv(test_path + "/" + test_filename, usecols=lambda col: col in cols)
174
+
175
+ if data_conf.index_col:
176
+ test_ds.set_index(data_conf.index_col, inplace=True)
177
+
178
+ # Verify if data was read successfully
179
+ read_verification(test_ds, data_conf.usecols)
180
+
181
+ # Apply extra processing to dataset if the user wants it
182
+ if extra_processing is not None:
183
+ extra_processing(test_ds, **kwargs)
184
+
185
+ X_test = test_ds.drop(columns=[data_conf.target])
186
+ y_test = test_ds[data_conf.target]
187
+
188
+ if isinstance(split, Tuple):
189
+ print(f"[WARN] You provided a tuple {split} of splitting distribution and a test directory and file has been found in {test_path}, the second value of the tuple will be used.\n")
190
+
191
+ X_val, X_test, y_val, y_test = train_test_split(X_test, y_test, test_size=split[1] if isinstance(split, Tuple) else split, random_state=seed)
192
+
193
+ if verbose:
194
+ data = [
195
+ ["X_train", X_train.shape],
196
+ ["X_val", X_val.shape],
197
+ ["X_test", X_test.shape],
198
+ ["y_train", y_train.shape],
199
+ ["y_val", y_val.shape],
200
+ ["y_test", y_test.shape],
201
+ ]
202
+ print("\n#### Data Information ####")
203
+ print(tabulate(data, headers=["Dataset", "Shape"], tablefmt="github"))
204
+ print("###########################\n")
205
+
206
+ # Check that the target column is binary
207
+ check_target(y_train, data_conf.target)
208
+ check_target(y_val, data_conf.target)
209
+ check_target(y_test, data_conf.target)
210
+
211
+ return (X_train, y_train), (X_val, y_val), (X_test, y_test)
212
+
213
+
214
+ ############# Experiments #############
215
+ def _experiment(seed, dataset_conf: BenchmarkDatasetConfig, benchmark_info: BenchmarkInfo, savefile):
216
+ np.random.seed(seed)
217
+ output_dir = f"{benchmark_info.output_dir}/{dataset_conf.name}/{benchmark_info.dp_method}/results/"
218
+
219
+ print(f"\n*********************** Fair-only - seed = {seed} ***********************\n")
220
+ extra_kwargs = {
221
+ "data_conf": dataset_conf,
222
+ "filename": dataset_conf.name + f"_split_dataset_seed_{seed}_train.csv",
223
+ "custom_loader": benchmark_info.custom_loader,
224
+ "epsilon": None,
225
+ "seed": seed,
226
+ "classifier": benchmark_info.classifier,
227
+ "classifier_kwargs": benchmark_info.classifier_kwargs
228
+ }
229
+
230
+ original_experiment = Benchmark(
231
+ name="baseline", data_loader=benchmark_info.data_loader,
232
+ normalize=benchmark_info.normalize, seed=seed, dlkwargs=benchmark_info.dlkwargs, ekwargs = extra_kwargs
233
+ )
234
+ original_experiment.run()
235
+
236
+ save_experiment(original_experiment, seed, filename=savefile, path=output_dir,synth=benchmark_info.dp_method)
237
+
238
+ del original_experiment
239
+
240
+ for epsilon in benchmark_info.eps:
241
+ print(f"\n*********************** DP & DP+Fair | ε={epsilon} ***********************\n")
242
+ extra_kwargs = {
243
+ "data_conf": dataset_conf,
244
+ "filename": dataset_conf.name + f"_split_dataset_seed_{seed}_epsilon-{epsilon}.csv",
245
+ "custom_loader": benchmark_info.custom_loader,
246
+ "epsilon": epsilon,
247
+ "seed": seed,
248
+ "classifier": benchmark_info.classifier,
249
+ "classifier_kwargs": benchmark_info.classifier_kwargs
250
+ }
251
+ dp_experiment = Benchmark(
252
+ name="dp", data_loader=benchmark_info.data_loader,
253
+ normalize=benchmark_info.normalize, seed=seed, dlkwargs=benchmark_info.dlkwargs, ekwargs=extra_kwargs
254
+ )
255
+ dp_experiment.run()
256
+
257
+ save_experiment(dp_experiment, seed, epsilon, filename=savefile, path=output_dir,synth=benchmark_info.dp_method)
258
+
259
+ del dp_experiment.data_loader, dp_experiment
260
+
261
+
262
+ def benchmark(data_conf: BenchmarkDatasetConfig, benchmark_info: BenchmarkInfo):
263
+ """
264
+ Execute benchmark of Fairness interventions on models trained on original data and differentially private synthetic data.
265
+
266
+ **The results obtained are output into a csv file in the defined output directory.**
267
+
268
+ Parameters
269
+ -----------
270
+ data_conf: BenchmarkDatasetConfig
271
+ Configurations on the dataset used
272
+
273
+ benchmark_info: BenchmarkInfo
274
+ Configurations about the experiments
275
+ """
276
+
277
+ print(f"Running DP Benchmark on dataset: '{data_conf.name}' with target: '{data_conf.target}' and sensitive attribute: '{data_conf.sensitive_attr}'")
278
+
279
+ savefile = f"benchmark_results_seeds_{'_'.join(str(seed) for seed in benchmark_info.seeds)}_eps_{'_'.join(str(e) for e in benchmark_info.eps)}_synth_{benchmark_info.dp_method}.csv"
280
+
281
+ for seed in benchmark_info.seeds:
282
+ _experiment(seed, data_conf, benchmark_info, savefile)
@@ -0,0 +1,58 @@
1
+ from typing import List, Optional
2
+
3
+ class BenchmarkDatasetConfig:
4
+ def __init__(self, name : str, target : str, sensitive_attr : str, sensitive_cols : List[str] = [], categorical_cols : List[str] = [],
5
+ ordinal_cols : List[str] = [], continuous_cols : List[str] = [], root_dir : str = "../../data/", usecols : Optional[List[str]] = None, index_col : Optional[str] = None):
6
+ """
7
+ Configuration of a given dataset.
8
+
9
+ Parameters
10
+ ----------
11
+ name : str
12
+ Name of the dataset, this will be used for outputing logs
13
+ dir : str
14
+ Path to the root directory of the dataset. For example "../../data/" for the Adult dataset already provided.
15
+ target : str
16
+ Column to be predicted and/or used as ground truth.
17
+ sensitive_attr : str
18
+ Senstive attribute in the dataset. So far, only one is possible. Ex: **race**.
19
+ categorical_cols : List[str]
20
+ Columns with categorical data.
21
+ ordinal_cols : List[str]
22
+ Columns with ordinal data.
23
+ continuous_cols : List[str]
24
+ Columns with continuous data.
25
+ usecols : List[str], optional
26
+ Columns to be read from the dataset file. If empty or none, all columns will be read.
27
+ """
28
+
29
+ self.name = name
30
+ self.dir = root_dir
31
+
32
+ self.target = target
33
+ self.sensitive_attr = sensitive_attr
34
+ self.sensitive_cols = sensitive_cols or [sensitive_attr]
35
+ self.categorical_cols = categorical_cols
36
+ self.ordinal_cols = ordinal_cols
37
+ self.continuous_cols = continuous_cols
38
+ self.index_col = index_col
39
+
40
+ if usecols is None or len(usecols) == 0:
41
+ usecols = [self.target] + self.categorical_cols + self.continuous_cols + self.ordinal_cols + self.sensitive_cols
42
+
43
+ self.usecols = usecols
44
+
45
+ if not name:
46
+ raise ValueError(f"Argument 'name' must not be empty as it is necessary for the benchmark.")
47
+
48
+ if not root_dir:
49
+ self.dir = "./"
50
+
51
+ if not sensitive_attr:
52
+ raise ValueError(f"A sensitive attribute is required for the benchmark.")
53
+
54
+ if len(categorical_cols) == 0 and len(ordinal_cols) == 0 and len(continuous_cols) == 0:
55
+ raise ValueError(f"The columns must be of one of the three categories: Categorical, Ordinal or Continuous.")
56
+
57
+ def __str__(self):
58
+ return f"BenchmarkDatasetConfig(name={self.name},dir={self.dir},target={self.target},sensitive_attr={self.sensitive_attr},categorical_cols={self.categorical_cols},ordinal_cols={self.ordinal_cols},continuous_cols={self.continuous_cols})"
File without changes
@@ -0,0 +1,94 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import os
4
+
5
+ def getMetrics(metric):
6
+
7
+ DI = metric.disparate_impact() if metric is not None else None
8
+ ACC = metric.accuracy() if metric is not None else None
9
+ ACC_PRIV = metric.accuracy(privileged=True) if metric is not None else None
10
+ ACC_UNPRIV = metric.accuracy(privileged=False) if metric is not None else None
11
+ PREC = metric.precision() if metric is not None else None
12
+ REC = metric.recall() if metric is not None else None
13
+ MAD = metric.accuracy(privileged=False) - metric.accuracy(privileged=True) if metric is not None else None
14
+ EOD = metric.equal_opportunity_difference() if metric is not None else None
15
+ TPR = metric.true_positive_rate() if metric is not None else None
16
+ FPR = metric.false_positive_rate() if metric is not None else None
17
+ TNR = metric.true_negative_rate() if metric is not None else None
18
+ FNR = metric.false_negative_rate() if metric is not None else None
19
+ SPD = metric.statistical_parity_difference() if metric is not None else None
20
+ EODD = metric.equalized_odds_difference() if metric is not None else None
21
+
22
+ if metric is not None:
23
+ del metric
24
+
25
+ return {
26
+ "DI": DI if (DI is not None and not np.isnan(DI)) else 'inf',
27
+ "ACC": ACC if (ACC is not None and not np.isnan(ACC)) else 'inf',
28
+ "ACC_PRIV": ACC_PRIV if (ACC_PRIV is not None and not np.isnan(ACC_PRIV)) else 'inf',
29
+ "ACC_UNPRIV": ACC_UNPRIV if (ACC_UNPRIV is not None and not np.isnan(ACC_UNPRIV)) else 'inf',
30
+ "PREC": PREC if (PREC is not None and not np.isnan(PREC)) else 'inf',
31
+ "REC": REC if (REC is not None and not np.isnan(REC) ) else 'inf',
32
+ "MAD": MAD if (MAD is not None and not np.isnan(MAD) ) else 'inf',
33
+ "EOD": EOD if (EOD is not None and not np.isnan(EOD) ) else 'inf',
34
+ "TPR": TPR if (TPR is not None and not np.isnan(TPR) ) else 'inf',
35
+ "FPR": FPR if (FPR is not None and not np.isnan(FPR) ) else 'inf',
36
+ "TNR": TNR if (TNR is not None and not np.isnan(TNR) ) else 'inf',
37
+ "FNR": FNR if (FNR is not None and not np.isnan(FNR) ) else 'inf',
38
+ "SPD": SPD if (SPD is not None and not np.isnan(SPD) ) else 'inf',
39
+ "EODD": EODD if (EODD is not None and not np.isnan(EODD) ) else 'inf',
40
+ }
41
+
42
+ def save_experiment(experiment, seed, eps=None, filename="exp_metrics.csv", path="../data/metrics/", synth=""):
43
+ results = []
44
+ exp_set_original = "original_classification_metrics"
45
+ exp_set_mitigator = "mitigated_classification_metrics"
46
+
47
+ logs = []
48
+
49
+
50
+ for r in experiment.results:
51
+ if "error" in r:
52
+ logs.append({
53
+ "Seed": seed,
54
+ "Epsilon": eps if eps is not None else "",
55
+ "Fair-Method": r["mitigator"] if "mitigator" in r else "",
56
+ "DP-Method": synth if "dp_method" in r else "",
57
+ "Error": r["error"],
58
+ "Info": r["info"]
59
+ })
60
+ r.pop("error", None)
61
+ r.pop("info", None)
62
+
63
+ for exp_set in [exp_set_original, exp_set_mitigator]:
64
+ if exp_set in r:
65
+ if r[exp_set] is None:
66
+ continue
67
+
68
+ results.append({
69
+ "Seed": seed,
70
+ "Epsilon": eps if eps is not None else "",
71
+ "Fair-Method": r["mitigator"] if "mitigator" in r else "",
72
+ "DP-Method": synth if "dp_method" in r else "",
73
+ **(r[exp_set]),
74
+ })
75
+
76
+ del experiment.results
77
+
78
+ # Check if file exists
79
+ file_exists = os.path.isfile(path+filename)
80
+
81
+ # Create directory if it doesn't exist
82
+ os.makedirs(os.path.dirname(path), exist_ok=True)
83
+
84
+ # Save results to CSV
85
+ pd.DataFrame(results).to_csv(path + filename, index=False, mode='a', header=not file_exists)
86
+
87
+ # Check if file exists
88
+ file_exists = os.path.isfile(path+"log/"+filename.replace(".csv", "-log.csv"))
89
+
90
+ # Create directory if it doesn't exist
91
+ os.makedirs(os.path.dirname(path + "log/"), exist_ok=True)
92
+
93
+ # Save logs to CSV
94
+ pd.DataFrame(logs).to_csv(path + "log/" + filename.replace(".csv", "-log.csv"), index=False, mode='a', header=not file_exists)
@@ -0,0 +1,176 @@
1
+ EXP_CLASSES = ["original", "pre", "pos", "in"]
2
+
3
+ import sys
4
+ import traceback
5
+ import pandas as pd
6
+ from sklearn.preprocessing import MinMaxScaler
7
+ from aif360.metrics import ClassificationMetric
8
+ from aif360.datasets import BinaryLabelDataset
9
+ from xgboost import XGBClassifier
10
+
11
+ from .pre import pre_mitigator_experiment
12
+ from .inp import in_mitigator_experiment
13
+ from .pos import pos_mitigator_experiment
14
+
15
+ from .auxiliar import getMetrics
16
+ from .verifiers import check_signatures
17
+
18
+ import gc
19
+
20
+ def original_experiment(x_train, y_train, x_test, y_test, sensitive_attr, target_column, seed=42, normalize=True, threshold=.5, classifier=None, classifier_kwargs=None):
21
+ privileged_groups = [{sensitive_attr: 1}] # Ex: White
22
+ unprivileged_groups = [{sensitive_attr: 0}] # Ex: Not white
23
+
24
+ scaler = None
25
+ if normalize:
26
+ scaler = MinMaxScaler()
27
+
28
+ if scaler is not None:
29
+ cols = x_train.columns
30
+ x_train = scaler.fit_transform(x_train)
31
+ x_train = pd.DataFrame(x_train, columns=cols)
32
+ x_test = scaler.transform(x_test)
33
+ x_test = pd.DataFrame(x_test, columns=cols)
34
+
35
+ model = XGBClassifier(objective='binary:logistic', random_state=seed)
36
+
37
+ if classifier is not None:
38
+ model = classifier(random_state=seed, **classifier_kwargs)
39
+
40
+ model.fit(x_train, y_train)
41
+
42
+ y_pred_prob = None
43
+ y_pred = None
44
+
45
+ y_pred_prob = model.predict_proba(x_test)[:, 1]
46
+ y_pred = (y_pred_prob >= threshold).astype(int)
47
+ y_preds = pd.DataFrame(y_pred, columns=[target_column])
48
+
49
+ # Reset the index
50
+ y_preds = y_preds.reset_index(drop=True)
51
+ x_test = x_test.reset_index(drop=True)
52
+ og_dataset_test = pd.concat([x_test, y_preds], axis=1)
53
+
54
+
55
+ og_dataset_test_pred = BinaryLabelDataset(df=og_dataset_test, label_names=[target_column], protected_attribute_names=[sensitive_attr],
56
+ unprivileged_protected_attributes=unprivileged_groups)
57
+
58
+
59
+ y_test = y_test.reset_index(drop=True)
60
+ df_test = pd.concat([x_test, y_test], axis=1)
61
+ df_test = BinaryLabelDataset(df=df_test, label_names=[target_column], protected_attribute_names=[sensitive_attr],
62
+ unprivileged_protected_attributes=unprivileged_groups)
63
+
64
+
65
+ og_classification_metrics = ClassificationMetric(df_test, og_dataset_test_pred, unprivileged_groups=unprivileged_groups, privileged_groups=privileged_groups)
66
+ og_metrics = getMetrics(og_classification_metrics)
67
+
68
+ og_classification_metrics.dataset = None
69
+ og_classification_metrics.classified_dataset = None
70
+
71
+ del scaler, y_preds, model, x_train, y_train, x_test, y_test, og_dataset_test, y_pred, y_pred_prob, og_dataset_test_pred, df_test, og_classification_metrics
72
+
73
+ return {
74
+ "original_classification_metrics": og_metrics,
75
+ }
76
+
77
+
78
+ class Benchmark:
79
+ def __init__(self, name, data_loader, normalize=None, seed=42, verbose=False, threshold=.5, dlkwargs=None, ekwargs = None):
80
+ """
81
+ :param name: name of the experiment
82
+ :param model: instance of a ML model.
83
+ :param normalize: should use a normalizer
84
+ :param data_loader: instance of DataLoader
85
+ :param seed: seed for reproducibility
86
+ :param verbose: verbosity of the experiment
87
+ """
88
+ self.name = name
89
+ self.data_loader = data_loader
90
+ self.normalize = normalize
91
+ self.seed = seed
92
+ self.verbose = verbose
93
+ self.mitigators = {
94
+ "reweigh":"pre",
95
+ "dir": "pre",
96
+ "lfr": "pre",
97
+ "egr": "in",
98
+ "gsr": "in",
99
+ "roc":"pos",
100
+ "eqodds":"pos",
101
+ "ceop": "pos",
102
+ }
103
+ self.threshold = threshold
104
+ self.results = []
105
+ self.dlkwargs = dlkwargs
106
+ self.ekwargs = ekwargs
107
+
108
+
109
+ def run(self):
110
+
111
+ data_conf = self.ekwargs["data_conf"]
112
+ classifier = self.ekwargs["classifier"]
113
+ ckwargs = self.ekwargs["classifier_kwargs"]
114
+
115
+ args = check_signatures(self.data_loader, self.dlkwargs|self.ekwargs)
116
+ train_data, cal_data, test_data = self.data_loader(**args)
117
+
118
+ X_train, y_train = train_data[0].copy(), train_data[1].copy()
119
+ X_cal, y_cal = cal_data[0].copy(), cal_data[1].copy()
120
+ X_test, y_test = test_data[0].copy(), test_data[1].copy()
121
+
122
+
123
+ # Run the original experiment
124
+ print("# Original - ", end="")
125
+ try:
126
+ self.results.append(original_experiment(X_train, y_train, X_test, y_test,
127
+ data_conf.sensitive_attr, data_conf.target, self.seed, self.normalize, self.threshold,
128
+ classifier=classifier, classifier_kwargs=ckwargs))
129
+ except Exception as e:
130
+ self.results.append({"original_classification_metrics": getMetrics(None), "error": e, 'info': traceback.format_tb(e.__traceback__)})
131
+ print("OK", flush=True)
132
+
133
+
134
+ # Run the experiment with mitigators
135
+ for mitigator, exp_class in self.mitigators.items():
136
+ print(f"# {exp_class.upper()} - {mitigator.upper()} - ", end="")
137
+
138
+ X_train, y_train = train_data[0].copy(), train_data[1].copy()
139
+ X_cal, y_cal = cal_data[0].copy(), cal_data[1].copy()
140
+ X_test, y_test = test_data[0].copy(), test_data[1].copy()
141
+
142
+ try:
143
+ if exp_class == "pre":
144
+ self.results.append(pre_mitigator_experiment(X_train, y_train, X_cal, y_cal, X_test, y_test,
145
+ data_conf.sensitive_attr, data_conf.target, mitigator, self.seed, self.normalize, self.threshold,
146
+ classifier=classifier, classifier_kwargs=ckwargs))
147
+ elif exp_class == "pos":
148
+ self.results.append(pos_mitigator_experiment(X_train, y_train, X_cal, y_cal, X_test, y_test,
149
+ data_conf.sensitive_attr, data_conf.target, mitigator, self.seed, self.normalize, self.threshold,
150
+ classifier=classifier, classifier_kwargs=ckwargs))
151
+ else:
152
+ self.results.append(in_mitigator_experiment(X_train, y_train, X_cal, y_cal, X_test, y_test,
153
+ data_conf.sensitive_attr, data_conf.target, mitigator, self.seed, self.normalize, self.threshold,
154
+ classifier=classifier, classifier_kwargs=ckwargs))
155
+
156
+ except Exception as e:
157
+ self.results.append({
158
+ "mitigator": mitigator, "original_classification_metrics": getMetrics(None),
159
+ "mitigated_classification_metrics": getMetrics(None), "error": e, "dp_method": True,
160
+ 'info': traceback.format_tb(e.__traceback__)
161
+ })
162
+
163
+ X_train = None
164
+ y_train = None
165
+ X_cal = None
166
+ y_cal = None
167
+ X_test = None
168
+ y_test = None
169
+
170
+ del X_train, y_train, X_cal, y_cal, X_test, y_test
171
+ print("OK", flush=True)
172
+
173
+ del train_data, cal_data, test_data
174
+ gc.collect()
175
+
176
+