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.
- BenchmarkDPFair/Benchmark/__init__.py +4 -0
- BenchmarkDPFair/Benchmark/benchmark.py +282 -0
- BenchmarkDPFair/Benchmark/dataconf.py +58 -0
- BenchmarkDPFair/Benchmark/utils/__init__.py +0 -0
- BenchmarkDPFair/Benchmark/utils/auxiliar.py +94 -0
- BenchmarkDPFair/Benchmark/utils/benchmark.py +176 -0
- BenchmarkDPFair/Benchmark/utils/inp.py +141 -0
- BenchmarkDPFair/Benchmark/utils/pos.py +183 -0
- BenchmarkDPFair/Benchmark/utils/pre.py +233 -0
- BenchmarkDPFair/Benchmark/utils/types.py +15 -0
- BenchmarkDPFair/Benchmark/utils/verifiers.py +102 -0
- BenchmarkDPFair/DataGenerator/__init__.py +5 -0
- BenchmarkDPFair/DataGenerator/dataconf.py +94 -0
- BenchmarkDPFair/DataGenerator/datagen.py +246 -0
- BenchmarkDPFair/DataGenerator/utils/verifiers.py +28 -0
- BenchmarkDPFair/__init__.py +4 -0
- benchmarkdpfair-0.1.0.dist-info/METADATA +165 -0
- benchmarkdpfair-0.1.0.dist-info/RECORD +21 -0
- benchmarkdpfair-0.1.0.dist-info/WHEEL +5 -0
- benchmarkdpfair-0.1.0.dist-info/licenses/LICENSE +21 -0
- benchmarkdpfair-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
|