diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 12460f1..0ef35fc 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -26,12 +26,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} # ${{ github.event.pull_request.head.sha }} - name: Setup Python 3.10 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.10" cache: 'pip' @@ -70,13 +70,13 @@ jobs: steps: - name: Checkout to latest changes - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ needs.formatting.outputs.new_sha }} fetch-depth: 0 - name: Set up Python 3.10 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.10" cache: 'pip' @@ -94,13 +94,13 @@ jobs: steps: - name: Checkout to latest changes - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ needs.formatting.outputs.new_sha }} fetch-depth: 0 - name: Set up Python 3.10 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.10" cache: 'pip' @@ -125,13 +125,13 @@ jobs: steps: - name: Checkout to latest changes - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ needs.formatting.outputs.new_sha }} fetch-depth: 0 - name: Set up Python 3.10 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.10" cache: 'pip' diff --git a/.gitignore b/.gitignore index 135e228..4d85b65 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ results/ data/ logs/ external/ +benchmark*/ +*.png +*.csv other/ # C extensions *.so diff --git a/benchmark.py b/benchmark.py new file mode 100644 index 0000000..f5e8dc9 --- /dev/null +++ b/benchmark.py @@ -0,0 +1,159 @@ +import subprocess +import time +import os +import yaml +import sys +from itertools import product + +experiment_name = "experiment_all_10percent" +benchmark_dir = "benchmark_results" + + +model_names = [ + "logistic_regression", + "elastic_net", + "lsvc", + "random_forest", + "balanced_random_forest", + # # "weighted_random_forest", + "xgb" + ] + +datasets = [ + # "kaggle_hf", + "diabetes", + # "ukbb_cvd", + # "cvd" + ] + +num_clients = [ + 3, + 5, + 10, + 20 +] + +dirichlet_alpha = [ + None, + # 1.0, + # 0.7 +] + +data_normalization = ["global"] +n_features = [None] + +# Normalization experiment +# experiment_name = "normalization" +# benchmark_dir = "benchmark_results_normalization" +# model_names = ["logistic_regression"] +# datasets = ["diabetes", "ukbb_cvd"] +# num_clients = [10] +# dirichlet_alpha = [0.7, None] +# data_normalization = ["global", "local", None] + +# # Feature selection experiment +# experiment_name = "feature_selection" +# benchmark_dir = "benchmark_results_feature_selection" +# model_names = ["balanced_random_forest"] +# datasets = ["ukbb_cvd"] +# num_clients = [5,10] +# dirichlet_alpha = [0.7, None] +# data_normalization = ["global"] +# n_features = [10, 20, 35, 40, None] + +# # Number of Clients ablation experiment +experiment_name = "num_clients_ablation" +benchmark_dir = "benchmark_results_num_clients_ablation" +model_names = [ + "logistic_regression", + "elastic_net", + "lsvc", + "random_forest", + "balanced_random_forest", + "xgb" + ] +datasets = ["diabetes"] +num_clients = [3,5,10,20] +dirichlet_alpha = [0.7, 1.0, None] +data_normalization = ["global"] +n_features = [None] + +os.makedirs(benchmark_dir, exist_ok=True) + +with open("config.yaml", "r") as f: + config = yaml.safe_load(f) + + +config_path = os.path.join(benchmark_dir, "config.yaml") +log_file_path = os.path.join(benchmark_dir, "run_log.txt") + +with open(config_path, "w") as f: + yaml.dump(config, f) + +config['data_path'] = 'dataset/' +config['experiment']['log_path'] = benchmark_dir + +start_time = time.time() + +# Flatten the nested loops into a single iterator +parameters = product(datasets, num_clients, dirichlet_alpha, model_names, data_normalization, n_features) + +try: + for ds_name, n_client, alpha, m_name, norm, n_feat in parameters: + print(f"Running benchmark: {ds_name}, {m_name}, clients: {n_client}, alpha: {alpha}, normalization: {norm}, features: {n_feat}") + + # Update config dictionary + config.update({ + 'model': m_name, + 'dataset': ds_name, + 'num_clients': n_client, + 'dirichlet_alpha': alpha, + 'data_normalization': norm, + 'n_features': n_feat + }) + if "forest" in m_name: + config['num_rounds'] = 1 # Set number of jobs for parallel processing + + config['experiment']['name'] = f"{experiment_name}_{ds_name}_{m_name}_c{n_client}_a{alpha}_norm{norm}_feat{n_feat}" + + with open(config_path, "w") as f: + yaml.dump(config, f) + + # subprocess.run is cleaner for synchronous execution + # Use a list for the command to avoid shell=True security/cleanup issues + cmd = f"python repeated.py {config_path} | tee {log_file_path}" + subprocess.run(cmd, shell=True, check=True) + +except KeyboardInterrupt: + print("\nBenchmark interrupted by user. Exiting...") + sys.exit(1) + + + +# # Run benchmark experiments +# # Iterate over datasets and models +# for dataset_name in datasets: +# for num_client in num_clients: +# for alpha in dirichlet_alpha: +# for model_name in model_names: +# print(f"Running benchmark for dataset: {dataset_name}, model: {model_name}") +# config['experiment']['name'] = f"{experiment_name}_{dataset_name}_{model_name}_clients_{num_client}_alpha_{alpha}" +# config['model'] = model_name +# config['dataset'] = dataset_name +# config['num_clients'] = num_client +# config['dirichlet_alpha'] = alpha + +# with open(config_path, "w") as f: +# yaml.dump(config, f) + +# try: +# run_process = subprocess.Popen(f"python repeated.py {config_path} | tee {log_file_path}", shell=True) +# run_process.wait() + +# except KeyboardInterrupt: +# run_process.terminate() +# run_process.wait() +# break + +total_time = time.time() - start_time +print("Benchmark experiments finished in", total_time/60, " minutes") diff --git a/config.yaml b/config.yaml index 4c561dc..917fdc1 100644 --- a/config.yaml +++ b/config.yaml @@ -10,8 +10,10 @@ ################################################################################ ############## Dataset type to use -# Possible values: , kaggle_hf, mnist, dt4h_format -dataset: dt4h_format +# Possible values: , kaggle_hf, diabetes, mnist, dt4h_format +dataset: kaggle_hf +# dataset: ukbb_cvd +# dataset: diabetes #custom #libsvm #kaggle_hf @@ -33,30 +35,54 @@ train_size: 0.7 # ****** * * * * * * * * * * * * * * * * * * * * ******************* ############## Number of clients (data centers) to use for training -num_clients: 1 +num_clients: 4 ############## Model type # Possible values: logistic_regression, lsvc, elastic_net, random_forest, weighted_random_forest, xgb # See README.md for a full list of supported models -model: random_forest +# model: xgb +model: logistic_regression +# model: random_forest #logistic_regression #random_forest ############## Training length -num_rounds: 50 +num_rounds: 10 ############## Metric to select the best model # Possible values: accuracy, balanced_accuracy, f1, precision, recall -checkpoint_selection_metric: precision +# checkpoint_selection_metric: precision +checkpoint_selection_metric: balanced_accuracy #balanced_accuracy ############## Experiment logging experiment: - name: experiment_1 + name: experiment_kaggle_standard log_path: logs debug: true +################################################################################ +# Federated Data Preprocessing +################################################################################ + +# Strategy to calculate data preprocessing parameters between clients. +# It covers missing data imputation, label encoding, normalization and feature selection +# It can be one of: + # "reference" - use reference center to calculate all parameters (largest or random) + # "equal_aggregate" - aggregate parameters from all clients based on mean and voting disregarding center size + # "weighted_aggregate" - aggregate parameters from all clients based on weighted mean and voting + +data_preprocessing_method: "equal_aggregate" +# data_preprocessing_method: "reference" + +# Toggle data normalization (Standard scaler) based on largest center (global) or local client +data_normalization: "global" + +# Determine target for feature selection number +n_features: Null + + ################################################################################ # Aggregation methods ################################################################################ @@ -87,9 +113,13 @@ smoothWeights: linear_models: n_features: 9 + +dirichlet_alpha: Null + # Random Forest random_forest: balanced_rf: true + tree_num: 300 # Weighted Random Forest weighted_random_forest: @@ -101,7 +131,7 @@ xgb: batch_size: 32 num_iterations: 100 task_type: BINARY - tree_num: 500 + tree_num: 300 held_out_center_id: -1 @@ -113,6 +143,6 @@ seed: 42 local_port: 8081 -data_path: dataset/icrc-dataset/ +data_path: dataset/ production_mode: False # Turn on to use environment variables such as data path, server address, certificates etc. diff --git a/flcore/client_selector.py b/flcore/client_selector.py index 76fa3d5..c0c616b 100644 --- a/flcore/client_selector.py +++ b/flcore/client_selector.py @@ -1,7 +1,7 @@ import numpy as np import flcore.models.linear_models as linear_models -import flcore.models.xgb as xgb +import flcore.models.xgblr as xgblr import flcore.models.random_forest as random_forest import flcore.models.weighted_random_forest as weighted_random_forest @@ -11,14 +11,14 @@ def get_model_client(config, data, client_id): if model in ("logistic_regression", "elastic_net", "lsvc"): client = linear_models.client.get_client(config,data,client_id) - elif model == "random_forest": + elif model in ("random_forest", "balanced_random_forest"): client = random_forest.client.get_client(config,data,client_id) elif model == "weighted_random_forest": client = weighted_random_forest.client.get_client(config,data,client_id) - elif model == "xgb": - client = xgb.client.get_client(config, data, client_id) + elif model == "xgblr": + client = xgblr.client.get_client(config, data, client_id) else: raise ValueError(f"Unknown model: {model}") diff --git a/flcore/compile_results.py b/flcore/compile_results.py index 8270d9b..4f8d7b8 100644 --- a/flcore/compile_results.py +++ b/flcore/compile_results.py @@ -8,6 +8,7 @@ def compile_results(experiment_dir: str): + print(f"Compiling results for experiment in {experiment_dir}") per_client_metrics = {} held_out_metrics = {} fit_metrics = {} @@ -21,6 +22,8 @@ def compile_results(experiment_dir: str): elif config['dataset'] == 'kaggle_hf': center_names = ['Cleveland', 'Hungary', 'VA', 'Switzerland'] + else: + center_names = [f"center_{i+1}" for i in range(config['num_clients'])] writer = open(f"{experiment_dir}/metrics.txt", "w") @@ -48,7 +51,10 @@ def compile_results(experiment_dir: str): history = yaml.safe_load(open(os.path.join(fold_dir, "history.yaml"), "r")) selection_metric = 'val '+ config['checkpoint_selection_metric'] + # selection_metric = config['checkpoint_selection_metric'] best_round= int(np.argmax(history['metrics_distributed'][selection_metric])) + # best_round = -1 + print(f"Best round for {directory} based on {selection_metric}: {best_round}") # client_order = history['metrics_distributed']['per client client_id'][best_round] client_order = history['metrics_distributed']['per client n samples'][best_round] for logs in history.keys(): @@ -98,7 +104,8 @@ def compile_results(experiment_dir: str): fit_metrics[metric] = np.vstack((fit_metrics[metric], values_history[best_round])) - execution_stats = ['client_id', 'round_time [s]', 'n samples', 'training_time [s]'] + # execution_stats = ['client_id', 'round_time [s]', 'n samples', 'training_time [s]'] + execution_stats = ['client_id', 'round_time [s]', 'n samples'] # Calculate mean and std for per client metrics writer.write(f"{'Evaluation':.^100} \n\n") writer.write(f"\n{'Test set:'} \n") @@ -124,12 +131,16 @@ def compile_results(experiment_dir: str): writer.write(f"\n{'Federated finetuned locally:'} \n") personalized_section = True - # Calculate general mean and std - mean = np.average(per_client_metrics[metric]) - # Calculate std of the average metric between experiment runs - std = np.std(np.mean(per_client_metrics[metric], axis=1)) - per_client_mean = np.around(np.mean(per_client_metrics[metric], axis=0), 3) - per_client_std = np.around(np.std(per_client_metrics[metric], axis=0), 3) + # Calculate general weighted mean and std + # Weighted by number of samples in each client + weights = np.array(per_client_metrics['n samples'][0]) + per_client_mean = np.mean(per_client_metrics[metric], axis=0) + per_client_std = np.std(per_client_metrics[metric], axis=0) + mean = np.average(per_client_mean, weights=weights) + std = np.sqrt(np.average((per_client_mean - mean) ** 2, weights=weights)) + # Round per client mean and std to 3 decimals + per_client_mean = np.around(per_client_mean, 3) + per_client_std = np.around(per_client_std, 3) if metric not in execution_stats: writer.write(f"{metric:<30}: {mean:<6.3f} ±{std:<6.3f} \t\t\t|| Per client {metric} {per_client_mean} ({per_client_std})\n".replace("\n", "")+"\n") for i, _ in enumerate(per_client_mean): @@ -161,25 +172,25 @@ def compile_results(experiment_dir: str): centralized_metrics[metric] = held_out_metrics[metric] held_out_metrics.pop(metric, None) - writer.write(f"\n{'Held out set evaluation':.^100} \n\n") - for metric in held_out_metrics: - center = int(held_out_metrics['client_id'][0]) - center = center_names[center]+' (held out)' - mean = np.average(held_out_metrics[metric]) - std = np.std(held_out_metrics[metric]) - - writer.write(f"{metric:<30}: {mean:<6.3f} ±{std:<6.3f}\n") - if center not in csv_dict: - csv_dict[center] = {} - csv_dict[center][metric] = mean - csv_dict[center][metric+'_std'] = std - - # Calculate mean and std for centralized metrics - writer.write(f"\n{'Centralized evaluation':.^100} \n\n") - for metric in centralized_metrics: - mean = np.average(centralized_metrics[metric]) - std = np.std(centralized_metrics[metric]) - writer.write(f"{metric:<30}: {mean:<6.3f} ±{std:<6.3f}\n") + # writer.write(f"\n{'Held out set evaluation':.^100} \n\n") + # for metric in held_out_metrics: + # center = int(held_out_metrics['client_id'][0]) + # center = center_names[center]+' (held out)' + # mean = np.average(held_out_metrics[metric]) + # std = np.std(held_out_metrics[metric]) + + # writer.write(f"{metric:<30}: {mean:<6.3f} ±{std:<6.3f}\n") + # if center not in csv_dict: + # csv_dict[center] = {} + # csv_dict[center][metric] = mean + # csv_dict[center][metric+'_std'] = std + + # # Calculate mean and std for centralized metrics + # writer.write(f"\n{'Centralized evaluation':.^100} \n\n") + # for metric in centralized_metrics: + # mean = np.average(centralized_metrics[metric]) + # std = np.std(centralized_metrics[metric]) + # writer.write(f"{metric:<30}: {mean:<6.3f} ±{std:<6.3f}\n") writer.close() @@ -194,7 +205,7 @@ def compile_results(experiment_dir: str): # Write to csv df.to_csv(f"{experiment_dir}/per_center_results.csv", index=True) - generate_report(experiment_dir) + # generate_report(experiment_dir) if __name__ == "__main__": diff --git a/flcore/datasets.py b/flcore/datasets.py index 699c4a0..68d048f 100644 --- a/flcore/datasets.py +++ b/flcore/datasets.py @@ -12,18 +12,569 @@ import pandas as pd from sklearn.datasets import load_svmlight_file -from sklearn.preprocessing import OrdinalEncoder, MinMaxScaler,StandardScaler +from sklearn.preprocessing import OrdinalEncoder, LabelEncoder, MinMaxScaler, StandardScaler from sklearn.model_selection import KFold, StratifiedShuffleSplit, train_test_split from sklearn.utils import shuffle -from sklearn.feature_selection import SelectKBest, f_classif +from sklearn.feature_selection import SelectKBest, f_classif, mutual_info_classif +from sklearn.ensemble import RandomForestClassifier +from ucimlrepo import fetch_ucirepo +import pickle -from flcore.models.xgb.utils import TreeDataset, do_fl_partitioning, get_dataloader + +from flcore.models.xgblr.utils import TreeDataset, do_fl_partitioning, get_dataloader XY = Tuple[np.ndarray, np.ndarray] Dataset = Tuple[XY, XY] +def calculate_preprocessing_params(subset_data, subset_target, n_features=None, feature_selection_method='mutual_info'): + """ + Calculate preprocessing parameters based on a subset of data (reference center) + + Args: + subset_data: DataFrame containing the subset data + subset_target: Series containing the target variable + n_features: Number of features to select (None for all features) + feature_selection_method: Method for feature selection ('mutual_info', 'f_classif', 'random_forest') + + Returns: + dict: Preprocessing parameters (imputation values, mean, std, label_encoders, feature_selector) + """ + data_copy = subset_data.copy() + target_copy = subset_target.copy() + + # Calculate imputation parameters + imputation_params = {} + label_encoders = {} + + for column in data_copy.columns: + # Handle missing values + if data_copy[column].isna().any(): + if data_copy[column].dtype in ['float64', 'int64']: + imputation_params[column] = data_copy[column].median() + else: + imputation_params[column] = data_copy[column].mode()[0] if not data_copy[column].mode().empty else 0 + + # Store label encoders for categorical variables + if data_copy[column].dtype == 'object': + le = LabelEncoder() + # Fit on non-null values only + non_null_data = data_copy[column].dropna() + if len(non_null_data) > 0: + # Add 'unknown' category for unseen labels + classes = np.append(non_null_data.astype(str).unique(), 'unknown') + le.fit(classes) + label_encoders[column] = le + + # Calculate normalization parameters for ALL columns (after conversion to numerical) + numeric_data = data_copy.copy() + + # Temporarily convert categorical to numerical for normalization parameter calculation + for column in numeric_data.columns: + if numeric_data[column].dtype == 'object': + # Use simple integer encoding for parameter calculation + numeric_data[column] = pd.Categorical(numeric_data[column]).codes + # Handle missing values temporarily for parameter calculation + if column in imputation_params: + numeric_data[column].fillna(imputation_params[column], inplace=True) + + # Convert all to numeric + numeric_data = numeric_data.apply(pd.to_numeric, errors='coerce') + + # Calculate normalization parameters + normalization_params = { + 'mean': numeric_data.mean().to_dict(), + 'std': numeric_data.std().to_dict() + } + + # Handle zero standard deviation + for col, std_val in normalization_params['std'].items(): + if std_val == 0 or np.isnan(std_val): + normalization_params['std'][col] = 1.0 + + # Feature Selection + feature_selector = None + selected_features = None + feature_scores = None + + if n_features is not None: + if n_features < len(numeric_data.columns): + # Prepare data for feature selection + X_temp = numeric_data.fillna(numeric_data.median()) + y_temp = target_copy + + # Handle any remaining NaN values + X_temp = X_temp.fillna(0) + + if feature_selection_method == 'mutual_info': + selector = SelectKBest(score_func=mutual_info_classif, k=min(n_features, X_temp.shape[1])) + elif feature_selection_method == 'f_classif': + selector = SelectKBest(score_func=f_classif, k=min(n_features, X_temp.shape[1])) + elif feature_selection_method == 'random_forest': + # Use Random Forest feature importance + rf = RandomForestClassifier(n_estimators=100, random_state=42) + rf.fit(X_temp, y_temp) + importances = rf.feature_importances_ + indices = np.argsort(importances)[::-1] + selected_indices = indices[:min(n_features, len(indices))] + + # Create a custom selector object + class CustomSelector: + def __init__(self, selected_indices, feature_names): + self.selected_indices = selected_indices + self.feature_names = feature_names + self.scores_ = importances + + def transform(self, X): + if isinstance(X, pd.DataFrame): + return X.iloc[:, self.selected_indices] + else: + return X[:, self.selected_indices] + + def get_support(self, indices=False): + if indices: + return self.selected_indices + else: + mask = np.zeros(len(self.feature_names), dtype=bool) + mask[self.selected_indices] = True + return mask + + selector = CustomSelector(selected_indices, numeric_data.columns.tolist()) + feature_scores = importances + else: + raise ValueError("feature_selection_method must be 'mutual_info', 'f_classif', or 'random_forest'") + + if feature_selection_method != 'random_forest': + selector.fit(X_temp, y_temp) + feature_scores = selector.scores_ + + feature_selector = selector + selected_features = numeric_data.columns[selector.get_support()].tolist() + + print(f"Feature selection: Selected {len(selected_features)} most informative features") + if feature_scores is not None: + # Print top feature scores + feature_importance = pd.DataFrame({ + 'feature': numeric_data.columns, + 'score': feature_scores + }).sort_values('score', ascending=False) + print("Top 5 features:") + for i, (_, row) in enumerate(feature_importance.head().iterrows()): + print(f" {i+1}. {row['feature']}: {row['score']:.4f}") + + return { + 'imputation': imputation_params, + 'normalization': normalization_params, + 'label_encoders': label_encoders, + 'feature_selector': feature_selector, + 'selected_features': selected_features, + 'n_features': n_features + } + +def apply_preprocessing(subset_data, preprocessing_params, normalization="global"): + """ + Apply preprocessing to a subset using pre-calculated parameters from reference center + + Args: + subset_data: DataFrame to preprocess + preprocessing_params: dict from calculate_preprocessing_params + + Returns: + tuple: (preprocessed_data, feature_names) + """ + data_copy = subset_data.copy() + + # Step 1: Handle missing values using reference center parameters + for column in data_copy.columns: + if column in preprocessing_params['imputation']: + missing_mask = data_copy[column].isna() + if missing_mask.any(): + data_copy.loc[missing_mask, column] = preprocessing_params['imputation'][column] + + # Step 2: Convert all features to numerical using reference center label encoders + for column in data_copy.columns: + if column in preprocessing_params['label_encoders']: + le = preprocessing_params['label_encoders'][column] + # Convert to string and handle unseen labels + encoded_values = [] + for val in data_copy[column]: + if pd.isna(val): + encoded_values.append(-1) # Special value for missing + else: + str_val = str(val) + if str_val in le.classes_: + encoded_values.append(le.transform([str_val])[0]) + else: + # Map unseen labels to 'unknown' class + encoded_values.append(le.transform(['unknown'])[0]) + data_copy[column] = encoded_values + elif data_copy[column].dtype == 'object': + # Fallback: use categorical codes for any remaining object columns + data_copy[column] = pd.Categorical(data_copy[column]).codes + + # Ensure all data is numerical + data_copy = data_copy.apply(pd.to_numeric, errors='coerce') + + # Step 3: Normalize ALL features using global parameters if enabled + if normalization == "global": + normalization_params = preprocessing_params['normalization'] + for column in data_copy.columns: + if column in normalization_params['mean']: + mean_val = normalization_params['mean'][column] + std_val = normalization_params['std'][column] + data_copy[column] = (data_copy[column] - mean_val) / std_val + # print("Applied global normalization during preprocessing.") + elif normalization == "local": + # Calculate local normalization parameters + local_mean = data_copy.mean() + local_std = data_copy.std() + for column in data_copy.columns: + mean_val = local_mean[column] + std_val = local_std[column] if local_std[column] != 0 else 1.0 + data_copy[column] = (data_copy[column] - mean_val) / std_val + # print("Applied local normalization during preprocessing.") + elif normalization is not None: + raise ValueError("Data normalization method must be 'global', 'local', or None") + + # Step 4: Apply feature selection if enabled + if preprocessing_params['feature_selector'] is not None: + selector = preprocessing_params['feature_selector'] + data_copy = pd.DataFrame(selector.transform(data_copy), + columns=preprocessing_params['selected_features']) + + return data_copy, data_copy.columns.tolist() + +def partition_data_dirichlet(labels, num_centers, alpha=1.0, min_samples_per_class=10): + """ + Partition data among centers using Dirichlet distribution + + Args: + labels: Array of class labels + num_centers: Number of centers to partition into + alpha: Dirichlet concentration parameter + min_samples_per_class: Minimum number of samples per class per center + """ + unique_labels = np.unique(labels) + n_samples = len(labels) + n_classes = len(unique_labels) + + if not alpha: + alpha = -1.0 + + if alpha <= 0: + # IID partitioning + shuffled_indices = np.random.permutation(n_samples) + center_indices = np.array_split(shuffled_indices, num_centers) + center_indices = [indices.tolist() for indices in center_indices] + # check lengths of each center + center_lengths = [len(indices) for indices in center_indices] + return center_indices + + # Create assignment matrix + center_indices = [[] for _ in range(num_centers)] + + # For each class, distribute samples to centers using Dirichlet distribution + for class_idx in unique_labels: + class_mask = (labels == class_idx) + class_indices = np.where(class_mask)[0] + n_class_samples = len(class_indices) + + if n_class_samples > 0: + # Generate Dirichlet distribution for this class + proportions = np.random.dirichlet(np.repeat(alpha, num_centers)) + proportions = proportions / proportions.sum() + + # Calculate number of samples for each center + center_samples = (proportions * n_class_samples).astype(int) + + # Ensure minimum samples per class per center + for i in range(num_centers): + if center_samples[i] < min_samples_per_class: + center_samples[i] = min(min_samples_per_class, n_class_samples // num_centers) + + # Adjust for rounding errors and minimum constraints + total_assigned = center_samples.sum() + diff = n_class_samples - total_assigned + if diff > 0: + # Distribute remaining samples + available_centers = [i for i in range(num_centers) if center_samples[i] < n_class_samples] + if available_centers: + additions = np.random.choice(available_centers, diff, replace=True) + for i in additions: + center_samples[i] += 1 + elif diff < 0: + # Remove excess samples + excess_centers = np.argsort(center_samples)[::-1] # Sort by size descending + for i in excess_centers: + if diff >= 0: + break + can_remove = center_samples[i] - min_samples_per_class + if can_remove > 0: + remove = min(can_remove, -diff) + center_samples[i] -= remove + diff += remove + + # Shuffle and assign indices + np.random.shuffle(class_indices) + ptr = 0 + for center_id in range(num_centers): + if center_samples[center_id] > 0: + center_indices[center_id].extend( + class_indices[ptr:ptr + center_samples[center_id]] + ) + ptr += center_samples[center_id] + + # Shuffle indices within each center + for center_id in range(num_centers): + np.random.shuffle(center_indices[center_id]) + + return center_indices +def select_reference_center(all_center_data, method='largest'): + """ + Select which center to use for calculating preprocessing parameters + """ + if method == 'largest': + center_sizes = [len(X) for X, y in all_center_data] + reference_center_id = np.argmax(center_sizes) + print(f"Selected largest center (ID: {reference_center_id}) with {center_sizes[reference_center_id]} samples") + + elif method == 'random': + reference_center_id = np.random.randint(0, len(all_center_data)) + print(f"Selected random center (ID: {reference_center_id})") + else: + raise ValueError("Method must be 'largest' or 'random'") + + return reference_center_id + +def aggregate_preprocessing_params(preprocessing_params_list, center_sizes, method='weighted_aggregate'): + """ + Aggregate preprocessing parameters from multiple centers using weighted aggregation. + + Args: + preprocessing_params_list: List of preprocessing parameter dictionaries from each center + center_sizes: List of center sizes (number of samples) + + Returns: + dict: Aggregated preprocessing parameters + """ + if not preprocessing_params_list: + raise ValueError("preprocessing_params_list cannot be empty") + + if "equal" in method: + # Equal weights + center_sizes = [1 for _ in center_sizes] + print("Using equal weights for aggregation of preprocessing parameters.") + + total_size = sum(center_sizes) + weights = [size / total_size for size in center_sizes] + + aggregated = { + 'imputation': {}, + 'normalization': {'mean': {}, 'std': {}}, + 'label_encoders': {}, + 'feature_selector': None, + 'selected_features': [], + 'n_features': preprocessing_params_list[0]['n_features'] # Assume same for all + } + + # Collect all columns + all_columns = set() + for params in preprocessing_params_list: + all_columns.update(params['imputation'].keys()) + all_columns.update(params['normalization']['mean'].keys()) + all_columns.update(params['label_encoders'].keys()) + + # Aggregate imputation + for col in all_columns: + numeric_values = [] + categorical_values = [] + weights_num = [] + weights_cat = [] + for params, weight in zip(preprocessing_params_list, weights): + if col in params['imputation']: + value = params['imputation'][col] + if isinstance(value, (int, float)) and not pd.isna(value): + numeric_values.append(value) + weights_num.append(weight) + else: + categorical_values.append(value) + weights_cat.append(weight) + + if numeric_values: + # Weighted mean for numeric + aggregated['imputation'][col] = sum(v * w for v, w in zip(numeric_values, weights_num)) / sum(weights_num) + elif categorical_values: + # Most frequent for categorical (simple mode) + from collections import Counter + counter = Counter(categorical_values) + aggregated['imputation'][col] = counter.most_common(1)[0][0] + + # Aggregate normalization + for col in all_columns: + means = [] + stds = [] + weights_norm = [] + for params, weight in zip(preprocessing_params_list, weights): + if col in params['normalization']['mean']: + means.append(params['normalization']['mean'][col]) + stds.append(params['normalization']['std'][col]) + weights_norm.append(weight) + + if means: + global_mean = sum(m * w for m, w in zip(means, weights_norm)) / sum(weights_norm) + aggregated['normalization']['mean'][col] = global_mean + + # Calculate global std: sqrt( sum(w_i * var_i) + sum(w_i * (mean_i - global_mean)^2) ) + variances = [s ** 2 for s in stds] + weighted_var_sum = sum(v * w for v, w in zip(variances, weights_norm)) + mean_diff_sq = [(m - global_mean) ** 2 for m in means] + weighted_mean_var = sum(md * w for md, w in zip(mean_diff_sq, weights_norm)) + global_var = weighted_var_sum + weighted_mean_var + global_std = np.sqrt(global_var) if global_var > 0 else 1.0 + aggregated['normalization']['std'][col] = global_std + + # For label_encoders, take from the largest center (simplest approach) + max_size_idx = center_sizes.index(max(center_sizes)) + aggregated['label_encoders'] = preprocessing_params_list[max_size_idx]['label_encoders'].copy() + + # Aggregate selected_features by frequency + if preprocessing_params_list[0]['selected_features']: + from collections import Counter + feature_counts = Counter() + for params, weight in zip(preprocessing_params_list, weights): + for feature in params['selected_features']: + feature_counts[feature] += weight + + # Select top n_features most frequent + n_features = aggregated['n_features'] + if n_features: + selected = [feat for feat, _ in feature_counts.most_common(n_features)] + aggregated['selected_features'] = selected + + return aggregated + +def prepare_dataset(X, y, center_id, config, center_indices=None): + """ + Load and preprocess raw dataset for federated learning with feature selection + + This function will extract the following config values: + center_id: Identifier for the federated node + num_centers: Total number of federated centers + alpha: Dirichlet concentration parameter for data partitioning + reference_method: How to select reference center ('largest' or 'random') + aggregation_method: How to aggregate preprocessing params ('reference' or 'weighted_aggregate') + global_preprocessing_params: Precomputed parameters (if None, will calculate) + n_features: Number of features to select (None for all features) + feature_selection_method: Method for feature selection + + Returns: + tuple: X_train, y_train, X_test, y_test + """ + + num_centers = config.get("num_clients", 5) + alpha = config.get("dirichlet_alpha", 1.0) + reference_method = config.get("reference_center_method", "largest") + preprocessing_method = config.get("data_preprocessing_method", "reference") + min_samples_per_class = config.get("min_samples_per_class", 10) + global_preprocessing_params = None + n_features = config.get("n_features", 20) + feature_selection_method = config.get("feature_selection_method", "mutual_info") + normalization_method = config.get("data_normalization", "global") + + np.random.seed(42) # For reproducibility of partitioning and reference selection + + # Convert target to binary classification if needed + if y.nunique() > 2: + y_binary = (y > y.median()).astype(int) + else: + y_binary = y + + if not center_indices: + # Partition data using Dirichlet distribution + all_center_indices = partition_data_dirichlet(y_binary.values, num_centers, alpha, min_samples_per_class) + else: + all_center_indices = center_indices + + # Get all center data for reference selection + all_center_data = [] + for i in range(num_centers): + if i < len(all_center_indices) and len(all_center_indices[i]) > 0: + X_center = X.iloc[all_center_indices[i]] + all_center_data.append((X_center, y_binary.iloc[all_center_indices[i]])) + else: + all_center_data.append((pd.DataFrame(), pd.Series())) + + # Calculate or use global preprocessing parameters + if global_preprocessing_params is None: + if preprocessing_method == 'reference': + # Select reference center and calculate parameters + reference_center_id = select_reference_center(all_center_data, reference_method) + X_reference = all_center_data[reference_center_id][0] + y_reference = all_center_data[reference_center_id][1] + + if len(X_reference) == 0: + # Fallback: use full dataset if reference center is empty + X_reference = X + y_reference = y_binary + print("Warning: Reference center empty, using full dataset for preprocessing parameters") + + global_preprocessing_params = calculate_preprocessing_params( + X_reference, y_reference, n_features=n_features, feature_selection_method=feature_selection_method + ) + elif "aggregate" in preprocessing_method: + # Calculate parameters for each center and aggregate + preprocessing_params_list = [] + center_sizes = [] + for X_center, y_center in all_center_data: + if len(X_center) > 0: + params = calculate_preprocessing_params( + X_center, y_center, n_features=n_features, feature_selection_method=feature_selection_method + ) + preprocessing_params_list.append(params) + center_sizes.append(len(X_center)) + + if preprocessing_params_list: + global_preprocessing_params = aggregate_preprocessing_params(preprocessing_params_list, center_sizes, method=preprocessing_method) + else: + # Fallback + global_preprocessing_params = calculate_preprocessing_params( + X, y_binary, n_features=n_features, feature_selection_method=feature_selection_method + ) + print("Warning: No valid centers, using full dataset for preprocessing parameters") + else: + raise ValueError("aggregation_method must be 'reference', 'equal_aggregate' or 'weighted_aggregate'") + + print("Calculated global preprocessing parameters using", preprocessing_method) + + if center_id is not None: + # Get indices for the requested center + if center_id >= len(all_center_indices) or len(all_center_indices[center_id]) == 0: + raise ValueError(f"Center ID {center_id} has no data assigned") + + center_indices = all_center_indices[center_id] + X_center = X.iloc[center_indices].reset_index(drop=True) + y_center = y.iloc[center_indices].reset_index(drop=True) + else: + # Use full dataset if no center_id specified + X_center = X + y_center = y + + # Split into train/test for this center + if len(X_center) > 1: + X_train, X_test, y_train, y_test = train_test_split( + X_center, y_center, test_size=0.2, random_state=config['seed'], stratify=y_center + ) + else: + X_train, y_train = X_center, y_center + X_test, y_test = X_center.iloc[:0], y_center.iloc[:0] + + # Apply GLOBAL preprocessing parameters to both train and test sets + X_train_processed, feature_names = apply_preprocessing(X_train, global_preprocessing_params, normalization=normalization_method) + X_test_processed, _ = apply_preprocessing(X_test, global_preprocessing_params, normalization=normalization_method) + + return X_train_processed, y_train, X_test_processed, y_test + def load_mnist(center_id=None, num_splits=5): """Loads the MNIST dataset using OpenML. OpenML dataset link: https://www.openml.org/d/554 @@ -67,109 +618,51 @@ def load_mnist(center_id=None, num_splits=5): return (x_train, y_train), (x_test, y_test) - -def load_cvd(data_path, center_id=None) -> Dataset: +def load_cvd(data_path, center_id, config) -> Dataset: id = center_id - if center_id == 1: - file_name = data_path+'data_center1.csv' - elif center_id == 2: - file_name = data_path+'data_center2.csv' - elif center_id == 3: - file_name = data_path+'data_center3.csv' - else: - file_name = data_path+'data_center3.csv' - - if id == None: - # id = 'All' - data_centers = ['All'] - else: - data_centers = [id] - - X_train_list, y_train_list = [], [] - X_test_list, y_test_list = [], [] - test_index_list = [] - train_index_list = [] - for id in data_centers: - # file_name = os.path.join(data_path, f"data_center{id}.csv") - # file_name = os.path.join(data_path, file_name) + code_id = "f_eid" + code_outcome = "Eval" - code_id = "f_eid" - code_outcome = "Eval" + data = pd.read_csv(os.path.join(data_path, "data_centerAll.csv")) + X_data = data.drop([code_id, code_outcome], axis=1) + y_data = data[code_outcome] - data = pd.read_csv(file_name) - X_data = data.drop([code_id, code_outcome], axis=1) - y_data = data[code_outcome] - f_eid = data[code_id] - - # Split the data - sss = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=None) - train_index, test_index = next(sss.split(X_data, y_data)) - X_test = X_data.iloc[test_index, :] - X_train = X_data.iloc[train_index, :] - y_test, y_train = y_data.iloc[test_index], y_data.iloc[train_index] - # We save the names - f_eid.iloc[test_index] - f_eid.iloc[train_index] - - X_train_list.append(X_train) - y_train_list.append(y_train) - X_test_list.append(X_test) - y_test_list.append(y_test) - train_index_list.append(train_index) - test_index_list.append(test_index) - - X_train = pd.concat(X_train_list) - y_train = pd.concat(y_train_list) - X_test = pd.concat(X_test_list) - y_test = pd.concat(y_test_list) - train_index = np.concatenate(train_index_list) - test_index = np.concatenate(test_index_list) - - # Verify set difference, data centers overlap - # print(len(train_index.tolist())) - # print(len(test_index.tolist())) - # train_set = set(train_index.tolist()) - # test_set = set(test_index.tolist()) - # diff = train_set.intersection(test_set) - # print(len(train_set)) - # print(len(test_set)) - # print( len(diff) ) - # print(f"SUBSET {id}") - # train_unique = np.unique(y_train, return_counts=True) - # test_unique = np.unique(y_test, return_counts=True) - # train_max_acc = train_unique[1][0]/len(y_train) - # test_max_acc = test_unique[1][0]/len(y_test) - # print(np.unique(y_train, return_counts=True)) - # print(np.unique(y_test, return_counts=True)) - # print(train_max_acc) - # print(test_max_acc) + X_train_processed, y_train, X_test_processed, y_test = prepare_dataset(X_data, y_data, center_id, config) - return (X_train, y_train), (X_test, y_test) + return (X_train_processed, y_train), (X_test_processed, y_test) def load_ukbb_cvd(data_path, center_id, config) -> Dataset: + """ + Load UKBB CVD mortality dataset + + Args: + data_path: Path to the dataset + center_id: ID of the center to load + config: Configuration dictionary - seed = config["seed"] + """ data_path = os.path.join(data_path, "CVDMortalityData.csv") data = pd.read_csv(data_path) - # print(len(data)) - center_key = 'f.54.0.0' patient_key = 'f.eid' label_key = 'label' - # center_id = None - # center_id = 1 - preprocessing_data = data.loc[(data[center_key] == 1)] - # center_id = None - if center_id is not None: - center_id = center_id - if center_id == 19: - center_id = 21 - elif center_id == 21: - center_id = 19 - data = data.loc[(data[center_key] == center_id)] + #Create a list of lists for each center_key with row indexes from that center + center_keys = sorted(list(data[center_key].unique())) + # convert to list of ints + center_keys = set(int(center) for center in center_keys) + center_indices = [] + for center in center_keys: + center_indices.append(data.loc[(data[center_key] == center)].index.tolist()) + + X = data.drop([label_key, center_key, patient_key], axis=1) + y = data[label_key] + + X_train, y_train, X_test, y_test = prepare_dataset(X, y, center_id, config, center_indices) + + # print("Center ", center_id, "with ", len(X_train), " samples, of which positive samples are ", len(X_train.loc[y_train == 1])) # center_names = ['Bristol', 'Newcastle', 'Oxford', 'Stockport (pilot)', 'Reading', # 'Middlesborough', 'Leeds', 'Liverpool', 'Nottingham', 'Glasgow', 'Croydon', @@ -182,228 +675,54 @@ def load_ukbb_cvd(data_path, center_id, config) -> Dataset: # center_dict = list(center_dict.values()) # print(center_dict) - # xx - - # for i in range(0, 23): - # center_data = data.loc[(data[center_key] == i)] - # print(f'Center ID: {i} {center_dict[i]} with {len(center_data)} samples of which positive samples are {len(center_data.loc[center_data[label_key] == 1])})') - # xx - # features = data.drop([label_key, center_key, patient_key], axis=1) - # target = data[label_key] - - # print(len(data)) - # print(features.head()) - # print(f'Center ID: {center_id} with {len(data)} samples of which positive samples are {len(data.loc[data[label_key] == 1])})') - # print(target.head()) - - def get_preprocessing_params(preprocessing_data): - - data = preprocessing_data - features = data.drop([label_key, center_key, patient_key], axis=1) - target = data[label_key] - X_train, X_test, y_train, y_test = train_test_split(features, target, test_size = 0.20, random_state = seed, stratify=target) - - n_features = 40 - fs = SelectKBest(f_classif, k=n_features).fit(X_train, y_train) - index_features = fs.get_support() - X_train = X_train.iloc[:, index_features] - - # print(X_train.head()) - - # Get the unique values of the categorical features - col = list(X_train.columns) - categorical_features = [] - numerical_features = [] - for i in col: - if len(X_train[i].unique()) > 24: - numerical_features.append(i) - # else: - # categorical_features.append(i) - - transformers_dict = {} - - for i in categorical_features: - transformers_dict[i] = OrdinalEncoder() - for i in numerical_features: - transformers_dict[i] = StandardScaler() - - # df1 = data.copy(deep = True) - - for feature in transformers_dict: - transformers_dict[feature].fit(X_train[feature].values.reshape(-1, 1)) - - return index_features, transformers_dict - - - index_features, transformers_dict = get_preprocessing_params(preprocessing_data) - - def preprocess_data(data, index_features, column_transformer): - # Scale the data using the precomputed parameters - data = data.copy(deep = True) - features = data.drop([label_key, center_key, patient_key], axis=1) - features = features.iloc[:, index_features] - target = data[label_key] - - for feature in column_transformer: - features[feature] = column_transformer[feature].transform(features[feature].values.reshape(-1, 1)) - - X_train, X_test, y_train, y_test = train_test_split(features, target, test_size = 0.20, random_state = seed, stratify=target) - - return X_train, X_test, y_train, y_test - - X_train, X_test, y_train, y_test = preprocess_data(data, index_features, transformers_dict) - - # print shapes of the data - # print(X_train.shape) - # print(X_test.shape) - # print(y_train.shape) - # print(y_test.shape) - - # features = features.iloc[:, index_features] - - # X_train, X_test, y_train, y_test = train_test_split(features, target, test_size = 0.20, random_state = None, stratify=target) - - # print(features.head()) - - print(f'Center ID: {center_id} with {len(data)} samples of which positive samples are {len(data.loc[data[label_key] == 1])})') - - return (X_train, y_train), (X_test, y_test) - def load_kaggle_hf(data_path, center_id, config) -> Dataset: - id = center_id - seed = config["seed"] - - if id == -1: - id = 'switzerland' - elif id == 1: - id = 'hungarian' - elif id == 2: - id = 'va' - elif id == 0: - id = 'cleveland' - elif id == None: - pass - else: - raise ValueError(f"Invalid center id: {id}") - - # elif id == 5: - # id = 'cleveland' - + """ + Load Kaggle Heart Failure dataset for federated learning using prepare_dataset + + Args: + data_path: Path to the dataset + center_id: ID of the center (0: cleveland, 1: hungarian, 2: va, 3: switzerland, None: all) + config: Configuration dictionary + + Returns: + tuple: ((X_train, y_train), (X_test, y_test)) + """ + file_name = os.path.join(data_path, "kaggle_hf.csv") data = pd.read_csv(file_name) - - scaling_data = data.loc[(data['data_center'] == 'hungarian')] - # scaling_data = data - - if id is not None: - data = data.loc[(data['data_center'] == id)] - - # print('Categorical Features :',*categorical_features) - # print('Numerical Features :',*numerical_features) - - def get_preprocessing_params(data): - - # Get the unique values of the categorical features - col = list(data.columns) - categorical_features = [] - numerical_features = [] - for i in col: - if len(data[i].unique()) > 6: - numerical_features.append(i) - else: - categorical_features.append(i) - - transformers_dict = {} - - categorical_features.pop(categorical_features.index('HeartDisease')) - if 'RestingBP' in numerical_features: - numerical_features.pop(numerical_features.index('RestingBP')) - elif 'RestingBP' in categorical_features: - categorical_features.pop(categorical_features.index('RestingBP')) - categorical_features.pop(categorical_features.index('RestingECG')) - categorical_features.pop(categorical_features.index('data_center')) - numerical_features.pop(numerical_features.index('Oldpeak')) - min_max_scaling_features = ['Oldpeak'] - - for i in categorical_features: - transformers_dict[i] = OrdinalEncoder() - for i in numerical_features: - transformers_dict[i] = StandardScaler() - for i in min_max_scaling_features: - transformers_dict[i] = MinMaxScaler() - - df1 = data.copy(deep = True) - - target = df1['HeartDisease'] - X_train, X_test, y_train, y_test = train_test_split(df1, target, test_size = 0.20, random_state = seed) - - for feature in transformers_dict: - if feature == 'ST_Slope': - # Change value of last row to 'Down' to avoid error as it is missing in some splits - X_train[feature].iloc[-1] = 'Down' - transformers_dict[feature].fit(X_train[feature].values.reshape(-1, 1)) - else: - transformers_dict[feature].fit(X_train[feature].values.reshape(-1, 1)) - - return transformers_dict - + # Define centers + centers = ['cleveland', 'hungarian', 'va', 'switzerland'] - def preprocess_data(data, column_transformer): - # Scale the data using the precomputed parameters - df1 = data.copy(deep = True) - features = df1[df1.columns.drop(['HeartDisease','RestingBP','RestingECG', 'data_center'])] - target = df1['HeartDisease'] - - for feature in column_transformer: - features[feature] = column_transformer[feature].transform(features[feature].values.reshape(-1, 1)) - - X_train, X_test, y_train, y_test = train_test_split(features, target, test_size = 0.20, random_state = seed, stratify=target) - - return (X_train, y_train), (X_test, y_test) + # Map center_id to index + center_id_mapped = None + if center_id is not None: + if center_id == 0: + center_id_mapped = 0 # cleveland + elif center_id == 1: + center_id_mapped = 1 # hungarian + elif center_id == 2: + center_id_mapped = 2 # va + elif center_id == 3: + center_id_mapped = 3 # switzerland + else: + raise ValueError(f"Invalid center id: {center_id}") - - preprocessing_params = get_preprocessing_params(scaling_data) - - (X_train, y_train), (X_test, y_test) = preprocess_data(data, preprocessing_params) - - # n_females = len(X_train[X_train['Sex'] == 0]) - # print(f'n_females{n_females}') - # n_males = len(X_train[X_train['Sex'] == 1]) - # print(f'n_males{n_males}') - # print(len(X_train)) - # Get indexes of rows with men (Sex == 0) - n_females = len(X_train[X_train['Sex'] == 0]) - n_males = len(X_train[X_train['Sex'] == 1]) - print(f'Center {center_id} of size {len(X_train)} with n_females {n_females} and n_males {n_males} in training set') - - if center_id == 0: - men_indexes = X_train.index[X_train['Sex'] == 1] - female_indexes = X_train.index[X_train['Sex'] == 0] - # print(len(female_indexes)) - n_females_to_drop = int(len(female_indexes)*0.9) - female_indexes = female_indexes[:n_females_to_drop] - copy_male_indexes = men_indexes[:n_females_to_drop] - # print(len(female_indexes)) - X_train = X_train.drop(index=female_indexes) - y_train = y_train.drop(index=female_indexes) - # print(len(X_train)) - # print(f'Adding males {len(copy_male_indexes)}') - X_train = pd.concat([X_train, X_train.loc[copy_male_indexes]]) - y_train = pd.concat([y_train, y_train.loc[copy_male_indexes]]) - - if center_id == 2 or center_id == -1: - X_train = pd.concat([X_train, X_train, X_train, X_train]) - y_train = pd.concat([y_train, y_train, y_train, y_train]) - - n_females = len(X_train[X_train['Sex'] == 0]) - n_males = len(X_train[X_train['Sex'] == 1]) - print(f'Center {center_id} of size {len(X_train)} with n_females {n_females} and n_males {n_males} in training set') - # xx - return (X_train, y_train), (X_test, y_test) - + # Create center_indices + center_indices = [] + for center in centers: + indices = data.loc[data['data_center'] == center].index.tolist() + center_indices.append(indices) + + # Prepare X and y + X = data.drop(['HeartDisease', 'data_center'], axis=1) + y = data['HeartDisease'] + + X_train_processed, y_train, X_test_processed, y_test = prepare_dataset(X, y, center_id_mapped, config, center_indices) + + return (X_train_processed, y_train), (X_test_processed, y_test) def load_libsvm(config, center_id=None, task_type="BINARY"): # ## Manually download and load the tabular dataset from LIBSVM data @@ -639,6 +958,55 @@ def load_dt4h(config,id): y_test = data_target[int(dat_len*config["train_size"]):].iloc[:, 0] return (X_train, y_train), (X_test, y_test) +def load_diabetes(center_id, config): + """ + Load and preprocess diabetes dataset for federated learning with feature selection + + Args: + center_id: Identifier for the federated node + num_centers: Total number of federated centers + alpha: Dirichlet concentration parameter for data partitioning + reference_method: How to select reference center ('largest' or 'random') + global_preprocessing_params: Precomputed parameters (if None, will calculate) + n_features: Number of features to select (None for all features) + feature_selection_method: Method for feature selection + + Returns: + tuple: ((X_train, y_train), (X_test, y_test), preprocessing_params) + """ + + dataset_file = "dataset/cdc_diabetes_health_indicators.pkl" + if os.path.exists(dataset_file): + # Load from pickle + with open(dataset_file, 'rb') as f: + cdc_diabetes_health_indicators = pickle.load(f) + else: + # Download the dataset + cdc_diabetes_health_indicators = fetch_ucirepo(id=891).data + # save as pickle for faster loading next time + dataset = {"features": cdc_diabetes_health_indicators.features, "targets": cdc_diabetes_health_indicators.targets} + with open(dataset_file, 'wb') as f: + pickle.dump(dataset, f) + + # Get features and target + X = cdc_diabetes_health_indicators['features'] + y = cdc_diabetes_health_indicators['targets'] + + # convert y to a pandas Series for easier handling + y = pd.Series(y.values.flatten()) + + # # # # Use fraction of data for faster testing (optional) + if not config['num_clients'] == 1: + fraction = 1.0 + # Sample indices first, then select from both X and y + sampled_indices = X.sample(frac=fraction, random_state=42).index + X = X.loc[sampled_indices].reset_index(drop=True) + y = y.loc[sampled_indices].reset_index(drop=True) + + X_train_processed, y_train, X_test_processed, y_test = prepare_dataset(X, y, center_id, config) + + return (X_train_processed, y_train), (X_test_processed, y_test) + def cvd_to_torch(config): pass @@ -690,11 +1058,13 @@ def load_dataset(config, id=None): if config["dataset"] == "mnist": return load_mnist(id, config["num_clients"]) elif config["dataset"] == "cvd": - return load_cvd(config["data_path"], id) + return load_cvd(config["data_path"], id, config) elif config["dataset"] == "ukbb_cvd": return load_ukbb_cvd(config["data_path"], id, config) elif config["dataset"] == "kaggle_hf": return load_kaggle_hf(config["data_path"], id, config) + elif config["dataset"] == "diabetes": + return load_diabetes(id, config) elif config["dataset"] == "libsvm": return load_libsvm(config, id) elif config["dataset"] == "dt4h_format": diff --git a/flcore/metrics.py b/flcore/metrics.py index 7788f61..9bbcb89 100644 --- a/flcore/metrics.py +++ b/flcore/metrics.py @@ -8,6 +8,7 @@ BinaryPrecision, BinaryRecall, BinarySpecificity, + BinaryAUROC, ) from torchmetrics.functional.classification.precision_recall import ( @@ -43,17 +44,18 @@ def compute(self) -> Tensor: return (recall + specificity) / 2 -def get_metrics_collection(task_type="binary", device="cpu"): +def get_metrics_collection(task_type="binary", device="cpu", threshold=0.5): if task_type.lower() == "binary": return MetricCollection( { - "accuracy": BinaryAccuracy().to(device), - "precision": BinaryPrecision().to(device), - "recall": BinaryRecall().to(device), - "specificity": BinarySpecificity().to(device), - "f1": BinaryF1Score().to(device), - "balanced_accuracy": BinaryBalancedAccuracy().to(device), + "accuracy": BinaryAccuracy(threshold=threshold).to(device), + "precision": BinaryPrecision(threshold=threshold).to(device), + "recall": BinaryRecall(threshold=threshold).to(device), + "specificity": BinarySpecificity(threshold=threshold).to(device), + "f1": BinaryF1Score(threshold=threshold).to(device), + "balanced_accuracy": BinaryBalancedAccuracy(threshold=threshold).to(device), + "auroc": BinaryAUROC().to(device), } ) elif task_type.lower() == "reg": @@ -61,13 +63,25 @@ def get_metrics_collection(task_type="binary", device="cpu"): "mse": MeanSquaredError().to(device), }) -def calculate_metrics(y_true, y_pred, task_type="binary"): - metrics_collection = get_metrics_collection(task_type) + +def calculate_metrics(y_true, y_pred_proba, task_type="binary", threshold=0.5): + metrics_collection = get_metrics_collection(task_type, threshold=threshold) if not torch.is_tensor(y_true): - y_true = torch.tensor(y_true.tolist()) - if not torch.is_tensor(y_pred): - y_pred = torch.tensor(y_pred.tolist()) - metrics_collection.update(y_pred, y_true) + if isinstance(y_true, list): + y_true = torch.cat(y_true) + else: + y_true = torch.tensor(y_true.tolist()) + if not torch.is_tensor(y_pred_proba): + if isinstance(y_pred_proba, list): + y_pred_proba = torch.cat(y_pred_proba) + else: + y_pred_proba = torch.tensor(y_pred_proba.tolist()) + + # Extract probabilities for the positive class if shape>1 + if y_pred_proba.ndim > 1 and y_pred_proba.shape[1] > 1: + y_pred_proba = y_pred_proba[:, 1] + + metrics_collection.update(y_pred_proba, y_true) metrics = metrics_collection.compute() metrics = {k: v.item() for k, v in metrics.items()} @@ -89,4 +103,16 @@ def metrics_aggregation_fn(distributed_metrics): metrics['per client n samples'] = [res[0] for res in distributed_metrics] - return metrics \ No newline at end of file + return metrics + +def find_best_threshold(y_true, y_pred_proba, metric="balanced_accuracy"): + best_threshold = 0.5 + best_metric_value = 0.0 + + for threshold in np.arange(0.0, 1.01, 0.01): + metrics = calculate_metrics(y_true, y_pred_proba, threshold=threshold) + if metrics[metric] > best_metric_value: + best_metric_value = metrics[metric] + best_threshold = threshold + + return best_threshold diff --git a/flcore/models/linear_models/client.py b/flcore/models/linear_models/client.py index b7561be..b0dd88b 100644 --- a/flcore/models/linear_models/client.py +++ b/flcore/models/linear_models/client.py @@ -9,7 +9,7 @@ import flwr as fl from sklearn.metrics import log_loss from flcore.performance import measurements_metrics, get_metrics -from flcore.metrics import calculate_metrics +from flcore.metrics import calculate_metrics, find_best_threshold import time import pandas as pd from sklearn.preprocessing import StandardScaler @@ -25,7 +25,7 @@ def __init__(self, data,client_id,config): (self.X_train, self.y_train), (self.X_test, self.y_test) = data # Create train and validation split - self.X_train, self.X_val, self.y_train, self.y_val = train_test_split(self.X_train, self.y_train, test_size=0.2, random_state=42, stratify=self.y_train) + self.X_train, self.X_val, self.y_train, self.y_val = train_test_split(self.X_train, self.y_train, test_size=0.2, random_state=config['seed'], stratify=self.y_train) # #Only use the standardScaler to the continous variables # scaled_features_train = StandardScaler().fit_transform(self.X_train.values) @@ -44,7 +44,7 @@ def __init__(self, data,client_id,config): self.first_round = True self.personalize = True # Setting initial parameters, akin to model.compile for keras models - utils.set_initial_params(self.model,self.n_features) + utils.set_initial_params(self.model, (self.X_train, self.y_train), self.n_features) def get_parameters(self, config): # type: ignore #compute the feature selection @@ -67,10 +67,17 @@ def fit(self, parameters, config): # type: ignore self.model.fit(self.X_train, self.y_train) # self.model.fit(self.X_train.loc[:, parameters[2].astype(bool)], self.y_train) # y_pred = self.model.predict(self.X_test.loc[:, parameters[2].astype(bool)]) - y_pred = self.model.predict(self.X_test) - - metrics = calculate_metrics(self.y_test, y_pred) - print(f"Client {self.client_id} Evaluation just after local training: {metrics['balanced_accuracy']}") + # If LSVC is used, use decision_function instead of predict_proba + if self.model_name == 'lsvc': + y_pred_proba = self.model.decision_function(self.X_val) + else: + y_pred_proba = self.model.predict_proba(self.X_val) + best_threshold = find_best_threshold(self.y_val, y_pred_proba, metric="balanced_accuracy") + if self.model_name == 'lsvc': + y_pred_proba = self.model.decision_function(self.X_test) + else: + y_pred_proba = self.model.predict_proba(self.X_test) + metrics = calculate_metrics(self.y_test, y_pred_proba, threshold=best_threshold) # Add 'personalized' to the metrics to identify them metrics = {f"personalized {key}": metrics[key] for key in metrics} self.round_time = (time.time() - start_time) @@ -81,10 +88,19 @@ def fit(self, parameters, config): # type: ignore if self.first_round: local_model = utils.get_model(self.model_name, local=True) - utils.set_initial_params(local_model,self.n_features) + # utils.set_initial_params(local_model,self.n_features) local_model.fit(self.X_train, self.y_train) - y_pred = local_model.predict(self.X_test) - local_metrics = calculate_metrics(self.y_test, y_pred) + # Calculate validation set metrics + if self.model_name == 'lsvc': + y_pred_proba = self.model.decision_function(self.X_val) + else: + y_pred_proba = self.model.predict_proba(self.X_val) + best_threshold = find_best_threshold(self.y_val, y_pred_proba, metric="balanced_accuracy") + if self.model_name == 'lsvc': + y_pred_proba = self.model.decision_function(self.X_test) + else: + y_pred_proba = self.model.predict_proba(self.X_test) + local_metrics = calculate_metrics(self.y_test, y_pred_proba, threshold=best_threshold) #Add 'local' to the metrics to identify them local_metrics = {f"local {key}": local_metrics[key] for key in local_metrics} metrics.update(local_metrics) @@ -96,10 +112,17 @@ def evaluate(self, parameters, config): # type: ignore utils.set_model_params(self.model, parameters) # Calculate validation set metrics - y_pred = self.model.predict(self.X_val) - val_metrics = calculate_metrics(self.y_val, y_pred) + if self.model_name == 'lsvc': + y_pred_proba = self.model.decision_function(self.X_val) + else: + y_pred_proba = self.model.predict_proba(self.X_val) + best_threshold = find_best_threshold(self.y_val, y_pred_proba, metric="balanced_accuracy") + val_metrics = calculate_metrics(self.y_val, y_pred_proba, threshold=best_threshold) - y_pred = self.model.predict(self.X_test) + if self.model_name == 'lsvc': + y_pred_proba = self.model.decision_function(self.X_test) + else: + y_pred_proba = self.model.predict_proba(self.X_test) # y_pred = self.model.predict(self.X_test.loc[:, parameters[2].astype(bool)]) if(isinstance(self.model, SGDClassifier)): @@ -107,19 +130,19 @@ def evaluate(self, parameters, config): # type: ignore else: loss = log_loss(self.y_test, self.model.predict_proba(self.X_test), labels=[0, 1]) - metrics = calculate_metrics(self.y_test, y_pred) + metrics = calculate_metrics(self.y_test, y_pred_proba, threshold=best_threshold) + metrics_not_tuned = calculate_metrics(self.y_test, y_pred_proba, threshold=0.5) + metrics_not_tuned = {f"not tuned {key}": metrics_not_tuned[key] for key in metrics_not_tuned} + metrics.update(metrics_not_tuned) metrics["round_time [s]"] = self.round_time metrics["client_id"] = self.client_id - print(f"Client {self.client_id} Evaluation after aggregated model: {metrics['balanced_accuracy']}") - - # Add validation metrics to the evaluation metrics with a prefix val_metrics = {f"val {key}": val_metrics[key] for key in val_metrics} metrics.update(val_metrics) - return loss, len(y_pred), metrics + return loss, len(y_pred_proba), metrics def get_client(config,data,client_id) -> fl.client.Client: diff --git a/flcore/models/linear_models/server.py b/flcore/models/linear_models/server.py index 9204430..a49da28 100644 --- a/flcore/models/linear_models/server.py +++ b/flcore/models/linear_models/server.py @@ -138,9 +138,9 @@ def evaluate_held_out( def get_server_and_strategy(config): model_type = config['model'] - model = get_model(model_type) + # model = get_model(model_type) n_features = config['linear_models']['n_features'] - utils.set_initial_params(model, n_features) + # utils.set_initial_params(model, n_features) # Pass parameters to the Strategy for server-side parameter initialization #strategy = fl.server.strategy.FedAvg( diff --git a/flcore/models/linear_models/utils.py b/flcore/models/linear_models/utils.py index cdc36c9..512642e 100644 --- a/flcore/models/linear_models/utils.py +++ b/flcore/models/linear_models/utils.py @@ -20,14 +20,24 @@ def get_model(model_name, local=False): case "lsvc": #Linear classifiers (SVM, logistic regression, etc.) with SGD training. #If we use hinge, it implements SVM - model = SGDClassifier(max_iter=max_iter,n_iter_no_change=1000,average=True,random_state=42,class_weight= "balanced",warm_start=True,fit_intercept=True,loss="hinge", learning_rate='optimal') + model = SGDClassifier( + max_iter=max_iter, + n_iter_no_change=1000, + average=True, + # random_state=42, + class_weight= "balanced", + warm_start=True, + fit_intercept=True, + loss="hinge", + learning_rate='optimal' + ) case "logistic_regression": model = LogisticRegression( penalty="l2", #max_iter=1, # local epoch ==>> it doesn't work max_iter=max_iter, # local epoch warm_start=True, # prevent refreshing weights when fitting - random_state=42, + # random_state=42, class_weight= "balanced" #For unbalanced ) case "elastic_net": @@ -38,7 +48,7 @@ def get_model(model_name, local=False): #max_iter=1, # local epoch ==>> it doesn't work max_iter=max_iter, # local epoch warm_start=True, # prevent refreshing weights when fitting - random_state=42, + # random_state=42, class_weight= "balanced" #For unbalanced ) @@ -73,7 +83,7 @@ def set_model_params( return model -def set_initial_params(model: LinearClassifier,n_features): +def set_initial_params(model: LinearClassifier, data, n_features): """Sets initial parameters as zeros Required since model params are uninitialized until model.fit is called. But server asks for initial parameters from clients at launch. Refer @@ -82,16 +92,18 @@ def set_initial_params(model: LinearClassifier,n_features): """ n_classes = 2 # MNIST has 10 classes #n_features = 9 # Number of features in dataset + + model.fit(data[0], data[1]) model.classes_ = np.array([i for i in range(n_classes)]) - if(isinstance(model,SGDClassifier)==True): - model.coef_ = np.zeros((1, n_features)) - if model.fit_intercept: - model.intercept_ = 0 - else: - model.coef_ = np.zeros((n_classes, n_features)) - if model.fit_intercept: - model.intercept_ = np.zeros((n_classes,)) + # if(isinstance(model,SGDClassifier)==True): + # model.coef_ = np.zeros((1, n_features)) + # if model.fit_intercept: + # model.intercept_ = 0 + # else: + # model.coef_ = np.zeros((n_classes, n_features)) + # if model.fit_intercept: + # model.intercept_ = np.zeros((n_classes,)) #Evaluate in the aggregations evaluation with diff --git a/flcore/models/random_forest/FedCustomAggregator.py b/flcore/models/random_forest/FedCustomAggregator.py index 0da2e6b..adb8842 100644 --- a/flcore/models/random_forest/FedCustomAggregator.py +++ b/flcore/models/random_forest/FedCustomAggregator.py @@ -153,14 +153,6 @@ def aggregate_fit( self.time_server_round = time.time() print(f"Elapsed time: {elapsed_time} for round {server_round}") metrics_aggregated['training_time [s]'] = self.accum_time - - filename = 'server_results.txt' - with open( - filename, - "a", - ) as f: - f.write(f"Accumulated Time: {self.accum_time} for round {server_round}\n") - return parameters_aggregated, metrics_aggregated @@ -194,15 +186,6 @@ def aggregate_evaluate( elif server_round == 1: # Only log this warning once log(WARNING, "No evaluate_metrics_aggregation_fn provided") - # filename = 'server_results.txt' - # with open( - # filename, - # "a", - # ) as f: - # f.write(f"Accuracy: {metrics_aggregated['accuracy']} \n") - # f.write(f"Sensitivity: {metrics_aggregated['sensitivity']} \n") - # f.write(f"Specificity: {metrics_aggregated['specificity']} \n") - return loss_aggregated, metrics_aggregated diff --git a/flcore/models/random_forest/aggregatorRF.py b/flcore/models/random_forest/aggregatorRF.py index a55b8b8..b059309 100644 --- a/flcore/models/random_forest/aggregatorRF.py +++ b/flcore/models/random_forest/aggregatorRF.py @@ -117,8 +117,8 @@ def aggregateRF_withprevious(rfs,previous_estimators,bal_RF): #weigth, we transform into probability /sum(weights) #and random choice select according to probability distribution def aggregateRFwithSizeCenterProbs(rfs,bal_RF,smoothing_method,smoothing_strenght): - rfa= get_model(bal_RF) numberTreesperclient = int(len(rfs[0][0][0])) + rfa= get_model(bal_RF, numberTreesperclient) number_Clients = len(rfs) random_select =int(numberTreesperclient/number_Clients) list_classifiers = [] diff --git a/flcore/models/random_forest/client.py b/flcore/models/random_forest/client.py index 52e07cb..e53984b 100644 --- a/flcore/models/random_forest/client.py +++ b/flcore/models/random_forest/client.py @@ -7,7 +7,7 @@ from flcore.serialization_funs import serialize_RF, deserialize_RF import flcore.models.random_forest.utils as utils from flcore.performance import measurements_metrics -from flcore.metrics import calculate_metrics +from flcore.metrics import calculate_metrics, find_best_threshold from flwr.common import ( Code, EvaluateIns, @@ -26,12 +26,14 @@ class MnistClient(fl.client.Client): def __init__(self, data,client_id,config): self.client_id = client_id n_folds_out= config['num_rounds'] - seed=42 # Load data (self.X_train, self.y_train), (self.X_test, self.y_test) = data - self.splits_nested = datasets.split_partitions(n_folds_out,0.2, seed, self.X_train, self.y_train) - self.bal_RF = config['random_forest']['balanced_rf'] - self.model = utils.get_model(self.bal_RF) + self.splits_nested = datasets.split_partitions(n_folds_out,0.2, config['seed'], self.X_train, self.y_train) + self.bal_RF = True if config['model'] == 'balanced_random_forest' else False + self.model = utils.get_model(self.bal_RF, config['random_forest']['tree_num']) + self.round_time = 0 + self.tree_num = config['random_forest']['tree_num'] + self.first_round = True # Setting initial parameters, akin to model.compile for keras models utils.set_initial_params_client(self.model,self.X_train, self.y_train) def get_parameters(self, ins: GetParametersIns): # , config type: ignore @@ -57,32 +59,37 @@ def fit(self, ins: FitIns): # , parameters, config type: ignore with warnings.catch_warnings(): warnings.simplefilter("ignore") train_idx, val_idx = next(self.splits_nested) - X_train_2 = self.X_train.iloc[train_idx, :] - X_val = self.X_train.iloc[val_idx,:] - y_train_2 = self.y_train.iloc[train_idx] - y_val = self.y_train.iloc[val_idx] + self.X_train_2 = self.X_train.iloc[train_idx, :] + self.X_val = self.X_train.iloc[val_idx,:] + self.y_train_2 = self.y_train.iloc[train_idx] + self.y_val = self.y_train.iloc[val_idx] #To implement the center dropout, we need the execution time start_time = time.time() - self.model.fit(X_train_2, y_train_2) - #accuracy = model.score( X_test, y_test ) - # accuracy,specificity,sensitivity,balanced_accuracy, precision, F1_score = \ - # measurements_metrics(self.model,X_val, y_val) - y_pred = self.model.predict(X_val) - metrics = calculate_metrics(y_val, y_pred) - # print(f"Accuracy client in fit: {accuracy}") - # print(f"Sensitivity client in fit: {sensitivity}") - # print(f"Specificity client in fit: {specificity}") - # print(f"Balanced_accuracy in fit: {balanced_accuracy}") - # print(f"precision in fit: {precision}") - # print(f"F1_score in fit: {F1_score}") - + self.model.fit(self.X_train_2, self.y_train_2) elapsed_time = (time.time() - start_time) + y_pred_proba = self.model.predict_proba(self.X_val) + metrics = calculate_metrics(self.y_val, y_pred_proba) + metrics["running_time"] = elapsed_time + self.round_time = elapsed_time - print(f"num_client {self.client_id} has an elapsed time {elapsed_time}") - print(f"Training finished for round {ins.config['server_round']}") + if self.first_round: + local_model = utils.get_model(self.bal_RF, self.tree_num) + # utils.set_initial_params(local_model,self.n_features) + local_model.fit(self.X_train_2, self.y_train_2) + + y_pred_proba = self.model.predict_proba(self.X_val) + best_threshold = find_best_threshold(self.y_val, y_pred_proba, metric="balanced_accuracy") + + y_pred_proba = local_model.predict_proba(self.X_test) + local_metrics = calculate_metrics(self.y_test, y_pred_proba, threshold=best_threshold) + #Add 'local' to the metrics to identify them + local_metrics = {f"local {key}": local_metrics[key] for key in local_metrics} + metrics.update(local_metrics) + self.first_round = False + # Serialize to send it to the server params = utils.get_model_parameters(self.model) parameters_updated = serialize_RF(params) @@ -102,12 +109,22 @@ def evaluate(self, ins: EvaluateIns): # , parameters, config type: ignore #Deserialize to get the real parameters parameters = deserialize_RF(parameters) utils.set_model_params(self.model, parameters) + # Get threshold based on validation set + y_pred_proba = self.model.predict_proba(self.X_val) + best_threshold = find_best_threshold(self.y_val, y_pred_proba, metric="balanced_accuracy") + # Get validation metrics + val_metrics = calculate_metrics(self.y_val, y_pred_proba, threshold=best_threshold) + val_metrics = {f"val {key}": val_metrics[key] for key in val_metrics} + y_pred_prob = self.model.predict_proba(self.X_test) loss = log_loss(self.y_test, y_pred_prob) # accuracy,specificity,sensitivity,balanced_accuracy, precision, F1_score = \ # measurements_metrics(self.model,self.X_test, self.y_test) - y_pred = self.model.predict(self.X_test) - metrics = calculate_metrics(self.y_test, y_pred) + # y_pred = self.model.predict(self.X_test) + metrics = calculate_metrics(self.y_test, y_pred_prob, threshold=best_threshold) + metrics.update(val_metrics) + metrics["round_time [s]"] = self.round_time + metrics["client_id"] = self.client_id # print(f"Accuracy client in evaluate: {accuracy}") # print(f"Sensitivity client in evaluate: {sensitivity}") # print(f"Specificity client in evaluate: {specificity}") diff --git a/flcore/models/random_forest/server.py b/flcore/models/random_forest/server.py index acbfd1b..97a1373 100644 --- a/flcore/models/random_forest/server.py +++ b/flcore/models/random_forest/server.py @@ -33,8 +33,8 @@ def fit_round( server_round: int ) -> Dict: def get_server_and_strategy(config): - bal_RF = config['random_forest']['balanced_rf'] - model = get_model(bal_RF) + bal_RF = True if config['model'] == 'balanced_random_forest' else False + model = get_model(bal_RF, config['random_forest']['tree_num']) utils.set_initial_params_server( model) # Pass parameters to the Strategy for server-side parameter initialization @@ -46,6 +46,7 @@ def get_server_and_strategy(config): min_evaluate_clients = config['num_clients'], #enable evaluate_fn if we have data to evaluate in the server #evaluate_fn = utils_RF.get_evaluate_fn( model ), #no data in server + fit_metrics_aggregation_fn=metrics_aggregation_fn, evaluate_metrics_aggregation_fn = metrics_aggregation_fn, on_fit_config_fn = fit_round ) diff --git a/flcore/models/random_forest/utils.py b/flcore/models/random_forest/utils.py index 026c294..1170122 100644 --- a/flcore/models/random_forest/utils.py +++ b/flcore/models/random_forest/utils.py @@ -21,11 +21,11 @@ from typing import cast -def get_model(bal_RF): +def get_model(bal_RF, tree_num) -> RandomForestClassifier: if(bal_RF == True): - model = BalancedRandomForestClassifier(n_estimators=100,random_state=42) + model = BalancedRandomForestClassifier(n_estimators=tree_num,max_depth=10) else: - model = RandomForestClassifier(n_estimators=100,class_weight= "balanced",max_depth=2,random_state=42) + model = RandomForestClassifier(n_estimators=tree_num,max_depth=10,class_weight= "balanced_subsample") return model diff --git a/flcore/models/weighted_random_forest/client.py b/flcore/models/weighted_random_forest/client.py index 74fa60e..bd7b801 100644 --- a/flcore/models/weighted_random_forest/client.py +++ b/flcore/models/weighted_random_forest/client.py @@ -94,7 +94,7 @@ def __init__(self, data,client_id,config): # Load data (self.X_train, self.y_train), (self.X_test, self.y_test) = data self.splits_nested = datasets.split_partitions(n_folds_out,0.2, seed, self.X_train, self.y_train) - self.bal_RF = config['weighted_random_forest']['balanced_rf'] + self.bal_RF = True if config['model'] == 'balanced_random_forest' else False self.model = utils.get_model(self.bal_RF) # Setting initial parameters, akin to model.compile for keras models utils.set_initial_params_client(self.model,self.X_train, self.y_train) diff --git a/flcore/models/weighted_random_forest/server.py b/flcore/models/weighted_random_forest/server.py index 877b871..20539c2 100644 --- a/flcore/models/weighted_random_forest/server.py +++ b/flcore/models/weighted_random_forest/server.py @@ -32,7 +32,7 @@ def fit_round( server_round: int ) -> Dict: def get_server_and_strategy(config): - bal_RF = config['weighted_random_forest']['balanced_rf'] + bal_RF = True if config['model'] == 'balanced_random_forest' else False model = get_model(bal_RF) utils.set_initial_params_server( model) diff --git a/flcore/models/xgb/__init__.py b/flcore/models/xgb/__init__.py deleted file mode 100644 index 034de7d..0000000 --- a/flcore/models/xgb/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -import flcore.models.xgb.client -import flcore.models.xgb.server -import flcore.models.xgb.fed_custom_strategy -import flcore.models.xgb.utils diff --git a/flcore/models/xgblr/__init__.py b/flcore/models/xgblr/__init__.py new file mode 100644 index 0000000..478cd6d --- /dev/null +++ b/flcore/models/xgblr/__init__.py @@ -0,0 +1,4 @@ +import flcore.models.xgblr.client +import flcore.models.xgblr.server +import flcore.models.xgblr.fed_custom_strategy +import flcore.models.xgblr.utils diff --git a/flcore/models/xgb/client.py b/flcore/models/xgblr/client.py similarity index 68% rename from flcore/models/xgb/client.py rename to flcore/models/xgblr/client.py index 6bcbc1a..2a1d65a 100644 --- a/flcore/models/xgb/client.py +++ b/flcore/models/xgblr/client.py @@ -22,9 +22,10 @@ from flwr.common.typing import Parameters from torch.utils.data import DataLoader from xgboost import XGBClassifier, XGBRegressor +from sklearn.model_selection import KFold, StratifiedShuffleSplit, train_test_split -from flcore.models.xgb.cnn import CNN, test, train -from flcore.models.xgb.utils import ( +from flcore.models.xgblr.cnn import CNN, test, train +from flcore.models.xgblr.utils import ( NumpyEncoder, TreeDataset, construct_tree_from_loader, @@ -34,17 +35,19 @@ tree_encoding_loader, train_test ) +from flcore.metrics import calculate_metrics, find_best_threshold + class FL_Client(fl.client.Client): def __init__( self, task_type: str, - trainloader: DataLoader, - valloader: DataLoader, + data, client_tree_num: int, client_num: int, cid: str, + config, log_progress: bool = False, ): """ @@ -52,9 +55,6 @@ def __init__( """ self.task_type = task_type self.cid = cid - self.tree = construct_tree_from_loader(trainloader, client_tree_num, task_type) - self.trainloader_original = trainloader - self.valloader_original = valloader self.trainloader = None self.valloader = None self.client_tree_num = client_tree_num @@ -66,13 +66,25 @@ def __init__( "task_type": self.task_type, } self.tmp_dir = "" - # instantiate model self.net = CNN(client_num=client_num, client_tree_num=client_tree_num) - # determine device self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") self.round_time = -1 + self.first_round = True + batch_size = "whole" + + (self.X_train, self.y_train), (self.X_test, self.y_test) = data + + self.X_train, self.X_val, self.y_train, self.y_val = train_test_split(self.X_train, self.y_train, test_size=0.2, random_state=config['seed'], stratify=self.y_train) + + trainset = TreeDataset(np.array(self.X_train, copy=True), np.array(self.y_train, copy=True)) + valset = TreeDataset(np.array(self.X_val, copy=True), np.array(self.y_val, copy=True)) + testset = TreeDataset(np.array(self.X_test, copy=True), np.array(self.y_test, copy=True)) + self.trainloader_original = get_dataloader(trainset, "train", batch_size) + self.valloader_original = get_dataloader(valset, "test", batch_size) + self.testloader_original = get_dataloader(testset, "test", batch_size) + self.tree = construct_tree_from_loader(self.trainloader_original, client_tree_num, task_type) def get_properties(self, ins: GetPropertiesIns) -> GetPropertiesRes: return GetPropertiesRes(properties=self.properties) @@ -125,6 +137,10 @@ def fit(self, fit_params: FitIns) -> FitRes: print("Client " + self.cid + ": recieved", len(aggregated_trees), "trees") else: print("Client " + self.cid + ": only had its own tree") + + # Don't prepare dataloaders if their number of clients didn't change + # if type(aggregated_trees) is list and len(aggregated_trees) != self.client_num or self.trainloader is None: + self.trainloader = tree_encoding_loader( self.trainloader_original, batch_size, @@ -139,6 +155,15 @@ def fit(self, fit_params: FitIns) -> FitRes: self.client_tree_num, self.client_num, ) + self.testloader = tree_encoding_loader( + self.testloader_original, + batch_size, + aggregated_trees, + self.client_tree_num, + self.client_num, + ) + # else: + # print("Client " + self.cid + ": reusing existing dataloaders") # num_iterations = None special behaviour: train(...) runs for a single epoch, however many updates it may be num_iterations = num_iterations or len(self.trainloader) @@ -160,6 +185,22 @@ def fit(self, fit_params: FitIns) -> FitRes: ) self.round_time = (time.time() - start_time) + metrics = {} + + if self.first_round: + #Get best threshold based on validation set + y_pred_proba_val = self.tree.predict_proba(self.X_val) + best_threshold = find_best_threshold(self.y_val, y_pred_proba_val, metric="balanced_accuracy") + y_pred_proba = self.tree.predict_proba(self.X_test) + local_metrics = calculate_metrics(self.y_test, y_pred_proba, threshold=best_threshold) + #Add 'local' to the metrics to identify them + local_metrics = {f"local {key}": local_metrics[key] for key in local_metrics} + metrics.update(local_metrics) + self.first_round = False + + metrics.update({ + "running_time": self.round_time, + "train_loss": train_loss}) # Return training information: model, number of examples processed and metrics if self.task_type == "BINARY": @@ -168,7 +209,7 @@ def fit(self, fit_params: FitIns) -> FitRes: # parameters=self.get_parameters(fit_params.config), parameters=self.get_parameters(fit_params.config).parameters, num_examples=num_examples, - metrics={"loss": train_loss, "accuracy": train_result, "running_time":self.round_time}, + metrics=metrics, ) elif self.task_type == "REG": return FitRes( @@ -194,8 +235,9 @@ def evaluate(self, eval_params: EvaluateIns) -> EvaluateRes: loss, result, num_examples = test( self.task_type, self.net, - self.valloader, + self.testloader, device=self.device, + valloader=self.valloader, log_progress=self.log_progress, ) @@ -230,38 +272,45 @@ def evaluate(self, eval_params: EvaluateIns) -> EvaluateRes: def get_client(config, data, client_id) -> fl.client.Client: (X_train, y_train), (X_test, y_test) = data - task_type = config["xgb"]["task_type"] + task_type = config["xgblr"]["task_type"] client_num = config["num_clients"] - client_tree_num = config["xgb"]["tree_num"] // client_num + client_tree_num = config["xgblr"]["tree_num"] // client_num batch_size = "whole" cid = str(client_id) + #measure time for client data loading + time_start = time.time() trainset = TreeDataset(np.array(X_train, copy=True), np.array(y_train, copy=True)) valset = TreeDataset(np.array(X_test, copy=True), np.array(y_test, copy=True)) + time_end = time.time() + print(f"Client {cid}: Data loading time: {time_end - time_start} seconds") + time_start = time.time() trainloader = get_dataloader(trainset, "train", batch_size) valloader = get_dataloader(valset, "test", batch_size) + time_end = time.time() + print(f"Client {cid}: Dataloader creation time: {time_end - time_start} seconds") + + # metrics = train_test(data, client_tree_num) + # from flcore import datasets + # if client_id == 1: + # cross_id = 2 + # else: + # cross_id = 1 + # _, (X_test, y_test) = datasets.load_dataset(config, cross_id) - metrics = train_test(data, client_tree_num) - from flcore import datasets - if client_id == 1: - cross_id = 2 - else: - cross_id = 1 - _, (X_test, y_test) = datasets.load_dataset(config, cross_id) - - data = (X_train, y_train), (X_test, y_test) - metrics_cross = train_test(data, client_tree_num) - print("Client " + cid + " non-federated training results:") - print(metrics) - print("Cross testing model on client " + str(cross_id) + ":") - print(metrics_cross) + # data = (X_train, y_train), (X_test, y_test) + # metrics_cross = train_test(data, client_tree_num) + # print("Client " + cid + " non-federated training results:") + # print(metrics) + # print("Cross testing model on client " + str(cross_id) + ":") + # print(metrics_cross) client = FL_Client( task_type, - trainloader, - valloader, + data, client_tree_num, client_num, cid, - log_progress=False, + config, + log_progress=False ) return client diff --git a/flcore/models/xgb/cnn.py b/flcore/models/xgblr/cnn.py similarity index 73% rename from flcore/models/xgb/cnn.py rename to flcore/models/xgblr/cnn.py index 849efc3..3a5331b 100644 --- a/flcore/models/xgb/cnn.py +++ b/flcore/models/xgblr/cnn.py @@ -13,7 +13,7 @@ from sklearn.metrics import accuracy_score, mean_squared_error from torch.utils.data import DataLoader from torchmetrics import Accuracy, MeanSquaredError -from flcore.metrics import get_metrics_collection +from flcore.metrics import calculate_metrics, find_best_threshold from tqdm import tqdm @@ -147,6 +147,7 @@ def test( net: CNN, testloader: DataLoader, device: torch.device, + valloader: DataLoader = None, log_progress: bool = True, ) -> Tuple[float, float, int]: """Evaluates the network on test data.""" @@ -157,39 +158,48 @@ def test( elif task_type == "REG": criterion = nn.MSELoss() - total_loss, total_result, n_samples = 0.0, 0.0, 0 - metrics = get_metrics_collection() net.eval() - with torch.no_grad(): - pbar = tqdm(testloader, desc="TEST") if log_progress else testloader - for data in pbar: - tree_outputs, labels = data[0].to(device), data[1].to(device) - outputs = net(tree_outputs) - - # Collected testing loss and accuracy statistics - total_loss += criterion(outputs, labels).item() - n_samples += labels.size(0) - num_classes = np.unique(labels.cpu().numpy()).size - - y_pred = outputs.cpu() - y_true = labels.cpu() - metrics.update(y_pred, y_true) - - # if task_type == "BINARY" or task_type == "MULTICLASS": - # if task_type == "MULTICLASS": - # raise NotImplementedError() - - # # acc = Accuracy(task=task_type.lower())( - # # outputs.cpu(), labels.type(torch.int).cpu()) - # # total_result += acc * labels.size(0) - # elif task_type == "REG": - # mse = MeanSquaredError()(outputs.cpu(), labels.type(torch.int).cpu()) - # total_result += mse * labels.size(0) - - metrics = metrics.compute() - metrics = {k: v.item() for k, v in metrics.items()} - - # total_result = total_result.item() + + # Collect predictions and true labels for the entire test set, to compute metrics at the end of the epoch + + def get_pred_proba(dataloader): + y_pred_list = [] + y_true_list = [] + total_loss, total_result, n_samples = 0.0, 0.0, 0 + with torch.no_grad(): + pbar = tqdm(dataloader, desc="TEST") if log_progress else dataloader + for data in pbar: + tree_outputs, labels = data[0].to(device), data[1].to(device) + outputs = net(tree_outputs) + # Collected testing loss and accuracy statistics + total_loss += criterion(outputs, labels).item() + n_samples += labels.size(0) + num_classes = np.unique(labels.cpu().numpy()).size + + y_pred = outputs.cpu() + y_true = labels.cpu() + y_pred_list.append(y_pred) + y_true_list.append(y_true) + + return y_true_list, y_pred_list, total_loss, n_samples + + metrics = {} + if valloader is not None: + y_true_val, y_pred_proba_val, val_loss, val_n_samples = get_pred_proba(valloader) + best_threshold = find_best_threshold(y_true_val, y_pred_proba_val, metric="balanced_accuracy") + metrics_val = calculate_metrics(y_true_val, y_pred_proba_val, task_type=task_type, threshold=best_threshold) + metrics_val = {f"val {key}": metrics_val[key] for key in metrics_val} + metrics.update(metrics_val) + else: + best_threshold = 0.5 + + # Add validation metrics to the evaluation metrics with a prefix + y_true, y_pred_proba, total_loss, n_samples = get_pred_proba(testloader) + metrics_test = calculate_metrics(y_true, y_pred_proba, task_type=task_type, threshold=best_threshold) + metrics_not_tuned = calculate_metrics(y_true, y_pred_proba, task_type=task_type, threshold=0.5) + metrics_not_tuned = {f"not tuned {key}": metrics_not_tuned[key] for key in metrics_not_tuned} + metrics.update(metrics_test) + metrics.update(metrics_not_tuned) if log_progress: print("\n") diff --git a/flcore/models/xgb/fed_custom_strategy.py b/flcore/models/xgblr/fed_custom_strategy.py similarity index 95% rename from flcore/models/xgb/fed_custom_strategy.py rename to flcore/models/xgblr/fed_custom_strategy.py index 20dbe55..9f74f4d 100644 --- a/flcore/models/xgb/fed_custom_strategy.py +++ b/flcore/models/xgblr/fed_custom_strategy.py @@ -143,4 +143,10 @@ def aggregate_fit( elif server_round == 1: # Only log this warning once log(WARNING, "No fit_metrics_aggregation_fn provided") + elapsed_time = (time.time() - self.time_server_round) + self.accum_time = self.accum_time+ elapsed_time + self.time_server_round = time.time() + print(f"Elapsed time: {elapsed_time} for round {server_round}") + metrics_aggregated['training_time [s]'] = self.accum_time + return [parameters_aggregated, trees_aggregated], metrics_aggregated \ No newline at end of file diff --git a/flcore/models/xgb/server.py b/flcore/models/xgblr/server.py similarity index 97% rename from flcore/models/xgb/server.py rename to flcore/models/xgblr/server.py index 046fc2d..4312d5d 100644 --- a/flcore/models/xgb/server.py +++ b/flcore/models/xgblr/server.py @@ -30,10 +30,10 @@ from xgboost import XGBClassifier, XGBRegressor from flcore.metrics import metrics_aggregation_fn -from flcore.models.xgb.client import FL_Client -from flcore.models.xgb.fed_custom_strategy import FedCustomStrategy -from flcore.models.xgb.cnn import CNN, test -from flcore.models.xgb.utils import ( +from flcore.models.xgblr.client import FL_Client +from flcore.models.xgblr.fed_custom_strategy import FedCustomStrategy +from flcore.models.xgblr.cnn import CNN, test +from flcore.models.xgblr.utils import ( TreeDataset, construct_tree, do_fl_partitioning, @@ -98,10 +98,13 @@ def fit(self, num_rounds: int, timeout: Optional[float]) -> History: for current_round in range(1, num_rounds + 1): # Train model and replace previous global model res_fit = self.fit_round(server_round=current_round, timeout=timeout) - if res_fit: - parameters_prime, _, _ = res_fit # fit_metrics_aggregated + if res_fit is not None: + parameters_prime, fit_metrics, _ = res_fit # fit_metrics_aggregated if parameters_prime: self.parameters = parameters_prime + history.add_metrics_distributed_fit( + server_round=current_round, metrics=fit_metrics + ) # Evaluate model using strategy implementation res_cen = self.strategy.evaluate(current_round, parameters=self.parameters) @@ -407,15 +410,15 @@ def get_server_and_strategy( # The number of clients participated in the federated learning client_num = config["num_clients"] # The number of XGBoost trees in the tree ensemble that will be built for each client - client_tree_num = config["xgb"]["tree_num"] // client_num + client_tree_num = config["xgblr"]["tree_num"] // client_num num_rounds = config["num_rounds"] client_pool_size = client_num - num_iterations = config["xgb"]["num_iterations"] + num_iterations = config["xgblr"]["num_iterations"] fraction_fit = 1.0 min_fit_clients = client_num - batch_size = config["xgb"]["batch_size"] + batch_size = config["xgblr"]["batch_size"] val_ratio = 0.1 # DATASET = "CVD" diff --git a/flcore/models/xgb/utils.py b/flcore/models/xgblr/utils.py similarity index 100% rename from flcore/models/xgb/utils.py rename to flcore/models/xgblr/utils.py diff --git a/flcore/report/generate_report.py b/flcore/report/generate_report.py index 1c92777..45e88f9 100644 --- a/flcore/report/generate_report.py +++ b/flcore/report/generate_report.py @@ -27,7 +27,8 @@ def generate_report(experiment_path: str): df = df.rename(columns={"Unnamed: 0": "center"}) # Convert metrics columns to 2 decimal places df = df.round(2) - colors = ['#FF6666', '#FF9999', '#FF3333', '#CC0000', '#990000', '#B22222', '#FF0044', '#960018'] + colors = ['#FF6666', '#FF9999', '#FF3333', '#CC0000', '#990000', '#B22222', '#FF0044', '#960018', '#FF0000', + '#B22222'] # print(df.head()) diff --git a/flcore/server_selector.py b/flcore/server_selector.py index 3ba5a06..dbcc26e 100644 --- a/flcore/server_selector.py +++ b/flcore/server_selector.py @@ -1,6 +1,6 @@ #import flcore.models.logistic_regression.server as logistic_regression_server #import flcore.models.logistic_regression.server as logistic_regression_server -import flcore.models.xgb.server as xgb_server +import flcore.models.xgblr.server as xgblr_server import flcore.models.random_forest.server as random_forest_server import flcore.models.linear_models.server as linear_models_server import flcore.models.weighted_random_forest.server as weighted_random_forest_server @@ -13,7 +13,7 @@ def get_model_server_and_strategy(config, data=None): server, strategy = linear_models_server.get_server_and_strategy( config ) - elif model == "random_forest": + elif model in ("random_forest", "balanced_random_forest"): server, strategy = random_forest_server.get_server_and_strategy( config ) @@ -22,8 +22,8 @@ def get_model_server_and_strategy(config, data=None): config ) - elif model == "xgb": - server, strategy = xgb_server.get_server_and_strategy(config, data) + elif model == "xgblr": + server, strategy = xgblr_server.get_server_and_strategy(config, data) else: raise ValueError(f"Unknown model: {model}") diff --git a/plots.ipynb b/plots.ipynb new file mode 100644 index 0000000..a5c008d --- /dev/null +++ b/plots.ipynb @@ -0,0 +1,1236 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4c815c0e", + "metadata": {}, + "source": [ + "## Select data to load based on keywords in experiment name" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9f05d536", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import os\n", + "\n", + "logs_dir = \"logs\"\n", + "logs_dir = \"benchmark_results\"\n", + "experiment_name = \"experiment_1percent\"\n", + "# experiment_name = \"experiment_good\"\n", + "# experiment_name = \"experiment_small\"\n", + "dataset_name = \"diabetes\"\n", + "# dataset_name = \"kaggle_hf\"\n", + "results_file = \"per_center_results.csv\"\n", + "keywords = [\n", + " experiment_name,\n", + " dataset_name,\n", + " # \"logistic_regression\",\n", + " # \"forest\",\n", + " # \"c10\"\n", + " # \"a0.7\"\n", + " # \"a1.0\"\n", + " \"aNone\"\n", + " ]\n", + "\n", + "def load_data(logs_dir, experiment_name, keywords, results_file=\"per_center_results.csv\"):\n", + " data = {}\n", + "\n", + " # iterate over all directories in logs_dir with names containing all the keywords\n", + " dirs = [d for d in os.listdir(logs_dir) if all(keyword in d for keyword in keywords)]\n", + " for d in dirs: \n", + " model_name = d\n", + " # model_name = model_name.replace(experiment_name+\"_\", \"\")\n", + " model_name = model_name.replace(experiment_name+\"_\"+dataset_name+\"_\", \"\")\n", + " model_name = model_name.replace(\"_\", \" \")\n", + " model_name = model_name.title()\n", + " model_name = model_name.replace(\"none\", \"N\")\n", + " # Find position of _c keyword\n", + " pos = model_name.find(\" C\")\n", + " # if pos != -1:\n", + " # if not model_name[pos+3].isdigit():\n", + " # model_name = model_name[:pos+2] + \"0\" + model_name[pos+2:]\n", + " #remove non-capital letters\n", + " # model_name = ''.join(c for c in model_name if c.isupper() or c == ' ' or c.isdigit())\n", + "\n", + " full_path = os.path.join(logs_dir, d)\n", + " metrics_file = os.path.join(full_path, results_file)\n", + " if os.path.isfile(metrics_file):\n", + " df = pd.read_csv(metrics_file)\n", + " data[model_name] = df\n", + "\n", + " print(\"Found \", len(data), \" experiments\")\n", + "\n", + " # Sort data by model_name\n", + " data = dict(sorted(data.items()))\n", + "\n", + " # for model_name, df in data.items():\n", + " # print(model_name)\n", + " \n", + " return data" + ] + }, + { + "cell_type": "markdown", + "id": "0389d57d", + "metadata": {}, + "source": [ + "### Print metric values" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "29bb08b0", + "metadata": {}, + "outputs": [], + "source": [ + "# metric = \"balanced_accuracy\"\n", + "# # metric = \"accuracy\"\n", + "# results = []\n", + "# #print average metric across all centers for each model\n", + "# for model_name, df in data.items():\n", + "# #weighted average by number of samples in each center\n", + "# total_samples = df[\"n samples\"].sum()\n", + "# weighted_sum = (df[metric] * df[\"n samples\"]).sum()\n", + "# avg_metric = weighted_sum / total_samples\n", + "# results.append(f\"{model_name}: {avg_metric:.4f}\")\n", + "# # print(f\"{model_name}: {avg_metric:.4f}\")\n", + "\n", + "# # Sort results alphabetically by model name\n", + "# results.sort()\n", + "# for result in results:\n", + "# print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "7893ad6c", + "metadata": {}, + "source": [ + "# Bar plot for all imported models" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "905d8cfa", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAJOCAYAAACqS2TfAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAADZyUlEQVR4nOzdd3hT5fsG8Lub7lJmKdNiS+lmlVFGWSKbKsh2AILKFgERmRVQULYMFRAQkT1kz8qSLS2UltkNpXs3bZL39we/nC+hDTbYNJTcn+vykpycnDy5k5PmOeM9RkIIASIiIiIiIiIqdcb6LoCIiIiIiIjodcWmm4iIiIiIiEhH2HQTERERERER6QibbiIiIiIiIiIdYdNNREREREREpCNsuomIiIiIiIh0hE03ERERERERkY6w6SYiIiIiIiLSETbdRERERERERDrCppuIXml//fUXPv74YzRv3hyenp5o164dxo0bhytXrui7tFKza9cuuLm54f79+/oupcRkMhk+/vhj+Pj4YOTIkcXOc/HiRbi5ucHPzw95eXnFznPixAm4ubmhffv2pVLX1KlTtV7W1KlT0apVq//0vG5ubli+fPl/Woauqd6Pixcv6rWO+Ph4uLu7w9vbGxkZGXqtRR+WL18ONzc36b8GDRqgZcuWGDJkCI4fP6718lTv619//aWDanVHm3V16NChcHNzw5YtW3RcFRGRbrDpJqJX1pIlSzBy5EjUrVsXa9euxZEjRzB//nzk5eVhyJAh+OOPP/RdYqno2rUrzp49i7p16+q7lBI7ffo0QkJCMG3aNHzzzTf/Ov+xY8eKnb5//35YWVmVdnn0CtuxYwdq1qwJU1NT/Pnnn/ouR29OnjyJs2fP4q+//sJPP/2EGjVqYPTo0Th37py+S3ulxMTE4NKlS3B3d8fOnTv1XQ4R0Uth001Er6SQkBCsWrUKM2bMwLRp0+Dt7Q1nZ2e0aNECa9asQadOnbBo0aJyvadMqVRCoVCgQoUKqFKlCkxMTPRdUomlpaUBAFq1aoXKlSu/cN7mzZtj7969RaZnZ2fj1KlTaNq0qU5qpFePUqnE7t270a1bN3To0KHMmijVuvYqqVy5MqpUqYKqVavCw8MD8+bNg6WlJU6cOKHv0l4pO3fuRPXq1TF58mTcvHkTd+7c0XdJAAAhBORyub7LIKJygk03Eb2S1q1bh/r162PAgAFF7jMyMsKcOXNw4sQJ2NvbA3j6A+jnn3/GW2+9BU9PT/j7+2Ps2LGIjY2VHrd48WK0atUK169fR48ePeDl5YWePXvi1q1buHLlCnr37g1vb290794dly9flh73+eefo0ePHjh79ix69OgBT09PdOjQAbt371ar6+zZsxg0aBCaNm0KPz8/9OnTB0ePHlWbx83NDWvXrsWoUaPg7e2NO3fuFDm8PD4+HuPHj0erVq3g5eWFjh07Yvny5WpNw/379zFq1Cg0adIEnp6e6Nq1K3777Tfp/sLCQri5uWHDhg1YsWIFAgIC4Ofnh0GDBv3rYexZWVmYOXMmAgIC4OnpibZt2yI4OFg6RHzq1KmYOXMmAKBDhw4YMmTIC5fXoUMHXLhwAUlJSWrTjx49Cjs7O3h6ehZ5zKlTp9CvXz94e3vD19cXAwYMwIULF9TmuX79OoKCguDp6YnAwECsX7++2OffvHkz3n77bXh6eqJly5aYMWMGsrKyNNZ76dIlDB48GE2bNoWvry/69OmDAwcOvPA1Ak8bu++//x4tW7aEl5cXhgwZgujoaOl+hUKBZcuW4a233oK3tzdatWqFsWPHIi4uTpqnoKAACxYsQPv27eHl5YVWrVphypQp0kYOAMjNzUVwcDDatGkDT09PdOrUCWvXroUQQponOzsbkydPRuPGjdGoUSOMGzdObRmalHQ9atKkCe7du4eBAwfCx8cHbdq0wYoVK/51+WfOnMGjR4/Qs2dP9O7dG7du3UJERIR0/5IlS+Dl5YXs7Gy1x/3zzz9wc3OTjphISUnBl19+iRYtWsDT0xPdunXDjh071B5T3LoGlGw9TUxMxKhRo+Dr6wt/f3988803OHjwINzc3BAVFSXNd/78efTv3x8+Pj5o1KgRPv7445c+TcTIyAgAYG5urjZ948aN6N69u1TLsGHD1DIrzp9//omgoCA0atQIjRs3xoABA3Dp0iXp/qioKLi5ueHgwYMIDg6Gv78/GjdujI8//hiJiYnSfEIIrF27Fh06dICXlxfeeustbNy4Ue25bt26hWHDhsHPzw8+Pj4YPHgwrl27pjZPSdfV5ykUCuzatQs9e/ZE8+bN4eTkVOR9Bp6uN9999x3atGkDb29v9OzZs8g6e/LkSQQFBcHLywutW7fG3LlzkZOTA0DzIfpDhgxBv379pNvt27dHcHAwpk2bBh8fH5w+fRoAEBYWhmHDhsHf3x8+Pj7o2rUrtm7dqrasF2W5efNmNGjQQG09A55+Dt3d3XlYPdHrQBARvWIKCgqEp6enWLBgQYkfs2TJEuHh4SHWr18vHjx4IP7++2/RvXt30a5dO5GTkyOEEGLZsmWiUaNGYsSIESIsLEz8888/IiAgQHTv3l0MHjxY3LhxQ4SHh4uuXbuKDh06SMuePHmyaNKkifjggw/EjRs3xN27d8XEiROFm5ubuHHjhhBCiNjYWNGwYUMxdepUce/ePRETEyO+++474e7uLm7duiUty9XVVXTu3FmsWbNGxMTECJlMJnbu3ClcXV3FvXv3hBBCDBgwQAwZMkTcunVLxMfHi0OHDokmTZqINWvWCCGESE5OFv7+/qJfv37iypUr4t69e+LHH38Ubm5uYuPGjWrP9fbbb4v58+eL+/fvi6tXr4pWrVqJwYMHvzDLgQMHilatWomjR4+KqKgo8eeff4omTZqI0aNHCyGEyMzMFGvXrhWurq7ixo0bIi0trdjl/P3338LV1VVER0eLVq1aiXXr1qnd/8EHH4h58+aJZcuWicDAQGn6uXPnhJubm/jqq6/E7du3xa1bt8S4ceNEw4YNpSzT0tJEkyZNRL9+/URoaKi4ffu2mDhxomjVqpXaslavXi0aNGggVq1aJR48eCBOnz4t2rZtK4YMGSLNM2XKFNGyZUvptfn6+oq5c+eKBw8eiOjoaLFmzRrh5uYmrl+/rjEzV1dX0aZNG/HNN9+Iu3fvivPnz4u2bduK7t27S/OsXLlSNGzYUBw4cEDEx8eLGzduiD59+og+ffpI8yxevFgEBASI8+fPi/j4eHH58mXRs2dPMWzYMGmejz76SDRr1kwcOHBAREVFid9//114enqK5cuXS/NMnjxZ+Pr6in379omHDx+KrVu3ivbt2wtXV1fx999/a3wdJV2PfHx8xODBg8WZM2dEbGys+Oabb4Srq6u4ePGixmULIcTo0aPFgAEDhBBCKBQK0a5dOzF37lzp/nv37glXV1exb98+tcd98803olmzZkImkwmZTCbVdPr0afHgwQOxatUq4erqKnbv3q32njy/rpV0PX3vvfeEv7+/OHHihIiKihJz584VnTt3Fq6uriI2NlYIIcTly5eFu7u7mDBhgoiIiBA3btwQgwcPFs2bNxcpKSkaM1i2bJlwdXUV+fn50rSMjAwxb9480bhxY/HgwQNp+p49e4Srq6vYuHGjiI2NFREREeKjjz4SrVu3Fnl5eUKI/61nISEhUl2urq5i0aJFIjo6Wty/f1988cUXwtfXVzx+/FgI8fT7ytXVVXTr1k2sXbtWREdHi5CQEOHj4yOmTp2qVquvr6/YvXu3iI6OFtu3bxfu7u5i8+bNQgghoqKihK+vrxg6dKgIDQ0VERERYty4ccLb21vcv39fCFHydbU4J06cEK6uriIqKkoI8XT98Pf3FwUFBWrzqdbhEydOiJiYGLF69Wrh5uYmTp48KYQQ4vz586JBgwZi8eLF4sGDB+L8+fMiICBAjBkzptgMVQYPHiz69u0r3Q4MDBSdO3cWwcHBIioqSmRnZ4vs7GzRuHFjMXz4cBERESFiY2PFhg0bhKurqzhx4kSJsszMzBQ+Pj5i2bJlas+/fv164ePjIzIzM1+YExG9+th0E9Er58mTJ8LV1VX8+uuvJZpfJpMJPz8/tR+LQghx/fp1tR/iqh+7165dk+aZPXu2cHV1FVeuXJGm/fTTT8LV1VX6oTNlyhTh6uoqbt++Lc2Tl5cnfHx8pIZB9YNe9UNYNY+rq6v46aefpGmurq5qTZYQokjT7e3tLTXYKnfv3hVxcXFCiP81kqof/yofffSR2sYCV1dXERQUpDbPnDlzhK+vb5EMVa5du1akeRFCiDVr1ghXV1cRHx8vhBBiy5Ytag1IcVQ/ZGNjY8W8efNEz549pfsSExNFgwYNRFhYWJGm+6OPPhIdO3YUSqVSmpaXlyeaNGkivvzySyGEENu2bROurq5qjZJMJhP+/v7SsgoKCkTjxo3FxIkT1eo6evSocHV1Ff/8848QQr3pvnHjhtp9Ki/auCCEkBqYZ+3atUu4urqK8PBwIcTTxkr1Hqr89ttvwtXVVWrShg8frtZgCyHE48ePRUREhFp9W7duVZtn7ty5onHjxkImk4nc3Fzh6emp1syq5nlR063teqRqaIQQIjU1tchn/XkpKSnCw8ND7Ny5U5q2bNkyqZlW6dOnj/j000+l20qlUrRu3VrMmDFDCCHEwYMHhaurqzhz5oza8keNGiW6dOki3S5uXSvJevrw4UPh6uoqfv75Z7XH9u3bV+0zP2LECNGuXTu1BvDJkyfCw8OjyPr7LFV+vr6+wtfXV/j4+AhXV1fRvHlztSZNCCGys7NFdHS02rTTp09LG7yEKNow5uXliZiYGFFYWCg9RrUx4+DBg0KI/zXdqqZTZeTIkaJr165SVo0aNRILFy5Um2fFihVi9erVQgghZs2aJXx9fdXWjfz8fNGyZUvp/SrJuqrJJ598oraRMCYmRri5uYnDhw9L0x4/fiwaNGggtmzZovbYuXPnim3btgkhhBg2bJha8yyEEIcPHxZffvmlKCgo0KrpbtmypZDL5dI0uVwuEhISRFZWltpjW7RoIWbNmiW93n/L8ssvvxSBgYFq33vvvvuumDx58gszIqLywVTfe9qJiJ5navr0q0mpVJZo/gcPHiAnJwfNmjVTm+7t7Q0TE5Mih2I2aNBA+reDgwMAoGHDhtK0ihUrAgAyMzNha2sLALC2tlZ7XIUKFfDGG2/gwYMHAJ4eEnr58mVs3boVUVFRKCgokOZNT09Xe/7iDqd+VqdOnbBy5UokJycjICAATZs2Rf369aX7w8LC4OTkhJo1a6o9zs/PD2fPnkV2djZsbGwAAD4+Pmrz2NvbIzc3FwUFBUUOY1UtG0CR86x9fX0BABEREahRo8YL6y9Or169sGHDBkRGRkqHtdapUweenp44depUkRo6dOggHW4LPM27QYMG0nt59+5dmJqawt3dXZrH3Nwcnp6e0nvy4MEDZGVlwd/fX235LVq0AABcu3atSD5vvvkm6tati7Fjx2LAgAFo0aIFvLy84O3t/a+vsXHjxmq3VbXdv39f+vf69etx5swZpKSkQKFQSOeEpqWlwdHREZ06dcLXX3+NcePGoXPnzmjevDmqVauGatWqAQBu3LgBAMW+pk2bNuHu3bswMTFBQUEBvLy81OZp1KgRNm3apLF+bdejZ7NTrUcvGmNh9+7dMDc3R5cuXaRpQUFBWLlyJU6cOIG3334bANC9e3csWbIEOTk5sLa2xtWrV5GYmIhevXpJGRgZGRWps0WLFjh58iTS09Olep5f10qynt67d6/I6wOAwMBAKX9VHQEBATAzM5OmValSBW+++WaRw6uLs337dumxmZmZuH79Or788ksMHjwYY8aMAQCYmZlh165dOHbsGJ48eQK5XC6dZvL894qKhYUFjh8/jn379iE+Ph6FhYXSqQfPP6a474ebN28CAB4+fIjs7Gx4eHiozfPZZ5+pZeDm5iblrXp+Pz8/KYOSrKvFSUpKQkhIiNpAjbVq1YK/vz927tyJt956C8DTw9uVSmWROqdPny79OywsDF27dlW7/6233pKWoY0GDRqojb9hYmKCyMhIrFu3Dvfu3ZNOw8nLy5PyLkmW/fv3x86dO3Hx4kU0b94csbGxCA0NxZQpU7SukYhePWy6ieiV4+DgAAsLiyLnt2miOv9TdX63irGxMWxsbIqcH2ppaSn9W9XYFTdNPHOOrKqJfX45qh9YJ0+exNSpUxEUFISpU6eiYsWKMDIyQufOnYs8zs7O7oWv59tvv4WPjw8OHDiATZs2wczMDL169cLkyZNha2uL7OxstR+5zy83JydHqvf5kcGLe23P0pSlatnPZ1lSDRs2RP369bF3715MnjwZ+/btQ48ePTTW8Pzzq2pSnW+anZ0NW1tbtcb8+bpV520HBwdj/vz5RZb3/DnmwNP3dOvWrVi3bh12796NxYsXo1KlSvjwww8xfPjwIs/3rOffE1X2qs/IV199hbNnz2LKlCnw8/NDhQoVcPToUSxatEh6TL9+/VC1alVs3boV06ZNg0wmQ4sWLTB9+nS4uLhIr6lPnz5qz6XaQJWcnAxra+siWQD//rnTdj1SPQ/w758r4Omo5Tk5OfDz8yty386dO6Wmu1u3bli4cCFOnz6Nbt264eDBg6hduzYaNWoE4On7KoQosuFBtQEjOTlZei+ef80lWU9Vr1O1wU2lUqVKarezsrJw5MgRnDx5Um26TCYr0aCItWrVgoWFhXTby8sLVlZWmD59Orp27QoXFxf88MMP2LhxIyZMmICAgABYWVnhxo0b+OKLLzQud/PmzViwYAGGDRuGt99+G3Z2dkhMTCx27IXivh9U76Hqs1ahQgWNz5WVlYX4+Pgi72lBQYHad8a/ravF2b17N+RyOaZMmVKk8TQxMUFiYiKqVatW4jpfdL82nv9MhYeH47PPPkNAQACWLl2KypUrw9jYWC3vktTo7e0NDw8P7Nq1C82bN8fBgwdRr149NGnSpFTqJiL9YtNNRK8cIyMjtGzZEqdOncK0adOK/QGbkZGBI0eOICgoSPpx/PxeHIVCgaysrCI/nl9GcQNv5eTkSCN3Hzx4EFWrVsW8efOkH5epqakv9VwmJiYYMmQIhgwZgoyMDBw7dgwLFy6EXC7H/PnzYWtri5iYmCKPU73+4jYQlNSzWT67HNWy/61xe5FevXphy5YtePfdd3Hr1i0sWbJEYw3F7cVLT0+X6rOyskJ+fn6x86ioftRPmjQJbdu2LfZ5ilOxYkV8/vnn+PzzzxEXF4ddu3bh+++/R+XKlYs0u8/KzMxUu63a62ttbY2CggKcOHECw4YNQ//+/aV5imvi27Vrh3bt2qGgoAB///03vv/+e3z88cc4fvy49Jo2bNhQ7IaXKlWq4OHDhwBQ5NromvaMquhyPbp69SoePHiAxYsXo169emr3Xbx4Ed9++63URFWrVg3NmjXD4cOH0aVLFxw5ckQtMzs7O1hYWGDPnj3FPpeTk5PGOkqynqo2wD2/keH5gejs7OykwfCeV9xRJCXh7u4OIQQiIyPh4uKCgwcPokuXLhgxYoQ0T3h4+AuXcfDgQfj6+mLy5MnStJe5yoOmz8Oz7OzsUL16dQQHBxe5z9j46Vi9JVlXi7Nz5050794dw4cPV5uuVCoxdOhQ7NmzByNHjixRnZq+U1Q0bUzLz89/4YY2ADhy5AiMjIzwww8/SBuilEql2uenJDUCwHvvvYcFCxYgPz8fBw4cQN++fV84PxGVHxy9nIheSe+//z4SEhKwatWqIvcJITBnzhx8++23SE5OxhtvvAFbW1u10XmBpz/0lUplkcNsX0Zubq7a4bV5eXl4+PAh3nzzTQCQ9j4/+wNt165dUr0llZ6ejr1790qHkNrb2+Pdd99Fnz59pMM+fXx8kJCQUORIgMuXL8PFxUVtD6S2VIebPp/llStXYGxsrHYYvrZ69OiBx48fY/Xq1fD19UXt2rU11nD58mW13HJychAeHi69l2+88Qby8vLURpKWyWQIDQ2VbterVw92dnaIj49HnTp1pP9q1qwJuVwOR0fHIs8dFRWltueyZs2aGDt2LBo2bIhbt2698PVdv35d7baqOapfvz5yc3OhUCjUnlMul2P//v3SbaVSiaNHj+LRo0cAnjZubdq0wbhx4xAXF4eMjAzp/UlOTlZ7TXZ2drC0tISVlRXq1KkDU1PTIoeDP/+ePk+X69GOHTvg7OyMrl27wt3dXe2/vn37wtzcXFpfgKeHmJ89exYXLlxAcnIyevbsKd3n6+sLmUyGvLw8tQwqVKgAOzu7Fza8JVlP69atCwC4ffu22mOfPw3C19cXDx8+VKuhTp06kMvlqFKlykvldPfuXQBA1apVpXqf/5yqNja86GgV1SkyKqorLWjzXfTGG2/AxsYGV65cUZu+dOlSfPXVVwD+l4GTk5NaBkII6TWUZF193qVLlxAVFYV+/foV+bx4eHigQ4cO0vvm6ekJY2PjInV+/fXXWLx4MYCnRxE8f8j/sWPHMGjQIOTk5BR7JE9+fr60AetFcnJyYG5urva9e+TIEeTk5Eh5lyRL4Onn3sjICD///DMePHjwwo18RFS+sOkmoldSixYtMGbMGKxYsQLTpk3D9evXER8fjwsXLuDjjz/GiRMn8N1336F69eowMzPDRx99hP3792PDhg2IiorC+fPn8fXXX+ONN95Ax44d/3M91tbWmDNnDq5evYp79+5h+vTpKCgokM4zbdSoEe7du4eDBw8iJiYG69atw40bN1CjRg2Eh4erXYbnRZRKJWbNmoXp06cjIiICjx49woULF3Ds2DHpHNagoCBUqlQJkyZNwvXr13H//n0sXrwYly5dwscff/yfXqe3tzdatGiB77//HsePH0d0dDR2796NdevWoXfv3tIP6Zfh5OSEpk2b4s8//9R4aDkADB8+HAkJCfj6669x584dhIWF4fPPP4dCoZAO2ezcuTOsrKzw9ddf49atWwgPD8fkyZPV9s6bmppi+PDh2LJlCzZv3ozo6Gjcvn0bX375Jfr164cnT54Uee6YmBiMGTMG69evR1RUFOLi4rBnzx7cvXtX4/XEVT+sU1NT8d133+H+/fu4cOECVq1aBQ8PD7i6usLBwQH16tXDrl27EBkZiZs3b2L06NHSIdOXL19Gbm4ufv75Z4wfPx5XrlzBo0ePcPPmTWzZskVahqenJwICAjB37lwcP34ccXFxuHTpEoYPH47PPvsMQgjY2NigQ4cO2Lx5Mw4dOoSoqChs2bIFFy9efOH7o6v1KDs7G4cPHy5yTq2KtbU12rZtq3YJvrfeegtyuRzff/89/Pz8UKdOHem+wMBAuLq64osvvsCFCxcQHx+PkJAQDB48GLNmzXphLSVZT93c3FC/fn2sXbsWZ86cQXR0NL755hvp8lIqw4cPR0REBGbPno07d+4gKioKa9eulS4v+G+Sk5ORlJSEpKQkREdHY9++fVi4cCGaNGkifS78/Pxw9OhR3LhxA3fv3sVXX30ljalw7dq1Yvdg+/n54eLFizh//jwePnyI77//HgqFAqampggNDS3xEThmZmYYOnQo9u7di61btyImJgZ79uzBTz/9JJ2fPXToUOTk5GDSpEm4desWYmNjsW3bNvTu3Vu6tFdJ1tXnbd++HVWrVtW4znXt2hVRUVG4cuUKqlatih49emDdunU4ePAgYmNjsX79euzYsUPaSDVs2DBER0cjODgY9+/fx99//4358+ejUqVKsLa2Rp06deDg4IC9e/ciNzcXmZmZmDt3bpHD74vj5+eHnJwcbNiwAbGxsdi5cyd+++03+Pn54e7du4iLiytRlsDTdaFnz55YtWoV2rdvX+yGQSIqp8p86DYiIi2cPXtWjBo1SrRq1Up4enqKwMBAMW3aNGmkbxWlUil++eUX0alTJ9GwYUPh7+8vJk2aJBITE6V5VKMGP6u4aarRxFWjFE+ZMkUEBgaKkydPiq5duwoPDw/RsWNHceDAAekxubm5YsqUKaJp06aiadOmYsqUKSIrK0usX79e+Pr6iuHDhwshno6o/PwIts+PXn79+nXx4YcfimbNmglPT0/RoUMH8d1336ldYuj+/fti5MiRolGjRsLDw0P06NGjyIjjxT1XcZcrel5WVpaYOXOmaNWqlWjYsKEIDAwUP/zwg9oozdqOXq6yfft20bBhQ7VLKj0/erkQT0dofvfdd4Wnp6fw9fUV77//fpERxc+fPy969OghPDw8RNu2bcUvv/wivvnmGxEQEKA236ZNm0SXLl2Eh4eHaNq0qRg5cqTaSPTPjl4uhBC7d+8Wffr0kUaX7tmzpzQKcnFkMplwdXUVa9asEd9++61o3ry58PT0FB988IHaaw8LCxNBQUHCy8tLdOrUSWzbtk0UFBSIgQMHCj8/P7Fr1y7x5MkTMWnSJNGqVSvh4eEhWrVqJSZOnKg26nlOTo4IDg4WrVu3Fh4eHiIgIEBMnz5dbQTptLQ0MXbsWOk1jBkzRpw7d064urqKs2fPanwt2qxHz3+Givu8CSHE1q1b1UZxL86hQ4eKXHJs9OjRwtXVVbo81bOSk5PF1KlTRfPmzYWHh4cIDAwUCxYsUBuVvLh6Srqe3rt3TwwaNEh4enqKVq1aiaVLl4o//vhDuLq6iidPnkjLO3funOjfv7/w9vYWvr6+om/fvuLIkSMaX+ez+T37n6+vr+jevbtYtWqVdGk2IZ6O1j148GDh4+Mj2rRpI1avXi2USqX03q5cubLIyNspKSnik08+EX5+fqJFixZi/vz5QiaTiQULFghfX1/x9ddfS6OXPz/i9/PrgkKhEKtWrRKBgYHCw8NDdO7cWe2yhEI8/Vx/9NFHwtfXV3h7e4sePXqI33//XW2ekq6rQjy9bJ+3t7cIDg7WmGFBQYFo2rSpNNK+TCYT8+fPFwEBAcLLy0t0795d/Pnnn2qPOXbsmOjVq5fw9PQUAQEBYs6cOWojjp8+fVp07dpVeHl5iY4dO4pt27aJ8ePHq42AHxgYKMaPH6+2XIVCIebPny+aN28u/Pz8xCeffCISExPFoUOHROPGjaXR4EuSpRBCXLx4Ubi6uoq//vpL4+snovLHSAgtjjUiIjJAU6dOxaVLl4oMmEREr6e8vDwUFBSoDfa1aNEi/Pbbb0VOIyAqTcHBwTh//jwOHDjwr+eTE1H5wYHUiIiIiJ4xfPhwPHnyBMHBwXB2dkZoaCh+//13DmxFOiGXy5GYmIiTJ0/it99+w8qVK9lwE71m2HQTERERPWPZsmX47rvvMHHiRGRmZqJGjRr44IMP/vOYCUTFSUpKQteuXWFra4uZM2eiffv2+i6JiEoZDy8nIiIiIiIi0hGOXk5ERERERESkI2y6iYiIiIiIiHSETTcRERERERGRjhjcQGpyuRwZGRmwsLCAsTG3ORAREREREZH2lEolZDIZ7O3tYWqqubU2uKY7IyMDUVFR+i6DiIiIiIiIXgN169ZFpUqVNN5vcE23hYUFgKfBWFpa6rmaV4dCocCdO3fg6uoKExMTfZfzSmE2mjEbzZiNZsymeMxFM2ajGbPRjNkUj7loxmw0YzbFy8vLQ1RUlNRjamJwTbfqkHJLS0tYWVnpuZpXh0KhAABYWVlxRXoOs9GM2WjGbDRjNsVjLpoxG82YjWbMpnjMRTNmoxmzebF/O22ZJzUTERERERER6QibbiIiIiIiIiIdYdNNREREREREpCNsuomIiIiIiIh0RK9Nd1xcHIYNGwZfX1+0aNECCxcuhFKpLDKfUqnE0qVLERgYCD8/P/To0QOHDx+W7pfJZJgxYwaaNWsGPz8/jB07FqmpqWX5UoiIiIiIiIiK0FvTLYTA6NGjUbFiRYSEhGDz5s04dOgQfv311yLzbtmyBTt27MC6detw9epVfP755/j8888RGRkJAFi4cCGuXbuGnTt34sSJE8jPz8e0adPK+iURERERERERqdFb0x0WFobIyEhMnz4d9vb2cHFxwYgRI7B169Yi896+fRuNGjVCvXr1YGxsjHbt2sHOzg4RERGQy+XYvXs3xo8fj1q1asHR0RFTpkzBqVOnkJiYqIdXRkRERERERPSU3q7THR4eDmdnZzg4OEjTPDw8EBUVhezsbNjY2EjT27Vrh5kzZyIiIgL169fH6dOnIZPJ0KxZM8TExCA7OxseHh7S/C4uLrC0tMStW7dQrVq1Yp9foVBI15uj/117j5kUxWw0YzaaMRvNmE3xmItmzEYzZqMZsykec9GM2WjGbIpX0jz01nSnpaXB3t5ebZrqdlpamlrT3alTJ4SHh6NXr14AAEtLS3z77bdwcnLC1atX1R6rYmdn98Lzuu/cuVMqr+N1ExYWpu8SXlnMRjNmoxmz0YzZFI+5aMZsNGM2mjGb4jEXzZiNZszm5eit6TYyMirxvHv27MHevXuxZ88euLi44MKFC5g4cSKcnJxeuJwX3efq6gorKyutan6dKRQKhIWFwcvLCyYmJvou55XCbDRjNpoxG82YTfGYi2bMRjNmoxmzKR5z0YzZaMZsipebm1uinbl6a7odHR2Rnp6uNi0tLU2671mbNm1Cv3794O7uDgBo27Yt/P39sWfPHgwdOhQAkJ6eLjXRQgikp6ejUqVKGp/fxMSEH5hiMBfNmI1mzEYzZqMZsykec9GM2WjGbDRjNsVjLpoxG82YjbqSZqG3gdS8vLyQkJAgNdoAEBoaivr168Pa2lptXiFEkUuJyeVyGBsbo1atWnBwcMCtW7ek+yIjI1FYWAhPT0/dvggiIiIiIiKiF9Bb0+3u7g5vb28EBwcjMzMTkZGRWLt2LQYNGgQA6NKlC65cuQIACAwMxI4dO3D37l0oFApcuHABFy5cQLt27WBiYoJ+/fphyZIliI2NRUpKCubPn4+33noLlStX1tfLIyIiIiIiItLf4eUAsHTpUsyYMQOtW7eGtbU1Bg4ciIEDBwIAHj58iNzcXADAqFGjIJfLMXLkSKSmpqJGjRqYNWsWAgICAABjxoxBTk4OgoKCoFAoEBgYiFmzZunrZREREREREREB0HPTXb16daxdu7bY+yIjI6V/m5mZYcKECZgwYUKx85qbm2PGjBmYMWOGTuokIiIiIiIiehl6O7yciIiIiIiI6HXHppuIiIiIiIhIR9h0ExEREREREekIm24iIiIiIiIiHWHTTURERERERKQjbLqJiIiIiIiIdIRNNxEREREREZGOsOkmIiIiIiIi0hE23UREREREREQ6wqabiIiIiIiISEfYdBMRERERERHpCJtuIiIiIiIiIh1h001ERERERESkI2y6iYiIiIiIiHSETTcRERERERGRjrDpJiIiIiIiItIRNt1EREREREREOsKmm4iIiIiIiEhH2HQTERERERER6QibbiIiIiIiIiIdYdNNREREREREpCNsuomIiIiIiIh0hE03ERERERERkY6w6SYiIiIiIiLSEVN9F0Ca1Z16oOyfdPvhMn26qAXdyvT5iIiIiIiIyhL3dBMRERERERHpCPd0ExERacAjjoiIiOi/4p5uIiIiIiIiIh1h001ERERERESkI2y6iYiIiIiIiHSETTcRERERERGRjrDpJiIiIiIiItIRjl5O5RJHFCYiIiIiovKAe7qJiIiIiIiIdIRNNxEREREREZGOsOkmIiIiIiIi0hE23UREREREREQ6wqabiIiIiIiISEfYdBMRERERERHpCC8ZRkRk4PRyCT6gTC/Dx0vwERERkb5wTzcRERERERGRjrDpJiIiIiIiItIRNt1EREREREREOqLXc7rj4uIwc+ZMXL16FZaWlggKCsLnn38OY2P1bQEfffQRLl++rDZNLpfjs88+w+jRozFkyBBcu3ZN7XH16tXDvn37yuR1EBERERERERVHb023EAKjR49G/fr1ERISguTkZIwYMQKVK1fGhx9+qDbvunXr1G5nZGSgW7du6NSpkzRt7ty5CAoKKpPaiYiIiIiIiEpCb4eXh4WFITIyEtOnT4e9vT1cXFwwYsQIbN269V8fu2TJEnTu3Blubm5lUCkRERERERHRy9Hbnu7w8HA4OzvDwcFBmubh4YGoqChkZ2fDxsam2Mc9ePAA+/fvx9GjR9WmHzx4EGvWrEFqaiq8vb0xY8YM1KlTR+PzKxQKKBSKUnkt9PL4HmhWHrJR1Vgeai1rzObVwvdBs/KQDdcnzZiNZsymeMxFM2ajGbMpXknz0FvTnZaWBnt7e7VpqttpaWkam+7Vq1ejb9++cHR0lKa5uLjA0tISCxYsgLGxMYKDgzFixAj8+eefMDc3L3Y5d+7cKaVXQv/FP//8o+8SXlnlKZuwsDB9l/DKYjavhvK0PpW18pQN1yfNmI1mzKZ4zEUzZqMZs3k5emu6jYyMtH5MSkoKDh06hAMHDqhNnzVrltrtOXPmoFmzZrh8+TJatWpV7LJcXV1hZWWldQ1lavthfVegc76+vi/3QGbzSlAoFAgLC4OXlxdMTEz0Xc4rpVxlw/VJM2bzSihX61MZYzaaMZviMRfNmI1mzKZ4ubm5JdqZq7em29HREenp6WrT0tLSpPuKc+LECbz55puoXbv2C5dtY2MDBwcHJCUlaZzHxMSEH5hXAN8DzcpTNlyfNGM2rwa+B5qVp2y4PmnGbDRjNsVjLpoxG82YjbqSZqG3gdS8vLyQkJAgNdoAEBoaivr168Pa2rrYx5w9exb+/v5q07KzszFr1iykpKRI09LS0pCWloZatWrppngiIiIiIiKiEtBb0+3u7g5vb28EBwcjMzMTkZGRWLt2LQYNGgQA6NKlC65cuaL2mIiICNSvX19tmo2NDUJDQzFv3jxkZWUhPT0ds2fPhru7O/z8/Mrs9RARERERERE9T29NNwAsXboUWVlZaN26NT788EP0798fAwcOBAA8fPgQubm5avMnJSWpjXausmLFCshkMnTo0AFvv/02hBBYtWoVjI31+vKIiIiIiIjIwOntnG4AqF69OtauXVvsfZGRkUWmXb9+vdh5a9SogRUrVpRqbURERERERET/FXcFExEREREREekIm24iIiIiIiIiHWHTTURERERERKQjbLqJiIiIiIiIdIRNNxEREREREZGOsOkmIiIiIiIi0hE23UREREREREQ6wqabiIiIiIiISEfYdBMRERERERHpiNZNd3R0tC7qICIiIiIiInrtaN10d+nSBf369cOmTZuQmpqqi5qIiIiIiIiIXgtaN90nTpxAt27dcOTIEbRt2xbDhw/Hvn37kJeXp4v6iIiIiIiIiMotrZvuGjVq4P3338fmzZtx6tQpdOrUCXv37kWbNm0wadIkXLhwQRd1EhEREREREZU7/2kgNVtbW9ja2sLa2hpyuRwPHz7EjBkz8O677yImJqa0aiQiIiIiIiIql0y1fYAQAufOncP+/ftx/Phx2NnZoUePHhg3bhxcXFygVCqxZMkSjB8/Hrt27dJFzURERERERETlgtZNd0BAAHJzc9G5c2esWLECzZs3h5GRkXS/sbExxo4di40bN5ZqoURERERERETljdZN9+TJk9G5c2dYWlpqXqipKQ4fPvyfCiMiIiIiIiIq77Q+pzswMBDTp09HSEiING3Lli34/PPPkZGRIU2rXr166VRIREREREREVE5pvad75syZyM3NhYuLizQtICAAFy5cwOzZs/HDDz+UaoFEpJ26Uw+U/ZNuL7sjW6IWdCuz5yIiIiIi+q+0brrPnTuHkJAQtcPLa9eujQULFiAwMLBUiyMiIqJXEzfwacZsiIjoWVofXm5iYoK0tLQi05OSkmBs/J+uQEZERERERET0WtF6T3efPn3w0UcfYcCAAXB2doYQAlFRUdi6dSuCgoJ0USMRERERERFRuaR10z1p0iQ4Oztj586diImJAQDUqlULH330EQYOHFjqBRIRERERERGVV1o33cbGxhg0aBAGDRpU5L7Tp0+jXbt2pVEXERERERERUbmnddMNAGlpabh79y4KCgqkaYmJiZg3bx6uXr1aasURERERERERlWdaN93Hjh3DpEmTIJPJYGRkBCEEAMDOzo7ndBMRERERERE9Q+vhxpcsWYLZs2cjNDQUZmZmuH37Nnbt2oVGjRqhf//+uqiRiIiIiIiIqFzSuulOSEhA7969YW5uDiMjIxgZGaFhw4b48ssv8eWXX+qiRiIiIiIiIqJySevDyytVqoTw8HA0bNgQlSpVQmRkJNzc3FCtWjXcvXtXFzUSEf1ndace0M8Tbz9cZk8VtaBbmT0XEREREZWM1k334MGD0a9fP1y4cAHt2rXDqFGj0LlzZ4SGhsLNzU0XNRIRERERERGVS1o33R988AE8PT1ha2uLKVOmoGLFirh58yZcXV0xcuRIXdRIREREREREVC5p1XQrlUocOHAAPXr0AABYWFhgzJgxOimMiIiIiIiIqLzTaiA1Y2NjBAcHIz8/X1f1EBEREREREb02tD68fOLEifjqq6/Qs2dP1KhRA6am6ouoV69eqRVHREREREREVJ5p3XTPnDkTAHDgwP9GAjYyMoIQAkZGRrh9+3bpVUdERERERERUjmnddJ84cUIXdRARERERERG9drRuup2dnXVRBxEREREREdFrR+umu3379jAyMir2PoVCgdOnT//XmoiIiIiIiIheC1o33R9//LHabSEEHj16hOPHj2PIkCGlVhgRERERERFRead1092/f/9ipw8cOBDTp0/HgAED/nNRRERERERERK8Dra7T/SLVq1dHdHR0aS2OiIiIiIiIqNzTek/32bNni0wrLCzElStXYGxcaj08ERERERERUbmnddM9fPjwItMsLCxQt25dzJo1S6tlxcXFYebMmbh69SosLS0RFBSEzz//vEjz/tFHH+Hy5ctq0+RyOT777DOMHj0aMpkM33zzDQ4fPozCwkK0bt0as2bNgqOjo7Yvj4iIiIiIiKjUaN10R0RElMoTCyEwevRo1K9fHyEhIUhOTsaIESNQuXJlfPjhh2rzrlu3Tu12RkYGunXrhk6dOgEAFi5ciGvXrmHnzp2wtrbG1KlTMW3aNKxevbpUaiUiIiIiIiJ6GS91PPi2bdtw69Yt6fbp06exbds2rZYRFhaGyMhITJ8+Hfb29nBxccGIESOwdevWf33skiVL0LlzZ7i5uUEul2P37t0YP348atWqBUdHR0yZMgWnTp1CYmKi1q+NiIiIiIiIqLRo3XQvWbIEq1atglwul6ZZWlri559/xtKlS0u8nPDwcDg7O8PBwUGa5uHhgaioKGRnZ2t83IMHD7B//36MHj0aABATE4Ps7Gx4eHhI87i4uMDS0lJtwwARERERERFRWdP68PIdO3bgjz/+gLOzszTN398fGzZsQL9+/TBu3LgSLSctLQ329vZq01S309LSYGNjU+zjVq9ejb59+0rna6elpak9VsXOzg6pqakan1+hUEChUJSoVtIdvgeaMZviMRfNmI1mzEYzZlM85qJZeclGVWd5qbesMBfNmI1mzKZ4Jc1D66Y7JycHFStWLDLd1tYWOTk5JV6OkZGRtk+NlJQUHDp0CAcOHCjRcl503507d7R+fip9//zzj75LeGUxm+IxF82YjWbMRjNmUzzmoll5yyYsLEzfJbySmItmzEYzZvNytG66W7VqhalTp+LTTz+Fs7MzlEoloqOjsXLlSrRq1arEy3F0dER6erraNNVea02jjp84cQJvvvkmateurbYcAEhPT4eVlRWAp4O0paeno1KlShqf39XVVZr/lbX9sL4r0DlfX9+XeyCz0ew1z4a5aMZsNGM2mjGb4r10LgCzeUUoFAqEhYXBy8sLJiYm+i7nlcFcNGM2mjGb4uXm5pZoZ67WTfecOXMwe/ZsBAUFQQghTe/UqZNWlwzz8vJCQkIC0tLSpD3noaGhqF+/PqytrYt9zNmzZ+Hv7682rVatWnBwcMCtW7dQo0YNAEBkZCQKCwvh6emp8flNTEz4gXkF8D3QjNkUj7loxmw0YzaaMZviMRfNyls2/M1XPOaiGbPRjNmoK2kWWjfdjo6OWLp0KTIzMxEfHw8AcHZ2hp2dnVbLcXd3h7e3N4KDgzFz5kw8evQIa9euxaeffgoA6NKlC4KDg9GkSRPpMREREWjbtq3ackxMTNCvXz8sWbIEDRo0gJWVFebPn4+33noLlStX1vblEREREREREZUarZtu4Oklwzw8PKQRw0+fPo3ExES89957Wi1n6dKlmDFjBlq3bg1ra2sMHDgQAwcOBAA8fPgQubm5avMnJSWpjXauMmbMGOTk5CAoKAgKhQKBgYFa7XUnIiIiIiIi0gWtm+4lS5Zg7969WLJkiTTN0tISv/zyCx4/flzi0csBoHr16li7dm2x90VGRhaZdv369WLnNTc3x4wZMzBjxowSPzcRERERERGRrml9ne4dO3Zg8+bN8PHxkaapLhm2ffv2Ui2OiIiIiIiIqDzTuukurUuGEREREREREb3utG66VZcMi4iIQFZWFjIyMhAaGopJkyZpdckwIiIiIiIioted3i4ZRkRERERERPS6K9VLhj3bhBMREREREREZOq0PL1exs7ODu7s73N3dkZaWhh9++KHINbSJiIiIiIiIDNlLXacbAPLz83Ho0CHs3LkTV69ehYeHBz755JPSrI2IiIiIiIioXNO66b5x4wZ27NiBQ4cOwdbWFklJSVi3bh1atGihi/qIiIiIiIiIyq0SH16+bt06dO/eHR999BHkcjlWrlyJkydPwszMDDVr1tRljURERERERETlUon3dH/33Xfo1q0bNm3aVOx1uomIiIiIiIhIXYn3dM+ePRsxMTFo3749Pv/8c/z1119QKBS6rI2IiIiIiIioXCvxnu733nsP7733HiIjI7Fjxw588cUXMDU1RWFhIR4+fIhatWrpsk4iIiIiIiKickfrS4a5ubnhq6++wpkzZzBt2jQ0bdoUI0eORO/evbFp0yZd1EhERERERERULr30dbrNzc3RrVs3rF+/HseOHUO7du2wbt260qyNiIiIiIiIqFx76ab7WTVr1sT48eNx8uTJ0lgcERERERER0WuhVJpuFSMjo9JcHBEREREREVG5VqpNNxERERERERH9D5tuIiIiIiIiIh0p0SXD2rdvX6JDx+VyOUJCQv5zUURERERERESvgxI13R9//LH075SUFGzbtg3t27dHnTp1oFAo8PDhQ5w5cwbDhg3TWaFERERERERE5U2Jmu7+/ftL/x42bBiWLl0KX19ftXmuXLmCH3/8EUOHDi3VAomIiIiIiIjKK63P6b527RoaNmxYZLq3tzeuX79eKkURERERERERvQ60brpr166N5cuXIysrS5qWnZ2NlStXombNmqVaHBEREREREVF5VqLDy581Z84cjBs3Dj///DNsbGwAPG267e3tsXLlylIvkIiIiIiIiKi80rrp9vHxwcmTJ3Hz5k08fvwYBQUFqFq1Knx8fGBhYaGLGomIiIiIiIjKJa2bbgAwNjaGsbExjIyM0L17dwCATCYr1cKIiIiIiIiIyjutz+mOjY1Fjx49MGjQIEycOBEAEB8fj8DAQISHh5d6gURERERERETlldZN99y5c9G2bVtcvnwZRkZGAABnZ2d8/PHHCA4OLvUCiYiIiIiIiMorrZvuGzduYOzYsTA3N5eabgAYPHgwbt++XarFEREREREREZVnWjfdRkZGyMzMLDI9JiaGA6kRERERERERPUPrprtr166YMGECLly4ACEEwsPDsXv3bnzyySfo1q2bLmokIiIiIiIiKpe0Hr186tSpWLlyJcaPH4+CggIEBQXBwcEB7733Hj777DNd1EhERERERERULmnddJubm2PChAmYMGECMjMzYWxsDBsbG13URkRERERERFSuaX14eUFBARYvXowrV67Azs4ONjY22L9/P3744QcUFhbqokYiIiIiIiKicknrpjs4OBhnzpyBnZ2dNM3FxQWXLl3iJcOIiIiIiIiInqF1033s2DH88ssvcHV1laY1bNgQq1atwtGjR0u1OCIiIiIiIqLyTOumWy6Xq12f+9npPLyciIiIiIiI6H+0HkitU6dO+PTTTzFs2DA4OztDqVQiKioKv/zyCzp37qyLGomIiIiIiIjKJa2b7hkzZmDZsmWYNm0aMjIyAAB2dnZ45513MHbs2FIvkIiIiIiIiKi80rrprlChAiZPnozJkycjMzMTANQGVSMiIiIiIiKip7RuugEgIiICDx8+hEwmK3Jf7969/2tNRERERERERK8FrZvu7777DuvWrYOtrS0sLCyK3M+mm4iIiIiIiOgprZvunTt3Yu3atWjTpo0u6iEiIiIiIiJ6bWh9yTBTU1O0bNmyVJ48Li4Ow4YNg6+vL1q0aIGFCxdCqVQWO+/9+/cxaNAg+Pj4oF27dtiwYYN035AhQ+Dh4QEvLy/pv549e5ZKjUREREREREQvS+um+8MPP8T69ev/8xMLITB69GhUrFgRISEh2Lx5Mw4dOoRff/21yLwymQwff/wxevXqhUuXLuHbb7/FH3/8gfv370vzzJ07F2FhYdJ/+/bt+881EhEREREREf0XWh9efu3aNVy/fh2//voratSoAWNj9b5969atJVpOWFgYIiMjsWHDBtjb28Pe3h4jRozAhg0b8OGHH6rNe+jQIdSrVw/9+vUDAPj7++PQoUPalk5ERERERERUprRuul1dXdGwYcP//MTh4eFwdnaGg4ODNM3DwwNRUVHIzs6GjY2NNP3KlSuoV68exo4di3PnzqFatWoYPXo0unbtKs1z8OBBrFmzBqmpqfD29saMGTNQp06d/1wnERERERER0cvSuukeP368xvtKupcbANLS0mBvb682TXU7LS1Nrel+/PgxQkNDsWjRInz33Xc4cOAAPv/8c9SrVw/u7u5wcXGBpaUlFixYAGNjYwQHB2PEiBH4888/YW5uXuzzKxQKKBSKEtdLusH3QDNmUzzmohmz0YzZaMZsisdcNCsv2ajqLC/1lhXmohmz0YzZFK+kebzUdbrv3LmDW7duoaCgQJqWmJiIDRs2oH///iVahpGRUYmfTy6Xo127dtKI6e+88w62bduGgwcPwt3dHbNmzVKbf86cOWjWrBkuX76MVq1aaXwNpH///POPvkt4ZTGb4jEXzZiNZsxGM2ZTPOaiWXnLJiwsTN8lvJKYi2bMRjNm83K0brp///13zJ07F5UqVUJycjKqV6+OpKQk1KhRA6NHjy7xchwdHZGenq42LS0tTbrvWfb29rC1tVWb5uzsjOTk5GKXbWNjAwcHByQlJWl8fldXV1hZWZW4Xr3YfljfFeicr6/vyz2Q2Wj2mmfDXDRjNpoxG82YTfFeOheA2bwiFAoFwsLC4OXlBRMTE32X88pgLpoxG82YTfFyc3NLtDNX66b7l19+wfr16+Hv7w9vb2+cOnUKKSkp+Prrr+Ht7V3i5Xh5eSEhIQFpaWmoWLEiACA0NBT169eHtbW12rweHh44efKk2rT4+Hi0bt0a2dnZWLRoEcaMGYNKlSoBeNq8p6WloVatWhqf38TEhB+YVwDfA82YTfGYi2bMRjNmoxmzKR5z0ay8ZcPffMVjLpoxG82YjbqSZqH1JcNSUlLg7++v9iSVKlXCrFmzMHv27BIvx93dHd7e3ggODkZmZiYiIyOxdu1aDBo0CADQpUsXXLlyBQDQu3dvREZGYuvWrZDJZNi3bx9u3bqFnj17wsbGBqGhoZg3bx6ysrKQnp6O2bNnw93dHX5+ftq+PCIiIiIiIqJSo3XT7eTkhNOnTwMAqlSpgsuXLwMALCwsEBcXp9Wyli5diqysLLRu3Roffvgh+vfvj4EDBwIAHj58iNzcXABA1apVsXbtWmzduhXNmjXDTz/9hB9//BG1a9cGAKxYsQIymQwdOnTA22+/DSEEVq1aVeRyZkRERERERERlSevDy0eNGoXPPvsMf//9N7p164ZPP/0U/v7+iIyMROPGjbVaVvXq1bF27dpi74uMjFS73bRpU+zZs6fYeWvUqIEVK1Zo9dxEREREREREuqZ1092zZ080btwYtra2GDduHGrVqoWbN2/C29sbAwYM0EWNREREREREROXSS10yzNnZWfp3UFAQgoKCSq0gIiIiIiIiotdFiZru9957r8TX1d66det/KoiIiIiIiIjodVGiprt169a6roOIiIiIiIjotVOipnv06NElWtjixYv/UzFEREREREREr5OXOqf79OnTuHnzJgoKCqRpiYmJOH78OCZMmFBqxRERERERERGVZ1o33cuXL8e6devg5uaG0NBQNGrUCA8ePEDVqlUxd+5cXdRIREREREREVC4Za/uAHTt2YPv27di6dStMTU2xefNmnD59GvXr14ep6UvtOCciIiIiIiJ6LWnddGdmZqJ+/foAABMTEyiVSpibm2PGjBlYtGhRqRdIREREREREVF5p3XTXq1cPv/32G5RKJZycnHDixAkAQHZ2NpKTk0u9QCIiIiIiIqLySuvjwSdOnIixY8eiV69eGDhwIMaPHw9XV1fExcUhMDBQFzUSERERERERlUtaN90BAQE4d+4cLC0tMXjwYLz55pu4efMmnJyc0LlzZ13USERERERERFQuvdTIZ5aWltK/fX194ePjgwoVKpRaUURERERERESvA63O6T58+DDWrFmD+/fvAwBmz54NX19fNG7cGJ988gmysrJ0UiQRERERERFReVTipnvt2rX48ssvcfToUQwcOBDr1q3D3bt3sWnTJmzYsAF5eXlYunSpLmslIiIiIiIiKldKfHj5rl27sGbNGjRr1gynTp3C2LFjsW/fPtSrVw8A8M0332DIkCGYPn26zoolIiIiIiIiKk9KvKf7yZMnaNasGQCgdevWUCqVUsMNAM7OzkhJSSn9ComIiIiIiIjKqRI33QqFQvq3qakpTE1fagw2IiIiIiIiIoOhVedcWFgIIYTG20RERERERET0PyVuumUyGby9vaXbQgi120RERERERESkrsRN98aNG3VZBxEREREREdFrp8RNt2oQNSIiIiIiIiIqmRIPpEZERERERERE2mHTTURERERERKQjbLqJiIiIiIiIdOQ/Nd3p6emlVAYRERERERHR60frpjsvLw+zZ8+Gn58fAgICADxtvkeNGoW0tLRSL5CIiIiIiIiovNK66Z4/fz6ioqLw008/wdj46cPNzMxgbW2NOXPmlHqBREREREREROVViS8ZpvLXX39h165dcHR0hJGREQDA2toaM2fORMeOHUu9QCIiIiIiIqLySus93RkZGbCxsSkyXalUorCwsFSKIiIiIiIiInodaN10+/v74/vvv0dBQYE0LT4+Hl999RX8/f1LtTgiIiIiIiKi8kzrpnvmzJkIDQ1Fo0aNIJPJ0KhRI3Ts2BGpqamYOXOmLmokIiIiIiIiKpe0PqfbyckJv//+OyIiIhAXFwcjIyPUrl0bb775pi7qIyIiIiIiIiq3tG66Y2JiYGpqCjs7OzRs2FCanpCQAGNjY1SpUgUmJialWiQRERERERFReaR10925c2dp1PLiGBsbIyAgAHPnzkXVqlX/U3FERERERERE5ZnWTfdPP/2EZcuWoV+/fvDw8ICxsTFu3ryJnTt3YuTIkahQoQLWrVuH4OBgLFu2TBc1ExEREREREZULWjfdixcvxvLly+Hs7CxNa9CgAfz9/TF58mT8/vvvaNiwITp37lyqhRIRERERERGVN1qPXh4dHQ1bW9si0x0cHBAZGQkAMDIygkKh+O/VEREREREREZVjWu/p9vPzw6effooPPvgANWvWBAA8fvwYmzZtgru7O+RyOcaMGYMWLVqUerFERERERERE5YnWTfeCBQswffp0TJgwAYWFhQAAExMTNG7cGN9++y1MTU3h7OyML774otSLJSIiIiIiIipPtG66K1eujNWrV0OhUCAlJQVCCDg6OsLMzAwREREAgG+++abUCyUiIiIiIiIq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9OgRjh8/juPHj+Pq1avw8PDA5MmTS7M2IiIiIiIionJN66Z75cqVOHHiBCIiIuDp6Ym3334b8+fPR40aNXRRHxEREREREVG5pXXTHRISgu7du2PFihVFGu3MzEzY2dmVWnFERERERERE5ZnWTfe2bduKTLtw4QK2b9+OEydO4MaNG6VSGBEREREREVF599LndCckJGDXrl3YvXs3kpKSEBgYiOXLl5dmbURERERERETlmlZNd0FBAY4fP47t27fj0qVL8PHxwZMnT7B9+3Y0aNBAVzUSERERERERlUslbrrnzp2LP//8Ew4ODujRowfmzJmDWrVqwc/PD9bW1rqskYiIiIiIiKhcKnHT/dtvv6Fbt24YP348atWqpcuaiIiIiIiIiF4LxiWd8eeff4ZCoUD37t3Rv39//P7770hPT9dhaURERERERETlW4n3dAcEBCAgIABpaWnYu3cvtmzZgm+++QZKpRJ///03nJycYGr60uOyEREREREREb12SrynW6VixYr44IMPsH//fmzevBl9+vTB/Pnz0aZNGyxYsEAXNRIRERERERGVS/9p17Svry98fX3x1Vdf4cCBA9i5c2dp1UVERERERERU7pXK8eBWVlbo27cv+vbtWxqLIyIiIiIiInotaH14ORERERERERGVDJtuIiIiIiIiIh1h001ERERERESkI2y6iYiIiIiIiHSETTcRERERERGRjrDpJiIiIiIiItIRNt1EREREREREOsKmm4iIiIiIiEhH2HQTERERERER6QibbiIiIiIiIiIdYdNNREREREREpCNsuomIiIiIiIh0hE03ERERERERkY6w6SYiIiIiIiLSETbdRERERERERDrCppuIiIiIiIhIR9h0ExEREREREekIm24iIiIiIiIiHWHTTURERERERKQjpvp88ri4OMycORNXr16FpaUlgoKC8Pnnn8PYWH1bwPLly/Hjjz/C1FS93FOnTqFy5coYMmQIrl27pva4evXqYd++fWXyOoiIiIiIiIiKo7emWwiB0aNHo379+ggJCUFycjJGjBiBypUr48MPPywyf69evbBgwQKNy5s7dy6CgoJ0WTIRERERERGRVvR2eHlYWBgiIyMxffp02Nvbw8XFBSNGjMDWrVv1VRIRERERERFRqdJb0x0eHg5nZ2c4ODhI0zw8PBAVFYXs7Owi80dGRqJv375o3Lgx+vTpg7Nnz6rdf/DgQbz11lto2rQphg0bhujoaF2/BCIiIiIiIqIX0tvh5WlpabC3t1ebprqdlpYGGxsbaXr16tVRq1YtjBs3Dk5OTti2bRtGjRqFvXv3wsXFBS4uLrC0tMSCBQtgbGyM4OBgjBgxAn/++SfMzc2LfX6FQgGFQqG7F0glwvdAM2ZTPOaiGbPRjNloxmyKx1w0Ky/ZqOosL/WWFeaiGbPRjNkUr6R56K3pNjIyKvG8ffv2Rd++faXbH3zwAf7880/s27cPEyZMwKxZs9TmnzNnDpo1a4bLly+jVatWxS7zzp07L1U3la5//vlH3yW8sphN8ZiLZsxGM2ajGbMpHnPRrLxlExYWpu8SXknMRTNmoxmzeTl6a7odHR2Rnp6uNi0tLU2679/UrFkTSUlJxd5nY2MDBwcHjfcDgKurK6ysrEpesD5sP6zvCnTO19f35R7IbDR7zbNhLpoxG82YjWbMpngvnQvAbF4RCoUCYWFh8PLygomJib7LeWUwF82YjWbMpni5ubkl2pmrt6bby8sLCQkJSEtLQ8WKFQEAoaGhqF+/PqytrdXmXbVqFRo3boxmzZpJ0x4+fIguXbogOzsbixYtwpgxY1CpUiUAT5v3tLQ01KpVS+Pzm5iY8APzCuB7oBmzKR5z0YzZaMZsNGM2xWMumpW3bPibr3jMRTNmoxmzUVfSLPQ2kJq7uzu8vb0RHByMzMxMREZGYu3atRg0aBAAoEuXLrhy5QoAIDMzE3PnzkVsbCxkMhnWrVuHmJgYBAUFwcbGBqGhoZg3bx6ysrKQnp6O2bNnw93dHX5+fvp6eURERERERET629MNAEuXLsWMGTPQunVrWFtbY+DAgRg4cCCAp3uyc3NzAQATJkyAQqHAgAEDkJeXBzc3N2zYsAHVqlUDAKxYsQLz5s1Dhw4dYGJigmbNmmHVqlUwNtbbNgUiIiIiIiIi/Tbd1atXx9q1a4u9LzIyUvq3ubk5pk2bhmnTphU7b40aNbBixQqd1EhERERERET0srgrmIiIiIiIiEhH2HQTERERERER6QibbiIiIiIiIiIdYdNNREREREREpCNsuomIiIiIiIh0hE03ERERERERkY6w6SYiIiIiIiLSETbdRERERERERDrCppuIiIiIiIhIR9h0ExEREREREekIm24iIiIiIiIiHWHTTURERERERKQjbLqJiIiIiIiIdIRNNxEREREREZGOsOkmIiIiIiIi0hE23UREREREREQ6wqabiIiIiIiISEfYdBMRERERERHpCJtuIiIiIiIiIh0x1XcBRERERPT6qzv1gH6eePvhMnuqqAXdyuy5iKj84J5uIiIiIiIiIh1h001ERERERESkI2y6iYiIiIiIiHSETTcRERERERGRjrDpJiIiIiIiItIRNt1EREREREREOsKmm4iIiIiIiEhH2HQTERERERER6QibbiIiIiIiIiIdYdNNREREREREpCNsuomIiIiIiIh0hE03ERERERERkY6w6SYiIiIiIiLSETbdRERERERERDrCppuIiIiIiIhIR0z1XQARERERkSGrO/VA2T/p9sNl+nRRC7qV6fMRvUq4p5uIiIiIiIhIR9h0ExEREREREekIm24iIiIiIiIiHWHTTURERERERKQjbLqJiIiIiIiIdIRNNxEREREREZGOsOkmIiIiIiIi0hG9Nt1xcXEYNmwYfH190aJFCyxcuBBKpbLIfMuXL4e7uzu8vLzU/ktOTgYAyGQyzJgxA82aNYOfnx/Gjh2L1NTUsn45RERERERERGr01nQLITB69GhUrFgRISEh2Lx5Mw4dOoRff/212Pl79eqFsLAwtf8qV64MAFi4cCGuXbuGnTt34sSJE8jPz8e0adPK8uUQERERERERFaG3pjssLAyRkZGYPn067O3t4eLighEjRmDr1q1aLUcul2P37t0YP348atWqBUdHR0yZMgWnTp1CYmKijqonIiIiIiIi+nd6a7rDw8Ph7OwMBwcHaZqHhweioqKQnZ1dZP7IyEj07dsXjRs3Rp8+fXD27FkAQExMDLKzs+Hh4SHN6+LiAktLS9y6dUvnr4OIiIiIiIhIE1N9PXFaWhrs7e3Vpqlup6WlwcbGRppevXp11KpVC+PGjYOTkxO2bduGUaNGYe/evUhPT1d7rIqdnV2x53WrzhnPycmBQqEozZdU6uo56O3tKTNZWVkv9Thmo9nrng1z0YzZaMZsNGM2xXvZXABmo8nrngvAbF7kv6xTZUXVJ2RnZ8PYmONNP4vZFC8/Px8Aih2X7FlGQghRFgU9b/Xq1Th27Bh27twpTYuOjkbnzp1x/Phx1KpV64WPf/fdd9GqVSu0bdsWAwYMwPXr12FlZSXd36ZNG4wbNw7vvPOO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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Make a bar plot comparing the models based on the average metric\n", + "import matplotlib.pyplot as plt\n", + "model_names = []\n", + "avg_metrics = []\n", + "for model_name, df in data.items():\n", + " total_samples = df[\"n samples\"].sum()\n", + " weighted_sum = (df[metric] * df[\"n samples\"]).sum()\n", + " avg_metric = weighted_sum / total_samples\n", + " \n", + " model_names.append(model_name)\n", + " avg_metrics.append(avg_metric)\n", + "\n", + "# Sort models by names\n", + "model_names, avg_metrics = zip(*sorted(zip(model_names, avg_metrics)))\n", + "\n", + "plt.figure(figsize=(10, 6))\n", + "plt.bar(model_names, avg_metrics)\n", + "plt.ylabel(f'Average {metric.replace(\"_\", \" \").title()}')\n", + "plt.title(f'Comparison of Models based on Average {metric.replace(\"_\", \" \").title()}')\n", + "#start y-axis from 0.5\n", + "plt.ylim(bottom=0.5)\n", + "plt.xticks(rotation=85)\n", + "plt.tight_layout()\n", + "plt.show() \n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "110609b3", + "metadata": {}, + "source": [ + "# Box Plots: Number of Clients \n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "07bd40b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 20 experiments\n" + ] + } + ], + "source": [ + "# Feature selection experiment\n", + "# experiment_name = \"experiment_good\"\n", + "# experiment_name = \"experiment_all_10percent\"\n", + "experiment_name = \"num_clients_ablation\"\n", + "benchmark_dir = \"benchmark_results_num_clients_ablation\"\n", + "model_names = [\"balanced_random_forest\"]\n", + "datasets = [\"diabetes\"]\n", + "# num_clients = [5,10]\n", + "dirichlet_alpha = [\"0.7\"]\n", + "# dirichlet_alpha = [\"aNone\"]\n", + "keywords = [experiment_name] + datasets + dirichlet_alpha\n", + "\n", + "data = load_data(benchmark_dir, experiment_name, keywords)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66277bb4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Logistic Regression\n", + " model run n_clients alpha \n", + "76 Logistic Regression 0 10 0.7 Normglobal FeatN \\\n", + "77 Logistic Regression 1 10 0.7 Normglobal FeatN \n", + "78 Logistic Regression 2 10 0.7 Normglobal FeatN \n", + "79 Logistic Regression 3 10 0.7 Normglobal FeatN \n", + "80 Logistic Regression 4 10 0.7 Normglobal FeatN \n", + "81 Logistic Regression 5 10 0.7 Normglobal FeatN \n", + "82 Logistic Regression 6 10 0.7 Normglobal FeatN \n", + "83 Logistic Regression 7 10 0.7 Normglobal FeatN \n", + "84 Logistic Regression 8 10 0.7 Normglobal FeatN \n", + "85 Logistic Regression 9 10 0.7 Normglobal FeatN \n", + "86 Logistic Regression 0 20 0.7 Normglobal FeatN \n", + "87 Logistic Regression 1 20 0.7 Normglobal FeatN \n", + "88 Logistic Regression 2 20 0.7 Normglobal FeatN \n", + "89 Logistic Regression 3 20 0.7 Normglobal FeatN \n", + "90 Logistic Regression 4 20 0.7 Normglobal FeatN \n", + "91 Logistic Regression 5 20 0.7 Normglobal FeatN \n", + "92 Logistic Regression 6 20 0.7 Normglobal FeatN \n", + "93 Logistic Regression 7 20 0.7 Normglobal FeatN \n", + "94 Logistic Regression 8 20 0.7 Normglobal FeatN \n", + "95 Logistic Regression 9 20 0.7 Normglobal FeatN \n", + "96 Logistic Regression 10 20 0.7 Normglobal FeatN \n", + "97 Logistic Regression 11 20 0.7 Normglobal FeatN \n", + "98 Logistic Regression 12 20 0.7 Normglobal FeatN \n", + "99 Logistic Regression 13 20 0.7 Normglobal FeatN \n", + "100 Logistic Regression 14 20 0.7 Normglobal FeatN \n", + "101 Logistic Regression 15 20 0.7 Normglobal FeatN \n", + "102 Logistic Regression 16 20 0.7 Normglobal FeatN \n", + "103 Logistic Regression 17 20 0.7 Normglobal FeatN \n", + "104 Logistic Regression 18 20 0.7 Normglobal FeatN \n", + "105 Logistic Regression 19 20 0.7 Normglobal FeatN \n", + "106 Logistic Regression 0 3 0.7 Normglobal FeatN \n", + "107 Logistic Regression 1 3 0.7 Normglobal FeatN \n", + "108 Logistic Regression 2 3 0.7 Normglobal FeatN \n", + "109 Logistic Regression 0 5 0.7 Normglobal FeatN \n", + "110 Logistic Regression 1 5 0.7 Normglobal FeatN \n", + "111 Logistic Regression 2 5 0.7 Normglobal FeatN \n", + "112 Logistic Regression 3 5 0.7 Normglobal FeatN \n", + "113 Logistic Regression 4 5 0.7 Normglobal FeatN \n", + "\n", + " balanced_accuracy \n", + "76 0.759 \n", + "77 0.752 \n", + "78 0.783 \n", + "79 0.741 \n", + "80 0.724 \n", + "81 0.748 \n", + "82 0.742 \n", + "83 0.753 \n", + "84 0.741 \n", + "85 0.741 \n", + "86 0.651 \n", + "87 0.669 \n", + "88 0.755 \n", + "89 0.796 \n", + "90 0.759 \n", + "91 0.749 \n", + "92 0.629 \n", + "93 0.756 \n", + "94 0.758 \n", + "95 0.726 \n", + "96 0.746 \n", + "97 0.741 \n", + "98 0.745 \n", + "99 0.717 \n", + "100 0.728 \n", + "101 0.735 \n", + "102 0.681 \n", + "103 0.742 \n", + "104 0.740 \n", + "105 0.746 \n", + "106 0.739 \n", + "107 0.745 \n", + "108 0.748 \n", + "109 0.738 \n", + "110 0.756 \n", + "111 0.730 \n", + "112 0.741 \n", + "113 0.746 \n", + "Logistic Regression [106 0.739\n", + "107 0.745\n", + "108 0.748\n", + "Name: balanced_accuracy, dtype: float64, 109 0.738\n", + "110 0.756\n", + "111 0.730\n", + "112 0.741\n", + "113 0.746\n", + "Name: balanced_accuracy, dtype: float64, 76 0.759\n", + "77 0.752\n", + "78 0.783\n", + "79 0.741\n", + "80 0.724\n", + "81 0.748\n", + "82 0.742\n", + "83 0.753\n", + "84 0.741\n", + "85 0.741\n", + "Name: balanced_accuracy, dtype: float64, 86 0.651\n", + "87 0.669\n", + "88 0.755\n", + "89 0.796\n", + "90 0.759\n", + "91 0.749\n", + "92 0.629\n", + "93 0.756\n", + "94 0.758\n", + "95 0.726\n", + "96 0.746\n", + "97 0.741\n", + "98 0.745\n", + "99 0.717\n", + "100 0.728\n", + "101 0.735\n", + "102 0.681\n", + "103 0.742\n", + "104 0.740\n", + "105 0.746\n", + "Name: balanced_accuracy, dtype: float64]\n", + "ElasticNet\n", + " model run n_clients alpha balanced_accuracy\n", + "38 ElasticNet 0 10 0.7 Normglobal FeatN 0.765\n", + "39 ElasticNet 1 10 0.7 Normglobal FeatN 0.700\n", + "40 ElasticNet 2 10 0.7 Normglobal FeatN 0.767\n", + "41 ElasticNet 3 10 0.7 Normglobal FeatN 0.745\n", + "42 ElasticNet 4 10 0.7 Normglobal FeatN 0.734\n", + "43 ElasticNet 5 10 0.7 Normglobal FeatN 0.741\n", + "44 ElasticNet 6 10 0.7 Normglobal FeatN 0.747\n", + "45 ElasticNet 7 10 0.7 Normglobal FeatN 0.758\n", + "46 ElasticNet 8 10 0.7 Normglobal FeatN 0.743\n", + "47 ElasticNet 9 10 0.7 Normglobal FeatN 0.743\n", + "48 ElasticNet 0 20 0.7 Normglobal FeatN 0.680\n", + "49 ElasticNet 1 20 0.7 Normglobal FeatN 0.651\n", + "50 ElasticNet 2 20 0.7 Normglobal FeatN 0.755\n", + "51 ElasticNet 3 20 0.7 Normglobal FeatN 0.727\n", + "52 ElasticNet 4 20 0.7 Normglobal FeatN 0.749\n", + "53 ElasticNet 5 20 0.7 Normglobal FeatN 0.742\n", + "54 ElasticNet 6 20 0.7 Normglobal FeatN 0.661\n", + "55 ElasticNet 7 20 0.7 Normglobal FeatN 0.738\n", + "56 ElasticNet 8 20 0.7 Normglobal FeatN 0.749\n", + "57 ElasticNet 9 20 0.7 Normglobal FeatN 0.720\n", + "58 ElasticNet 10 20 0.7 Normglobal FeatN 0.743\n", + "59 ElasticNet 11 20 0.7 Normglobal FeatN 0.736\n", + "60 ElasticNet 12 20 0.7 Normglobal FeatN 0.730\n", + "61 ElasticNet 13 20 0.7 Normglobal FeatN 0.695\n", + "62 ElasticNet 14 20 0.7 Normglobal FeatN 0.718\n", + "63 ElasticNet 15 20 0.7 Normglobal FeatN 0.735\n", + "64 ElasticNet 16 20 0.7 Normglobal FeatN 0.698\n", + "65 ElasticNet 17 20 0.7 Normglobal FeatN 0.732\n", + "66 ElasticNet 18 20 0.7 Normglobal FeatN 0.733\n", + "67 ElasticNet 19 20 0.7 Normglobal FeatN 0.747\n", + "68 ElasticNet 0 3 0.7 Normglobal FeatN 0.743\n", + "69 ElasticNet 1 3 0.7 Normglobal FeatN 0.744\n", + "70 ElasticNet 2 3 0.7 Normglobal FeatN 0.748\n", + "71 ElasticNet 0 5 0.7 Normglobal FeatN 0.741\n", + "72 ElasticNet 1 5 0.7 Normglobal FeatN 0.770\n", + "73 ElasticNet 2 5 0.7 Normglobal FeatN 0.727\n", + "74 ElasticNet 3 5 0.7 Normglobal FeatN 0.742\n", + "75 ElasticNet 4 5 0.7 Normglobal FeatN 0.746\n", + "ElasticNet [68 0.743\n", + "69 0.744\n", + "70 0.748\n", + "Name: balanced_accuracy, dtype: float64, 71 0.741\n", + "72 0.770\n", + "73 0.727\n", + "74 0.742\n", + "75 0.746\n", + "Name: balanced_accuracy, dtype: float64, 38 0.765\n", + "39 0.700\n", + "40 0.767\n", + "41 0.745\n", + "42 0.734\n", + "43 0.741\n", + "44 0.747\n", + "45 0.758\n", + "46 0.743\n", + "47 0.743\n", + "Name: balanced_accuracy, dtype: float64, 48 0.680\n", + "49 0.651\n", + "50 0.755\n", + "51 0.727\n", + "52 0.749\n", + "53 0.742\n", + "54 0.661\n", + "55 0.738\n", + "56 0.749\n", + "57 0.720\n", + "58 0.743\n", + "59 0.736\n", + "60 0.730\n", + "61 0.695\n", + "62 0.718\n", + "63 0.735\n", + "64 0.698\n", + "65 0.732\n", + "66 0.733\n", + "67 0.747\n", + "Name: balanced_accuracy, dtype: float64]\n", + "Linear SVC\n", + " model run n_clients alpha balanced_accuracy\n", + "114 Linear SVC 0 10 0.7 Normglobal FeatN 0.754\n", + "115 Linear SVC 1 10 0.7 Normglobal FeatN 0.738\n", + "116 Linear SVC 2 10 0.7 Normglobal FeatN 0.779\n", + "117 Linear SVC 3 10 0.7 Normglobal FeatN 0.747\n", + "118 Linear SVC 4 10 0.7 Normglobal FeatN 0.750\n", + "119 Linear SVC 5 10 0.7 Normglobal FeatN 0.746\n", + "120 Linear SVC 6 10 0.7 Normglobal FeatN 0.744\n", + "121 Linear SVC 7 10 0.7 Normglobal FeatN 0.757\n", + "122 Linear SVC 8 10 0.7 Normglobal FeatN 0.746\n", + "123 Linear SVC 9 10 0.7 Normglobal FeatN 0.746\n", + "124 Linear SVC 0 20 0.7 Normglobal FeatN 0.641\n", + "125 Linear SVC 1 20 0.7 Normglobal FeatN 0.692\n", + "126 Linear SVC 2 20 0.7 Normglobal FeatN 0.742\n", + "127 Linear SVC 3 20 0.7 Normglobal FeatN 0.779\n", + "128 Linear SVC 4 20 0.7 Normglobal FeatN 0.750\n", + "129 Linear SVC 5 20 0.7 Normglobal FeatN 0.728\n", + "130 Linear SVC 6 20 0.7 Normglobal FeatN 0.592\n", + "131 Linear SVC 7 20 0.7 Normglobal FeatN 0.747\n", + "132 Linear SVC 8 20 0.7 Normglobal FeatN 0.755\n", + "133 Linear SVC 9 20 0.7 Normglobal FeatN 0.725\n", + "134 Linear SVC 10 20 0.7 Normglobal FeatN 0.742\n", + "135 Linear SVC 11 20 0.7 Normglobal FeatN 0.742\n", + "136 Linear SVC 12 20 0.7 Normglobal FeatN 0.764\n", + "137 Linear SVC 13 20 0.7 Normglobal FeatN 0.744\n", + "138 Linear SVC 14 20 0.7 Normglobal FeatN 0.727\n", + "139 Linear SVC 15 20 0.7 Normglobal FeatN 0.732\n", + "140 Linear SVC 16 20 0.7 Normglobal FeatN 0.708\n", + "141 Linear SVC 17 20 0.7 Normglobal FeatN 0.745\n", + "142 Linear SVC 18 20 0.7 Normglobal FeatN 0.741\n", + "143 Linear SVC 19 20 0.7 Normglobal FeatN 0.742\n", + "144 Linear SVC 0 3 0.7 Normglobal FeatN 0.729\n", + "145 Linear SVC 1 3 0.7 Normglobal FeatN 0.750\n", + "146 Linear SVC 2 3 0.7 Normglobal FeatN 0.752\n", + "147 Linear SVC 0 5 0.7 Normglobal FeatN 0.731\n", + "148 Linear SVC 1 5 0.7 Normglobal FeatN 0.768\n", + "149 Linear SVC 2 5 0.7 Normglobal FeatN 0.732\n", + "150 Linear SVC 3 5 0.7 Normglobal FeatN 0.749\n", + "151 Linear SVC 4 5 0.7 Normglobal FeatN 0.750\n", + "Linear SVC [144 0.729\n", + "145 0.750\n", + "146 0.752\n", + "Name: balanced_accuracy, dtype: float64, 147 0.731\n", + "148 0.768\n", + "149 0.732\n", + "150 0.749\n", + "151 0.750\n", + "Name: balanced_accuracy, dtype: float64, 114 0.754\n", + "115 0.738\n", + "116 0.779\n", + "117 0.747\n", + "118 0.750\n", + "119 0.746\n", + "120 0.744\n", + "121 0.757\n", + "122 0.746\n", + "123 0.746\n", + "Name: balanced_accuracy, dtype: float64, 124 0.641\n", + "125 0.692\n", + "126 0.742\n", + "127 0.779\n", + "128 0.750\n", + "129 0.728\n", + "130 0.592\n", + "131 0.747\n", + "132 0.755\n", + "133 0.725\n", + "134 0.742\n", + "135 0.742\n", + "136 0.764\n", + "137 0.744\n", + "138 0.727\n", + "139 0.732\n", + "140 0.708\n", + "141 0.745\n", + "142 0.741\n", + "143 0.742\n", + "Name: balanced_accuracy, dtype: float64]\n", + "Random Forest\n", + " model run n_clients alpha balanced_accuracy\n", + "152 Random Forest 0 10 0.7 Normglobal FeatN 0.771\n", + "153 Random Forest 1 10 0.7 Normglobal FeatN 0.816\n", + "154 Random Forest 2 10 0.7 Normglobal FeatN 0.749\n", + "155 Random Forest 3 10 0.7 Normglobal FeatN 0.743\n", + "156 Random Forest 4 10 0.7 Normglobal FeatN 0.761\n", + "157 Random Forest 5 10 0.7 Normglobal FeatN 0.732\n", + "158 Random Forest 6 10 0.7 Normglobal FeatN 0.750\n", + "159 Random Forest 7 10 0.7 Normglobal FeatN 0.747\n", + "160 Random Forest 8 10 0.7 Normglobal FeatN 0.744\n", + "161 Random Forest 9 10 0.7 Normglobal FeatN 0.747\n", + "162 Random Forest 0 20 0.7 Normglobal FeatN 0.737\n", + "163 Random Forest 1 20 0.7 Normglobal FeatN 0.700\n", + "164 Random Forest 2 20 0.7 Normglobal FeatN 0.742\n", + "165 Random Forest 3 20 0.7 Normglobal FeatN 0.800\n", + "166 Random Forest 4 20 0.7 Normglobal FeatN 0.761\n", + "167 Random Forest 5 20 0.7 Normglobal FeatN 0.762\n", + "168 Random Forest 6 20 0.7 Normglobal FeatN 0.610\n", + "169 Random Forest 7 20 0.7 Normglobal FeatN 0.734\n", + "170 Random Forest 8 20 0.7 Normglobal FeatN 0.756\n", + "171 Random Forest 9 20 0.7 Normglobal FeatN 0.753\n", + "172 Random Forest 10 20 0.7 Normglobal FeatN 0.756\n", + "173 Random Forest 11 20 0.7 Normglobal FeatN 0.737\n", + "174 Random Forest 12 20 0.7 Normglobal FeatN 0.755\n", + "175 Random Forest 13 20 0.7 Normglobal FeatN 0.713\n", + "176 Random Forest 14 20 0.7 Normglobal FeatN 0.717\n", + "177 Random Forest 15 20 0.7 Normglobal FeatN 0.739\n", + "178 Random Forest 16 20 0.7 Normglobal FeatN 0.714\n", + "179 Random Forest 17 20 0.7 Normglobal FeatN 0.746\n", + "180 Random Forest 18 20 0.7 Normglobal FeatN 0.743\n", + "181 Random Forest 19 20 0.7 Normglobal FeatN 0.751\n", + "182 Random Forest 0 3 0.7 Normglobal FeatN 0.737\n", + "183 Random Forest 1 3 0.7 Normglobal FeatN 0.750\n", + "184 Random Forest 2 3 0.7 Normglobal FeatN 0.753\n", + "185 Random Forest 0 5 0.7 Normglobal FeatN 0.744\n", + "186 Random Forest 1 5 0.7 Normglobal FeatN 0.771\n", + "187 Random Forest 2 5 0.7 Normglobal FeatN 0.739\n", + "188 Random Forest 3 5 0.7 Normglobal FeatN 0.747\n", + "189 Random Forest 4 5 0.7 Normglobal FeatN 0.747\n", + "Random Forest [182 0.737\n", + "183 0.750\n", + "184 0.753\n", + "Name: balanced_accuracy, dtype: float64, 185 0.744\n", + "186 0.771\n", + "187 0.739\n", + "188 0.747\n", + "189 0.747\n", + "Name: balanced_accuracy, dtype: float64, 152 0.771\n", + "153 0.816\n", + "154 0.749\n", + "155 0.743\n", + "156 0.761\n", + "157 0.732\n", + "158 0.750\n", + "159 0.747\n", + "160 0.744\n", + "161 0.747\n", + "Name: balanced_accuracy, dtype: float64, 162 0.737\n", + "163 0.700\n", + "164 0.742\n", + "165 0.800\n", + "166 0.761\n", + "167 0.762\n", + "168 0.610\n", + "169 0.734\n", + "170 0.756\n", + "171 0.753\n", + "172 0.756\n", + "173 0.737\n", + "174 0.755\n", + "175 0.713\n", + "176 0.717\n", + "177 0.739\n", + "178 0.714\n", + "179 0.746\n", + "180 0.743\n", + "181 0.751\n", + "Name: balanced_accuracy, dtype: float64]\n", + "Balanced Random Forest\n", + " model run n_clients alpha \n", + "0 Balanced Random Forest 0 10 0.7 Normglobal FeatN \\\n", + "1 Balanced Random Forest 1 10 0.7 Normglobal FeatN \n", + "2 Balanced Random Forest 2 10 0.7 Normglobal FeatN \n", + "3 Balanced Random Forest 3 10 0.7 Normglobal FeatN \n", + "4 Balanced Random Forest 4 10 0.7 Normglobal FeatN \n", + "5 Balanced Random Forest 5 10 0.7 Normglobal FeatN \n", + "6 Balanced Random Forest 6 10 0.7 Normglobal FeatN \n", + "7 Balanced Random Forest 7 10 0.7 Normglobal FeatN \n", + "8 Balanced Random Forest 8 10 0.7 Normglobal FeatN \n", + "9 Balanced Random Forest 9 10 0.7 Normglobal FeatN \n", + "10 Balanced Random Forest 0 20 0.7 Normglobal FeatN \n", + "11 Balanced Random Forest 1 20 0.7 Normglobal FeatN \n", + "12 Balanced Random Forest 2 20 0.7 Normglobal FeatN \n", + "13 Balanced Random Forest 3 20 0.7 Normglobal FeatN \n", + "14 Balanced Random Forest 4 20 0.7 Normglobal FeatN \n", + "15 Balanced Random Forest 5 20 0.7 Normglobal FeatN \n", + "16 Balanced Random Forest 6 20 0.7 Normglobal FeatN \n", + "17 Balanced Random Forest 7 20 0.7 Normglobal FeatN \n", + "18 Balanced Random Forest 8 20 0.7 Normglobal FeatN \n", + "19 Balanced Random Forest 9 20 0.7 Normglobal FeatN \n", + "20 Balanced Random Forest 10 20 0.7 Normglobal FeatN \n", + "21 Balanced Random Forest 11 20 0.7 Normglobal FeatN \n", + "22 Balanced Random Forest 12 20 0.7 Normglobal FeatN \n", + "23 Balanced Random Forest 13 20 0.7 Normglobal FeatN \n", + "24 Balanced Random Forest 14 20 0.7 Normglobal FeatN \n", + "25 Balanced Random Forest 15 20 0.7 Normglobal FeatN \n", + "26 Balanced Random Forest 16 20 0.7 Normglobal FeatN \n", + "27 Balanced Random Forest 17 20 0.7 Normglobal FeatN \n", + "28 Balanced Random Forest 18 20 0.7 Normglobal FeatN \n", + "29 Balanced Random Forest 19 20 0.7 Normglobal FeatN \n", + "30 Balanced Random Forest 0 3 0.7 Normglobal FeatN \n", + "31 Balanced Random Forest 1 3 0.7 Normglobal FeatN \n", + "32 Balanced Random Forest 2 3 0.7 Normglobal FeatN \n", + "33 Balanced Random Forest 0 5 0.7 Normglobal FeatN \n", + "34 Balanced Random Forest 1 5 0.7 Normglobal FeatN \n", + "35 Balanced Random Forest 2 5 0.7 Normglobal FeatN \n", + "36 Balanced Random Forest 3 5 0.7 Normglobal FeatN \n", + "37 Balanced Random Forest 4 5 0.7 Normglobal FeatN \n", + "\n", + " balanced_accuracy \n", + "0 0.769 \n", + "1 0.740 \n", + "2 0.767 \n", + "3 0.747 \n", + "4 0.742 \n", + "5 0.739 \n", + "6 0.755 \n", + "7 0.745 \n", + "8 0.749 \n", + "9 0.748 \n", + "10 0.713 \n", + "11 0.691 \n", + "12 0.740 \n", + "13 0.768 \n", + "14 0.756 \n", + "15 0.759 \n", + "16 0.628 \n", + "17 0.756 \n", + "18 0.755 \n", + "19 0.763 \n", + "20 0.758 \n", + "21 0.741 \n", + "22 0.756 \n", + "23 0.716 \n", + "24 0.725 \n", + "25 0.737 \n", + "26 0.707 \n", + "27 0.750 \n", + "28 0.746 \n", + "29 0.751 \n", + "30 0.741 \n", + "31 0.752 \n", + "32 0.755 \n", + "33 0.744 \n", + "34 0.769 \n", + "35 0.739 \n", + "36 0.746 \n", + "37 0.747 \n", + "Balanced Random Forest [30 0.741\n", + "31 0.752\n", + "32 0.755\n", + "Name: balanced_accuracy, dtype: float64, 33 0.744\n", + "34 0.769\n", + "35 0.739\n", + "36 0.746\n", + "37 0.747\n", + "Name: balanced_accuracy, dtype: float64, 0 0.769\n", + "1 0.740\n", + "2 0.767\n", + "3 0.747\n", + "4 0.742\n", + "5 0.739\n", + "6 0.755\n", + "7 0.745\n", + "8 0.749\n", + "9 0.748\n", + "Name: balanced_accuracy, dtype: float64, 10 0.713\n", + "11 0.691\n", + "12 0.740\n", + "13 0.768\n", + "14 0.756\n", + "15 0.759\n", + "16 0.628\n", + "17 0.756\n", + "18 0.755\n", + "19 0.763\n", + "20 0.758\n", + "21 0.741\n", + "22 0.756\n", + "23 0.716\n", + "24 0.725\n", + "25 0.737\n", + "26 0.707\n", + "27 0.750\n", + "28 0.746\n", + "29 0.751\n", + "Name: balanced_accuracy, dtype: float64]\n", + "XGBoost\n", + "Empty DataFrame\n", + "Columns: [model, run, n_clients, alpha, balanced_accuracy]\n", + "Index: []\n", + "XGBoost [Series([], Name: balanced_accuracy, dtype: float64), Series([], Name: balanced_accuracy, dtype: float64), Series([], Name: balanced_accuracy, dtype: float64), Series([], Name: balanced_accuracy, dtype: float64)]\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAO7CAYAAAC76s0MAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzde0BUZf7H8c8w4IW7mJq6GqhlroJYm2KKYlpaodJE1lr6WyurLbuJZbp2L82Syrab3csuVkRUrJlloaCYlZaiSTcp73kBuSnCzPn94TLrKCOMDMwMvF+77Mo5zzzznTnMfOd85znPYzIMwxAAAAAAAAAAADiOn6cDAAAAAAAAAADAW1FEBwAAAAAAAADACYroAAAAAAAAAAA4QREdAAAAAAAAAAAnKKIDAAAAAAAAAOAERXQAAAAAAAAAAJygiA4AAAAAAAAAgBMU0QEAAAAAAAAAcIIiOgAAAAAAAAAATlBEB2pw3nnnqWfPnnriiSca9X7T09PVs2dPnXfeefXu6+uvv1bPnj3Vs2fPevdV/Xwc+9OvXz9dccUVysjIqPd9eLu77rpLPXv21F133eXpUAAAPsZZHj36Z9u2bZowYYJ69uypf//7340S18l+7qjOiT179tT7779/3P6jHxMAAN6uruf/vnhOuH37ds2ZM0ejRo1Sv3791KdPHyUkJOiOO+7Q1q1bJUk//PCDPXfn5uYe18dvv/1m35+VlWXfvnXrVt1333264IILFBMTo5iYGF144YV66qmntH///sZ6iECj8fd0AAD+p0ePHpo4caLCwsJcvu3IkSPVvn17LVy4UJJ06qmnauLEiW6NLyYmRrGxsZIkwzD0008/6euvv9a6deu0d+9eXXvttW69P28yaNAghYSEKCYmxtOhAAB81NF59FjBwcENet8VFRU699xzdf755+uRRx6RVL/PHdX+/e9/a/To0WrVqlW9Y3z++ef1xBNPaNmyZfrLX/5S7/4AAHAnXzsn3LVrly699FIVFhYqKipKF198sQ4fPqysrCx9/PHH+vrrr5WRkaG+ffuqa9eu+uOPP7Rs2TINHDjQoZ9ly5ZJksLDwzVo0CBJRwbt/fOf/1RZWZn+8pe/aMyYMSorK9PKlSv1zDPP6OOPP9Ybb7yhTp06NfrjBhoKRXTAi1R/e+uq9evXq6CgQO3bt7dvO+200/Svf/3LneHp3HPP1e233+6w7f7779fbb7+tF154QZMmTZLZbHbrfXqL0aNHa/To0Z4OAwDgw2rKo41l2bJlKi0tddh2sp87qvn5+Wn37t164403dN1119U3RH3yySf17gMAgIbia+eE77//vgoLC9WpUyd9/PHHatGihaQjI8gvvPBCHThwQCtWrFBSUpISExP17LPP6ssvv9SsWbMc+vnyyy8lSaNGjVJAQIDKy8s1depUlZWVadSoUZo3b54CAgIkSQcOHNCkSZO0ceNGPfzww3rmmWca90EDDYjpXIB62rx5s26++WYNGDBAffr00XnnnafZs2erqKjIod3777+v888/X9HR0UpKStLq1at12WWXqWfPnkpPT5dU82XVe/bs0T333KPzzjtP0dHRGjx4sKZPn66dO3dKOnJJ2WWXXSZJWrNmjb0/Z9O5fPbZZ7rsssvUt29fDRw4UDfccIN+/PHHk3781d9EHzhwwH7Jls1m02uvvaakpCT169dPAwcO1KxZs1RcXOxw2+eff15DhgxRTEyM/v73v2vz5s0699xz1bNnT3399deSjoxw69mzp6ZPn65///vfOvvss/X8889LkkpKSvTggw9q5MiRiomJ0fDhw7VgwQIZhlHn50+SSktL9eijj2rkyJH25+Xmm2/WTz/9ZG9T06V7VVVVevHFF5WYmKjo6GidddZZmjBhgsMlbtL/Lmv/9ddfdd9992nAgAHq16+fpk+frrKyspN+7gEATV9+fr5uvPFGDR48WLGxsRozZow++OADhzYFBQVKSUnR0KFDFR0drWHDhumBBx6w590JEybYi/cffvihPc86m85l0aJFGj16tKKjoxUfH6+UlBT7Jd9HS0hIkCS9+OKLOnDgwAkfR1ZWliZMmGDPgddcc41++eUXSf+bgq769+HDh2vChAmuP1kAADSgY88Jt23bpp49e6pXr17av3+/br/9dp111lk655xz9Mgjj6iqqsp+28OHD2v+/Pm6+OKL1bdvX8XHx2vu3Lk6fPiwvY3NZtNLL72kiy++WLGxsRo8eLBSUlK0Y8cOe5sTnR8fa9++ffb7PjqWLl26aMWKFfr++++VlJQkSUpMTJR0ZPqXzZs329vu379f33//vUObxYsXa+/evQoICNB9991nL6BLUlhYmB566CHNmDFD06dPd/k5BrwZRXSgHtavX6/LL79cS5cuVdeuXTV69GgdPnxYr7/+uq666iodOnRIkrRy5UrNmjVLf/zxh84880z17NlTU6dOrdNcoddff73effddtWvXTsnJyerZs6cyMjJ05ZVXqrKyUoMGDVLfvn0lSR06dNDEiRPVo0ePGvv68MMPdcsttygvL08JCQnq27evvvrqK40fP95+4uqqwsJCSZK/v7/Cw8MlSY899pjmzJmjbdu2adSoUerWrZvef/993XTTTfbbvf/++3riiSe0e/dunXXWWTr11FN14403Hldor7Z27Vq9++67uvDCC9WtWzdZrVZdffXVevPNN2UYhsaMGSN/f389/vjjevrpp+v8/EnSzJkz9fLLL6tFixayWCz629/+ps8//1zjx48/4VxuU6dO1bx587Rjxw77HHNr1qzR9ddfX+M88f/617/0888/a9CgQaqoqFBGRkajz7sPAPAdf/75pyZOnKhly5ape/fuuvDCC7VlyxbNnDlTn3/+uaQj07RMnDhRmZmZ6t69u5KTk9WhQwe99dZb9tHhI0eOVPfu3SVJ3bt318SJE3XqqafWeJ9PP/207r33Xv3+++8aNWqUIiMjlZmZqfHjx2vPnj0ObXv37q1hw4apuLhYCxYscPo4vvzyS91www367rvvNGDAAA0ZMkSrVq3ShAkTtH//fp166qmyWCz29haLRSNHjqzXcwcAQGOx2Wy68cYbVVZWpgEDBqi4uFivvvqqfapVSZo2bZqeffZZHThwQKNHj1bbtm31yiuv6J577rG3eeKJJ/TYY49pz549GjNmjNq1a6fMzEzdeOONstlsDvd57PlxTU4//XRJ0t69ezV27Fg988wz+uabb3To0CFFRETIZDLZ23bv3l1//etfJf1v+hbpyJfgNptNHTt21N/+9jdJ0nfffSdJOvPMM9WmTZvj7vevf/2r/vGPf6hr164uPY+At2M6F6Ae5s6dq0OHDik+Pl4vvviiTCaTdu3apfPPP18///yz0tPTNX78eL3xxhuSjpxsLlq0SGazWZ9++qluu+22E/ZfWFiojRs3SpKee+45RURESJJeeOEFGYah4uJijR49WgUFBfrhhx8cpnCpHsldzTAMe8F28uTJmjp1qqQjheCvvvpKb731lu699946P3abzaaffvpJL730kiTp/PPPV0BAgPbt22d/vE8++aQGDx4sSbriiiu0Zs0aff311xowYIBef/11SUdGmz377LOSpJdeekmPPfZYjfe3detWffLJJ/YPAp9//rnWr1+vwMBAvffeewoPD1dRUZESEhL08ssv69prr9WhQ4dqff7atm2r7OxsSdLDDz9sv6x90aJF2r9/v0pKSuy3O1pubq4+++wzSdLLL7+sfv36Sfrf9Dbz5s3TmDFj5Of3v+8qw8PD9dxzz8lkMqlTp0568cUXtXTp0uMulwMANE2rVq1SeXn5cdtjYmJqvDx8x44dGjlypPz9/fWvf/1LZrNZAQEBevfdd7VkyRL7543du3crKChIL730kvz8/GSz2fTkk08qJCREhw4d0lVXXaW8vDz9+uuviomJsX9WqD4JrlZSUqIXX3xR0pEvfi+//HIZhqG///3vys/P14cffugwbYthGLr99tu1fPlyvfnmm06L808++aQMw9C1115r//zx+OOPa8GCBXrrrbd0880366abbrJfmXfTTTcxJzoAwKf07t1bd999tyTp9ttv1+LFi7V06VJNmjRJmzZtsp87Lly4UFFRUaqsrNQFF1ygDz/8UDfddJO6dOkiPz8/XX755TrvvPOUkJCgP//8U/Hx8frxxx/1+++/Kyoqyn5/x54f1+TSSy/VJ598onXr1umPP/7QU089JUkKCAjQueeeq+uvv15nn322vf3o0aO1adMmffnll/YBcNVTuVx00UX2ovuff/4p6cggPqA5oYgOnKSDBw9q7dq1ko5c1lSdUE499VT169dPX3/9tdasWaPx48fbp0s577zz7HOGjxw5Uq1bt9bBgwed3kdwcLBOOeUU7d27V5dddpnOO+889evXT5dddlmN3/ieyJYtW7R7925J0rBhw+zbH3/88Tr38fzzz9d4qVj//v3tBfj169fbLxX7/PPPtXz5ckmyT1uyfv16nXXWWfaR7xdccIG9n6SkJKdF9KioKIcPCNXPfcuWLR3mWWvRooUOHDign3/+Wb169arT8xcVFaWNGzfqn//8p4YPH65+/fpp2LBhJ/xQsGrVKknSX/7yF3sBXTry4eLtt9/Wnj17tGXLFvvIP0kaM2aM/e/kb3/7m1588cXjRvUBAJqu9evXa/369cdtv+SSS2ososfGxqpz58767LPPlJqaqsrKSvsl1tUnsKeeeqpatWqlsrIyjRkzRkOHDlW/fv103XXXubxY6ffff2+/iq76s4LJZNKiRYuc3qZnz54aM2aMMjIy9NRTT2n27NkO+0tLS5Wfny9J+umnn/Twww9LOjIFjaQanw8AAHzN2LFj7f/+29/+psWLF9vP9arPXQMCAvT222/b21WfG27YsEFdunTRbbfdpuzsbG3YsEGrVq06bprSo4vox54f16RVq1Z68803lZmZqSVLlujbb79VSUmJKisrtXz5cq1cuVIvvPCCfYrWiy++WI899pg2btyo3bt3q02bNlq5cqWk/03lcnTcVqvV9ScK8GEU0YGTVFxcbL+k6tiCdvXv1fODVk8JcvSIZj8/P4WFhZ2wiB4QEKCXXnpJ999/v9atW6c33nhDb7zxhlq0aKGJEyfqjjvuqHO81dOuSFJoaGidb3e0mJgYxcbGSpLWrVunDRs26LTTTtOrr74qf/8jbyclJSX29jWddO/evVuFhYX2DwRHP3cn+mLg2NHg1fdTWFhoH/l+tF27dikmJqZOz99TTz2le++9VytXrtS7776rd999V2azWWPGjNGDDz7oMMdbtern09mxl3TcvPht27a1/7t169aSdNxleQCApuuGG25waWHRb7/9VldffbUqKiqctjnllFP0/PPPa/bs2frpp5/0888/S5KCgoJ0yy236B//+Eed7+9kPyvccsstWrx4sTIyMnTNNdc47Dt67Y+vvvrquNvu2rWrzvcDAIC3Ovp89dhzvepz18rKyhrPXXfv3i2bzaYpU6Y4TKVytKML6sfe34n4+/srKSlJSUlJMgxDP//8sz7++GO98sorqqqq0ksvvWQvonfo0EF/+9vftGbNGn355Zfq1KmTysvL1a1bN/tUL5LUqVMne9xAc0IRHThJoaGh9kumqxfsqFb9e3ViCw8P1549exwW3bLZbLUuwiVJvXr10qJFi/Tnn39q3bp1WrVqlT744AO99NJL6t27ty666KI6x1vt6JPksrIylZSUyN/fX6eccsoJ+zj33HPtJ/9bt25VYmKifv/9d73yyiv2y7vDwsLs7b/55psaT8KPXjzl6OfgRPOPHz0tytGP58wzz9RHH33k9HZ1ef7+8pe/6OWXX1ZRUZHWrVunNWvWaNGiRfrwww/VvXt3TZ48+bh+q4vlx8a8d+9e+7+PLpoDAOCqxx9/XBUVFYqOjtazzz6r9u3ba968efYpV6oNHDhQn3zyibZt26Z169Zp+fLlyszM1Jw5c9SvXz/72im1OTpnFxUV2admKS4uVnl5uVq0aFHjSXvnzp3197//Xa+//rpSU1MVGBhon7YmJCREJpNJhmHomWee0YgRI0726QAAwCdV59eQkBB9++23NbbJzc21F9Dnzp2riy66SIZh2KcbPdax58c12bVrl37++Wf99a9/Vdu2bWUymXTGGWdo2rRpKi0t1TvvvKPt27c73GbMmDFas2aNVqxYYZ9a7ehR6JIUFxend9991z7NzGmnneawPy8vTw8//LDGjx+viy++uE6xAr6Av2TgJLVu3do+f9h//vMf+zfD27Zts69eXT0f+BlnnCHpyAis6m+jlyxZcsJR6JL066+/6vHHH9drr72m9u3ba+TIkbr//vt17rnn2u9L+t/lVEeP9jpWt27d7Ce+R3+7fffdd2vo0KH2y6vrqkuXLvbi8jPPPKM//vhDkhQdHW0fuZ2Tk2Nv//bbb+u1117Tr7/+qhYtWigyMlKS9MUXX9jb1LQYpzPVz/2vv/6qnTt3SpLKy8v17LPP6q233lJJSUmdnr/du3frqaee0hNPPKHw8HANGzZM06dPt69S7mzx1+pv67dv365169bZt//nP/+RdGSal2M/TAAA4IrqL5pjY2PVvn17HT582D6au/oL6R9++EFz585VRkaG/vKXv2j06NGaN2+efTqx6pPj6s8KNc3JXi0mJsaew6vzs2EYuu666zR06FC98sorTm97ww03KCgoSMuWLXP4sjwwMFC9evWSJPsaJJK0fPlyvfTSS8rNzXWIr7YYAQDwNdXnriUlJfrhhx8kHRlU9+KLL2rhwoXavXu3w1XMw4YNU4sWLbRkyRL7tqNza10cPnxYSUlJuvbaazVv3jyHkeyGYWjr1q2SdNw568iRIxUQEKA1a9bUOJWLJI0YMUJdu3aVYRiaPXu2wxVzRUVFuvvuu7V27Vq99957FNDRpDASHTiBd955R4sXLz5u+7nnnqv7779fd9xxhyZMmKCcnBxdeeWVioyM1PLly1VZWal+/frZk8348eO1cuVK/fDDD/r73/+uqKgo5eTkKDQ0VMXFxU7vPzg4WAsXLtTBgwf1zTffqGPHjtq9e7eys7PVqlUrJSQkSPrfgh6bNm1SSkqKEhMTFRgY6NCX2WzWrbfeqnvvvVevvvqqtm/fbj8Zb9Wqla6//nqXn5/rrrtOH330kf744w97vxEREbryyiv12muvacaMGVq2bJkKCwu1cuVKtWvXThdffLH9OZk9e7Y+++wzXX311QoPD1deXl6d7zshIUHR0dHasGGDxo0bp8GDBysvL08//fSTBgwYoPHjx6u8vLzW5y8sLEzvv/++/vzzT61fv17dunVTUVGRPv/8c/n5+TnM2X60uLg4nX/++fr888913XXXacSIEdq1a5dWrVols9msmTNnOhQEAABwtrCodGSB7mPFxMTol19+UXp6ug4ePKh169apW7du+uWXX7Rx40bdfffduvzyy/XGG2/IZDIpOztb4eHh+v333/XLL78oIiJC55xzjiSpffv2ko58oT9jxgyNGzfuuPuLiIjQpEmT9MILL+iRRx7R999/r127dmndunWKiIjQhAkTnD62iIgIXXPNNXrqqafsa6NUu+mmmzRlyhQtWrRI27dvV0hIiL788kv7ZeTSkWlp/P39VVVVpbvuukvx8fEuTX0DAEB91Xb+f7LOPPNM+7nj5MmTNWzYMP3+++9at26devToocsuu0x9+vSx58Ebb7xR7du319dff63+/ftrzZo1mj9/vktzkLdo0ULTp0/XzJkzlZ6erry8PMXExMhkMumHH37QTz/9pFatWmnKlCkOtwsNDdWQIUO0bNkylZaWKjo6+rhCe4sWLTR//nxdc801ysrK0siRI3XuueeqoqJCq1at0v79+9W1a1c9+uijJ/2cAd6Ir4SAEzhw4ID++OOP436qFwjp27ev3nnnHSUkJNjnFgsMDNQNN9ygV155xT6aa8SIEZoxY4bat2+vTZs26ddff9X8+fPtc6XVNOe2dKQ4/uabbyohIUHffvutFi1apLVr12rIkCF67bXX7CPcL774Yg0ePFgBAQHKyclxOk3MFVdcodTUVP31r39VVlaWvv76a8XHx+vtt9/WmWee6fLz06JFC82aNUvSkcJA9Ujy6dOn64477tCpp56qzz77TBs2bNCFF16ot99+W+3atZMkTZgwQdddd53atGmj7777Tnv37rWvFn6i56Sa2WzWK6+8oiuuuEKGYeijjz5SUVGRrr76aj377LMymUx1ev5atWqlt99+WxdddJHy8/P17rvvauXKlerbt6+ef/55+4jzmjzxxBO69dZbFRERoU8++UQbNmzQ4MGD9frrr2v48OEuP58AgKZt/fr19vU5jv3ZtGnTce2nTZtmn/4kKytL559/vubPn2//kv7rr79Wnz599OKLL+rss8/WihUr9O677+qnn37SRRddpIULF9rz7vjx4xUbGyvDMLRixQr7AqLHmjp1qmbNmqXTTjtNS5Ys0ebNmzVq1CgtWrTohAtuS9KkSZNqnBpuxIgRevbZZ9W3b1+tWbNGy5YtU+/evfXyyy9r4MCBko4sFH7HHXcoPDxcv/zyi30xUgAAGktt5//18fjjj9sX/c7MzFRBQYHGjRun119/Xa1atVKXLl308MMPq0uXLsrLy9POnTv14osv6rbbblP79u31yy+/uBzHJZdcooULFyoxMVFFRUXKyMhQRkaGysrKlJSUpPfff7/GKd+OXuz82FHo1f76178qMzNT11xzjQIDA7V48WItXbpUbdq00a233qoPPvhAHTt2dO1JArycyTh2dQIAbrdjxw79/vvvMplMiouLkyTt3LlT5513nmw2m95//32nc501VQUFBdq+fbuCg4Ptifu7777T+PHjZTKZlJOTU+sc7QAAAAAAAEBDYzoXoBFs2rRJN910k0wmkwYPHqyOHTsqOztbNptN55xzTrMroEvSihUr9PDDDysgIMA+rUr1/KtjxoyhgA4AAAAAAACvwEh0oJEsXbpUr776qn755RdVVlaqU6dOio+P180336zg4GBPh+cR7733nhYtWqSCggJJUufOnTVq1ChNnjxZLVq08GxwAAAAAAAAgCiiAwAAAAAAAADglEcXFt22bZuuueYaxcbGauDAgXrsscdks9mOa2ez2TR//nwNGzZM/fr10+jRo7VkyRL7/gkTJqh3796Kjo62/4wZM6YxHwoAAM0CuRsAAN9C7gYAoP48Nie6YRiaMmWKevTooeXLl2vv3r2aPHmyTjnlFE2aNMmh7dtvv620tDS98cYbOu2007RixQrddNNNioqKUs+ePSVJDz74oCwWiyceCgAAzQK5GwAA30LuBgDAPTw2En3Dhg3Kz8/XrFmzFBYWpu7du2vy5MlatGjRcW1//PFHnXXWWYqKipKfn58SEhIUGhqqzZs3eyByAACaJ3I3AAC+hdwNAIB7eGwk+qZNm9S5c2eFh4fbt/Xu3VsFBQUqLS11WGgxISFB9957rzZv3qwePXooKytLFRUV6t+/v73N4sWLtWDBAu3fv18xMTG65557dNpppx13v1VVVTpw4IBatmwpPz+PzmYDAMAJ2Ww2VVRUKCwsTP7+HkvZduRuAABOjNx9BLkbAOAr6pq7PZbVCwsLFRYW5rCt+vfCwkKHZH7++edr06ZNGjt2rCSpdevWmjt3rjp27ChJ6t69u1q3bq1HHnlEfn5+euihhzR58mRlZmaqRYsWDvdx4MABFRQUNOAjAwDAvSIjI9W2bVtPh0HuBgCgjsjd5G4AgG+pLXd7rIhuMpnq3DYjI0MfffSRMjIy1L17d+Xm5mrq1Knq2LGjYmJidN999zm0f+CBB9S/f3998803GjRokMO+li1bSjryxLRq1arej+NoVqtVP//8s04//XSZzWa39t1QfDFmOOIYAp7VkK/BQ4cOqaCgwJ67PI3c7R18MWY44hgCnkXurhm5u+H4YsxwxDEEPMsbcrfHiugREREqKipy2FZYWGjfd7SFCxdq3Lhx6tWrlyRp6NChGjBggDIyMhQTE3Nc38HBwQoPD9eePXuO21d9KVnr1q0VGBjojodiZ7VaJUlBQUE+86bqizHDEccQ8KyGfA1Wn/h6y2XQ5G7v4IsxwxHHEPAscje5u7H5YsxwxDEEPMsbcrfHiujR0dHasWOHCgsL1aZNG0nS+vXr1aNHDwUFBTm0NQxDNpvNYVtVVZX8/PxUWlqqefPm6eabb7YPuS8sLFRhYaG6dOnicly//fabNm/erMrKSpdva7PZ9Ntvv+mPP/7wmg9NJpNJoaGhOvvssxUSEuLpcAAAPozc3ThMJpPatm2rs88+2+2j9wAAzQu5u3Fw3g0ATZ/Hiui9evVSTEyMHnroId17773auXOnXnjhBd14442SpFGjRumhhx7S3/72Nw0bNkxpaWk6//zz1a1bN61Zs0a5ubmaOHGigoODtX79es2ePVv33XefrFar7r//fvXq1Uv9+vWrczx79uzR7bffro0bNx73wcEVlZWVCggIOOnbNwSTyaQWLVro//7v/3TjjTe6dEkfAADVyN2Nx2QyKSgoSLfeeqvGjRvn6XAAAD6K3N14OO8GgKbNo8uFz58/X/fcc4/i4+MVFBSk8ePHa/z48ZKkLVu2qLy8XJJ0ww03qKqqStdff73279+vTp066b777tPgwYMlSU8//bRmz56t4cOHy2w2q3///nruuefq/K20YRi66aabtHfvXt1xxx2KiYk56ZFf5eXlbr9crT5sNpv27dunpUuX6sUXX9Qpp5yiyy+/3NNhAQB8FLm74dlsNu3atUsfffSR5syZo1NPPVVDhgzxdFgAAB9F7m54nHcDQNNnMgzD8HQQjam8vFw//vijevXqZU+6Gzdu1FVXXaVZs2apb9++9e7fm5L50ebNm6cDBw7onXfesW+zWq36/vvvFRsby7xePopjCHhWQ74Ga8pZzVFzzd2GYeiuu+5SZGSkUlNT7dt53/d9HEPAs8jdDa+55m6J8+6mimMIeJY35G7vmEDMw/Ly8mQymRQdHe3pUBpUbGys8vPzT2reOQAAvElzyN0mk0kxMTHasGGDp0MBAKDemkPuljjvBoCmyqPTuXiLiooKtWzZ0ullaI899phycnL0xhtvKCws7KTuY9++fZo/f74KCgrk7++vcePGadSoUTW2feutt7R06VIZhqHevXvr1ltvtV/mtn//fs2bN0+///673nrrLYfbffPNN3rppZdktVrVtm1bpaSkqH379vb91X1UVFR43fxxAAC4wpdy99tvv62vvvpKktShQwfdfPPN6tChgyoqKrRgwQJt2LBBhmGoW7dumjJlikJDQ+39BgYGqqKi4qTiBwDAm/hS7nZ23v3UU09p48aN9t+tVqt2796tTz75xL6N824AaJoYif5fzhb9KC0t1erVq9WjRw99+eWXJ93/U089pa5du+qNN97QI488ooULF+rXX389rl12draysrL09NNP67XXXpMkvfHGG5KkvXv3avr06erWrdtxt/vzzz/1zDPP6L777tNLL72kfv362U/Ya3uMAAD4Il/I3Z9//rlWrlyp+fPn68UXX1TXrl31/PPPS5LeeecdlZSU6Pnnn9eCBQtks9m0cOHCk44XAABv5wu5+0Tn3bfccosWLFhg/7nwwguPK9Jz3g0ATRNF9FpkZWXp9NNPV2Jior744guHfZ988oleeumlWvsoLy/X2rVrdemll0qS2rdvr3PPPVcrVqw4rm12drYuuOAChYSEyM/PT2PHjtXy5cslSX5+fnrwwQfVv3//42735ZdfaujQoerYsaMk6YorrmAhEwBAs+RNubtbt266/fbb7XPrnXXWWdq2bZsk6eyzz9bVV18ts9kss9ms2NhYbd++vV6PHQAAX+RNuftE591H279/vzIzMzVx4sS6PkwAgA+jiF6LpUuXavjw4Ro4cKB2796tn3/+2b5v9OjRuvbaa2vtY8eOHWrZsqXatGlj39axY0dt3br1uLbbt29Xp06d7L936tRJRUVFKikpUUREhE499dQa7+O3336Tn5+fZs6cqcmTJ2vu3Lk6cOCAKw8VAIAmwZtyd/fu3dWjRw/7vtzcXPXu3VuSFB0dbf/yu7i4WCtWrFBcXJzrDxgAAB/nTbn7ROfdR3vrrbd00UUXKSQkpNa2AADfRxH9BH799Vft2LFD8fHxatWqlYYMGXLct+J1cejQIbVo0cJhW4sWLXTo0KFa21b/u7b5UEtLS7Vu3TpNnz5dzz77rPz8/PTvf//b5VgBAPBl3py7Fy9erG+//Vb/+Mc/HLbfdddduuqqq9SpUyddfPHFLscKAIAv8+bc7UxRUZFyc3OVmJjocpwAAN9EEf0Eli5dqsGDB9sXBhkxYoSysrJqXWU7Pz9f119/va6//nqlpqaqdevWKi8vd2hTVlam1q1bH3fbY9uWlZVJ+t/iJM4EBwdryJAhCgsLU0BAgC655BKtXbtWhmHU6bECANAUeGvufuutt/TRRx9p7ty5Cg8Pd7j9I488ovfee08mk0mPPfaYS48XAABf5625+0Q+//xz9e/fv8a+AQBNk7+nA/BWlZWVWr58ue6++277tr/+9a8KCwtTbm6uhgwZ4vS2PXv21IIFC+y/Hzx4UDabTX/++afat28vSdq2bZu6du163G27dOniMB/qtm3b1LZtWwUHB58w3o4dO6q0tNT+u8lkktlsZlETAECz4a25+6233tLatWv12GOPKTQ01N5u5cqV6tmzp0455RS1atVKF110ke66666TfwIAAPAx3pq7a/Ptt99qzJgxdWoLAGgaGInuxKpVqxQSEmKft7TaiBEj9Pnnn7vUV+vWrTVgwABlZGRIOjJX2zfffKNhw4Yd1zYhIUHLli1TSUmJrFarMjIydN5559V6H8OHD9eXX36p/fv3S5KWLFmis846y6U4AQDwZd6Yuzdt2qQvv/xSDzzwgEMBXTqyqNmbb74pq9Uq6ch86d27d3cpTgAAfJk35u7aGIah/Px8RUVFuRQfAMC3MRLdiXXr1qmoqEjXX3+9w/aKigrt27dP0pFVwnfv3l2nRU6mTJmixx9/XBMnTlRAQIBuuOEG+zfir732msLCwnTJJZdowIAB+v333zVlyhQZhqF+/frpyiuvlCQtW7ZM7733nioqKlRcXGyPbcGCBeratauuvPJK3XnnnTIMQ5GRkZoyZYo7nxIAALyaN+bujz/+WKWlpZo6dapD3/Pnz9c///lPPfvss7r++utlMpl06qmn6vbbb3fHUwEAgE/wxtx9ovNu6chi4JWVlcdNzwYAaNooojtx22236bbbbjthm9GjR9e5v7CwMN1///017jt2gbFx48Zp3Lhxx7UbPny4hg8f7vQ+zj//fJ1//vl1jgkAgKbEG3P3iaZnadWqlWbMmFHneAAAaGq8MXfXdt4dFham//znP3WOCQDQNDCdCwAAAAAAAAAATlBEBwAAAAAAAADACYrokgICAnT48GEZhuHpUBpURUWFJKlFixYejgQAgPppTrk7ICDA02EAAFBvzSl3S5x3A0BTQxFd0hlnnKGqqir99NNPng6lQW3atEldu3YlmQMAfF5zyt09e/b0dBgAANRbc8rdnHcDQNNDEV1Sv3791LlzZz333HP67bffmtw34xUVFfrss8+0fPlyJSYmejocAADqrann7rKyMr3//vvatGmTLr74Yk+HAwBAvTX13M15NwA0bf6eDsAb+Pn56ZlnntE///lP3XnnnQoKClLLli1d7scwDFVWViogIEAmk6kBInWdYRgqKSmR1WpVYmKiJk2a5OmQAACot6acu202m4qLiyVJ1157rS688EIPRwQAQP015dzNeTcANH0U0f8rMjJSn3zyib755hvl5+fr8OHDLvdhs9m0detWdenSRX5+3jPIPywsTAMHDlTXrl09HQoAAG7TVHO3yWRS27ZtNXjwYLVv397T4QAA4DZNNXdLnHcDQFNHEf0o/v7+GjhwoAYOHHhSt7darfr+++8VGxsrs9ns5ugAAMCxyN0AAPgWcjcAwBd5z9e2AAAAAAAAAAB4GYroAAAAAAAAAAA4QREdAAAAAAAAAAAnKKIDAAAAAAAAAOAERXQAAAAAAAAAAJygiA4AAAAAAAAAgBMU0QEAAAAAAAAAcIIiOgAAAAAAAAAATlBEBwAAAAAAAADACYroAAAAAAAAAAA4QREdAAAAAAAAAAAnKKIDAAAAAAAAAOAERXQAAAAAAAAAAJygiA4AAAAAAAAAgBMU0QEAAAAAAAAAcIIiOgAAAAAAAAAATlBEBwAAAAAAAADACYroAAAAAAAAAAA4QREdAAAA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srIkSYcPH3bkw7Hy+++/F9vYxcUda4Y1XkPAuYrzPZiVlWWVi85CdrsWd6wZ1ngNAeciu8nukuaONcMaryHgXM7Mbqc10U0mk93r5uTkKDo6Wm3btpUkPfDAA/r888/13XffqX379oW6j6CgINWpU0fly5eXh4dTr6sKAMBV5ebmKisrK98HVmchuwEAuDqy+zKyGwDgLuzNbqc10UNCQpSammq1LCUlxXLblYKCghQQEGC1LDQ0VGfOnLGsm5qaKl9fX0mXL56SmpqqSpUq5btfLy+vApcDAOCKXOEotjxkNwAA10Z2k90AAPdiT3Y77Svh8PBwnThxwhLgkpSYmKj69evLz8/Pat1GjRppz549Vsv++OMPhYaGqmbNmgoODra6PSkpSdnZ2WrcuHHxPggAAMoQshsAAPdCdgMA4BhOa6I3bNhQERERmjp1qtLS0pSUlKSFCxeqf//+kqQuXbrol19+kST16NFDSUlJWrp0qbKysvT1119rz549uu++++Tp6anevXtrzpw5OnbsmM6ePavp06erc+fOuuGGG5z18AAAKHXIbgAA3AvZDQCAY5gMR18quxBOnjypiRMnavPmzfLz81O/fv00YsQISVKDBg307rvvWuZj27p1q1555RUdOnRItWrV0rPPPmu57dKlS3r11Vf1zTffyGw2q3379po8eXK+U9EAAEDRkN0AALgXshsAgKJzahO9NNm3b59effVV7d69W15eXmrRooX++c9/qkqVKs4uzaYGDRqoXLlyVheC6d27t1588UUnVoWrSUhI0Lhx49SiRQu98cYbVrd9++23+te//qUTJ06odu3aGj9+vFq3bu2kSoHS6fjx43rllVe0bds2eXp6KioqSv/85z8VFBSk3377TS+99JL27t2r4OBgDRo0SIMGDXJ2ybgKshslgewGnIvsLl3IbpQEshtwLlfNbi6T7QCXLl3S4MGDdfvtt2vDhg367rvvlJycrMmTJzu7tGtatWqVdu3aZfmPIHdd7777rqZOnaratWvnu2337t0aN26cnn76aW3dulUPP/ywnnzySZ08edIJlQKl1+OPP67g4GD9/PPP+uqrr3TgwAG99tprunDhgoYOHapmzZpp48aN+te//qW33npL33//vbNLhg1kN0oC2Q04H9ldepDdKAlkN+B8rprdNNEd4MKFC3rmmWc0bNgw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+vTpo2effbbQtQEASh+ym+wGALgXspvsBhyB6VwAJ5s4caIWLVpkdRGNvG+4s7OzLctOnjxZpPu5cvyMjAylpqaqatWqqlWrljIzM61uT09PV0pKit1j16pVS4cPH7b8nJOTo+PHj6tmzZrX3LZixYpq0aKFZY60K128eFExMTHatm2bze0DAgIUHBys/fv3Wy2359v4gqSmpqpcuXLq0KGDJk+erLfffltLly69rrEAAKUT2U12AwDcC9lNdgNFRRMdcLKGDRuqR48emjNnjmVZSEiIAgMDLSG2f/9+bd68uUj3s2PHDq1fv16XLl3Shx9+qKCgIEVGRurmm29WZGSkpk2bppSUFKWlpWnSpEkaN26c3WP37NlT//73v/X777/r4sWLWrBggQzDUIcOHezafsKECdq1a5cmTpyoU6dOyTAM7du3T48++qi8vLyu+k23JPXt21cLFizQgQMHlJ2drUWLFqlnz566cOHCNe87b8fp4MGDSk9PV58+ffTuu+8qKytLOTk52r17t107JQCAsoPsJrsBAO6F7Ca7gaKiiQ64gFGjRiknJ8fys4eHhyZNmqR3331XnTp10vz589W3b1+rdQojOztbvXr10meffabmzZvr22+/1Zw5c+Tt7S1Jmj17tnJzc9WhQwd16NBB2dnZevXVV+0ev2/fvuratasefvhhtW7dWps2bdJHH32kwMBAu7avX7++YmNjdfHiRT3wwANq2rSpRo4cqVtvvVWLFy+21GnLE088odatW6tfv366/fbbtWrVKr377rvy8fG55n03bNhQkZGRevDBBxUbG6u5c+cqISFBrVq1UsuWLbVmzRrNmjXLrscBACg7yG6yGwDgXshushsoCpNR0IRPAAAAAAAAAACAI9EBAAAAAAAAALCFJjoAAAAAAAAAADbQRAcAAAAAAAAAwAaa6AAAAAAAAAAA2EATHQAAAAAAAAAAG2iiAwAAAAAAAABgA010AAAAAAAAAABsoIkOAAAAAAAAAIANNNEBAAAAAAAAALCBJjoAAAAAAAAAADbQRAcAAAAAAAAAwAaa6AAAAAAAAAAA2EATHQAAAAAAAAAAG2iiAwAAAAAAAABgg5ezCwBKsw4dOuiPP/7It9zX11cNGjRQ37591aNHD6fUNH36dMXExJTofRdUhy0DBw7UP//5zxKsCABQUgrKgHLlyqlq1apq2LChnnjiCd1yyy2FGvP555/X8uXLdf/99+vVV191ZLklwp7633zzTc2bNy/f8nLlyunGG29Uhw4d9OSTTyooKKi4y81XU/PmzbVkyZISu19bddgSEBCgX375pQQrAgCUFomJierTp49yc3M1f/583XnnnVa39e7dW4Zh6O2331aHDh0kST/++KO++OIL7d69W+fOnVNwcLBq1Kihbt26qVevXvL29raMURz7RQAcjyY6UAIiIiLUtGlTSZJhGNq/f782b96sHTt26MyZM3r00UedW6ATXfncXKl58+YlX8w1ZGVl6Y477tBdd93llg0aAHA1V2ZAVlaWNm3apB9++EHr1q3Tl19+qXr16jm3QBfl6+urnj17Wn5OSUnR6tWrtXjxYm3evFmxsbEqV66cEyt0nr8/N3kqVKjghGqu7cUXX9Tnn3+upKQkZ5cCALAhIiJCAwYM0OLFizVt2jS1adNGFSpUUG5uriZPnizDMNShQwd16NBBhmFo/PjxWr58uTw8PNSqVSvVqFFDhw8ftvQAVqxYoffee09+fn757seV94vILJR1NNGBEnDHHXfomWeesVr20ksv6dNPP9XChQs1aNAgeXp6Oqk65yrouXFVq1evVnp6urPLAIBS4+8ZkJ6ervbt2ystLU1fffWVRo8e7cTqXFdAQEC+s7USExPVq1cv7du3Tz///LM6derkpOqcq6DnxlVdunRJ//nPf5xdBgDADqNGjdKPP/6oP/74QwsWLNDTTz+tpUuXas+ePfLx8dGECRMkSZ9++qmWL1+ucuXKacGCBWrdurVljLVr1+qJJ57Q9u3b9f7772vkyJFW9+HK+0VkFsCc6IDT5IXpuXPnlJycLEm6cOGC3njjDXXq1ElNmjRRdHS0Jk2apNTUVMt2zz//vBo0aKA333xT3333nbp06aLGjRvrvvvu065du6zuY/ny5erUqZPCw8PVvXt3rVu3rsBakpOTNWXKFEVHR6tx48Zq1aqVRo0apf/+97+WdTZv3qwGDRqoQ4cOOnjwoPr27auIiAjdfffdWr9+vU6fPq0hQ4aoSZMmat++vRISEhz2XG3ZskVDhgzRbbfdpsaNG6tz587617/+paysrHzPy5w5c/Tiiy8qMjJSX3/9tSTp1KlTGjdunDp27Kjw8HDdfffd+uKLL6zu4/DhwxozZozatWun8PBwtW/fXlOmTFFaWpokacCAAZYdmuXLl6tBgwbavHmzwx4jAEDy9/dXzZo1JUnnz5+3LLcnHwty/PhxjR07Vu3atVNERIS6dOmi9957T7m5uZZ1OnTooAYNGmjjxo2aM2eO2rRpo4iICA0fPlxnz561Gu8///mPevXqpSZNmqhVq1YaPny4fvvtN6t1duzYoUcffVStW7dWkyZN9OCDD2rbtm1W6+SdFh4REaEOHTroww8/vJ6ny0pERIQCAwMlXc60PD/++KP69u2r22+/XbfffrsGDBhgVc+V+f7HH39oyJAhatq0qVq1aqWFCxda3cehQ4c0ePBgNWnSRG3atNHs2bNlNpsLrOeLL75QTEyMmjRpoqZNm6pXr16Ki4uzWifvud+wYYOmTp2q2267TS1atNBrr71mOWW+VatWioyM1CuvvGLzvgqrMPs97du31+rVq9WuXTsNHjxYkpSbm6tFixapR48eioyMVKtWrTRhwgTLPoMk5eTk6O2339a9996ryMhItWjRQkOGDLFMKbNs2TKFh4fr3LlzkqQGDRro+eefd8jjAwA4nq+vryZNmiRJeu+99/Trr79qzpw5kqTHH39coaGhkmTJ9N69e1s10CWpbdu2euGFFzRv3jwNHz78mvdpa79Ikn744Qf169dPkZGRioiI0H333adFixZZ7eNI9n2WJrMA+9BEB5wkJSVFkuTl5aXg4GBJ0j//+U+98847ysrKUo8ePeTt7a2lS5cWGFDr1q3TjBkzFBkZqRtuuEFJSUkaNmyYLl68KEnauHGjnn/+eR05ckTNmjVTs2bNNG7cuHwNh7S0NPXp00effPKJPDw8dN9996lKlSpauXKlevXqZfWBUrrc9B87dqxq1aqlihUr6uDBgxozZozGjh0rPz8/hYaG6sSJExo1apQyMjKK/Dz9+OOPeuSRR7Ru3To1atRI9957r5KTkzV//nwNHz5chmFYrf/tt99q48aN6tatm6pVq6b09HT169dPcXFxCggIUPfu3ZWenq4JEyZo2bJlki6fKjdw4ECtWLFC9erVU8+ePVW1alV98skneuyxxyRJnTt3tpw+V69ePQ0cOFA33nhjkR8fAOD/paen6+jRo5IuN4XzFCYf82RlZenhhx/WN998o8qVK6t79+46ffq0Zs6cqcWLF+dbf+7cuYqPj9cdd9whDw8P/fzzz1ZHNC9fvlwjR47U7t27FR0drSZNmujnn39Wv379LFm5e/duDRw4UAkJCWrYsKE6d+6sPXv2aPDgwZZ1zp07p8GDB2vnzp0KDQ1Vhw4d9Omnnxb5y+eLFy8qMzNTklSpUiVJl494GzFihH799Ve1a9dOzZo105YtWzR06FCdOHHCavtz587p8ccfl7+/v8LDw5WcnKzZs2fr+++/l3T5A/ajjz6q9evXq2LFiurSpYvWrl2b70tpSZoxY4YmTJig/fv3q0OHDoqKitLu3bs1bty4Auctf+ONN/T777/rlltuUWpqqt5//32NHj1aq1atUrNmzZSZmamPPvrI8uV4URR2v+f8+fOaPHmymjdvrpYtW0qSZs6cqenTp+v48ePq0qWL6tatqy+++EJPPvmkZbvZs2drzpw5unjxou677z5FRUVp8+bNeuSRR/T777+rfv366ty5s2X9gQMH5mu2AABcS7t27dS1a1ddunRJAwYM0Llz51S3bl3Ll6x//vmnjh07Jkk2/6b369dPd911l9Wc6LbY2i/6+OOPNWLECG3fvl0tWrTQXXfdpcOHD2v69OlW+y72fpYmswD7MJ0LUMJyc3O1f/9+vffee5Kku+66S+XKlZPZbFZwcLD69OmjmJgYNW3aVDt27FDfvn21du1aXbx40Wo+z3379mnVqlWqVq2aDhw4oHvuuUdnz57V9u3bdccdd1gaBM2aNdOiRYtkMpnUtm3bfN94f/jhhzp69KgqVaqkr776SgEBAcrOzlbPnj21b98+zZs3z/INu3Q5yPv3768HHnhAW7du1UMPPaSUlBTVrl1bU6ZM0Z9//qn27dsrPT1d27dvV1RU1HU/V4ZhaNq0aTKbzerTp4+mTJki6f9PWd+wYYPWrl2rdu3aWbY5ffq0fvrpJ4WEhEiSFi1apOPHjys0NFSff/65vL29dejQIXXp0kXz5s1TTEyMfv/9d506dUp+fn5677335OHhodzcXM2ZM0cBAQG6ePGiHnroIe3evVsHDhxQRESE25wqDgCubMOGDZbGb97cn+np6brvvvvUrVs3SSp0Pub5888/LR/wRo8ebbmg1+uvv67//Oc/GjRokNX6WVlZ+uKLL1SuXDlFRkZq8uTJio+P16VLl1SuXDm98cYbkqShQ4daTqcePXq0fv75Z33yySeaNGmS5s+fr0uXLqlr166aPXu2JOnWW2/VxIkT9d577+nVV1/VsmXLdP78efn6+mrp0qUKCgrS448/ro4dO17385iSkqK5c+cqJydHvr6+at++vaTLR1z37t1bdevW1SOPPCJJ6tKliw4dOqSEhAT16dPHMkZ6erp69OihwYMHyzAM9e3bVzt37tT333+vTp066aefftLx48dlMpn0wQcfqG7dusrKytJdd91lVcuRI0csR+HNmDFD9957r6TLR+3NnDlTCxcu1IABA6wuflqhQgUtWrRIhmGoS5cuOnLkiDZu3KiffvpJfn5+euSRR7Rx40YlJCTo/vvvv+7nSSr8fs/58+f1zDPPqH///pKks2fP6qOPPpIky5kLktS3b19t2bJFmzdvVosWLSxn/40dO1Z33323pMtH3h84cECXLl1SRESE+vfvbzk1nv0KAHAP48eP16pVqyxHco8ZM8ZyHZJTp05Z1qtevXqhx7Znvyg9Pd2yjzF69GjLQV8rV67UqFGjtGzZMj366KOqW7eu3Z+lySzAPjTRgRLwzjvv6J133sm3vHnz5pZTwjw9PfXCCy9o9erVWrt2rb799lvL/Ntms1lnz561nCKWt221atUkXT4yumLFikpJSdFff/0lSZbTyzt27CiTySRJio6Olq+vryWYpctBLV3+Vj0gIEDS5SuB33XXXdq3b5+2bNmSr+68q5GHh4dblrVt21aSVK1aNYWEhOjs2bP5ToMvzHMzffp0NW3a1HKV8rydBunyt/A1atTQ8ePHtWXLFqsm+m233WZpoEvS9u3bJUkeHh6aOXOmZbmnp6f++OMPnT17VjfeeKMqVKigjIwM3XfffWrXrp0iIyP12GOPyd/f/5qPAQBwfRITE5WYmGi1rHLlygoJCVFaWppCQkIKnY956tSpo2effVYrV67U+++/r4sXL+rAgQOSZMnKK91zzz2WD8G33XabpMtf5p49e1YXLlywfDDOa1BL0uuvv241Rl7mnDp1Sq+88ook6cyZM5bHKkl79+6VJN1+++2WRnKlSpXUvHlzrVmzxq7n7dSpU2rQoEG+5SEhIZoxY4YlB3v06KF//OMfSkhI0PTp05Wbm2vZBzh9+nS+7bt37y5JMplMatasmXbu3GlZL6/uevXqqW7dupKk8uXLq2PHjvr0008tY2zcuFGGYcjLy0tdunSxLL/nnns0c+ZMZWVlaefOnVbZnbevYjKZdMstt+jIkSO67bbbLBdca9SokTZu3GjXfoWt56Z58+ZasmTJde33XHn0XWJionJyciRdPpU+7zXLO/suMTFRLVq0UJ06dbR//369+OKLWrNmjSIjI3XHHXfonnvuueZjAAC4rh9//NGSA9LlLMj7fJz3uVuS1RRkGRkZatasWb6x/n6BTnv2i3bs2GHJ8q5du1rW69y5s7y8vJSTk6PNmzfLZDLZ/VmazALsQxMdKAFXXmV7x44d2rVrl2rXrq0PP/xQXl6X34YXLlzQwIED84Vmnr9PW5J3qnYeX19fpaSkWOZAy/ugeWUT2GQyKTAw0KqJnjetTMWKFa3Gy/s5b96zK+V96L/yyL+8D6JXLv/7fGwFufK5uVL9+vUttdmq7/jx4/nqu7KBLv3/3HHHjh2zHDl2pZMnT6pRo0Z65513NG3aNO3fv1+///67JMnPz08jR460HL0HAHCs4cOHW643kZubq2PHjmn69OlatGiR1q9fr+XLlysnJ6dQ+Zjn0KFD6tu37zXnTc9zZa76+PhY/m02m63yKG/e8YLkNfe3bt2qrVu3Wt128uRJSbJcB+XvX9JeeWT2tfj6+qpnz56SLufc8uXLJV2ekqZ58+aW9d5//3299tprBY5R0PN25XPg6+sr6f+z3N66856rwMBAq4umX5njf8/uK5/TvH2I692vuPK5uVLt2rWt6ivMfs+V+xZXzkm7dOnSfOvmfdkydepUeXp66ocfftDy5cstr1F0dLRmz57Nl/QA4IZOnz5tOQq8e/fu+uqrr/TVV1+pV69euu2226ym+zxx4oQaN24s6fKXtQMHDpR0OSdsXaDTnv0iW5+RPTw8FBgYqOTkZJ07d65Qn6XJLMA+NNGBEnDlVbaPHTumrl276siRI/rggw8sp1+tWLFCiYmJMplM+vDDD3X77bfryJEj1/3tb3BwsE6fPm350CtdbgT8vZlQsWJFHTlyxGo96f+b8H9vSjva369AfqWDBw9a1VO/fv189f39ywQPD+tLPeR9MO/YsaPeeustm3W0atVK33zzjY4fP64dO3ZozZo1WrFihaZPn67IyEg1adKkcA8MAFAoHh4eql27tp588kn9/PPP+v3333XgwAHt2rXruvLxrbfeUmpqqkJDQ7Vo0SLVrFlTS5cu1eTJkwtd25VN3is/lGZkZOj8+fPy8vLSDTfcYPnwOn78eJtfwOZdB8VW7tojICDA6lTq06dPa926dXr55Ze1fPlyeXl56eLFi5YpaPr27atnn31W/v7+6t27t3799Ve776uwdV/ZjM7JybEcLJB3RL6UP7sd6e/Pzd9dz37PlfsWV35psHXrVptfqgQFBWnOnDlKT0/Xr7/+qm3btunf//634uPjNWvWrOv6PQQAONcrr7yitLQ0RUZGasaMGcrIyNCPP/6oKVOmaPny5apatapq166tI0eOaMWKFerUqZMkydvb25JNmzdvttlEv5Kt/aIrG+Jnz55VjRo1JF2+dkleU7xSpUr51rvaZ2kyC7APFxYFSljNmjU1dOhQSdL8+fMtFwrJCzx/f3+1bNlSXl5eVuF66dKlQt1P3qnMP//8s+XIrRUrVlguPJonb77Y+Ph4yxF0ly5dslxILG+uT2cICwuznKL/7bffWpZv27bNckG0a13Q5NZbb5V0+QyAvFOtT58+rfnz52vp0qXKycnRr7/+qhkzZiguLk41atRQt27dNGvWLMuFRPNOg8s7Pe/KI/kBAI515YUdvb29rzsf87Zr0KCBatWqJcMw9MMPP1x1G1vq1q1raa6uXr3asvzFF19Uu3btLFO35GXO+vXrLets375dCxcu1I8//mipR7qcS3mN3GPHjhU4jYi9XnzxRXl7e2v//v16//33JV3OquzsbEmXs9zf319HjhyxTPd2vfsVR48e1f79+yVdPio7b38hT96FWc1ms1atWmVZvmLFCkmXz/Iq6Ay0klLU/Z7w8HDLtD95c8hK0qeffqpFixbpwIEDSk9P11tvvaUpU6bI19dXrVu31siRIzVkyBBJ0vHjxyVZn/bviIuxAwCKz5o1a7Ry5Up5eHho4sSJMplMeuGFF+Tj46OkpCR9/PHHkmS5yOiPP/5oyf482dnZWrt2baHu9+/7RZGRkZazxa78jPzdd9/JbDbLw8NDrVq1svuzNJkF2I8j0QEneOyxx/TVV1/p6NGjmjRpkj788EPLkc7nz5/X8OHDZTKZdPDgQTVo0EBJSUmaMmWKRo0aZfd99O/fX+vWrVNiYqL69eunWrVqaf369QoODrY6Gv3hhx/W119/rWPHjikmJkbNmzfXr7/+qt9//10VK1bUiBEjHPzo7WcymTR+/HiNHDlSn332mf7880+FhIRYmiCdO3e2Om29IDExMVq0aJH++OMPxcTEqFmzZvrll1909OhRxcTEqG/fvvL09NRHH30kk8mkhIQEBQcH68iRI/rvf/+rkJAQ3X777ZKkKlWqSLr8xcT48ePVu3dvRUZGFu+TAACl2JUX0DIMQ2fOnNFPP/0k6fK1NurWrWs5Wqqw+RgREaE1a9YoISFBzz//vH7//XdVqVJFnp6eOn36tJ577jmNHz/erjo9PT319NNPWzL7jz/+0KVLl/Tzzz+rQoUKGjZsmCRp2LBhio+P19q1a9WvXz+FhoYqPj5eaWlpevXVVyVdzqX58+fr4sWL6t27t5o3b661a9eqevXqOnLkyHU9j3Xq1NHgwYP1zjvvaP78+erSpYtq166tmjVr6tixY3rttdf0008/KT4+Xm3bttWPP/6or776SpUqVVLDhg3tuo+77rpLlStX1unTpzVo0CC1bdtW27ZtU1BQkNV+Ra1atTRw4EAtWrRI48eP15o1a5SRkWF5XceMGWOZ69wZirrfExISov79+1se3+rVq5WSkqL169ercuXKuvfee+Xv768ffvhBe/fu1e7duxUeHq6MjAzLFzB5c6xXrVrVMu7w4cPVoUOHfBe8BQA4X2Zmpl566SVJl8/uuuWWWyRJoaGhGjZsmObMmaM333xT9957r/r06aNff/1Vy5Yt04gRI9SiRQuFhYUpNTVVW7dutZyZVdAZdfbsF0nSqFGjNG3aNM2ZM0d79uyRl5eX5cvgRx55RDVr1pQkuz9Lk1mAfTgSHXACb29vTZgwQdLloIyLi9Ptt9+uZ599VlWrVtXmzZuVnZ2t999/X08++aSCg4O1a9euAufptKVDhw4aP368qlSpoj179ujAgQOaO3duvquEBwUFaenSperdu7cuXLhgmWetR48eio2NLfBibSXprrvu0gcffKDmzZtr27Zt+u6771S9enU9++yz+S7oVhB/f399+umn6tq1q86dO6evv/5aZrNZzzzzjOUK5Y0bN9a7776rW2+9VWvXrtVnn32m/fv365577tGSJUtUuXJlSVK/fv3UtGlTGYahtWvX5juqHwBQOImJifroo4/00UcfacmSJVq/fr3q16+v8ePHa/78+ZJ03fk4ZMgQ3X///fL19dVPP/2kRo0a6Y033tCgQYNUvnx5bd682eqiX9fSt29fzZ49W7fccovi4+O1efNmRUVF6dNPP9U//vEP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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Federated Learning Benchmark Summary - Box Plot Analysis\n", + "================================================================================\n", + "\n", + "Logistic Regression:\n", + "----------------------------------------\n", + " 3 clients: 0.7440 ± 0.0046 [0.7390, 0.7480]\n", + " 5 clients: 0.7422 ± 0.0097 [0.7300, 0.7560]\n", + " 10 clients: 0.7484 ± 0.0154 [0.7240, 0.7830]\n", + " 20 clients: 0.7285 ± 0.0407 [0.6290, 0.7960]\n", + " Performance degradation (3→20 clients): 2.09%\n", + "\n", + "ElasticNet:\n", + "----------------------------------------\n", + " 3 clients: 0.7450 ± 0.0026 [0.7430, 0.7480]\n", + " 5 clients: 0.7452 ± 0.0156 [0.7270, 0.7700]\n", + " 10 clients: 0.7443 ± 0.0189 [0.7000, 0.7670]\n", + " 20 clients: 0.7219 ± 0.0297 [0.6510, 0.7550]\n", + " Performance degradation (3→20 clients): 3.09%\n", + "\n", + "Linear SVC:\n", + "----------------------------------------\n", + " 3 clients: 0.7437 ± 0.0127 [0.7290, 0.7520]\n", + " 5 clients: 0.7460 ± 0.0152 [0.7310, 0.7680]\n", + " 10 clients: 0.7507 ± 0.0112 [0.7380, 0.7790]\n", + " 20 clients: 0.7269 ± 0.0428 [0.5920, 0.7790]\n", + " Performance degradation (3→20 clients): 2.25%\n", + "\n", + "Random Forest:\n", + "----------------------------------------\n", + " 3 clients: 0.7467 ± 0.0085 [0.7370, 0.7530]\n", + " 5 clients: 0.7496 ± 0.0124 [0.7390, 0.7710]\n", + " 10 clients: 0.7560 ± 0.0235 [0.7320, 0.8160]\n", + " 20 clients: 0.7363 ± 0.0369 [0.6100, 0.8000]\n", + " Performance degradation (3→20 clients): 1.39%\n", + "\n", + "Balanced Random Forest:\n", + "----------------------------------------\n", + " 3 clients: 0.7493 ± 0.0074 [0.7410, 0.7550]\n", + " 5 clients: 0.7490 ± 0.0116 [0.7390, 0.7690]\n", + " 10 clients: 0.7501 ± 0.0105 [0.7390, 0.7690]\n", + " 20 clients: 0.7358 ± 0.0328 [0.6280, 0.7680]\n", + " Performance degradation (3→20 clients): 1.81%\n", + "\n", + "XGBoost:\n", + "----------------------------------------\n", + " 3 clients: nan ± nan [nan, nan]\n", + " 5 clients: nan ± nan [nan, nan]\n", + " 10 clients: nan ± nan [nan, nan]\n", + " 20 clients: nan ± nan [nan, nan]\n", + " Performance degradation (3→20 clients): nan%\n", + "\n", + "================================================================================\n", + "COMPARATIVE ANALYSIS:\n", + "================================================================================\n", + "Best at 3 clients: Balanced Random Forest (balanced_accuracy: 0.7493)\n", + "Best at 5 clients: Random Forest (balanced_accuracy: 0.7496)\n", + "Best at 10 clients: Random Forest (balanced_accuracy: 0.7560)\n", + "Best at 20 clients: Random Forest (balanced_accuracy: 0.7363)\n", + "\n", + "Overall best model: Random Forest (Avg balanced_accuracy: 0.7441)\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from sklearn.metrics import roc_auc_score\n", + "\n", + "# Set style for academic paper\n", + "plt.style.use('seaborn-v0_8-whitegrid')\n", + "# plt.rcParams['font.family'] = 'serif'\n", + "plt.rcParams['font.size'] = 10\n", + "\n", + "# Define models and client configurations (performance decreases with more clients)\n", + "models = ['Logistic Regression', 'ElasticNet', 'Linear SVC', 'Random Forest', 'Balanced Random Forest', 'XGBoost']\n", + "clients = [3, 5, 10, 20] # Only these client numbers\n", + "# clients = [3, 5, 10] # Only these client numbers\n", + "\n", + "extracted_data = []\n", + "# metric = \"auroc\" \n", + "metric = \"local balanced_accuracy\" \n", + "# metric = \"balanced_accuracy\" \n", + "for model_name, df in data.items():\n", + " model = model_name.split(\" C\")[0]\n", + " if model == \"Elastic Net\":\n", + " model = \"ElasticNet\"\n", + " if model == \"Lsvc\":\n", + " model = \"Linear SVC\"\n", + " num_clients = int(model_name.split(\" C\")[-1][:2])\n", + " alpha = model_name.split(\" A\")[-1]\n", + " metric_scores = df[metric].values\n", + " for center, score in enumerate(metric_scores):\n", + " extracted_data.append({\n", + " 'model': model,\n", + " 'run': center, # Placeholder, as run info is not available\n", + " 'n_clients': num_clients,\n", + " 'alpha': alpha,\n", + " metric: score\n", + " })\n", + " \n", + "# Convert to DataFrame\n", + "df = pd.DataFrame(extracted_data)\n", + "\n", + "# print(df)\n", + "\n", + "# Create 3x3 subplot grid\n", + "fig, axes = plt.subplots(2, 3, figsize=(15, 10))\n", + "axes = axes.flatten()\n", + "\n", + "# Define colors for each model\n", + "colors = {\n", + " 'Logistic Regression': '#1f77b4',\n", + " 'ElasticNet': '#ff7f0e', \n", + " 'Linear SVC': '#2ca02c',\n", + " 'Random Forest': '#d62728',\n", + " 'Balanced Random Forest': '#8c564b',\n", + " 'XGBoost': '#9467bd',\n", + " 'MLP': '#8c564b'\n", + "}\n", + "\n", + "x_positions = clients\n", + "\n", + "# Plot box plots for each model in separate subplots\n", + "for i, model in enumerate(models):\n", + " if i < len(axes): # Ensure we don't exceed subplot count\n", + " ax = axes[i]\n", + " model_data = df[df['model'] == model]\n", + " print(model)\n", + " print(model_data)\n", + " # Prepare data for boxplot\n", + " boxplot_data = []\n", + " client_labels = []\n", + " box_positions = []\n", + "\n", + " for client_idx, client in enumerate(clients):\n", + " client_data = model_data[model_data['n_clients'] == client][metric]\n", + " boxplot_data.append(client_data)\n", + " box_positions.append(x_positions[client_idx])\n", + " client_labels.append(f'{client}')\n", + " \n", + " \n", + " # print(f\"Model: {model}\")\n", + " # print(\"Box data:\", boxplot_data)\n", + " \n", + " \n", + " # Create box plot with custom positions\n", + " # Adjust width relative to the x-axis scale\n", + " # Base width on the smallest gap between client numbers\n", + " min_gap = min([x_positions[i+1] - x_positions[i] for i in range(len(x_positions)-1)])\n", + " box_width = min_gap * 0.9 # Adjust this factor to control box width\n", + " \n", + " box_plots = ax.boxplot(boxplot_data, positions=box_positions, \n", + " widths=box_width, patch_artist=True,\n", + " showmeans=False, \n", + " meanprops={'marker':'o', 'markerfacecolor':'white', \n", + " 'markeredgecolor':'black'})\n", + " # Color the boxes\n", + " for patch in box_plots['boxes']:\n", + " patch.set_facecolor(colors[model])\n", + " patch.set_alpha(0.7)\n", + " \n", + " # Customize box plot elements\n", + " for element in ['whiskers', 'caps', 'medians']:\n", + " for line in box_plots[element]:\n", + " line.set_color('black')\n", + " line.set_linewidth(1.5)\n", + "\n", + " # Set x-ticks to client numbers\n", + " ax.set_xticks(box_positions)\n", + " ax.set_xticklabels(client_labels)\n", + " \n", + " print(model, boxplot_data)\n", + " # Set subplot title and labels\n", + " ax.set_title(f'{model}', fontsize=12, fontweight='bold')\n", + " ax.set_xlabel('Number of Clients', fontsize=10)\n", + " metric_formatted = metric.replace(\"_\", \" \").title()\n", + " ax.set_ylabel(metric_formatted, fontsize=10)\n", + " \n", + " # Set consistent y-axis across all subplots\n", + " # ax.set_ylim(0.5, 0.78)\n", + " ax.set_ylim(0.6, 0.85)\n", + "\n", + " # Set x-axis limits with some padding\n", + " ax.set_xlim(min(box_positions) - min_gap * 0.5, \n", + " max(box_positions) + min_gap * 0.5)\n", + " \n", + " # Add grid\n", + " ax.grid(True, alpha=0.3, axis='y')\n", + " \n", + " # Add trend annotation\n", + " means = [np.mean(client_data) for client_data in boxplot_data]\n", + " trend = means[0] - means[-1] # Performance drop from 3 to 20 clients\n", + " \n", + " # Add performance degradation annotation\n", + " ax.text(0.02, 0.98, f'Δ: -{trend:.3f}', transform=ax.transAxes, \n", + " fontsize=9, verticalalignment='top',\n", + " bbox=dict(boxstyle='round', facecolor='lightgray', alpha=0.8))\n", + "\n", + "# Remove empty subplot if we have 6 models in 3x3 grid\n", + "if len(models) < len(axes):\n", + " for i in range(len(models), len(axes)):\n", + " fig.delaxes(axes[i])\n", + "\n", + "# Add overall title\n", + "# fig.suptitle('Federated Learning Benchmark: Model Performance Distribution vs Number of Clients\\n'\n", + " # 'Box Plots Showing Performance Degradation with Increasing Clients', \n", + " # fontsize=14, fontweight='bold', y=0.98)\n", + "\n", + "plt.tight_layout()\n", + "plt.subplots_adjust(top=0.93)\n", + "plt.show()\n", + "\n", + "# Print detailed statistics for the paper\n", + "print(\"Federated Learning Benchmark Summary - Box Plot Analysis\")\n", + "print(\"=\" * 80)\n", + "\n", + "for model in models:\n", + " print(f\"\\n{model}:\")\n", + " print(\"-\" * 40)\n", + " model_data = df[df['model'] == model]\n", + " \n", + " for client in clients:\n", + " client_data = model_data[model_data['n_clients'] == client][metric]\n", + " mean_auc = client_data.mean()\n", + " std_auc = client_data.std()\n", + " min_auc = client_data.min()\n", + " max_auc = client_data.max()\n", + " \n", + " print(f\" {client:2d} clients: {mean_auc:.4f} ± {std_auc:.4f} \"\n", + " f\"[{min_auc:.4f}, {max_auc:.4f}]\")\n", + " \n", + " # Calculate overall degradation\n", + " perf_3 = model_data[model_data['n_clients'] == 3][metric].mean()\n", + " perf_20 = model_data[model_data['n_clients'] == 20][metric].mean()\n", + " degradation = ((perf_3 - perf_20) / perf_3) * 100\n", + " print(f\" Performance degradation (3→20 clients): {degradation:.2f}%\")\n", + "\n", + "# Comparative analysis\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"COMPARATIVE ANALYSIS:\")\n", + "print(\"=\" * 80)\n", + "\n", + "# Find best performing model at each client count\n", + "for client in clients:\n", + " client_data = df[df['n_clients'] == client]\n", + " best_model = None\n", + " best_auc = 0\n", + " \n", + " for model in models:\n", + " model_auc = client_data[client_data['model'] == model][metric].mean()\n", + " if model_auc > best_auc:\n", + " best_auc = model_auc\n", + " best_model = model\n", + "\n", + " print(f\"Best at {client:2d} clients: {best_model} ({metric}: {best_auc:.4f})\")\n", + "\n", + "# Overall best model\n", + "overall_means = df.groupby('model')[metric].mean()\n", + "best_overall_model = overall_means.idxmax()\n", + "best_overall_auc = overall_means.max()\n", + "\n", + "print(f\"\\nOverall best model: {best_overall_model} (Avg {metric}: {best_overall_auc:.4f})\")" + ] + }, + { + "cell_type": "markdown", + "id": "30c417a3", + "metadata": {}, + "source": [ + "# Table: Normalization impact" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec9feea2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Weighted Average Metrics Table:\n", + "\n", + "Model Balanced Accuracy Auroc\n", + "Diabetes Logistic Regression C10 A0.7 NormN FeatN 0.654 ± 0.021 0.729 ± 0.034\n", + "Diabetes Logistic Regression C10 A0.7 Normglobal FeatN 0.745 ± 0.041 0.803 ± 0.049\n", + "Diabetes Logistic Regression C10 A0.7 Normlocal FeatN 0.730 ± 0.024 0.801 ± 0.056\n", + "Diabetes Logistic Regression C10 AN NormN FeatN 0.665 ± 0.017 0.725 ± 0.014\n", + "Diabetes Logistic Regression C10 AN Normglobal FeatN 0.755 ± 0.011 0.829 ± 0.009\n", + "Diabetes Logistic Regression C10 AN Normlocal FeatN 0.759 ± 0.011 0.830 ± 0.010\n", + "Ukbb Cvd Logistic Regression C10 A0.7 NormN FeatN 0.515 ± 0.009 0.529 ± 0.027\n", + "Ukbb Cvd Logistic Regression C10 A0.7 Normglobal FeatN 0.742 ± 0.035 0.814 ± 0.024\n", + "Ukbb Cvd Logistic Regression C10 A0.7 Normlocal FeatN 0.746 ± 0.037 0.818 ± 0.021\n", + "Ukbb Cvd Logistic Regression C10 AN NormN FeatN 0.518 ± 0.008 0.530 ± 0.024\n", + "Ukbb Cvd Logistic Regression C10 AN Normglobal FeatN 0.741 ± 0.034 0.814 ± 0.023\n", + "Ukbb Cvd Logistic Regression C10 AN Normlocal FeatN 0.746 ± 0.036 0.818 ± 0.021\n", + "\n", + "LaTeX Table:\n", + "\n", + "\\begin{tabular}{lcc}\n", + "Model & Balanced Accuracy & Auroc \\\\\n", + "\\hline\n", + "Diabetes Logistic Regression C10 A0.7 NormN FeatN & 0.654 $\\pm$ 0.021 & 0.729 $\\pm$ 0.034 \\\\\n", + "Diabetes Logistic Regression C10 A0.7 Normglobal FeatN & 0.745 $\\pm$ 0.041 & 0.803 $\\pm$ 0.049 \\\\\n", + "Diabetes Logistic Regression C10 A0.7 Normlocal FeatN & 0.730 $\\pm$ 0.024 & 0.801 $\\pm$ 0.056 \\\\\n", + "Diabetes Logistic Regression C10 AN NormN FeatN & 0.665 $\\pm$ 0.017 & 0.725 $\\pm$ 0.014 \\\\\n", + "Diabetes Logistic Regression C10 AN Normglobal FeatN & 0.755 $\\pm$ 0.011 & 0.829 $\\pm$ 0.009 \\\\\n", + "Diabetes Logistic Regression C10 AN Normlocal FeatN & 0.759 $\\pm$ 0.011 & 0.830 $\\pm$ 0.010 \\\\\n", + "Ukbb Cvd Logistic Regression C10 A0.7 NormN FeatN & 0.515 $\\pm$ 0.009 & 0.529 $\\pm$ 0.027 \\\\\n", + "Ukbb Cvd Logistic Regression C10 A0.7 Normglobal FeatN & 0.742 $\\pm$ 0.035 & 0.814 $\\pm$ 0.024 \\\\\n", + "Ukbb Cvd Logistic Regression C10 A0.7 Normlocal FeatN & 0.746 $\\pm$ 0.037 & 0.818 $\\pm$ 0.021 \\\\\n", + "Ukbb Cvd Logistic Regression C10 AN NormN FeatN & 0.518 $\\pm$ 0.008 & 0.530 $\\pm$ 0.024 \\\\\n", + "Ukbb Cvd Logistic Regression C10 AN Normglobal FeatN & 0.741 $\\pm$ 0.034 & 0.814 $\\pm$ 0.023 \\\\\n", + "Ukbb Cvd Logistic Regression C10 AN Normlocal FeatN & 0.746 $\\pm$ 0.036 & 0.818 $\\pm$ 0.021 \\\\\n", + "\\end{tabular}\n" + ] + } + ], + "source": [ + "# Normalization experiment\n", + "experiment_name = \"normalization\"\n", + "logs_dir = \"benchmark_results_normalization\"\n", + "model_names = [\"logistic_regression\"]\n", + "datasets = [\"diabetes\"]\n", + "num_clients = [10]\n", + "dirichlet_alpha = [\"None\"]\n", + "data_normalization = [\"global\", \"local\", None]\n", + "keywords = [experiment_name]\n", + "data = load_data(logs_dir, experiment_name, keywords, results_file=\"per_center_results.csv\")\n", + "\n", + "# Write a code to extract the following metrics, calculate weighted averages and standard deviations and create a table with rows as models and columns as metrics in latex format\n", + "metrics_to_extract = [\"balanced_accuracy\", \"auroc\"]\n", + "table_data = {}\n", + "for model_name, df in data.items():\n", + " # model = model_name.split(\" Norm\")[1]\n", + " model = model_name\n", + " total_samples = df[\"n samples\"].sum()\n", + " table_data[model] = {}\n", + " for metric in metrics_to_extract:\n", + " weighted_sum = (df[metric] * df[\"n samples\"]).sum()\n", + " avg_metric = weighted_sum / total_samples\n", + " std_metric = ( ((df[metric] - avg_metric)**2 * df[\"n samples\"]).sum() / total_samples )**0.5\n", + " table_data[model][metric] = (avg_metric, std_metric)\n", + "\n", + "# Print nicely formatted table\n", + "print(\"\\nWeighted Average Metrics Table:\\n\")\n", + "header = \"Model\".ljust(30)\n", + "for metric in metrics_to_extract:\n", + " header += f\"{metric.replace('_', ' ').title():>30}\"\n", + "print(header)\n", + "for model, metrics in table_data.items():\n", + " row = model.ljust(30)\n", + " for metric in metrics_to_extract:\n", + " avg, std = metrics[metric]\n", + " row += f\"{avg:.3f} ± {std:.3f}\".rjust(30)\n", + " print(row)\n", + "\n", + "\n", + "# Create latex table\n", + "latex_table = \"\\\\begin{tabular}{l\" + \"c\" * len(metrics_to_extract) + \"}\\n\"\n", + "latex_table += \"Model\"\n", + "for metric in metrics_to_extract:\n", + " latex_table += f\" & {metric.replace('_', ' ').title()}\"\n", + "latex_table += \" \\\\\\\\\\n\\\\hline\\n\"\n", + "for model, metrics in table_data.items():\n", + " latex_table += model\n", + " for metric in metrics_to_extract:\n", + " avg, std = metrics[metric]\n", + " latex_table += f\" & {avg:.3f} $\\\\pm$ {std:.3f}\"\n", + " latex_table += \" \\\\\\\\\\n\"\n", + "latex_table += \"\\\\end{tabular}\"\n", + "print(\"\\nLaTeX Table:\\n\")\n", + "print(latex_table)\n" + ] + }, + { + "cell_type": "markdown", + "id": "35133b50", + "metadata": {}, + "source": [ + "# Table: Feature Selection" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "add792d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 19 experiments\n", + "\n", + "Weighted Average Metrics Table:\n", + "\n", + "Model Balanced Accuracy Auroc Round Time [S]\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal Feat10 0.749 ± 0.029 0.817 ± 0.027 1.425 ± 0.274\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal Feat20 0.759 ± 0.021 0.829 ± 0.024 1.501 ± 0.275\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal Feat35 0.754 ± 0.024 0.829 ± 0.024 1.591 ± 0.292\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal Feat40 0.753 ± 0.022 0.827 ± 0.024 1.638 ± 0.346\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal FeatN 0.754 ± 0.027 0.826 ± 0.024 1.636 ± 0.321\n", + "Ukbb Cvd Balanced Random Forest C10 AN Normglobal Feat10 0.749 ± 0.031 0.817 ± 0.026 1.416 ± 0.276\n", + "Ukbb Cvd Balanced Random Forest C10 AN Normglobal Feat20 0.758 ± 0.022 0.828 ± 0.025 1.511 ± 0.303\n", + "Ukbb Cvd Balanced Random Forest C10 AN Normglobal Feat35 0.755 ± 0.027 0.828 ± 0.025 1.582 ± 0.339\n", + "Ukbb Cvd Balanced Random Forest C10 AN Normglobal Feat40 0.752 ± 0.025 0.826 ± 0.023 1.624 ± 0.311\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat10 0.757 ± 0.024 0.818 ± 0.022 0.966 ± 0.199\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat20 0.742 ± 0.028 0.823 ± 0.027 1.032 ± 0.212\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat35 0.750 ± 0.027 0.825 ± 0.025 1.098 ± 0.226\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat40 0.747 ± 0.018 0.825 ± 0.025 1.128 ± 0.235\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal FeatN 0.750 ± 0.031 0.824 ± 0.027 1.146 ± 0.240\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat10 0.755 ± 0.023 0.819 ± 0.022 0.983 ± 0.199\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat20 0.742 ± 0.028 0.823 ± 0.026 1.035 ± 0.216\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat35 0.750 ± 0.030 0.824 ± 0.024 1.075 ± 0.225\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat40 0.747 ± 0.023 0.824 ± 0.025 1.106 ± 0.233\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal FeatN 0.747 ± 0.032 0.823 ± 0.027 1.120 ± 0.236\n", + "\n", + "LaTeX Table:\n", + "\n", + "\\begin{tabular}{lccc}\n", + "Model & Balanced Accuracy & Auroc & Round Time [S] \\\\\n", + "\\hline\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal Feat10 & 0.749 $\\pm$ 0.029 & 0.817 $\\pm$ 0.027 & 1.425 $\\pm$ 0.274 \\\\\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal Feat20 & 0.759 $\\pm$ 0.021 & 0.829 $\\pm$ 0.024 & 1.501 $\\pm$ 0.275 \\\\\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal Feat35 & 0.754 $\\pm$ 0.024 & 0.829 $\\pm$ 0.024 & 1.591 $\\pm$ 0.292 \\\\\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal Feat40 & 0.753 $\\pm$ 0.022 & 0.827 $\\pm$ 0.024 & 1.638 $\\pm$ 0.346 \\\\\n", + "Ukbb Cvd Balanced Random Forest C10 A0.7 Normglobal FeatN & 0.754 $\\pm$ 0.027 & 0.826 $\\pm$ 0.024 & 1.636 $\\pm$ 0.321 \\\\\n", + "Ukbb Cvd Balanced Random Forest C10 AN Normglobal Feat10 & 0.749 $\\pm$ 0.031 & 0.817 $\\pm$ 0.026 & 1.416 $\\pm$ 0.276 \\\\\n", + "Ukbb Cvd Balanced Random Forest C10 AN Normglobal Feat20 & 0.758 $\\pm$ 0.022 & 0.828 $\\pm$ 0.025 & 1.511 $\\pm$ 0.303 \\\\\n", + "Ukbb Cvd Balanced Random Forest C10 AN Normglobal Feat35 & 0.755 $\\pm$ 0.027 & 0.828 $\\pm$ 0.025 & 1.582 $\\pm$ 0.339 \\\\\n", + "Ukbb Cvd Balanced Random Forest C10 AN Normglobal Feat40 & 0.752 $\\pm$ 0.025 & 0.826 $\\pm$ 0.023 & 1.624 $\\pm$ 0.311 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat10 & 0.757 $\\pm$ 0.024 & 0.818 $\\pm$ 0.022 & 0.966 $\\pm$ 0.199 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat20 & 0.742 $\\pm$ 0.028 & 0.823 $\\pm$ 0.027 & 1.032 $\\pm$ 0.212 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat35 & 0.750 $\\pm$ 0.027 & 0.825 $\\pm$ 0.025 & 1.098 $\\pm$ 0.226 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat40 & 0.747 $\\pm$ 0.018 & 0.825 $\\pm$ 0.025 & 1.128 $\\pm$ 0.235 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal FeatN & 0.750 $\\pm$ 0.031 & 0.824 $\\pm$ 0.027 & 1.146 $\\pm$ 0.240 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat10 & 0.755 $\\pm$ 0.023 & 0.819 $\\pm$ 0.022 & 0.983 $\\pm$ 0.199 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat20 & 0.742 $\\pm$ 0.028 & 0.823 $\\pm$ 0.026 & 1.035 $\\pm$ 0.216 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat35 & 0.750 $\\pm$ 0.030 & 0.824 $\\pm$ 0.024 & 1.075 $\\pm$ 0.225 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat40 & 0.747 $\\pm$ 0.023 & 0.824 $\\pm$ 0.025 & 1.106 $\\pm$ 0.233 \\\\\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal FeatN & 0.747 $\\pm$ 0.032 & 0.823 $\\pm$ 0.027 & 1.120 $\\pm$ 0.236 \\\\\n", + "\\end{tabular}\n" + ] + } + ], + "source": [ + "# Feature selection experiment\n", + "experiment_name = \"feature_selection\"\n", + "benchmark_dir = \"benchmark_results_feature_selection\"\n", + "model_names = [\"balanced_random_forest\"]\n", + "datasets = [\"ukbb_cvd\"]\n", + "num_clients = [5,10]\n", + "dirichlet_alpha = [0.7, None]\n", + "data_normalization = [\"global\"]\n", + "n_features = [10, 20, 35, 40, None]\n", + "keywords = [experiment_name]\n", + "\n", + "data = load_data(benchmark_dir, experiment_name, keywords)\n", + "# Write a code to extract the following metrics, calculate weighted averages and standard deviations and create a table with rows as models and columns as metrics in latex format\n", + "metrics_to_extract = [\"balanced_accuracy\", \"auroc\", \"round_time [s]\"]\n", + "table_data = {}\n", + "for model_name, df in data.items():\n", + " # model = model_name.split(\" Norm\")[1]\n", + " model = model_name\n", + " total_samples = df[\"n samples\"].sum()\n", + " table_data[model] = {}\n", + " for metric in metrics_to_extract:\n", + " weighted_sum = (df[metric] * df[\"n samples\"]).sum()\n", + " avg_metric = weighted_sum / total_samples\n", + " std_metric = ( ((df[metric] - avg_metric)**2 * df[\"n samples\"]).sum() / total_samples )**0.5\n", + " table_data[model][metric] = (avg_metric, std_metric)\n", + "\n", + "# Print nicely formatted table\n", + "print(\"\\nWeighted Average Metrics Table:\\n\")\n", + "header = \"Model\".ljust(30)\n", + "for metric in metrics_to_extract:\n", + " header += f\"{metric.replace('_', ' ').title():>30}\"\n", + "print(header)\n", + "for model, metrics in table_data.items():\n", + " row = model.ljust(30)\n", + " for metric in metrics_to_extract:\n", + " avg, std = metrics[metric]\n", + " row += f\"{avg:.3f} ± {std:.3f}\".rjust(30)\n", + " print(row)\n", + "\n", + "\n", + "# Create latex table\n", + "latex_table = \"\\\\begin{tabular}{l\" + \"c\" * len(metrics_to_extract) + \"}\\n\"\n", + "latex_table += \"Model\"\n", + "for metric in metrics_to_extract:\n", + " latex_table += f\" & {metric.replace('_', ' ').title()}\"\n", + "latex_table += \" \\\\\\\\\\n\\\\hline\\n\"\n", + "for model, metrics in table_data.items():\n", + " latex_table += model\n", + " for metric in metrics_to_extract:\n", + " avg, std = metrics[metric]\n", + " latex_table += f\" & {avg:.3f} $\\\\pm$ {std:.3f}\"\n", + " latex_table += \" \\\\\\\\\\n\"\n", + "latex_table += \"\\\\end{tabular}\"\n", + "print(\"\\nLaTeX Table:\\n\")\n", + "print(latex_table)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "flc", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/repeated.py b/repeated.py index 567870e..6a226d3 100644 --- a/repeated.py +++ b/repeated.py @@ -1,13 +1,19 @@ import subprocess import time import os +import sys import yaml -with open("config.yaml", "r") as f: +if len(sys.argv) == 2: + config_path = sys.argv[1] +else: + config_path = "config.yaml" + +with open(config_path, "r") as f: config = yaml.safe_load(f) -repetitions = 4 +repetitions = 5 experiment_name = config['experiment']['name'] config['experiment']['log_path'] = os.path.join(config['experiment']['log_path'], config['experiment']['name']) @@ -21,6 +27,12 @@ config_path = os.path.join(config['experiment']['log_path'], "config.yaml") log_file_path = os.path.join(config['experiment']['log_path'], config['experiment']['name'], "run_log.txt") os.makedirs(os.path.join(config['experiment']['log_path'], config['experiment']['name']), exist_ok=True) + + # Kill any existing process using the same port + if 'local_port' in config: + kill_command = f"lsof -ti tcp:{config['local_port']} | xargs kill -9" + subprocess.run(kill_command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + with open(config_path, "w") as f: yaml.dump(config, f) try: @@ -35,6 +47,35 @@ with open(config_path, "w") as f: yaml.dump(config, f) + +# processes = [] +# try: +# for i in range(repetitions): +# print(f"Experiment run {i + 1}") +# config['experiment']['name'] = 'run_' + str(i + 1) +# config['seed'] = i + 10 +# config['local_port'] = 8081 + i +# config_path = os.path.join(config['experiment']['log_path'], config['experiment']['name'], "config.yaml") +# log_file_path = os.path.join(config['experiment']['log_path'], config['experiment']['name'], "run_log.txt") +# os.makedirs(os.path.join(config['experiment']['log_path'], config['experiment']['name']), exist_ok=True) +# with open(config_path, "w") as f: +# yaml.dump(config, f) +# run_process = subprocess.Popen(f"python run.py {config_path} | tee {log_file_path}", shell=True) +# # run_process.wait() +# processes.append(run_process) + +# for run_process in processes: +# run_process.wait() + +# except KeyboardInterrupt: +# run_process.terminate() +# run_process.wait() + +# config['experiment']['name'] = experiment_name +# config_path = os.path.join(config['experiment']['log_path'], "config.yaml") +# with open(config_path, "w") as f: +# yaml.dump(config, f) + run_process = subprocess.Popen(f"python flcore/compile_results.py {config['experiment']['log_path']}", shell=True) run_process.wait() diff --git a/requirements.txt b/requirements.txt index 13078ec..fc7ee35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,6 @@ scikit_learn==1.2.2 torch==2.0.1 torchmetrics==0.11.4 tqdm==4.65.0 +ucimlrepo==0.0.7 xgboost==1.7.5 pdfkit==1.0.0 diff --git a/server.py b/server.py index 0b9784a..24f5ec6 100644 --- a/server.py +++ b/server.py @@ -93,7 +93,7 @@ def check_config(config): # filename = os.path.join( checkpoint_dir, 'final_model.pt' ) # joblib.dump(model, filename) # Save the history as a yaml file - print(history) + # print(history) with open(experiment_dir / "metrics.txt", "w") as f: f.write(f"Results of the experiment {config['experiment']['name']}\n") f.write(f"Model: {config['model']}\n") @@ -101,10 +101,19 @@ def check_config(config): f.write(f"Number of clients: {config['num_clients']}\n") # selection_metric = 'val ' + config['checkpoint_selection_metric'] - selection_metric = config['checkpoint_selection_metric'] + selection_metric = "val " + config['checkpoint_selection_metric'] # Get index of tuple of the best round - best_round = int(numpy.argmax([round[1] for round in history.metrics_distributed[selection_metric]])) - training_time = history.metrics_distributed_fit['training_time [s]'][-1][1] + best_round = int(numpy.argmax([round[1] for round in history.metrics_distributed[selection_metric]])) + # Use the last round as final checkpoint, since no validation set is used + # best_round = -1 + # print(history) + # check if history has attribute metrics_distributed_fit + if hasattr(history, 'metrics_distributed_fit') and 'training_time [s]' in history.metrics_distributed_fit: + # check if training_time is in metrics_distributed_fit + training_time = history.metrics_distributed_fit['training_time [s]'][-1][1] + else: + training_time = 0.0 + f.write(f"Total training time: {training_time:.2f} [s] \n") f.write(f"Best checkpoint based on {selection_metric} after round: {best_round}\n\n") print(f"Best checkpoint based on {selection_metric} after round: {best_round}\n\n") diff --git a/tests/test_models.py b/tests/test_models.py index 669f1d0..f5969f7 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -12,12 +12,18 @@ LOGGING_LEVEL = logging.INFO # WARNING # logging.INFO model_names = [ -# "logistic_regression", -# "elastic_net", -# "lsvc", + "logistic_regression", + "elastic_net", + "lsvc", "random_forest", - # "weighted_random_forest", - # "xgb" + "balanced_random_forest", + # # "weighted_random_forest", + "xgblr" + ] + +datasets = [ + "kaggle_hf", + "diabetes", ] def free_port(port): @@ -34,17 +40,29 @@ def setup_class(self): with open("config.yaml", "r") as f: self.config = yaml.safe_load(f) - self.num_clients = 3 + self.config["num_clients"] = 3 + self.config["num_rounds"] = 2 + + # To speed up tests, reduce number of trees in xgboost and random forest + self.config["random_forest"]["tree_num"] = 5 + self.config["xgblr"]["tree_num"] = 5 + self.config["xgblr"]["num_iterations"] = 2 @pytest.mark.parametrize( "model_name", - model_names + model_names, + ) + @pytest.mark.parametrize( + "dataset_name", + datasets, ) def test_get_model_client( - self, model_name + self, model_name, dataset_name ): self.config["model"] = model_name + self.config['data_path'] = 'dataset/' + self.config["dataset"] = dataset_name from flcore.client_selector import get_model_client from flcore.datasets import load_dataset @@ -57,22 +75,27 @@ def test_get_model_client( @pytest.mark.parametrize( "model_name", - model_names + model_names, + ) + @pytest.mark.parametrize( + "dataset_name", + datasets, ) - def test_run(self, model_name): + def test_run(self, model_name, dataset_name): self.config["model"] = model_name + self.config["dataset"] = dataset_name with open("config.yaml", "r") as f: config = yaml.safe_load(f) config = self.config - with open("config.yaml", "w") as f: + with open("tmp_test_config.yaml", "w") as f: yaml.dump(config, f) free_port(config["local_port"]) run_log = open("run.log", "w") - run_process = subprocess.Popen("python run.py", shell=True, stdout=run_log, stderr=run_log) + run_process = subprocess.Popen("python run.py tmp_test_config.yaml", shell=True, stdout=run_log, stderr=run_log) timer = Timer(180, run_process.kill) try: @@ -85,5 +108,8 @@ def test_run(self, model_name): run_log.close() run_log = open("run.log", "r") print(run_log.read()) + + # Delete temporary config file + os.remove("tmp_test_config.yaml") assert run_process.returncode == 0