From 08b18f068498b62e88951169db0588d758587ce3 Mon Sep 17 00:00:00 2001 From: faildeny Date: Thu, 13 Nov 2025 13:11:40 +0100 Subject: [PATCH 01/26] Add support for Diabetes dataset --- config.yaml | 10 +- flcore/datasets.py | 431 +++++++++++++++++++++++++++++++++++++++++---- requirements.txt | 1 + 3 files changed, 401 insertions(+), 41 deletions(-) diff --git a/config.yaml b/config.yaml index 4c561dc..9ee8e31 100644 --- a/config.yaml +++ b/config.yaml @@ -10,7 +10,7 @@ ################################################################################ ############## Dataset type to use -# Possible values: , kaggle_hf, mnist, dt4h_format +# Possible values: , kaggle_hf, diabetes, mnist, dt4h_format dataset: dt4h_format #custom #libsvm @@ -33,7 +33,7 @@ train_size: 0.7 # ****** * * * * * * * * * * * * * * * * * * * * ******************* ############## Number of clients (data centers) to use for training -num_clients: 1 +num_clients: 3 ############## Model type # Possible values: logistic_regression, lsvc, elastic_net, random_forest, weighted_random_forest, xgb @@ -43,7 +43,7 @@ model: random_forest #random_forest ############## Training length -num_rounds: 50 +num_rounds: 5 ############## Metric to select the best model # Possible values: accuracy, balanced_accuracy, f1, precision, recall @@ -87,6 +87,8 @@ smoothWeights: linear_models: n_features: 9 +n_features: 9 + # Random Forest random_forest: balanced_rf: true @@ -101,7 +103,7 @@ xgb: batch_size: 32 num_iterations: 100 task_type: BINARY - tree_num: 500 + tree_num: 10 held_out_center_id: -1 diff --git a/flcore/datasets.py b/flcore/datasets.py index 699c4a0..d7f4e84 100644 --- a/flcore/datasets.py +++ b/flcore/datasets.py @@ -12,10 +12,13 @@ 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 from flcore.models.xgb.utils import TreeDataset, do_fl_partitioning, get_dataloader @@ -23,6 +26,273 @@ 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 and 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): + """ + 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 reference center parameters + 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 + + # 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): + """ + Partition data among centers using Dirichlet distribution + """ + unique_labels = np.unique(labels) + n_samples = len(labels) + n_classes = len(unique_labels) + + # 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) + + # Adjust for rounding errors + diff = n_class_samples - center_samples.sum() + if diff > 0: + center_samples[np.random.choice(num_centers, diff, replace=True)] += 1 + + # 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 load_mnist(center_id=None, num_splits=5): """Loads the MNIST dataset using OpenML. @@ -343,7 +613,7 @@ def get_preprocessing_params(data): 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' + X_train.loc[X_train.index[-1], feature] = 'Down' transformers_dict[feature].fit(X_train[feature].values.reshape(-1, 1)) else: transformers_dict[feature].fit(X_train[feature].values.reshape(-1, 1)) @@ -358,7 +628,9 @@ def preprocess_data(data, column_transformer): target = df1['HeartDisease'] for feature in column_transformer: - features[feature] = column_transformer[feature].transform(features[feature].values.reshape(-1, 1)) + features.loc[:, feature] = column_transformer[feature].transform(features[feature].values.reshape(-1, 1)) + + features = features.infer_objects() X_train, X_test, y_train, y_test = train_test_split(features, target, test_size = 0.20, random_state = seed, stratify=target) @@ -369,39 +641,6 @@ def preprocess_data(data, column_transformer): (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) @@ -639,6 +878,122 @@ 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) + """ + num_centers = config.get("num_clients", 5) + alpha = config.get("dirichlet_alpha", 1.0) + reference_method = config.get("reference_center_method", "largest") + global_preprocessing_params = None + n_features = config.get("n_features", 20) + feature_selection_method = config.get("feature_selection_method", "mutual_info") + + # Load the dataset + cdc_diabetes_health_indicators = fetch_ucirepo(id=891) + + # Get features and target + X = cdc_diabetes_health_indicators.data.features + y = cdc_diabetes_health_indicators.data.targets + + # convert y to a pandas Series for easier handling + y = pd.Series(y.values.flatten()) + + # Use fraction of data for faster testing (optional) + fraction = 0.02 + X = X.sample(frac=fraction, random_state=42).reset_index(drop=True) + y = y.loc[X.index].reset_index(drop=True) + + # Set random seed for reproducible partitioning + np.random.seed(42) + + # Convert target to binary classification if needed + if y.nunique() > 2: + y_binary = (y > y.median()).astype(int) + else: + y_binary = y + + # Partition data using Dirichlet distribution + all_center_indices = partition_data_dirichlet(y_binary.values, num_centers, alpha) + + # 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: + # 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, feature_selection_method + ) + print("Calculated new global preprocessing parameters with feature selection") + + if center_id: + # 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=42, 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) + X_test_processed, _ = apply_preprocessing(X_test, global_preprocessing_params) + + # Convert targets to numpy arrays + # y_train_processed = y_train.values + # y_test_processed = y_test.values + + # # Print center statistics + # print(f"Center {center_id}/{num_centers} (alpha={alpha}):") + # print(f" Samples: {len(X_center)} (Train: {len(X_train_processed)}, Test: {len(X_test_processed)})") + # print(f" Features: {X_train_processed.shape[1]}/{len(X.columns)} selected") + # print(f" Data range: [{X_train_processed.min():.3f}, {X_train_processed.max():.3f}]") + # print(f" Normalized stats - Mean: {X_train_processed.mean():.4f}, Std: {X_train_processed.std():.4f}") + + return (X_train_processed, y_train), (X_test_processed, y_test) + def cvd_to_torch(config): pass @@ -695,6 +1050,8 @@ def load_dataset(config, id=None): 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/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 From 4d0bc62524acc9355e2ecba80ae22b89c0029a0a Mon Sep 17 00:00:00 2001 From: faildeny Date: Thu, 13 Nov 2025 13:16:48 +0100 Subject: [PATCH 02/26] Add Balanced Random Forest selection as separate model --- flcore/client_selector.py | 2 +- .../models/random_forest/FedCustomAggregator.py | 17 ----------------- flcore/models/random_forest/client.py | 10 +++++++--- flcore/models/random_forest/server.py | 2 +- flcore/models/weighted_random_forest/client.py | 2 +- flcore/models/weighted_random_forest/server.py | 2 +- flcore/server_selector.py | 2 +- 7 files changed, 12 insertions(+), 25 deletions(-) diff --git a/flcore/client_selector.py b/flcore/client_selector.py index 76fa3d5..3f92915 100644 --- a/flcore/client_selector.py +++ b/flcore/client_selector.py @@ -11,7 +11,7 @@ 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": 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/client.py b/flcore/models/random_forest/client.py index 52e07cb..7c464f7 100644 --- a/flcore/models/random_forest/client.py +++ b/flcore/models/random_forest/client.py @@ -30,8 +30,9 @@ 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['random_forest']['balanced_rf'] - self.model = utils.get_model(self.bal_RF) + self.bal_RF = True if config['model'] == 'balanced_random_forest' else False + self.model = utils.get_model(self.bal_RF) + self.round_time = 0 # 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 @@ -64,6 +65,7 @@ def fit(self, ins: FitIns): # , parameters, config type: ignore #To implement the center dropout, we need the execution time start_time = time.time() self.model.fit(X_train_2, y_train_2) + elapsed_time = (time.time() - start_time) #accuracy = model.score( X_test, y_test ) # accuracy,specificity,sensitivity,balanced_accuracy, precision, F1_score = \ # measurements_metrics(self.model,X_val, y_val) @@ -76,8 +78,8 @@ def fit(self, ins: FitIns): # , parameters, config type: ignore # print(f"precision in fit: {precision}") # print(f"F1_score in fit: {F1_score}") - elapsed_time = (time.time() - start_time) metrics["running_time"] = elapsed_time + self.round_time = elapsed_time print(f"num_client {self.client_id} has an elapsed time {elapsed_time}") @@ -108,6 +110,8 @@ def evaluate(self, ins: EvaluateIns): # , parameters, config type: ignore # 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) + 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..06b538c 100644 --- a/flcore/models/random_forest/server.py +++ b/flcore/models/random_forest/server.py @@ -33,7 +33,7 @@ def fit_round( server_round: int ) -> Dict: def get_server_and_strategy(config): - bal_RF = config['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/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/server_selector.py b/flcore/server_selector.py index 3ba5a06..8c5e010 100644 --- a/flcore/server_selector.py +++ b/flcore/server_selector.py @@ -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 ) From 5aefbd6ddde9d76fc62b6a9b9f1fc59eba4a679a Mon Sep 17 00:00:00 2001 From: faildeny Date: Thu, 13 Nov 2025 13:17:28 +0100 Subject: [PATCH 03/26] Minor xgb fix for results compatibility --- flcore/models/xgb/fed_custom_strategy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flcore/models/xgb/fed_custom_strategy.py b/flcore/models/xgb/fed_custom_strategy.py index 20dbe55..9f74f4d 100644 --- a/flcore/models/xgb/fed_custom_strategy.py +++ b/flcore/models/xgb/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 From 4da5e2ed23502b984e1f690fa6644704f3fd6c99 Mon Sep 17 00:00:00 2001 From: faildeny Date: Thu, 13 Nov 2025 13:18:24 +0100 Subject: [PATCH 04/26] Update tests with Diabetes dataset and unlock models testing --- tests/test_models.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 669f1d0..cd67f79 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", + "balanced_random_forest", # "weighted_random_forest", - # "xgb" + "xgb" + ] + +datasets = [ + "kaggle_hf", + "diabetes", ] def free_port(port): @@ -39,12 +45,17 @@ def setup_class(self): @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["dataset"] = dataset_name from flcore.client_selector import get_model_client from flcore.datasets import load_dataset @@ -57,22 +68,27 @@ def test_get_model_client( @pytest.mark.parametrize( "model_name", - model_names + model_names, ) - def test_run(self, model_name): + @pytest.mark.parametrize( + "dataset_name", + datasets, + ) + 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 +101,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 From 891c7d75b2a542c0cd39f349e9b0890728f326ce Mon Sep 17 00:00:00 2001 From: faildeny Date: Thu, 13 Nov 2025 13:30:06 +0100 Subject: [PATCH 05/26] Fix for config independent tests --- tests/test_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_models.py b/tests/test_models.py index cd67f79..feb2cc2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -17,7 +17,7 @@ "lsvc", "random_forest", "balanced_random_forest", - # "weighted_random_forest", + # # "weighted_random_forest", "xgb" ] @@ -55,6 +55,7 @@ def test_get_model_client( 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 df5d480ea3a6a5749838ee7aecfd013b34c06fe1 Mon Sep 17 00:00:00 2001 From: faildeny Date: Thu, 13 Nov 2025 16:48:15 +0100 Subject: [PATCH 06/26] Update github actions to meet latest github system changes --- .github/workflows/python-ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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' From 3e3a0ef174def6d94dc6938c9646d39b02c9f788 Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 3 Feb 2026 17:25:31 +0100 Subject: [PATCH 07/26] Add AUROC calculation and switch to predict_proba in models --- flcore/metrics.py | 31 ++++++++++++--------- flcore/models/linear_models/client.py | 23 ++++++++-------- flcore/models/linear_models/server.py | 4 +-- flcore/models/linear_models/utils.py | 36 ++++++++++++++++--------- flcore/models/random_forest/client.py | 8 +++--- flcore/models/xgb/client.py | 39 ++++++++++++++++++--------- 6 files changed, 86 insertions(+), 55 deletions(-) diff --git a/flcore/metrics.py b/flcore/metrics.py index 7788f61..5de33bc 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,18 @@ 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 not torch.is_tensor(y_pred_proba): + y_pred_proba = torch.tensor(y_pred_proba.tolist()) + + # Extract probabilities for the positive class + 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()} diff --git a/flcore/models/linear_models/client.py b/flcore/models/linear_models/client.py index b7561be..a4cd1ac 100644 --- a/flcore/models/linear_models/client.py +++ b/flcore/models/linear_models/client.py @@ -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,9 +67,8 @@ 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) + y_pred_proba = self.model.predict_proba(self.X_test) + metrics = calculate_metrics(self.y_test, y_pred_proba) print(f"Client {self.client_id} Evaluation just after local training: {metrics['balanced_accuracy']}") # Add 'personalized' to the metrics to identify them metrics = {f"personalized {key}": metrics[key] for key in metrics} @@ -81,10 +80,10 @@ 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) + y_pred_proba = local_model.predict_proba(self.X_test) + local_metrics = calculate_metrics(self.y_test, y_pred_proba) #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 +95,10 @@ 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) + y_pred_proba = self.model.predict_proba(self.X_val) + val_metrics = calculate_metrics(self.y_val, y_pred_proba) - y_pred = self.model.predict(self.X_test) + 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,7 +106,7 @@ 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) metrics["round_time [s]"] = self.round_time metrics["client_id"] = self.client_id @@ -119,7 +118,7 @@ def evaluate(self, parameters, config): # type: ignore 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/client.py b/flcore/models/random_forest/client.py index 7c464f7..e4e1595 100644 --- a/flcore/models/random_forest/client.py +++ b/flcore/models/random_forest/client.py @@ -69,8 +69,8 @@ def fit(self, ins: FitIns): # , parameters, config type: ignore #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) + y_pred_proba = self.model.predict_proba(X_val) + metrics = calculate_metrics(y_val, y_pred_proba) # print(f"Accuracy client in fit: {accuracy}") # print(f"Sensitivity client in fit: {sensitivity}") # print(f"Specificity client in fit: {specificity}") @@ -108,8 +108,8 @@ def evaluate(self, ins: EvaluateIns): # , parameters, config type: ignore 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) metrics["round_time [s]"] = self.round_time metrics["client_id"] = self.client_id # print(f"Accuracy client in evaluate: {accuracy}") diff --git a/flcore/models/xgb/client.py b/flcore/models/xgb/client.py index 6bcbc1a..d2e358e 100644 --- a/flcore/models/xgb/client.py +++ b/flcore/models/xgb/client.py @@ -125,6 +125,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 they 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 +143,8 @@ def fit(self, fit_params: FitIns) -> FitRes: 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) @@ -235,25 +241,32 @@ def get_client(config, data, client_id) -> fl.client.Client: client_tree_num = config["xgb"]["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, From fa7191d3cd413ad975656f8323780cc61a5de4c0 Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 3 Feb 2026 17:26:32 +0100 Subject: [PATCH 08/26] Improve Random Forest hyperparameters --- flcore/models/random_forest/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flcore/models/random_forest/utils.py b/flcore/models/random_forest/utils.py index 026c294..426e9f7 100644 --- a/flcore/models/random_forest/utils.py +++ b/flcore/models/random_forest/utils.py @@ -23,9 +23,9 @@ def get_model(bal_RF): if(bal_RF == True): - model = BalancedRandomForestClassifier(n_estimators=100,random_state=42) + model = BalancedRandomForestClassifier(n_estimators=300,max_depth=10) else: - model = RandomForestClassifier(n_estimators=100,class_weight= "balanced",max_depth=2,random_state=42) + model = RandomForestClassifier(n_estimators=300,max_depth=10,class_weight= "balanced_subsample") return model From 157fd8c6b58d12a3f48fe59a0e701411a0427ff3 Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 3 Feb 2026 17:27:21 +0100 Subject: [PATCH 09/26] Minor changes with report generation --- flcore/compile_results.py | 48 +++++++++++++++++--------------- flcore/report/generate_report.py | 3 +- server.py | 13 +++++++-- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/flcore/compile_results.py b/flcore/compile_results.py index 8270d9b..4e7f0c3 100644 --- a/flcore/compile_results.py +++ b/flcore/compile_results.py @@ -21,6 +21,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") @@ -47,7 +49,8 @@ def compile_results(experiment_dir: str): # Read history.yaml history = yaml.safe_load(open(os.path.join(fold_dir, "history.yaml"), "r")) - selection_metric = 'val '+ config['checkpoint_selection_metric'] + # selection_metric = 'val '+ config['checkpoint_selection_metric'] + selection_metric = config['checkpoint_selection_metric'] best_round= int(np.argmax(history['metrics_distributed'][selection_metric])) # client_order = history['metrics_distributed']['per client client_id'][best_round] client_order = history['metrics_distributed']['per client n samples'][best_round] @@ -98,7 +101,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") @@ -161,25 +165,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 +198,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/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/server.py b/server.py index 0b9784a..5149c1e 100644 --- a/server.py +++ b/server.py @@ -103,8 +103,17 @@ def check_config(config): # selection_metric = 'val ' + config['checkpoint_selection_metric'] selection_metric = 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") From 185789a5ced6057004e952f4a40e41a7a1bf96de Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 3 Feb 2026 17:29:14 +0100 Subject: [PATCH 10/26] Move to one commonm federated preprocessing function for public datasets. --- config.yaml | 45 ++- flcore/datasets.py | 969 +++++++++++++++++++++++++-------------------- 2 files changed, 575 insertions(+), 439 deletions(-) diff --git a/config.yaml b/config.yaml index 9ee8e31..1319554 100644 --- a/config.yaml +++ b/config.yaml @@ -11,7 +11,9 @@ ############## Dataset type to use # Possible values: , kaggle_hf, diabetes, mnist, dt4h_format -dataset: 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: 3 +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: 5 +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: "local" + +# Determine target for feature selection number +n_features: Null + + ################################################################################ # Aggregation methods ################################################################################ @@ -87,7 +113,8 @@ smoothWeights: linear_models: n_features: 9 -n_features: 9 + +dirichlet_alpha: Null # Random Forest random_forest: @@ -103,7 +130,7 @@ xgb: batch_size: 32 num_iterations: 100 task_type: BINARY - tree_num: 10 + tree_num: 300 held_out_center_id: -1 @@ -115,6 +142,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/datasets.py b/flcore/datasets.py index d7f4e84..f82a776 100644 --- a/flcore/datasets.py +++ b/flcore/datasets.py @@ -19,6 +19,7 @@ from sklearn.ensemble import RandomForestClassifier from ucimlrepo import fetch_ucirepo +import pickle from flcore.models.xgb.utils import TreeDataset, do_fl_partitioning, get_dataloader @@ -96,69 +97,70 @@ def calculate_preprocessing_params(subset_data, subset_target, n_features=None, selected_features = None feature_scores = None - if n_features is not None and 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))] + 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 - # 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] + # 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 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 + 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'") - 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}") + 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, @@ -169,7 +171,7 @@ def get_support(self, indices=False): 'n_features': n_features } -def apply_preprocessing(subset_data, preprocessing_params): +def apply_preprocessing(subset_data, preprocessing_params, normalization="global"): """ Apply preprocessing to a subset using pre-calculated parameters from reference center @@ -213,13 +215,26 @@ def apply_preprocessing(subset_data, preprocessing_params): # Ensure all data is numerical data_copy = data_copy.apply(pd.to_numeric, errors='coerce') - # Step 3: Normalize ALL features using reference center parameters - 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] + # 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: @@ -236,6 +251,18 @@ def partition_data_dirichlet(labels, num_centers, alpha=1.0): 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)] @@ -287,13 +314,362 @@ def select_reference_center(all_center_data, method='largest'): 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_old(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') + 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") + 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) + + # 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) + 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 aggregation_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 + ) + print("Calculated global preprocessing parameters using reference center") + elif aggregation_method == 'weighted_aggregate': + # 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) + print("Calculated global preprocessing parameters using weighted aggregation") + 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' or 'weighted_aggregate'") + + 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=42, 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) + + # shuffle the training data + X_train_processed, y_train = shuffle(X_train_processed, y_train) + + return X_train_processed, y_train, X_test_processed, y_test + +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") + 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) + + # 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) + 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=42, 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) + + # shuffle the training data + X_train_processed, y_train = shuffle(X_train_processed, y_train) + + 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 @@ -337,109 +713,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] + X_train_processed, y_train, X_test_processed, y_test = prepare_dataset(X_data, y_data, center_id, config) - # 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) - - 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', @@ -452,197 +770,55 @@ 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"] + """ + 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)) + """ - 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' - 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.loc[X_train.index[-1], feature] = '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.loc[:, feature] = column_transformer[feature].transform(features[feature].values.reshape(-1, 1)) - - features = features.infer_objects() - - 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: + # print(f"Invalid center id: {center_id}", type(center_id)) + 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) - - 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 @@ -894,104 +1070,37 @@ def load_diabetes(center_id, config): Returns: tuple: ((X_train, y_train), (X_test, y_test), preprocessing_params) """ - num_centers = config.get("num_clients", 5) - alpha = config.get("dirichlet_alpha", 1.0) - reference_method = config.get("reference_center_method", "largest") - global_preprocessing_params = None - n_features = config.get("n_features", 20) - feature_selection_method = config.get("feature_selection_method", "mutual_info") - # Load the dataset - cdc_diabetes_health_indicators = fetch_ucirepo(id=891) - + 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.data.features - y = cdc_diabetes_health_indicators.data.targets - + 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) - fraction = 0.02 - X = X.sample(frac=fraction, random_state=42).reset_index(drop=True) - y = y.loc[X.index].reset_index(drop=True) - - # Set random seed for reproducible partitioning - np.random.seed(42) - - # Convert target to binary classification if needed - if y.nunique() > 2: - y_binary = (y > y.median()).astype(int) - else: - y_binary = y - - # Partition data using Dirichlet distribution - all_center_indices = partition_data_dirichlet(y_binary.values, num_centers, alpha) - - # 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: - # 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, feature_selection_method - ) - print("Calculated new global preprocessing parameters with feature selection") + # # # # 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) - if center_id: - # 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 + X_train_processed, y_train, X_test_processed, y_test = prepare_dataset(X, y, center_id, config) - # 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=42, 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) - X_test_processed, _ = apply_preprocessing(X_test, global_preprocessing_params) - - # Convert targets to numpy arrays - # y_train_processed = y_train.values - # y_test_processed = y_test.values - - # # Print center statistics - # print(f"Center {center_id}/{num_centers} (alpha={alpha}):") - # print(f" Samples: {len(X_center)} (Train: {len(X_train_processed)}, Test: {len(X_test_processed)})") - # print(f" Features: {X_train_processed.shape[1]}/{len(X.columns)} selected") - # print(f" Data range: [{X_train_processed.min():.3f}, {X_train_processed.max():.3f}]") - # print(f" Normalized stats - Mean: {X_train_processed.mean():.4f}, Std: {X_train_processed.std():.4f}") - return (X_train_processed, y_train), (X_test_processed, y_test) @@ -1045,7 +1154,7 @@ 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": From af74c35798608163a2723f8f09d4538cfa432257 Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 3 Feb 2026 17:30:26 +0100 Subject: [PATCH 11/26] Add scripts for automated benchmarking --- benchmark.py | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ repeated.py | 45 +++++++++++++++- 2 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 benchmark.py diff --git a/benchmark.py b/benchmark.py new file mode 100644 index 0000000..3f78ef0 --- /dev/null +++ b/benchmark.py @@ -0,0 +1,142 @@ +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] + +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/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() From 5094455d2557489d059776a0661e103e91c4c74e Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 3 Feb 2026 17:31:09 +0100 Subject: [PATCH 12/26] Add notebook for results visualisation. --- plots.ipynb | 732 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 732 insertions(+) create mode 100644 plots.ipynb diff --git a/plots.ipynb b/plots.ipynb new file mode 100644 index 0000000..7078efd --- /dev/null +++ b/plots.ipynb @@ -0,0 +1,732 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4c815c0e", + "metadata": {}, + "source": [ + "## Select data to load based on keywords in experiment name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f05d536", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import os\n", + "\n", + "logs_dir = \"logs\"\n", + "logs_dir = \"benchmark_results\"\n", + "\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", + "\n", + "results_file = \"per_center_results.csv\"\n", + "\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", + "# 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", + "\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": 5, + "id": "29bb08b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Logistic Regression C10 AN NormN FeatN: 0.6632\n", + "Logistic Regression C10 AN Normglobal FeatN: 0.7546\n", + "Logistic Regression C10 AN Normlocal FeatN: 0.7586\n" + ] + } + ], + "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": "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HWHTTURERERERKQjbLqJiIiIiIiIdIRNNxEREREREZGOsOkmIiIiIiIi0hE23UREREREREQ6wqabiIiIiIiISEfYdBMRERERERHpCJtuIiIiIiIiIh1h001ERERERESkI2y6iYiIiIiIiHSETTcRERERERGRjrDpJiIiIiIiItIRNt1EREREREREOsKmm4iIiIiIiEhH2HQTERERERER6QibbiIiIiIiIiIdYdNNREREREREpCNsuomIiIiIiIh0hE03ERERERERkY6w6SYiIiIiIiLSEVN9F0Ca1Z16oOyfdPvhMn26qAXdyvT5iIiIiIiIyhL3dBMRERERERHpCPd0ExERacAjjoiIiOi/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/fdd9GqVSu0bdsWAwYMwPXr12FlZSXd36ZNG4wbNw7vvPOO2uNSUlIQFRVVqq+FiIiIiIiIDFPdunVRqVIljffrbbOao6OjtJdaJS0tTbrv39SsWRNJSUnSvOnp6VLTLYRAenp6sS/c3t4edevWhYWFBbfSEBERERER0UtRKpWQyWRFjrp+nt6abi8vLyQkJCAtLQ0VK1YEAISGhqJ+/fqwtrZWm3fVqlVo3LgxmjVrJk17+PAhunTpglq1asHBwQG3bt1CjRo1ADw9/7uwsBCenp5FntfU1PSFWyGIiIiIiIiISuLZ06I10duuXnd3d3h7eyM4OBiZmZmIjIzE2rVrMWjQIABAly5dcOXKFQBAZmYm5s6di9jYWMhkMqxbtw4xMTEICgqCiYkJ+vXrhyVLliA2NhYpKSmYP38+3nrrLemSYkRERERERET6oNdRG5YuXYoZM2agdevWsLa2xsCBAzFw4EAAT/dk5+bmAgAmTJgAhUKBAQMGIC8vD25ubtiwYQOqVasGABgzZgxycnIQFBQEhUKBwMBAzJo1S18vi4iIiHRAqVTCyMgIAKT/ExERver0NpAaEb1eZDIZTE1NYWJiou9SiMgACSGk//g9REREr5LX//oEpLWcnByYm5vDzMxM36W8cphNUUIIGBkZ4ddff0V6ejrs7e1haWkJKysrWFtbw8rKChUqVICDgwOsra3h5ORkcD+IMzMzUaFCBZibm+u7FKJybebMmahcuTJq1KiB6tWro1q1aqhcuTIcHBxgZGRksHu/c3NzYWJiAhMTExgbG3OgWAAFBQWIiIhAxYoVYW1tDWtra1hYWOi7rFcCs9FMLpcjLi4ONjY2qFChAiwsLPibD093rFy+fBlVq1aFvb09bG1t1a4aRf+OTTdJVM3TsmXL8PjxY9jb28PCwgLW1tawtbWVVrBq1arBzs4OLi4uMDU1jI8Qs9FM9SM3NzcX6enpSE5ORkFBARQKBeRyOYyMjFC5cmWkp6cjPj4eP//8szR44utOqVTC2NgYc+bMQVRUFOzs7GBhYQEbGxvY29vDzs4OdnZ20oCQXl5eBtOYjxkzBhUrVkT16tVRpUoVVK1aVWqenv2xY4iYTfHkcjkyMjIQFRWFlJQUpKenIycnBzKZDEIImJubo3r16qhatSo2bdqk73LL1NSpU2FtbQ1HR0c4ODigYsWKcHBwgJ2dHaytrWFpaQlLS0s4OTnpu9Qyk5iYiEWLFqFixYoQQsDMzAwWFhawtLREhQoVYGtri0qVKqFOnTpqA/UaAmaj2ePHj/Hdd9+hSpUqMDMzQ4UKFaQdCRUqVICVlRUcHBxQvXp1uLm56bvcMvPkyRP88MMPsLGxQWFhIYyMjGBqairlo8qkfv366NSpk77LfSXx8HIqYvv27UhOToZMJkNeXh7y8/ORl5cHhUIBBwcH3LlzB/fv38e+ffsMbrA6ZvPv5HI5cnNzkZWVBQBITk7Ghg0bcOjQIVStWhXHjh0zuIbh7NmzSEtLQ15eHrKzs5GdnY2MjAzI5XKYmpri/PnzePjwIUJCQqSxKl5nCoUC8+fPR3JyMp48eSI1ULm5uSgsLISZmRmqVKkCBwcH7Nq1S9/llilmox0hBLKzs5Geno5Tp05h3rx5sLe3x8WLF6WNpa87pVKJNWvWIDU1FSkpKUhLS0NGRgby8vIgk8lgZmYGExMTKBQKHDlyRN/llpn09HScPXsWSqUSWVlZyM3NRV5ennRUQE5ODnbt2oXGjRtj/fr1+i63TDEbzRITE7Fnzx4olUpkZmYiNzdX+s2nWpf+/vtv+Pj4YNmyZfout8zk5ubizp07kMvlyMnJQW5urvRbz8jICDExMfjtt9/g6+uLrVu36rvcVxKbbtJIqVQiPz8f+fn5sLCwwL1797B+/XqcOXMGTk5O2Llzp8E1TyrMpqjifuAeOnQIe/fuhbGxMerXr48+ffqgXr16eqpQ/4QQKCwshEKhgKWlJS5fvoxNmzbh5s2bcHZ2xi+//GIwe7o1USgU2LRpExYsWAAXFxccOHDAYJqnf8Nsnq5DSqVSOkUlKysLd+7cwfLly/HgwQN8+OGH6NixI2rVqqXnSl8N2dnZWLZsGTZu3IiAgAD8/PPPBveZeV5BQQG2bduG1atXw9raGp988gl69+6t77JeCcxGM4VCgWPHjmH+/PnIzMzEhAkTMHToUH2XpXfZ2dlYtWoV9u7di/r162PIkCHo0KGDvst6JbHppiKK+4P8008/4a+//oKdnR2aNGmCd999F7a2tnqqUH+YTckcOHAAq1evho2NDfz9/fHWW2/B3d1d32XpzfOfm9TUVCxfvhy3b99GtWrVEBgYaHA/bAoKCgBA2siQmJiIq1evYuXKlTA3N8eIESPQpEkTVK1aVZ9l6gWzKd6z61FqaiquXbuGHTt24J9//sEHH3yAd955BxUrVoSpqanBNZYFBQUwMjKCmZkZlEolYmJicOzYMfz6669o2LAhhg0bBldXV4M5tQd4unFcqVRKp3rl5eXhzJkzWLZsGaysrPDOO++gVatWqFmzpp4rLXvMRjMhBORyuXQet0KhwK1bt7Bo0SKkpKSgV69eCAgIgKurq8GcRgj878oRRkZGEEIgLS0Nu3fvxqZNm+Dm5obevXvD398fjo6OBvf9W1JsukmjgoICrFu3Dtu2bYOrqytatWqFTp06oXr16vouTe+YTVHZ2dm4dOkSfvjhByiVSgwYMAAtW7aEi4sLgKeZmZqaGvQAPykpKViyZAlCQkLQsmVLtGnTBu3bt0eFChUM9o9UdHQ0Ll26hG3btiElJQUTJkxA+/btYW1tre/S9I7Z/I9q/cjOzsbRo0fx119/ITIyEm+//TaGDx+uNqCPaiwFQxQaGopz585hz549sLe3xxdffAFfX1+DGwjq2e/TxMREXL58GWvWrEFBQQE++eQTtGjRwiBO5SkOsymZrKws3Lx5Ez///DMiIyMxcOBAdO7cGS4uLgb3t/rZ79SHDx/i9OnT2LBhA5ydnfHZZ5/By8sLdnZ2AIrfOUVPsekmNUIIpKam4tChQ1i9ejXeeOMN9O/fH76+vqhRowYASOeIGdqPGmZTlOrLNSIiAuPHj0dOTg6+/PJLvP322/zS/X9CCMTFxWHjxo3Yu3cvOnTogD59+qBhw4awsbEB8HRLuqGN6J6QkIBjx47hzJkzSE1NxdChQ9X29qsu/WQo69KzmI061ffM2bNnsWbNGiQkJKBFixYYM2YMqlWrJo2NYKiEELh16xZCQkLw999/w8TEBGPGjEHjxo2leRQKBYyMjAzmMwMA8fHxuHbtGg4cOICEhAQMGzYMnTp1kjbQyOVyKJVKgzylh9lolpqairCwMBw+fBiXL19G7969MWDAADg6OsLIyAhKpRJyudxgslF9/96/fx9nzpzBvn37UKFCBUycOBF+fn7SbxelUgkABvUdoy023QTgfyvV9evXMXz4cNSqVQszZsxAo0aN9F2a3jEbzVTZ7NmzB1OnTkWNGjVgZmYGOzs7VKhQAfb29qhcuTKqVasGa2trNGnSBA0bNtR32WVClc358+fx0UcfwcvLCxMnToSnp6fBnn6gyuTYsWNYtGgRsrKy0KdPHwwbNgwVK1ZEYWGhwfyQeR6z0ezZqwBs2bIF7du3R8OGDVFYWAgrKytphG4bGxuYmJjA3d3dIA69V31m9u3bh5kzZ8LU1BQjRoxA3759YWZmJo1IbWhUn5dhw4bh3Llz6N69OyZMmICqVasa3B7/5zEbzVTZTJ48Gfv27UPz5s3x9ddfo169egbdSKp2CvTp0we3b99Gnz590K9fP+myYarLzRnajoOXYbibhqlY8fHxyMnJQVRUFIYOHQpbW1tYWlrC0dERVatWRY0aNWBra4uAgAC1reiGgNkUpdqb3b59e+zYsQMFBQXIzMxEeno60tPTkZaWhpSUFISHhyM+Ph6mpqYG03SrssnIyAAA3L17F8OHD4e5uTnMzc3h4OCASpUqoVq1aqhQoQI6deqE9u3b67NknVM1Cf/88w9iY2PRpUsXyOVyfP/999IlWSwtLWFtbQ0hBFq3bo033nhD32WXCWajmeoHb79+/eDt7Y0nT54gMTEROTk5yM7ORm5urnROc2JiImbNmoWqVasazGGO8fHxsLe3R9OmTfHgwQNMmzYNZmZmMDc3ly5RKJfL0atXL3h7e+u7XJ1TfV5q1KgBX19fXLt2DW+99RbkcjmMjY1hYWEBW1tbVK5cGSYmJli5cqVBbKQBmM2LqL4r8vPz4e3tjZycHIwYMQJCCOnSWDY2NqhUqRKMjY0xa9YsODo66rlq3VM10y1atEDFihVx7do1HD58WLo0rKmpqXQZNYVCgc2bNxvUpQm1wT3dpEZ1+Yj8/Hykp6cjJSUFT548wZMnT/D48WMkJycjOjoaQUFBeP/99/VdbpliNiWnGogEgLT1/NGjR7CwsDCIP1Iqz//oz87ORkpKChITE/Ho0SMkJCQgKSkJsbGx6NSpE/r166fHastOTEwMHj58KF3eKDc3V7oESX5+PhQKBR4/fozx48ejadOmBtM8AczmZcnlcuTn56OwsBDW1tYGdVRAUlISHj16hIyMDOlvlOpyPqrLHSUmJuL999+Hj4+PwX1mlEolZDIZcnNzkZ6ejtTUVDx58gRJSUlIS0vDyJEj1cYEMCTMpijVJdRkMpmUTVZWlpRPWloaMjMzMXHiRIMcX+NZMpkMycnJSExMlLLp0aMHKlSooO/SXklsuklrGRkZMDExkc5Hpf8x1Gxe9CPu8uXLuHXrFi5cuICRI0ca3GH5JTn/Njs7W9qSbugUCoV07WUrKyuDap7+jaFno1AopD0rqvWpsLAQxsbGyMnJwfnz57F582YsXLiQe1r+n+q7+cmTJ9JpP4bk375/L126hGbNmpVxVa8GZvPyQkNDDeKokWcVFhZK3yfFnYpQUFCAc+fOITAwUA/VlQ88vJxKJDU1FWfPnkV4eDiuXLmCSZMmoXnz5vou65XAbKDWcCsUCpw7dw779+9HXFyc1CjY29sb1OVqVFSX2HheRESENAJzaGgoZs6ciYCAAD1UWPZUzdOzTWN+fj5MTU3x8OFDnDp1Ctu2bcP69esN7nrLzEYzExMT6VDHmJgYpKam4saNGzh+/DiuX7+OGjVqoFGjRtIouoZCdVTRs4PJZWdnw8jICFevXsWRI0dw6NAh7N69G3Xq1NFXmWVO1SCovn9VA2CdOHECx44dw40bN1C5cmX88ccfeq607DEbzZ4f2FSVVVhYGI4dO4bz588DAHbs2KGvEvXi2UZb9Z3z5MkTHDx4EMePH0dcXBzq1KnDpvsF2HRTEaqRYJ88eYJjx47hyJEjSE1Nlc4Nc3NzQ+3atfVdpl4wm+IlJCRIo32ePn0a1atXR+vWrbF//36MGTMG7777rkFfgqSgoABmZmYICwvDgQMHcO7cOWRlZaFSpUqoWbMmevfubVDXMVc1T9nZ2YiNjUVCQoLUHCQmJsLHxwfdunVD5cqV9V1qmWM2moWHh+Off/5BREQE4uPjERERgUqVKuHevXtYvnw5/Pz8YGlpCUtLS32XWqZUzXZcXBweP36Mu3fv4sqVKzhx4gTMzc3RqlUrTJkyxeD2/hsZGSE2NhaZmZkIDw/HyZMncfbsWdja2qJFixYYOnQoXF1d9V2mXjAbzUxMTJCamor09HQ8fvxY2hAhl8vRoEED+Pv7w8fHR99llrmCggI8ePAAqampuHz5MkJCQhAeHg5HR0d069YNQ4cONaiNei+Dh5eTmoiICFy8eBH79+/HvXv34OnpibZt2+KHH37A1KlTMXjwYIMdoZDZaObp6QknJyf06tULbdq0QcWKFeHk5ISAgAD89ttvcHFxMcjLYgFASEgIzp8/jwMHDkCpVKJp06bo0KEDvvrqK8yePRtBQUH6LrHMnTlzBjdv3sSDBw8QHR2N2NhY+Pj44OzZs9i8eTNcXFxgbW1tkCPGMpviZWRkoFu3bnB2dkbDhg3h4+ODtm3bwsHBAY0bN8bx48cNaryIZ+3duxcRERF49OgR7ty5A7lcjrZt2+KPP/7A9u3bUbt2bYPbEBEfH489e/YgPj4ely9fhrGxMfr374/CwkIpF9XnxdDOcWc2miUlJeH48eOIiYnB+fPnER8fjx49esDMzAzHjx/H5s2bpUvEGlI2UVFRWLduHZKTkxEZGYnatWtj0KBByMrKwooVK/Dnn38a3HfMy+CeblLTu3dv+Pr6YvDgwfD394exsTGqVauGdevWoWXLljAxMTHY5onZFE+pVMLHxwd5eXkAgJo1a0p/sGUymTSPoeWiMmrUKLRs2RKLFy+Gu7s75HI5HBwcMH/+fGkkd0P63KSnp2PkyJGoW7cuOnfujPfeew9NmjSBXC5Hs2bNULt2bYO9pBqz0aygoABVqlRBxYoV0bp1a3h5eaFixYqIiYmBubm5wZ2rrJKamoopU6agRo0aGDRoEL788ktUq1YNBQUF2LlzJ6pUqWJQP4ZVjdCVK1ewfPlydOvWDStXrpT22u7btw8ODg5wdHQ02OvdM5uiVNmcPXsWs2fPRqtWrfDll19Kpwru2bMHV69elRpu1eXFXneqXG7fvo1t27ahf//+mDlzpnTk4v79+2Fvbw9LS0solUqNp9PRU6//J4ZKTAiBbt26IS4uDhcvXkRKSoq0YslkMhgbG0MIYTDNwbOYjWbGxsb47bffMG7cOPzzzz9o164dvvjiCxw7dgwVKlRA7dq1YWxsDKVSqe9S9WL48OG4desWli1bhnPnzsHBwQHA08+NmZmZwW2QUCqVaNOmDTw8PFCzZk3pPP/4+HjpEkeGitloVrlyZSxatAj169fH0qVL8fXXX+PIkSO4c+cOrKysYGFhAaVSCUM7eM/U1BT9+/dHQEAACgoKEB8fD5lMhoSEBIP8zKh+8Pv4+OCdd95BYmIi/vjjD1y8eBHA03NQn91wZQiNkwqz0UyVjZubG95++23Y2NggLCwMd+/eBQAkJyerjRVhKNmocmnUqBGGDx+O0NBQzJ49GwcOHADw9MgAVS5suP8dDy+nIh4+fIj169fj0KFDePPNN/H222/jxx9/xPnz52FkZGRQh9Q8j9kUT/W6hRAIDQ3F77//jrNnzyI5ORkLFixA165dDW6k5Wfl5uZi8+bN2L59O4QQ6Ny5M3bv3o0LFy7ouzS9SE5Oxt69e3H48GGYmJigR48esLa2xrJly3D8+HEAhvsHnNn8u8TERGzcuBGHDx9GYmIiXF1d8fPPPxvs4eUFBQU4ePAg9u/fj0ePHqFz585wcHDA5s2bcezYMWlDhKE0Cs/+HQ4NDcUvv/yCkJAQBAYGIjY2Fr6+vpg+fToAw9ljqcJsNHs2m4MHD2L9+vV48OABBg8ejLCwMNSpUwczZ84EYHjZqCQnJ2PDhg3Yvn076tSpg/T0dLRr1w7Tpk3Td2nlAptuUvPsF0lsbCy2bduGY8eOISoqCpMmTUKvXr1QpUoVPVepH8ym5DIyMnDz5k0cP34cN27ckA59bNGihb5LK3PP/iFPSUnB4cOHsW/fPty4cQMffPABgoKCDHbAGqVSia1bt2Lbtm2IiIhA/fr18cMPPxhsHs9iNkWpjpZRfQ8XFBTg999/x8aNG5GWloagoCC8//77Bjeq+7POnDmDDRs24Ny5c6hVqxamT5+Otm3bAjCsc1BVP21Vr/fRo0fYsGEDtm3bBgsLC4wcORKDBg0yyI3BzEaz55vpf/75B2vWrMGpU6dQt25dTJw4EZ07d9ZjhfohhFA7Kq+goADbt2/HunXr8OTJE/Tr1w8ffPCBQX/3lgSbbnqhvLw83L17F3/99RdCQkJgZWWFAQMGoEuXLvouTe+Yzb9LT0/H33//jc2bN+ONN97AnDlz9F2S3ikUCkRHR+PChQs4cOAA5HI5evfujYEDB+q7tDLz/B9wADh9+jTWrFmDsLAwtGjRAmPGjDG466ACzKaknh0HobCwEMePH8fChQsxYMAAjBgxwqDGSQCKjgtx9+5drF69GidOnECNGjUwZcoUtG3b1qAab6DotaifPHmCPXv2YP/+/cjKysKqVasM6soRz2I2mj2fTUREBP744w+EhITA1tYWP/zwA1xcXPRcpX48u2EiLy8Phw8fxp49e3D79m2sXbsWvr6++i3wFcamm0pEoVDg6tWr2LhxIxwdHdk8PYPZ/LuCggIkJiZyK+hz7t27h02bNsHc3BxfffWVvsvRi+cHX7l+/Tq+/fZbdO3aFUOHDjW45ulZzEY7jx49grm5OSpVqqTvUvTm+WYhNjYWixcvRosWLdC3b19+ZvB0PI24uDhs2rQJH374IS9z9Axmo1l2djYePHiAn376CRMmTMAbb7yh75JeCUqlEo8fP8bu3bvRq1cv1KxZU98lvbLYdNO/en7LeGZmptqAEoaM2RRP9cNP9Z/qOrL0lKHtbdJWamoqjI2NpUHn6H8MPZvnDzGnf5ednQ0AsLGx0XMlrxaOtqwZs9EsKysLVlZWBr/xirTHppv+lRACCoUCSqXSIM/x0eTZplKpVMLMzEzfJb2yuHflqWc/M6rbJiYmBvfDRjXKtJGREZunZxjq4DwlwWw0e/4cXXrxhk3V9w//JhXFjVqaPztcz4rHz0zJsemmIvjj5uUY8t7L1NRUhIaGIjw8HFlZWbCwsICTkxO8vLyka1EbshetU4b8udGEmWhmyNnExMTg2rVryM/Ph6mpKSpXrgwXFxeDPm3l2c+DQqGATCaDqakpzMzMDPZzopKbmwuZTCZdeu95hvpbRy6X/+vRZ4b8PQM8PVe5sLAQFhYWapfdM+QjAIQQiI+Ph4mJCWxtbYscOWPon5mSYNNNEqVSiVOnTiE0NBTNmzdHixYtkJaWhoMHDyIzMxOtWrUyuMF7VF8ix44dg5+fHypXrgzg6WUTjhw5gtjYWNSsWRNdu3Y12MvVhIaGYurUqUhJSYGrqytsbGxQWFiIvLw8ZGZmws/PD+PHjzfYfG7cuIETJ07gn3/+QVZWFipUqICaNWvC19cX3bp1M7jDhBUKBXbt2oWUlBT4+/vDz88PqampuHv3LiwtLQ3uO+ZZ27ZtQ69evaQfebGxsYiOjkZ+fj4aNGhg0OfK/fXXX/jxxx+hVCqRkZGB/Px8ZGdnIzc3F3Xr1sWIESMQFBSk7zL14uTJk7h69SoyMzNhamoKe3t71KpVCy1btoSTk5O+y9OL69ev47fffoOZmRl69+6Nxo0b4+jRozh48CBq1qyJPn36wM3NTd9llrnk5GRs2rQJEyZMAPD0UOnjx4/j1KlTUCgUaNKkicGOXA48Hfz12LFjiIiIQHp6OgoLC2FjY4O6deuibdu2BvmZAYD79+/jjz/+wP3795GQkIDs7GxYWFigdu3aaN++Pfr378/TCEuATTdJjeUvv/yCdevWwcnJCffu3cPSpUvx008/QaFQwNraGomJiZg6dSpatWql75LLjGpLuK+vL3777Td4eHjg2rVrmDRpEuzt7eHs7Iz09HQYGRnh66+/NqjL+ag+Nz169EC3bt0watQoKJVK5OTkIDc3FykpKdKgI506dcKoUaMM5ktZlc2+ffuwYMECNGzYED4+PjAxMUF2djaSk5ORmJgIY2NjTJ061aD+kK9atQq7du2CpaUlKlSogHnz5mHq1KmIjIxEYWEhXFxc8NNPP6FGjRr6LrXMNW/eHGfPnoWpqSkuX76MGTNmIDExUdqD+fbbb+Pbb781uB/EeXl5eO+999CuXTv06dMHNWrUgLm5OfLz85GQkIC//voLv/zyCz7//HP06dNH3+WWqS1btuD333+Hg4MDLC0tIZfLkZGRgbi4OGRmZqJHjx4IDg42qM9MdHQ0pk2bBiMjI9jb2yM+Ph79+vXD4sWL0atXL9y5cwfp6elYvHixwY1AfeHCBYwbNw6XLl1CamoqlixZgr1796JXr14wNjbGtWvXYG9vj1WrVhncGACpqalYsWIFjh49Ch8fH1SpUgVCCGRnZ0sbQLt3746vv/5a36WWqYSEBMyZMwdPnjxBnz59pJ1PGRkZiI6Oxt9//w07OzssXbrU4HYiaE2QwVMqlUIIIdq1aycuX74shBDi9OnTokuXLmLJkiXizp074u7du2LWrFnio48+EllZWfost0ypsmnatKlITEwUQgjx3nvvicWLF4vo6GiRkJAgbty4IYYPHy5mzJgh8vPz9VmuXnh5eQmZTKbx/tu3b4uWLVsaVDaqz03btm3F6dOnpemFhYUiLy9PJCcnixs3boiRI0eKyZMni7y8PH2VWqZycnJEQECACAkJESkpKWLBggWiT58+YsGCBUKpVIrHjx+Lzz77TEyfPl3fpZa5rKws4enpKYQQIjc3V/To0UN88803IicnRwghRGhoqHjnnXfEjz/+qM8y9SIqKko0a9ZMuq1UKqX/VA4dOiR69eol3W8IcnJyROvWrcWBAweKvf/mzZuiX79+4ocffijjyvRD9b4fOHBA9OvXT5r+/fffi+7du4uHDx8KIYSIjY0VX375pQgODtZHmXqhymb//v2ib9++QgghDh8+LHr16iViYmKEEEIUFBSI8PBw8emnn4qff/5Zb7WWNVU2R44cEd26dRNJSUlq9ysUCiGTycTJkydF//79xa5du/RRZplT5fLnn3+KPn36FDuPQqEQt2/fFp9++qlYvnx5WZZXLhneySxUhOocjPT0dDRq1AgA0Lp1azx8+BDvv/8+3nzzTdSvXx8zZ87EzZs3DWZvJfC/bAoKClC1alUATy/zNGzYMNSuXRtOTk7w9vbG999/j+PHj+uzVL3Izc1FnTp1cObMGY3z1KhRQzoUyVAUt04BgKmpKSpUqIBKlSrB29sbS5YswbFjx/RVZplLTk4GALRp0waOjo7o27cvwsPDMWXKFAghUK1aNYwePRrnzp0D8L+BawxBSkqKtGcpJSUFqampmDZtGiwtLSGEgJeXFyZPnoyDBw8CMKxs8vLyYGNjg4iICACQzql89vzBqlWrIjU1FcD/BvZ53SUlJUEmk6Fr164Anv6dKiwshEKhgBACHh4emDJlCvbv3w/AcD4zjx49QrVq1aTbjo6OcHJyQt26dSGXy1GzZk00bNgQMTExeqxSPxISEuDs7Azg6Z7dunXrolatWtJgsO7u7mjUqBGuXbum50rLXnR0NGrXro3KlSurfYcYGxvD3NwcgYGBaNasGU6dOqXHKsteUlIS7O3ti73PyMgIDRo0QKNGjXDjxo0yrqz8YdNNAJ42Ty4uLrh48SKApwNtDB06FA4ODtIf6tTUVCiVSlSoUEGfpZa5jIwMyGQyFBYWIiMjAx4eHsjNzVWbx8zMDPn5+QbVWAJAhQoV8N5772Hq1KlYvHgxjh8/jvDwcNy/fx+3bt3Crl27MGbMGHTp0kXfpZY5mUwGHx8fbNq0SeOP3fv378PY2Nhg1qm8vDw4ODjgzp07AJ42AZ06dZL+DUAaCAownOYJePrDxtLSEsDT71oXFxfk5+erNZdWVlbIy8sDYFjZ1KlTB507d8bEiRPx66+/4uzZs7hz5w4SExORn5+PsLAwLF++HO3atdN3qWVKqVSiSpUq2L59u3R1ETMzM7UrIqg+L6r5DUHt2rWRnp6Ov//+GwDg7e2N999/HwCk75aHDx9KzachUH2/Pn78GPHx8UhNTUVSUhKsrKyQnZ2tNqBccnKyQV7rvmbNmkhNTcWJEycgk8mgUCiKzJORkaGxAX1deXp6Ii8vD2vXrkViYiIyMzMhl8sBPG26CwoKcOPGDdStW1e/hZYDhrPLkl7I0tISffr0wbRp07B69Wq4ublh4sSJAIDCwkLs3bsXhw8fRseOHfVcadnLzc2FpaUlRowYgfz8fDx58gQ//vgjZs+eDYVCgevXr2PTpk0Gda67irGxMQYPHgxHR0f89ttv2LJlC/Ly8mBpaYkqVaqgUqVKqFu3Lj799FN9l1rmLCwsMHLkSHz22WcICQmBt7c3qlevDktLSxQWFiIqKgpXrlzBBx98oO9Sy0zdunXRsmVLLFiwAPPmzYOLiwuWL18OADAxMcGNGzewbNkytGnTRs+Vlh3x/+f/p6enIyEhAdOnT0d6ejoyMjKwe/duDBgwAMDTQdV++eUXNG3aVM8Vlz1LS0t88MEHUCqV2L17t9Q8KhQKJCUlITc3F++99x7GjRsHAAZzKag6depg8ODB+Pnnn3Ht2jU0bNgQ9erVk86rPH/+PPbs2YN+/foBeP0vdaR6fW3btsWpU6dw6NAhNGzYEI0aNZI+M9evX8fChQuhVCoxadIkfZZbplTZ1K5dG9evX8cXX3yBzMxM5OTk4OrVq2jbti0SExOxbNkyRERESAOtGYJnPzeXLl3CkiVLcOHCBdSqVQvVqlWDubk5MjMzsXPnThQUFOCLL77Qc8VlQ5VLkyZN0KVLF2zevBkhISFwdnaGnZ0dzMzMkJSUhPPnz8PX1xcDBw7Uc8WvPg6kRhK5XI4rV67A2dlZOtzI2NgY0dHRmDp1KmrXro2JEyeqHbZlCHJzc3HlyhUolUpkZ2cjKysLVatWRYcOHZCcnIw1a9YgLi4OU6dORZ06dfRdbpkTz1wmIi8vD8nJyUhNTUVOTg5sbW3h5eVVZD5DkpycjK1bt+LmzZtIT08H8HSPpa2tLXx8fDBo0CCDOkIiKioK586dQ4cOHVC9enW1KwSMGTMG77zzDqZMmQI7Ozt9l1qmkpOTcfLkSTx58gQ5OTmQyWRo2bIlOnbsiNu3b+PLL79ElSpVMH/+fFSuXNlg16cnT57g/v37SEtLg1wuR5UqVVC/fn1UqVJF36XpRV5eHg4fPoyjR4/i3r17SEtLQ0FBAZRKJd544w18+OGH6Nmzp8FsiFDJzs5GRkZGkb3ZGzZsQFhYGAYOHIjGjRvrqTr9SUpKQlpaGoyNjZGXlweZTIb69evDwcEBf/31Fw4fPoxOnTohMDBQ36XqRUFBgbST6e7du8jIyIC5uTmcnZ3RpEkTdO3aFX5+fgb53fvo0SMcO3YM4eHh0meoSpUq8PPzQ9OmTQ366holxaab1Gi6yH10dDScnJwMagTU56lWlee/bB8/fgwLCwtUrFjRYH8Iv0h4eDhycnIMcg+dEAIymQwVKlSAUqlEWloasrOzoVAoUKlSJekwNUP53CgUCunHf3Z2NrKzs2Fubg4bGxvIZDLk5ubC0dERZmZmeq5U/+RyOWQyGUxMTCCXy5GTkwMbGxtYW1vru7QyJ4RAYWEhjI2NNY4pEhERAWtra4O+Zjfw9NSEwsJCVKxYUe3vtaF8xzwrOjpaGg/A0dERVlZWAJ4evWeI3zHPfgZiY2NRUFAAa2tr2NnZSdnIZDKD2gj8rGf/Pj2rsLAQubm5MDMzk3IyNElJScjIyEClSpWk694rFArI5XKD/by8DB5eTpLQ0FDs27cP58+fx5MnT2BiYoLKlSvD29sbQUFBBrkXV+XZbJKSkmBsbIzKlSvD09MT77zzDpo1awbg9T9873m5ubkoKCiAlZWVxg0yf/75J9LS0gyu6VZd937Hjh3IyspC165dMXDgQLVz5a5du4bs7GyDOZzaxMQEISEhOHToELKysmBiYgJzc3M4OjrC09MTHTt2hJmZmXSUjaE5ffo0Dh48iMzMTBgbG8PS0hKVKlWCl5cX2rdvD2tra4PMxsjISOP3iyqP77//Ht27d1c7Sut1J4TA9u3bcfLkSdja2qJr167SHkrV+ajnzp1DxYoV0bBhQ32WWqYeP36MtWvXIjIyEjExMUhNTYVCoYC9vT2aNGmCkSNHwtvbW99lljkjIyMkJCTgp59+UstGqVTC3t4ejRs3xqhRowwyG0DzaSlmZmbSBvLg4GCMHDnSoI6siYiIwMqVK5GQkAAzMzP069cPPXv2hKmpqZTZhAkTMHnyZDg5Oem52lcb93QbONWWz3PnzmHRokVwcHBAz549YWFhgezsbDx+/BixsbGIjIxEr1698OGHHxrEjxmg5NncuXMHPXv2NKhsVD9q161bh6tXr8LJyQkWFhawtraW9sZZWlrCxcUFc+fORevWrTFy5Eh9l10mVJ+bAwcOYPXq1XjzzTdRt25dbNu2Da1bt8acOXNgZGQEU1NTTJ8+HZmZmVi2bJm+y9YpVSYHDx7Exo0bYWdnh9q1awMAcnJy8OTJE9y8eRMmJiZYtGgRWrZsqeeKyw6z0Uz1PRMcHIykpCRUq1YNVapUQfXq1VGtWjVUrlwZ9vb2qFSpErp3745JkyahXbt2r33TrfrM/P7779i+fTuqV68OuVyOu3fv4rPPPsO7774LuVwOU1NTDB8+HN26dUOfPn1e+1xUJk6ciMTERLzzzjuoX78+LCwskJOTg9jYWFy7dg3Hjh1DcHAw2rdvr+9Sy9yLsrl+/TqOHj1qsNncvn37/9q77/imCv3/46+kaTrSXdqyyhYZMgURQUUUEbfiwnEFFcHxFRAuooLgBBVkCjiqDEVwoAiCiqCCCAgKQoGirNJS6G7TNGmTJvn9we8ESnJaFznens/z8bgPry338fjwvp+meecsTCaT7/1LeHg4YWFh1X5mOnXqxLp163zPqtaDwYMHExMTQ9++fcnOziYtLY3//ve/3Hnnnb7XojZt2rBx40ZdfRjxV8iRbp1TfmCWLl1Knz59fDeiUbhcLsrLy1m5ciWrVq2iU6dOdOvWTaNpg0uyUacc0T948CBbt26lS5cuuFwuKioqqKqq8l2mEB0dzfbt27nrrru0HDeolL358ssvuf766xk6dCgA1157LY899hjvvPOO7wMIu92uiyNQSiZvv/0211xzDffff3/APzdjxgzmz59P06ZNdXNnYclGnfI68+OPP2Kz2WjevDmbN2+mpKQEm82G0+nE7XZjNptxOp2+s7Hq+hlHp/9u+s9//sPAgQMBWLZsGbNnz6ZRo0b07NkTOPmIqISEBC3HDSqXy8W6devYsWOH3wcMXbt2pV+/fpxzzjmkpaXprlj+kWxatWqly2zcbjejR4/2vYZERkZisVh8/1QOKCiXhumFy+Vi9+7d/Pzzz76vXXTRRQwdOtT3qDA4ebNLPX0Q8VdJ6RbAyevAlB+Y06/7CQ0NJS4ujnvuuYcvvviCrKws3RRLhWTjT8ngscceIysri5EjR9K+fXvKy8spLy/HarVSWFhISEgIGRkZunwxtlqtvrsIu91uWrRowYQJE5gwYQLnnHMOffv25cSJE7q66/2JEye4/PLLgZNHMZWfJ4PBgNfrZeTIkfTu3dvvkXx6INn4U15nXnzxRZ577jnGjx9Pw4YNqays9N0DoKysjKqqKgYPHuw7BbSul25FSUkJPXr0AE6+xtx+++0UFxfz8ssvM3v2bFJTU7Farb6bn+rhKLfVaiUiIsJ3ZsSZIiMjueaaa5g6daoG02lLslFXUVFBeHg4ubm5XHzxxeTm5lJUVER2djYOhwOPx4Pdbic8PFw3ry9wcmeioqIoLCwkMTERr9dL165deeaZZ3jsscdYu3ZttceHiZrV/VdgUSPlh6R37958/fXX/PbbbwF/cHJycsjPz9fNERaQbP6IlJQUrrzyStauXYvdbsdisZCcnEyrVq3o0aMH3bp1w+FwEB0drfWoQXP63nz11VccPHiQkJAQvF4vPXr04O6772bq1KkUFRVht9upX7++xhOffcqb/Z49e/Lqq6/6ntMdEhKC0WjEYDBgNBopKCjA5XLp6kiCZFO7Ll260Lt3b3Jzc7FYLCQkJNCgQQNatmxJ586dfR92Kjf4qetO35lp06b5XmPcbjfDhw+nUaNGvPzyy8DJuzHr6UPP8PBw+vXrx9ixY/nll1/Izs723bxS8e6779KyZUsNp9SGZKPOYrEwduxYkpKSGDVqFFOmTGHOnDm88847LFiwgHfffZcxY8YQFRWl9ahBFRERwWWXXcYTTzzBsWPHfO9vrr76agYMGMD999/PoUOHdHmDz79CrukWwMlP+R599FF+/vln2rVrR2pqKtHR0RgMBgoLC/n11199p1jrqUCBZFMTtWsEvV4vXq/Xdz3mqFGjdPeiXFlZyYMPPkh4eDjTpk3z/bL2eDzMnDmT9PR0Nm3axBdffKGbNzmHDh3imWeewWAw0LhxY1q2bEm9evUICQnhxIkTvPfee/Tr14/x48drPWrQSTa1y83NDXiEzm63M27cuDp/b4QzZWZmMmHCBPr27cvgwYN9X3e5XNx7771ERESwdetWdu/eraujUAcPHuS5554jJyeH5ORkoqKiMJlMlJaW8vvvv9O4cWMmTZrke5ylnkg26mw2GytWrKBRo0b06dPH727mW7ZsYcSIEWzdulXDKYNv//79zJgxg759+3Lrrbf6cikrK2PWrFksXryYc845h5UrV2o96r+elG5RzU8//cSmTZs4duwYFRUVmEwmIiIiaNWqFbfffrvuPuU7nWQT2IkTJ/B4PDRs2FDrUf6VlNOyTud0Olm0aBHr16/n7bff1tVjSDIzM/nqq6/YunUrOTk5WK1WqqqqaNmyJTfddBM33XST6mOh6jrJRl1hYSEej6fajXpOf4yjzWbT5Wvw0aNHsdvttGnTptrXnU4nr776Kl9//TXff/+9RtMF3+kfBG/bto309HTy8vKoqqoiJiaGc845hw4dOujyzDTJRp3a48JOl5WVxcGDB+nTp09whvoXOH1nnE6n31Mk7HY7n376KYCu7t3zV0npFj4nTpzAYDCQkpJCZWUlDocDl8tFVFQUERERWo+nKckmsPT0dEaNGsVVV13F6NGjfdeh2mw2Ro0axe23384VV1yh9ZiaOXHiBF6vV/UxGllZWbp7rrDNZiMsLEyXz8mtjWQT2Pr160lLS+Oee+7hqquu8r1BPnLkCB988AEPPPCAbu+aq9xQ7vSbpSlnGnm9XkpLS3VzIzXl9092djbp6el07dqV5ORk3/etVqvvLDXl7u56IdmoU4plTk4OO3fu5LzzzvM9QQJO/oyFh4frKhM4tTPHjh1j165dnHfeedXer5z+QWdZWZnuzvT8K+SabgGcLE/33HMPixcvBiAsLIy4uDhiY2MZNWoUX3/9tcYTakeyCezEiRPMnDmT7t278/jjjwOnrmcODw+nbdu2zJkzh/z8fC3H1IyyN0uWLAFOHZWz2Wzcf//9rF27VneFe/369QwdOtT3M6PcgOXQoUO89NJLut0VkGzU7Nu3j6VLl5Kamuq7dlt5nYmMjGTv3r2MGzdOyxE1s27dOh588EHf6a7KtblHjx7l5Zdfpri4WDeFG07uRWlpKa+99hpvv/02J06cAE4eoQNYvnw5N9xwA3v37tVdgZJs1BmNRkpLS5k6dSrvvvsuZWVlwKlsPvroI26++Wb27dun5ZhBp+zMtGnTeOeddygtLQWq53LdddeRkZFBdHQ0cgy3dlK6RbXyNHr0aL/vn3POOcydO1eXb/okG3/KC2tGRgZWq5UXXnjB73pBk8nEsGHDaNmyJcuWLdNiTE3V9oFE+/btmTNnDnl5eVqOGVRKeWratKnvjsvKaWtRUVHs27dPt+VJsvGnvM5s3LiRyMhIpkyZQr169XxHpbxeL8nJyUycOJGwsDBWr15d7X9X1+3bt49ly5bRpEkTunfvDpx6jYmIiGDv3r088cQTWo4YVMpjKhcvXkxZWRkTJ06kY8eOAL5TYgcPHkxqairvv/++rp4CINmoOzObZ555hvbt2wOnshkyZAhNmjThvffe0002gXI577zzgOq5NG3alEWLFlFeXq6r+0b8VVK6deyPlCez2czw4cN1V54km9odPXqUuLg4jEaj76icwuPxYLFYOP/880lPT9dowuD7Mx9ItGrVig8//FCLMYNKypM6yUad8nc8cOAAzZs3B07eIEz5MMJgMOB2u2nVqhWJiYkcPHgQOPVmsa6SnanZ999/z8CBA1VvBPb4449z9OhRfv/99yBPpj3JRl1t2YwaNUqX2fyRXLKysjhw4ECQJ/vfJKVbSHmqgWSjzmaz+Z6Le+bpaMppjkVFRbq8zkf25hQpT+okm9o5HA7fo8CUZ5YrlP9eUFCgm9cZ2Zma5eXl+XI584MGt9tNy5YtycvL0+VROclGnWQTmOTyz5LSLaQ81UCyUde+fXuKiop81xMqN+6pqqoiNDSUsrIy9u/f7ztVS09kb/xJeVIn2fhT3sR17tyZTZs2UVJSgslkqvbmzmQykZ+fT2lpqe+NoV7e/MnOVKd86BAXF0dRURHgvwvKv5eXl+vq2eWSjTrJJjDJ5eyQ0i2kPNVAsvGnvNBeeumlxMTE8MILL7Bp0yYMBgMGgwGTycSePXsYOnQoJpNJl3cvl705RcqTOslGnfJ3vP322ykuLmbcuHFs2rSJ48ePU1xcTElJCbm5uYwdO5bGjRv7Tn9U3izWVbIzNbv55puZNWsWmZmZft8zGo18+umnNGrUSJclQbJRJ9kEJrn8s+SRYQI4eS3P77//zrhx4+jVq5fv63v27OH555+nQYMGjB49msaNG2s4pTYkG3VlZWVMmjSJ1atXEx4ejsViwWg0UlFRQefOnRk1ahRt27bVekxNyN5UZ7PZuO+++0hISOCee+6hRYsWhIeHYzAYqKysZNy4cSQlJTFu3Dhd3XEZJJva7Ny5k9dee43y8nKSkpKIioqivLycH3/8kdatWzNnzhxSUlK0HjOoZGcCKyws5JFHHsFsNnP77bdz3nnnERsbS1FREUuWLOGTTz5hxowZXHrppVqPGnSSjTrJJjDJ5Z8lpVsAUp5qItnULisri507d1JYWEhoaCipqal06tTJd4q1Hsne+JPypE6yqVlubi7fffcde/bswWq1Ehsby8UXX0zfvn3r/NFtNbIzgSmPTNu+fTtOpxOXy0VVVRWtWrVixIgR9OvXT+sRNSPZqJNsApNc/jlSukU1Up7USTaBeb1e36mLyh10xSmyN9VJeVIn2QR2+muMqE52Rl1ubi6ZmZl4vV7q169P/fr1CQsL03qsfwXJRp1kE5jk8vdJ6RY+Up7USTZ/jrxJPkn2pjrZC3WSTc2U+yIoOSn/0TPZmcCUPdH7620gko06ySYwyeWfI6Vb1Eh+qauTbGrndrsJCQnReox/Fb3vjZQndZJNzZS3K5LJKbIzgen9dbYmko06ySYwyeWfIR9biBopP2TKY47EKZLNSTabze9ryptjKdz+9L43SikwGo0YjUb5RX4ayaZmUij9yc5UpzyP/MzHqCmUDyj0SLJRJ9kEJrn8s6R0Cx8pT+okm+qqqqoA+PHHHxkzZozf9w0GA7m5uaxZsybYo/2ryN4EJuVJnWQTmNPp5Pfff+f3338nJyeH0tJSXC6X1mP9K8jOnGI0Gtm/fz9FRUV+mZx+NoAeSTbqJJvAJJd/lpRunZPypE6yUWe321m9ejVpaWn89ttvrFy5kk8//ZRvvvmGzZs3s2fPHubOncvChQu1HjXoZG9qJuVJnWRTnfIB1YEDB5g4cSJPP/00kydPZsKECTz55JM89dRTPPbYY3z33XfV/ryeyM6cUlRUhN1u57nnnuOHH34AoLKystplCbNnz+bXX3/VckxNSDbqJJvAJJd/nknrAYS27HY7P/zwA5988gmHDx9m5cqVVFVVER0djcViISYmhg8//JD9+/czYMAArccNKslGXUhICKGhoRQUFGC1Wpk7dy52ux3Ad4pjaGgogwYN0njS4JO98ad8In7gwAHS0tI4ePAgUVFRGAwGwsLCsFgsVFZWcvPNN9OnTx9dXT8m2ahzu92YTCbee+89fvvtN/r160d4eDglJSXYbDYcDgf5+fm+P6+XbGRnAjtx4gQrVqxg165dhIWFsW/fPiIiIoiJiSE2NhaDwcCbb75Jt27dtB416CQbdZJNYJLLP09Kt85JeVIn2aizWCz069cPo9FIZWUlV199NWVlZZSVlWG32ykpKQGgdevW2g6qAdkbf1Ke1Ek2tdu9ezfDhg3jyiuv9Puex+PxHXnRy911ZWcCS0lJoXv37qxYsQKz2UxGRgbFxcU4HA7f84Uvv/xy2rRpo/WoQSfZqJNsApNc/nlSunVOypM6yaZmbrebyy+/nA0bNnD48GGaN29OdHQ0W7Zswel0cskll2g9oiZkb9RJeVIn2fgzmU6+RXnggQcoLy/H6XRiNpur/Rk95XEm2ZnqEhMTueKKKygvL+eGG26o9j2Px4PNZsPr9RIbG6vRhNqRbNRJNoFJLv88fbwSixop5SkqKorDhw8THR1Nw4YNKSgowG63061bN2JiYrQeUxOSTWAej4eQkBAWL17M+PHjOXHiBACPPvooM2bMYO7cubz55pu+I7x6I3tTXaDydCaj0ajLm8tJNuqU0rh9+3ZeffVVnn/+eVauXMm2bds4fPgwVqtV4wm1ITujzuPx0L17d6ZMmYLX68XtdrN9+3Zee+01fvzxR10XBMlGnWQTmOTyz5Ij3Tp3enl66623ePnll2nevDmPPvooBQUFAGRkZHD33XcTGRmp8bTBJdmoU94ML126lGeffZaePXuydu1ajh07xsiRI0lOTmb8+PFceumlnHvuuRpPG1yyN/6U01u3b9/OmjVr+OWXX7jggguoX78+9erVIzExUVcfQpxOslGnnBJtt9vp2LEjO3fu5Ntvv6W8vJyKigoMBgMej4ft27cTFRWl8bTBIzujrqysjJEjR9K4cWMMBgM7duxgyJAhXHvttSxYsIDy8nIGDhyo9ZiakGzUSTaBSS7/LCndOiflSZ1ko045XbG4uJhWrVoB8Pnnn9OzZ0969uyJ2WwmNzfX71RQPZC98SflSZ1kU7tnnnmGiIgIAN81yxUVFRQXF1NSUqK7XGRn/CkfRGRlZVFQUMCHH35IUVERy5cv58Ybb+T5559n7dq1pKWl6a4kSDbqJJvAJJezQ0q3zkl5UifZqFPe9LVp04YdO3ZQXl7Ot99+y4cffojZbMbj8eByuahXr57Gkwaf7I06KU/qJBt1ERERrF+/nmPHjhESEkL9+vVp3749HTp00MUNwtTIzpyilIT8/HzfKa8HDhwgIyODsWPHAhAeHo7NZtNyTE1INuokm8Akl7NDSrfOSXlSJ9nU7uGHH2b48OFUVFQwdOhQ2rVrR0lJCcOHD+eyyy4jOjpa6xGDTvZGnZQndZJNYHa7nffff58PPviA8PBwKisrKS0txePxcPfdd/P444/r5u7cZ5KdOUX5+yYlJZGSksKCBQvYvXs38fHxXHjhhQDs37+f1NRULcfUhGSjTrIJTHI5O6R0C0DKU00kG3XdunVj+/btWK1W3zWERqORCy+8UFePxApE9qY6KU/qJBt/yt93z549rFmzhnHjxnHJJZdgMpmoqKjgm2++YeHChTRr1oybb75Zd/nIzlSn/D3PO+88+vbty6xZs+jevTtPPfUUAPPmzWPFihVMnDhRyzE1Idmok2wCk1zODoNXuQBRCKhWnqxWK++88w6DBg0iJSVF48m0J9n4s9lsZGRksHHjRpKTk7nrrrvwer1UVFT4TnvUO73vjfLGf9u2bUyePJnhw4cHLE/33HOP7sqTZKPO4/FgNBp5//332bJlC7Nnz6aqqsp3+YbRaGThwoXs2LGDGTNm4Ha7dXG3btmZP668vJzIyEgMBgO//fYbDoeDTp06aT3Wv4Jko06yCUxy+fvkSLcAApen6Ohohg0bpvvyJNkEVlpayuzZs1m5ciVJSUm+bFavXs3KlSuZOnWqrq4pPJPszUnKm/7ffvuNRo0aceWVV/rKU2RkJDfeeCOlpaVs2LCBm2++2Xf3dz2QbGpnMBh8z7k/8+yQvLw8Xx56OX4gO1O7tWvXsnPnTvbs2cOIESPo0qULkZGRNGvWTOvRNCfZqJNsApNc/jnynG5BaWkpM2bM4JFHHmHdunWsW7cOgNWrVzNq1Chd3yhBsvHn8XgAWLduHb/99htbt27lrrvu8n2/RYsWuFwuVq5cqdWImpO98Xd6eTKZTBiNRt9RSz2Wp9NJNv6Uv3/fvn2xWCyMGDGCTz75hC1btrB//37eeecdtmzZwqWXXgqgu6O5sjPVKb+XVq1axcyZMykqKmLbtm3Y7XYARo4cyeuvv47b7dZyTE1INuokm8Akl7NDSreOSXlSJ9moU97E/fbbb76baBw5csR3unTbtm1p3749u3bt0mxGrcje+JPypE6yqV39+vW57777CA8P5/XXX2fMmDHceuutLFy4kHvvvZfrrrsOQDdHc2VnajZ37lxGjRrF5MmTSUlJIS4uDoAJEybwww8/kJOTo+2AGpJs1Ek2gUku/yw5vVzH/kx50ttNsSQbdcqbuKioKPLz8wEoKiqqdhfLI0eO0LJlS03m05LsjTqlPKWlpfH666/jdDqxWq3Ex8czevRo3ZWn00k2gSnXdZ9//vmcf/75OJ1OCgsLCQsLIyEhQevxNCU7U53yeyk/P5/OnTsDJ884Ul57O3XqRFZWFqGhoVqNqBnJRp1kE5jkcnZI6dYxKU/qJBt1ypGWG2+8kTFjxvDf//6X9PR0QkNDWbNmDYsXL8btdnP//fdrPGnwyd4EJuVJnWTjr1+/fsyfP5+WLVvy9NNP43A4aNq0KY0aNaJ+/fokJCRQWVlJfHw84eHhWo8bdLIz/pTX3vPPP58PPviAQYMG4XK5fPcVSU9Px2QykZiYqOWYmpBs1Ek2gUkuZ4eUbh2T8qROsqld48aNmTVrFmlpaYSGhvLzzz9z4MABWrZsycCBA+nYsaPWIwad7E11Up7USTbqRo0aRePGjQEwmUwUFxeTmZlJcXExVqsVp9OJx+OhqqqKTZs26eaNn+xM7YYNG8bzzz/P4cOH8Xg8vPPOOxw8eJBvv/2WBx98UNdH5iQbdZJNYJLLP0seGSaAkzdeSUtLY/PmzVRWVhIbG+srT+eff77urgs7nWRTnXL33GXLlnHdddcRGRlJVVUVVqsVu91OYmKiru7OrUb25uSN4y6//HLCwsKYOHEiR48exWq16r48gWTzZzkcDpxOJwaDgYKCApxOJ6WlpVxwwQW6+FkC2Zk/6uDBg3zwwQdkZ2djtVpp0qQJV199NRdddBEmk76PNUk26iSbwCSXf46Ubh2T8qROsqldv379ePvtt2natKnf91wuly4/AZW9qZ2UJ3WSjb9du3axfPlyJk2aBMC2bdvYu3cvHTp0oGvXrtoO9y8gO3PqdXfPnj18+eWXjB49WuuR/jUkG3WSTWCSy9kjdy/XMeUX8ttvv+27/tRkMpGQkEDjxo2JiIjA5XJpOaJmJJva3XjjjcyYMYM9e/ZQVVVV7Xt6LNwge1OTXbt2MWnSJCIiIoiNjWX//v1s3LgRu91Ojx49dFMQApFsAsvLy2Pq1Km+15cffviBBx54gDVr1jBmzBjWr1+v8YTakZ3xZ7fbyczMxOl0+n3P6/X6ni6hR5KNOskmMMnlnyelW0h5qoFkE5jNZmPBggX8+OOPDBw4kM6dO9OtWzcuv/xybr75Zh577DGtR9SU7E11Up7USTb+lBPwDh48SF5eHi+88AK5ubmsWLGCm266iaVLl/Lf//6XRYsWAejuzZ/sTHXKvlRVVZGdnc2oUaNYt24d+/fvp6ioCLfbjcFgwGg06ua55QrJRp1kE5jkcvbIyfg6p5Qno9HImjVrMJlMhIeHExsbS2xsrO9mWXok2aiLiIjg448/xu12U1ZWRklJCYWFheTn55Obm6ubx9QEIntzinKamlKeFi1aVK08TZo0iTVr1rBo0SL69u3ruyuzHkg26pRscnNzSUpKAk4W8CNHjvDEE0/4/ozD4QDQTTayM4EpR/Xz8vJwuVxkZmYyduxYKisrqaqqwmw243Q6GTNmDA888IDG0waXZKNOsglMcjl7pHTrnJQndZKNupCQEJo2bYrNZqNevXpUVFSQmJjoy0Svp1CD7M3ppDypk2zUKW/6GjZsiMFgYM6cOfz66680aNCAbt264Xa72bdvH02aNNF40uCSnQlM2ZerrrqKfv36YTabsdlsWK1WysrKsFqtHDt2TJdP1JBs1Ek2gUkuZ4+Ubp2T8qROslHndrv56quvmD59OllZWZhMJsxmM506deKxxx6jS5cuvjeIeiN7c4qUJ3WSjTolmy5dunDTTTexfPlymjZtyl133QXA4sWL2bJlCyNHjgTQRbEE2ZmaeL1ewsLCOHbsGIcOHaKyshKLxULr1q1p37691uNpSrJRJ9kEJrmcHXL3cp2T8qROsvGnHDlZu3Yt8+fP55JLLuGqq66isrKSEydOsHz5cqqqqpgwYULAu5rrgeyNP5fLxapVq6qVp7Zt27JgwQK++OILRo4cSa9evXRzZO50kk1gp/+MFBYWEhcXR0hICF6vl3Xr1lGvXj06d+6s7ZAakZ0JbM+ePbz22mvs27ePqqoqnE4nFRUV9O7dm8mTJ/vODtAjyUadZBOY5PLPk9KtU1Ke1Ek26txuNyEhIUyaNInY2FhGjRpV7fvFxcWMHz+eCy+8kHvuuUejKbUhexOYlCd1kk3NsrOzee2117j//vtp374927dv54cffiAyMpKbbrpJl2/6ZGf8KZncf//9NGrUiLvvvpvGjRtTWVlJZmYmr776Ki1btuTJJ5/U3SMbJRt1kk1gksvZI6eX65TyWcumTZvo3bs3I0aM8H2vY8eOdO/enfHjx7NhwwbdlSfJpnY2m42EhATfv3u9XqqqqoiPjyckJISKigoNp9OG7E1gBoOhWnlKTEysVp46deqk9YiakWwCUz7AmjlzJg6Hg/j4eMrLy3nyySdp0aIFpaWlHDx4kHHjxhEfH6/1uEElO+NP+RAiPT2dtLQ04OTrcWRkJPHx8cyYMYNBgwbp7i73INnURLIJTHI5e/Rz3pEIyGazVbuxk9frxeVy6bo8KSQbf8qL8SWXXMLmzZv55ptvfF8PDQ1l48aN/Pbbb7Ru3VrLMTUle3OK8ks5UHnat28f69evZ+rUqRQXF2s8afBJNrXbuHEjTz31FA0bNmThwoU0bdqU2bNns3TpUnbu3InNZtN6xKCSnVHndDqJiopix44dANUu37FYLOTn52OxWLQaT1OSjTrJJjDJ5eyQI906dXp5+uCDD2jXrh1XXHGFX3kaOHCgxpMGn2SjTrk+8Prrr2ffvn2MGjWKkJAQoqKifM9ufOCBB+jevbvGkwaf7I26jRs38tFHH9GwYUPmzp3rK09ms5n+/ftjs9l0d8RSIdn4U15nDAYD9erVA+Dzzz9n6NChvp+z/Px8YmNjNZtRS7Iz/oxGI7fddhvPP/88Q4YMoVWrViQmJmK1Wpk3bx5du3bVekTNSDbqJJvAJJezQ0q3Tkl5UifZqMvLyyM5ORmAJ554giFDhrBnzx4KCwsxm800a9aMtm3bEhoaqvGkwSd740/KkzrJpmYej4cBAwbw6KOPkpycjMPhoH///hiNRrZu3Up8fDwxMTFajxlUsjPqTCYTgwYNIisri1deeQWXy4XD4cDlcnHppZcyefJkrUfUjGSjTrIJTHI5O+RGajp1enlS/l3K00mSjbpRo0Yxbdo0jEaj77rLnTt30rFjR13dJTcQ2ZvAPB4PL7zwApmZmSQnJ/Pjjz/yxRdfEBERwfbt23nqqadYt26d1mNqQrKpWVZWFm+99RYVFRXcdtttdOvWjaysLIYMGcKQIUN8jxDTE9mZ6oqLiyktLaVZs2a+rxUVFZGdnQ1A/fr1q70u64lko06yCUxyObv0/S5ZxyZPnuy7Pszj8ZCcnEx8fDw333wz119/PR07dtRdOVBINoHZbDbWrFmD0WjE6/ViNBqpqKhg8ODBvsKt58/wZG8CMxqNDBkyhEaNGuF2u5k2bRpRUVHk5OTw9NNPc99992k9omYkm5qlpqby3HPPMWrUKNq2bQtAUlISU6ZM0WXhBtkZhfK75ptvvuG9994DTl6HCieP9jscDjp27KjLgiDZqJNsApNcgkNOL9chpTxNnz7drzzt3LkTQHfPEVZINuoKCwt9pzR6PB5CQkIoLCwkLCzM9zW9Hu2WvamZUp6OHz/uOyVYKU/dunXTeDptSTbqtm3bxnvvvUdBQQHdunVj1KhRlJSU6O565TPJzpyyd+9eKisrgVM3mlu6dCklJSX06NGDqqoqTCZ9vtWVbNRJNoFJLmeXPt8h69yZ5Un52unlSa/lQLJRl5eX53smY1VVle9ryh0s9XyUW/amZtu2bWPEiBGMGTOGN998E0DK0/8n2VSn/Pzs3LmTN954w/cc6oyMDABWrFjBQw89RF5enpZjakp25tTvm6KiIpo0aQKcupmlzWajRYsW1b6mJ5KNOskmMMklOKR065CUJ3WSjT/l73z8+HEaNmwI4CuT+fn5xMXFAeB2u3G73bp8dqPsjT8pT+okG3XKz8p3331HVFQUzz77LL179/bdHGzIkCF07dqVDz/8EEA3rzeyM9Upe5Kfn+/7wNPtdgNw4sQJ32mweiwJko06ySYwySU4pHTriJQndZJN7fLy8sjMzGT37t1s376dyspKDh8+7HsxNpvNhISE+K751gPZG3VSntRJNrXLzs6mZcuWwMlTHpWjL2azGa/X67veUG+vNbIzJylv/ouKiujYsSMAkZGRAFitVpo2ber7c3rZEYVko06yCUxyCQ45MV+HTi9PlZWVdOjQwa88KfR2Hapkoy45ORmj0ciYMWOw2+243W4qKirweDxcc801WCwWvF4vQ4YM4eqrr9Z63KCSvVF3Znlq1aoVoN/ydDrJxp/ys9G5c2dWrVrFgAEDKCws9F2rvHv3bo4cOcJFF11U7c/rhexMdUVFRbz66qt07NiR6OhoWrduzdGjR7Hb7TidTkJDQ3W3IwrJRp1kE5jkcnZJ6dYhKU/qJBt/ygvsJZdcQrt27aiqqqK8vByHw0FZWRk2m43S0lLKyso4cuSI75RqPZG98SflSZ1ko065GePNN9/MiRMnePnll/nll18AOHz4MOvWraNHjx6+bPRy80bZmeqU/98HDhzI0aNH2bZtGzabjfLycuLj4xk/fjxer5fQ0FBcLhdfffWVbp7rLtmok2wCk1yCQ0q3jkh5UifZ1C4uLs53uvSZlNOnnU6n7/RqPZC9USflSZ1kU7vIyEiGDRvGunXraNu2LUeOHCE/P5+RI0dyzTXX6O7xe7IzgT388MNUVVXhdDqprKykoqKC8vJy3z9tNhslJSVER0drPWrQSTbqJJvAJJezy+DVyzlI4g85szzp7Y1NTSQb/Z0a/U+QvYGysjLWrVvHoUOHOHLkCAB9+/bVZXk6k2QT2K233spHH32k9Rj/SrIzf46eH2dZG8lGnWQTmOTy10np1ikpT+okG/FXyN4EJuVJnWSj7rHHHuP222+nV69eWo/yryI7o055Oyuvw/4kG3WSTWCSyz9PTi/XKfkhUifZ/Hler9f3Aq3XT0BlbwJr0KABmzZtkvIUgGQTmN1uJycnh+eee45evXrRokULUlJSSEpKIjExkfj4eKKiorQeUxOyM0II8b9JjnSLaqQ8qZNsTikuLiYkJITIyEhMJvnsriZ63hu73c5//vMfysrKpDydQbJRV1xczMsvv4zdbiczMxO3243D4cDpdFJeXk7Tpk359NNPdXeao+yMEEL875LSrXNSntRJNv6qqqr4+uuvWbFiBZWVlRgMBiIiIrBYLERGRpKcnMwjjzyi9Ziakr05RcqTOsmmdl6vF5fLhd1up7y8nLKyMoqLizGZTHTv3l13l3TIzqgrKiri8OHDhIaGEhMTQ0xMDFFRUdUe16hXko06ySYwyeXskNKtU1Ke1Ek2/pQ3twcPHuTWW2/lsssuo2nTplRUVGC323E4HJSWlpKQkMBLL72k9biakL1RJ+VJnWRzivJ33bVrF8uXL+e8884jMjKS6OhoEhISSEhIwGKxEB4erus3f7IzJykfLvz888+kpaWRk5NDeHg4Xq+XkJAQIiIiKC8vZ8iQIfTv31/rcYNKslEn2QQmuZx9+j4Mo0PKL+PMzEzGjx/PZZddRvv27auVp9zcXFwul9ajBp1kU7vs7GyaN2/OtGnTfF+rqqryPU7C4/FoOJ02ZG/8/ZHy1LBhQ8LDwwF9XQ8v2dTO6/WyefNmPv74Yxo1akR0dLTvUXwxMTEkJCTQqVMnbrvtNho1alTnS6bsTGBKSVi0aBEOh4N7770Xg8Hge76ww+EgJyeHxMRErUcNOslGnWQTmORy9knp1ikpT+okG3/Km7hmzZrRu3dvMjIyaNGiBSaTCZPJRFRUlO9awrr+BliN7I0/KU/qJBt/yt/RarXSpUsXbrnlFtq1a0dVVRU5OTmsXr0ai8VC586dWbFiBTt37uSll16iUaNGWo8eFLIz1Sl/t6KiIh544AEuvfRSvz+jx9ddkGxqItkEJrmcfVK6dUbKkzrJpnYNGjTwfQLav39/EhMTsVgsREdHYzQa6dChA23atNF6zKCSvfEn5UmdZKNOOdLy2Wef0aVLF+6+++5q37/uuuuYPn06F1xwAcOHD2fIkCFs27atzmcjOxNYSEgIAOPGjeO7774jPDyc1NRUwsPDCQ8PJywszPdn9EayUSfZBCa5nH1SunVKypM6ycaf2+0mJCSEN998kzVr1tC6dWuOHTvG77//TmVlJQB5eXmMGDFCd9koZG9OkfKkTrJRp3wgVVBQQFFRkd/3o6Ki+PXXX+nYsSNdu3bF4XAEe0RNyM7UrKCggHnz5tGgQQNat25NVFSU77U3JCSE+++/H4vFovWYmpBs1Ek2gUkuZ4+Ubp2R8qROslGn3G9x+/btPPjgg9xzzz2+7zkcDqxWK4WFhSQlJWk1omZkb/xJeVIn2ahTjqLcc889PPfccxw4cIAePXpQr1494uPj2b59OyUlJXTo0IH09HSKi4tp3ry5xlOffbIzNXvqqae48cYbSUxMxGq1UlpaSnZ2Nna7HavVyrBhw7QeUTOSjTrJJjDJ5eyR0q0zUp7USTbqlDfDffr0ITIykoqKCsxmM0ajkYiICCIiIkhJSdF4Sm3I3viT8qROsqldnz59KCkpYc2aNaxcuRKn00lJSQnl5eWMGjWKFi1acPnll3PrrbfSrl07rcc962Rn1NntdpxOJy+88ELA7zudTt3e6V6yUSfZBCa5nF1SunVGypM6yUadck2h2+1m+vTp/PTTT/Ts2ZOEhASio6OxWCyEhoaSmpqqu2dTy96ok/KkTrJRZzKZuOWWW7j55ptJT0+noqKCiIgIOnTo4Pszy5cvJzIyktDQUA0nDS7ZGX9Go5GhQ4fy9ddf061bNywWCyEhIRiNRoxGo64LgmSjTrIJTHI5u+Q53TqjXBuWlpbGu+++S69evaQ8/X+STe2GDx9OXl4eJSUlFBcXU1FRgdfrxWw243Q6+eGHH6hXr57WYwaV7E3tPB6PankqLS3VXXk6nWTjr7KykjfeeIP9+/fTokULRo8ejcfj4fDhw7Rs2VLr8TQnO3Pqg+BDhw7xn//8h4KCAvr27Uvjxo2pV68e9erVIyYmhgYNGtC+fXutxw0qyUadZBOY5BIcUrp1SsqTOsnmz7FarZw4cYL8/Hx69uyJ0WjUeiRNyN74k/KkTrIJzGaz8dprr3HgwAGSkpL48ssv2bNnD3v27GHEiBEsWLCAxo0baz2mJmRn/OXk5LBo0SIMBgOZmZkUFBRQUFCAzWbDarXSo0cPFi5cqPWYmpBs1Ek2gUkuZ5eUbuEj5UmdZHNSZWUlv/zyCwcPHsTpdJKQkECXLl1o2rSp1qP9K+l5b6Q8qZNs/ClHWnbt2sUTTzzBhx9+SF5eHkOHDmX9+vXYbDZmz55NXl4e06dP951hoheyM3+NXIOqTrJRJ9kEJrn8Pfo811GolqfWrVvTunVrrcfTlGQTmNPp5KOPPmLatGlERUURGhpKeXk5VquV/v37M336dF08g1qN7M1Jp5+mtnnzZl952rFjBwBNmzbl8ssvZ9q0aborT5KNOiWb7OxsYmJiiI6OZv369cTFxQEn79B90UUXMXPmTN+f1wPZmdpt3ryZL774AqvVSmRkJA0aNOCyyy6jY8eOui8Iko06ySYwyeXskdKtQ1Ke1Ek2/pQ3ffv37+f9999n7ty59OzZ0/f9n376iVmzZrFkyRLuuusuDSfVjuzNKVKe1Ek26pSfD4vFgtPpJDs7G6fT6XvWdElJCRs3bvQdzdVLNrIzgSkfLmzevJm5c+dSUVFBgwYNyM/PZ8eOHcybN49x48YxePBgrUcNOslGnWQTmOQSHFK6dUTKkzrJRp2STWZmJsnJyfTs2ROXy4XJZMJgMHDBBRdw9dVXs27dOt1mI3tzipQndZKNOiWbXr16kZGRwWOPPUZeXh6RkZEsWrSI77//nrKyMh599FEA3RzNlZ0JTPl7Ll68mE6dOjFmzJhq3//qq69499136dq1Kx07dtRiRM1INuokm8Akl+DQx28tAZz6oTqzPClfV8rT+vXrtRxTE5JN7ZQ3f8ePHyc0NLTakduKioo6f7fcQGRv/J1enq666ioee+wxZs6cyf79+1m0aBGjR49m165d3HLLLYB+yhNINn+EyWTitttuY9CgQfTr148WLVqwfPlyoqOjeeaZZ7jkkksA/WQjO1OzrKws+vXrB4DL5cLtdlNVVUX//v2pqKigvLxc4wm1I9mok2wCk1zOLjnSrUOnl6cGDRpU+55ey5NCslHXtWtXvvrqK8aNG8fAgQNp2LAhYWFhfPPNN6xfv557771X6xE1I3vjTylPCQkJ7N27l+PHj7N8+XKaNWvGqFGjOO+88wD9lQSQbGoTHx/PrbfeCsiNexSyM9Upf89zzz2Xjz/+mOTkZN9rr8fjobS0lOLiYhITE7UcUxOSjTrJJjDJJTjk7uU6olyzcfz4cSZPnkxpaalqeVI+NdcLyeaPOXr0KDNmzGDTpk2Ul5fj8Xho06YNDzzwAFdccYXu3hzL3vxxUp7USTYnzZkzh5UrVwLQr18/HnnkESIiIoCTGX3//fd069aN+Ph4Lcf8V5CdOSkjI4NJkyYRHh5O27ZtSU5OpqqqilWrVtGyZUtefPFF3w7pjWSjTrIJTHI5u6R065SUJ3WSzSlZWVm4XC4sFgtGo5GYmBjCwsIAsNvtvk9Hw8PDtRzzX0H25hQpT+okm+qU+yLMnDmT7777jiuvvBKPx8Nnn33G/fffz2233cYvv/zCp59+yieffMLq1atp0aKF1mMHlexMzdLT0/nss8/IyMigoKAAk8nENddcw3333ef7faVXko06ySYwyeXskdKtE1Ke1Ek26oYPH87hw4dJTk4mPj6euLg4QkNDiY+PJyEhgbi4OOLi4nA6nXTr1o2oqCitRw4a2ZvqpDypk2xq17dvX6ZOnUrXrl0BWL16NfPmzePcc88lPT2d5ORkRo8eTadOnTSeNDhkZwJzOp388MMPxMTEEBkZidlsJikpidjYWK1H05xko06yCUxyCS4p3Toh5UmdZKNuw4YNZGZmUlJSQkZGBlu2bCEyMpKQkBDsdjtWq5Xo6GjCwsL45JNPSElJ0XrkoJG9CUzKkzrJJrCqqirOP/98fv3112pfb9OmDb179+bOO++kb9++Gk2nLdmZ6g4dOsTVV19NSkoKoaGhxMbG+m7smZycTGxsLPHx8cTHx5OcnMzVV1+t9chBI9mok2wCk1yCS0q3Tkh5UifZ/DHz588nOjq62qOvcnNzeeGFF2jTpg0PPvigrm4YJnvjT8qTOslGXW5uLtdccw3bt2/H5XIRGhpKQUEB/fv35+effwZO5mcy6ever7Iz/rxeLzk5OZSXl3PkyBFWrVrFgQMHaNeuHTabjby8PI4ePYrD4eCqq65i2rRpWo8cNJKNOskmMMkluPT1G0zHlMeswMnydNFFF6mWp4SEBC1G1Ixko87j8WAwGHA4HMyePZs9e/ZU+35KSgrjx49n5MiRPPLIIxpNqQ3ZG3+FhYW+D15OL08Wi4W3334b0Gd5AsmmJvn5+b4zQZSMHA5HtbND9JiL7Iw/g8Hge0Z5WVkZiYmJPPnkk9WeGrFv3z6WLl3K9ddfr9WYmpBs1Ek2gUkuwaWPZ0sIPB4PXq8Xu93O7Nmzq5UDOFWefvjhB10drQTJpiZGoxGDwYDT6aRRo0asWrWKkpISnE6n71nUdrud/fv3azxp8Mne+JPypE6y8ae8hpSUlFBSUsJnn33GBx98wPfff8/nn3+O0WikoKCAw4cPk5+fj8Ph0Hji4JKdCczlcgHw7bffYrVaqxUEr9dL27ZtSUlJ4bPPPtNoQu1INuokm8Akl+DR36u1Tik3dTq9PPXu3ZvIyEjf9Rt6LU+STe0sFguDBg1i2rRpbNy4kfr162OxWMjJyWHjxo1ce+21Wo8YdLI3pyg3fTq9PDkcDho2bEh6erqvPJWVlREVFUVUVJRuHjsi2ahTnm8fEhJCcnIyy5Yto7KykpCQEMrLy3G5XDz11FMYjUbcbjd9+vThrrvu8mVaV8nO1CwkJASApk2bsmHDBj755BMuuOAC32uv2+0mIyODZs2aaTuoBiQbdZJNYJJL8Ejp1hkpT+okG3WhoaEMGTKE5s2b89VXX7Ft2zZsNhtJSUncc889un4GteyNlKeaSDa169mzJ6tWraKsrIzS0lJKSkp8/z03NxebzUZWVpbvsXsej8f3RrEukp2pmfKB54ABAzh06BDvvfceX375JRaLhfDwcHbs2EFcXBwPPvigxpMGn2SjTrIJTHIJHrmRmk599913fPXVV2RmZvrK08UXX8wtt9yim7ssq5FsAvN6vZSXlxMeHh7wlEa9vOFTI3tzktPprLU89ezZk1tvvRW3212ny9OZJBvxZ8nO1MzlcvHzzz+za9cusrOzcTqdtGjRgptuuomkpCStx9OUZKNOsglMcjm7pHTrkJQndZJNdcrfNzc3l48++ohffvmF0NBQQkNDiYyMJCYmBo/Hw2WXXcbFF1+s9biakb0R4p+hvCU5/Z/Kz45yREYIRU5ODmVlZVgsFmJiYggLC8NgMGA0GnV5vfvpJBt1kk1gksvZJQnqhJQndZKNOuU0ziVLlrBq1Sp69uxJXFwcVqsVu91OXl4eOTk5dOvWTetRg072pmZSntRJNjUzGAzyIdUZZGf8LVmyhA8++ICIiAhCQkIICQnBbDYTGRmJ1+tlypQpREdHaz2mJiQbdZJNYJLL2SelWyekPKmTbNQpb/C2bdvGww8/zMCBA6t93+l0YrPZCA8P12I8Tcne1E7KkzrJxp+cEVIz2ZnqXn/9dW655RZatmyJw+HAarVSVlaGzWajtLRUl7+XFJKNOskmMMnl7JPSrRNSntRJNuqU04muu+66gI+9MpvNunkG9Zlkb9RJeVIn2agzGAysXr2aJk2a0K5dO4xGI8eOHcPj8ZCamqr1eJqRnfFns9mw2+2MGjVK61H+dSQbdZJNYJJLcOjznCQd+qPlKTIyMtijaU6yUacUy8rKSubPn8/rr79Oeno6ubm5VFZWajydtmRv1CnlKT09HY/HA8CxY8fIysrSeDLtSTbq5s+fz5w5cygvL/edMp2ens6LL77I0aNHNZ5OO7Iz/kJDQxk7diwbN27UepR/HclGnWQTmOQSHHKkWyeUT8orKytZvHgxWVlZXHrppSQlJREXF0dYWJjWI2pGslGnHF3ZvHkzYWFhLFmyhHnz5lFVVQVAREQELpeLDRs26O6It+yNuvnz5/P5558zceLEauXp008/5amnnqJJkyYaT6gdySYwm83GBx98wPTp02nTpo3v6x07duS7777jlVdeYc6cORpOqB3ZmVOU193s7GzS0tIoLy9nwIABNGnShAYNGpCcnExCQgJJSUm6+8BTslEn2QQmuQSX3L1cZ4YNG0ZeXh55eXmUlpZKeTqNZKOupKQEk8mE2+2mvLzc99iaoqIiKioquOGGG3R7+qPsTXU2m41rrrnGV56UX9THjx9n1qxZlJWV6bY8STbqMjMzufPOO9m0aZPf94qLi7n66qvZvHmz7k61lp0J7PDhw8yfPx+DwcDhw4cpLi7GarVSWVmJw+GgV69epKWlaT2mJiQbdZJNYJJLcEjp1hkpT+okm5oVFhZSWVlJVFQU4eHhmM1mAGbOnMmIESM0nk47sjfVSXlSJ9moy87OZtSoUfTu3ZvBgwcTGxsLQH5+PsuXL+frr7/mk08+0d2zqGVnAvN4PLjdbt+lPR6PB7vdjtPp5Pjx40RGRtK8eXONp9SGZKNOsglMcgkOOb1cZ+Li4qqVp3r16lUrT3r6pX0mySawqqoqvvvuO5YtW4bT6cTj8RAdHU1ERAQnTpwgJydH16Vb9qa6kJAQGjZsyMyZMwOWp4YNGwKn7v6uJ5KNusaNGzNy5EimTZvGmjVrSEpKIjY2FqvVSkVFBcOHDweQnyfZGbxeL0ajEYfDwfbt2wGIiYnBYrHQuHFjVq5cidls1mVJkGzUSTaBSS7BI0e6deSPlKdvv/1W6zE1Idn4U46cZGRkMHr0aLp3747b7WbNmjXccMMNbNiwgaZNm/LQQw9x/vnnaz2uJmRvAtu0aRPTpk3Dbrf7laehQ4fSr18/PB6PLp8vLNnU7ODBg/z8888cPnyYkpISEhISuPHGGznnnHN0dzRXITtzirID6enpzJ07lxMnTlBZWUlYWBhms5mqqioOHjzIE088wR133KH1uEEl2aiTbAKTXIJLSrcOSHlSJ9moU97EffrppyxfvpzFixezZs0aFi1axAcffEB6ejrvv/8+9957b7UbH+mB7E3tpDypk2xO+umnn4iOjqZt27bk5uZSVlZGcnIy0dHRfhnoKZdAZGdOUi4vGD16NACDBg3i6aefpmvXrjRq1IivvvqKO++8k4EDB/rOONILyUadZBOY5BJccnq5Dii/kPft20dCQgKTJk1izZo1HDhwgAkTJvjKk8Vi0XrUoJNs1Cmfx5WVlflOW8zKyiIuLg6A8847j1atWvHxxx8zfvx4rcbUhOxNdWrl6dZbb9V9eZJs1C1dupR27drRtm1bpk+fzmeffUZcXBwRERHExcURFxdHcnIyoaGh3HnnnbRr107rkYNCdqZ2u3fvZvr06bRv3x6DwcDdd99N+/btad68Oenp6dhsNl3dwPJ0ko06ySYwySU4pHTrgJQndZKNOiWP888/33cjoxYtWrB+/Xp2795Nhw4d2Lt3LzExMRpPGnyyN9VJeVIn2QTm9Xp57bXXfP/+7LPPMnjwYI4dO8aJEyfIzc0lNzeXgoICMjMzueqqqwB0cRq17Iw65f/76OhoduzYQfv27XG73ZSWlgJwzTXXMGPGDG699VbdlQTJRp1kE5jkElxSunVAypM6yaZmXq+X9u3bc+ONN2I0GunTpw+ffvopt99+O0ajkSZNmjBx4kStxww62ZtTpDypk2zUGQwGvF4vVVVVhIaG8sQTTzBhwoSAl6rYbDYiIiIA6nwusjM1U47qDx8+nOeff54bbriBSy+9lLfeest3Myir1UpiYqLGkwafZKNOsglMcgkuuaZbJ5RT0BYsWEDjxo3p06cPI0aM4Ntvv61Wnnr06KH1qEEn2QSmdtqi2+1m+/btlJWV0a5dO9/dc/VG9uaU08vTyJEjmTBhQsBf0kp50stdlkGyqc0vv/zCzp07mT59OsOGDaNFixaYzWYsFgsWiwWz2cyNN97IypUrOeecc7QeNyhkZ2rm9XqpqKjgm2++oX///mRnZ/PUU0+RlZWFw+Hg3nvv1e0TNSQbdZJNYJJL8Ejp1gEpT+okm8CUXHbs2MHPP/9MeHg4F1xwAa1bt6725/T2zFyF7I0/KU/qJBt1Bw4cYNmyZSxevJjWrVsTGhqK0WgkJCSE0NBQKioqKCsr4+OPPyYqKkrrcYNGdubP8Xg87N+/n5SUFDkN9gySjTrJJjDJ5eyQ08vruJrKU0hIiO8onNvt1njS4JNs1BkMBr7//ntmzJiByWTC4XCwYsUKpkyZQsuWLQH48ccfeeKJJ1i6dCmNGjXSeOLgkb0JLCYmhuPHj+Nyufj6668DlqdmzZrRoEEDrUcNOslGXatWrfjvf/9LUVER48aN4/jx4xQXF1NeXk5FRQU2m43zzz9fV4UbZGfUFBUV8dZbb7F3715atmzJHXfcQevWrTEajbRt25bS0lIOHTpEixYttB416CQbdZJNYJJLcMmRbh04szxFRERIefr/JBt1t912G5dddhk33HADTqeT1157DZPJxKBBg5g3bx47duzgjjvu4PHHHyc0NFTrcYNK9iYwp9PJk08+WWN5at++vdZjakKyqVlRURG//fYbF154IQCVlZXs27eP5s2bExsbq/F02pCdqe748eNMnTqVAwcO0KNHD3bt2kV8fDyvvvoqERER/PTTT6SlpVFeXs4HH3yg9bhBJdmok2wCk1yCT0q3Dkh5UifZBOZwOOjVqxe//PKL72tFRUX06tULi8VC//79+b//+z/q1auHyaS/E2Zkb9RJeVIn2QRWVFTE5MmTsdlszJs3j927dzNlyhSMRiMNGjTg8ccfp379+lqPqQnZmVNnF3322WesWLGCZ599ltTUVPbs2cP8+fMJDw8nMTGRlStX0qlTJ0aOHMm5556r9dhBIdmok2wCk1y0o793yzrjcDg4cOAAH374oe9rkyZNolevXmzYsIH+/fvz0ksv6bI8STbqCgoKfGXR6XRiNptxOp2EhYWxcuVK3Z3SeDrZG3Wnl6cLL7xQytNpJBt/yh2309PTycjIYOHChZSXl7NixQrcbjcTJkxg0aJFvPnmmzzzzDO6uUO3Qnamuv3799OoUSOaNGmCx+PhvPPO47zzzmPGjBlceeWVzJkzh65du2o9piYkG3WSTWCSS/Dp57eXTp1ZnpR/KuXpxRdfpH79+rorByDZ1CQvL893FMVsNgNQWFhIQkICDRo00N31yqeTvfHn8XgAfOXpxRdfrFaenn76aUwmE2+++Wa1P68Hkk3tsrKyqF+/PgkJCfz6668cOHCAYcOG0aZNG7p3787hw4eBk0do9EB2pjrl//fc3FySk5OBkx9+AmRmZnLHHXcwa9YsunbtqrvfTZKNOskmMMlFO1K66zgpT+okG3/Ki3FhYSEGgwGbzcbhw4dxuVwcPHjQdzOjuv4mryayN+qkPKmTbNQZDAZCQ0Ox2Wx88cUXhISE0KtXLwCOHDlCUlISoL9sZGdOUv5+eXl5NGnSBACLxeL7WocOHXx/Tm9P05Bs1Ek2gUku2tHPoRidUa7ZOL085efn07hxY7/ypLcfKsmmdqWlpWRmZvLwww/j9XpJTk7m6NGjWK1W3n//fbxeL2FhYbRp08b3Al3Xyd7UTsqTOsnGn3Kq+IABA9i1axcXXXQRbdq04eGHH8ZsNvPZZ5/xyy+/cOutt1b783ohO3OS8nhGm83GkSNHSE9Pp7KykhYtWnD8+HGMRiNer5fy8nLMZrPvw1A9kGzUSTaBSS7akdJdx0l5UifZ+FNejPv06cNrr71GUVER+fn5FBQU0KhRIywWC0uXLsXpdJKTk8OwYcN0k41C9saflCd1kk1gyr0iAOLj45kwYQJ33nkniYmJvjv+Z2Vl0adPH6699lpAP9nIzgQWGhrKkiVL+OKLLzAajcTGxnL48GE+/vhjtm3bRlRUFCEhITz44IPEx8drPW5QSTbqJJvAJJfgk7uX13H5+fls27atWnkqLy+npKSEwsLCauXp0Ucf1XrcoJJs/p7y8nLg1GlJeiF7U93p5QlO7sXBgwerlafZs2cTGRnJkCFDdFMQQLKpSVpaGldeeSWpqan88MMPuFwuUlJSMJlMGAwGwsPDiYmJwWQy6eo1RnZGncPhoLCwkKKiIgoKCigoKMBut5OVlUV+fj5Wq5WcnByWLVumu5Ig2aiTbAKTXIJPSrfQbXn6I/Scjcfj8Ttt0WAw+P4j1Olpb6Q8qZNs1A0fPpwnnniC5s2bc+edd/L7779jsVgIDQ0lMjISi8VCTEwMXq+X5557jpSUFK1HDgrZGSGEqJvk9HIdqK086fkXt2SjTk9HUP4s2ZtTtm3bRt++fQGYO3eulKfTSDbq5s+f7/vv77zzDqWlpRQXF/uOuChnkWRlZREZGanhpMElOyOEEHWTHOkWQgjxj6ioqKixPL388stER0drPaYmJJvqTn/m9tdff01CQgLx8fFER0djsVh09cGVGtkZIYSoO6R0CyGqOXHihO+OlSaTCaPR6PunEGeS8qROsqmd3W7nqquuwuPxUFZWRmxsLBaLhcjISJo2bUpERATnnHMOl19+OampqVqPe9bJzgghRN0kpbsOk/KkTrIJzOPx0KVLF6Kjo4mJiSE5OZnU1FTCwsKIj4+nXr16JCYmEhsbS1hYGB07dtR65KCSvVEn5UmdZKPO6XSyZMkSvvvuO+644w48Hg+5ubls2bKFnTt30rZtWw4ePEhoaChvvPEG55xzjtYjB4XsjBBC1C1SuusoKU/qJBt1Ho+HrVu3UlxczPHjx9m+fTsbNmygZcuWVFZWUlJSgs1mw+12ExcXx5YtW7QeOWhkb2om5UmdZONPee799u3bmTVrFmlpaYSGhvq+73A4WLp0KR06dKBbt2688MIL5OfnM3PmTA2nDh7ZGSGEqFukdNdRUp7USTZ/jMvlYvr06XTu3Jkrr7zS9/WtW7fy/vvvc+edd3LhhRdqOGFwyd4EJuVJnWSjTjmNevXq1SxYsIAPP/zQ78988803zJ07l+XLl/Pll1+SlpbGRx99pMG0wSM7I4QQdZPcvbyOMhqN9OzZEzhZngoLC7nppptUy5OeSDY1q6qqwmQysWnTJr7//nvGjh1b7fs9evSgoqKCBQsW6Kp0y94EppSEvLw8KioqqhUEgIiICFJTU3nppZdYvnw53bp1Iy0tTaNpg0uyUac8djA1NRW73c5DDz3ELbfcQr169TCbzeTk5LBkyRI6d+4MwJYtW2jWrJl2AweJ7IwQQtRNUrrrMClP6iQbdcr1yQaDAbfbzaZNm+jYsSPh4eEYDAZMJhPZ2dmUlZVpPGnwyd74k/KkTrJRZzAY8Hq9dOjQgbFjx/L222/zyiuvACefb19eXk7z5s0ZNmwYCxcuZOvWrbz88ssaT332yc4IIUTdJKW7DpPypE6yUadk06VLFy6++GKmTJlChw4dqFevHnFxcezdu5ddu3Zx7733ajxp8Mne+JPypE6yqZlSMC+55BK6d+9OYWEh+fn5lJSUYDKZ6NixI7GxsVx44YX07dtXFzcMk50RQoi6Sa7p1gGr1crs2bPZsmWLanm66667tB5TE5JNzYqLi1m7di0//fQTBw4coKysjCZNmnDXXXdxxRVXaD2eZmRv1DkcDtXytH//fiIjI3VRngKRbALLyMjg66+/Jjc3l/j4eFq0aMFFF11E/fr1tR5Nc7IzQghRN0jp1gkpT+okm5qVl5djt9tJSkrSepR/Fdkbf1Ke1Ek21SnXLq9bt44pU6bQoEEDkpOTKSsrw2q14na7efLJJ+nSpYvWo2pGdkYIIeoOKd06IuVJnWTjz2azMX/+fI4cOcLPP//MjBkz6NGjB2vXrqVjx46kpKRoPaLmZG+kPNVEslGnZHPLLbcwaNAgLrnkEsxmM5WVlRQWFpKWlobL5eLFF18kKipK63GDRnZGCCHqJrmmWwekPKmTbPwpj/J544032LlzJ7fccgubNm0iIiICgLS0NM4991zGjRvn+5reyN74mzdvHsOHDw9YnhYsWMA555yjq/J0OsnGn3I9d2ZmJgMGDCAyMtL3veTkZKZOncpFF12EXo8LyM4IIUTdYtR6AHH2eDweAF95uuKKK6ioqKhWnubOnYvD4dByTE1INuqUN8PLly/nueee48YbbyQ8PJz4+HgA5s6dy5YtWygtLdVyTE3I3vg7szwlJSURGxtLcnIybdu2ZerUqWzbtk2X5UmyqZnL5aJx48Zs27bN73vZ2dk4HA6io6M1mEw7sjNCCFE3Semuw6Q8qZNs1CnZuFwuEhISAKioqKBRo0YAJCQkkJubq7s3wyB7o0bKkzrJRl1oaChDhw5l+PDh/N///R9z5sxhwYIFzJo1izFjxnD99ddrPaImZGeEEKLukdPL6zApT+okm5q53W4GDBjApEmTGDRoEC6XC5vNRlVVFWvWrKFBgwZYLBatxww62ZvATi9PV1xxBeeeey5RUVFYrVZ+/PFH3ZYnkGxqc9VVVxEXF8fnn3/O7t27cTqdeDweOnfuzKOPPqr1eJqQnRFCiLpHSncdJ+VJnWSjLiQkhHvvvZdXXnmFuXPnEhERwcSJE/n999/Jy8tj6tSpWo+oGdmbwKQ8qZNs1BmNRi666CLatWtHSUkJFRUVJCcnk5CQoOtTqGVnhBCibpG7l+vAoUOHeOWVV3A4HOzdu5fevXtXK0+XXHKJ1iNqRrKp3WeffUZOTg5Wq5WWLVvSr18/4uLitB5LU7I36kpKSgKWJ+UsAT2TbP647du3k52dzY033qj1KJqSnRFCiLpBSreOSHlSJ9n427VrF7t372bgwIGEh4cDkJubS7169QgJCdF4un8H2ZvaSXlSJ9lU5/V6cblcmM1mJk+eTHZ2Nq+//jpVVVWYTHJiHsjOCCHE/yr5LaYDauVJb9edBiLZVKccQdm3bx+zZs0iMjKSyy+/nPr163P8+HEmTpxISEgIs2fP1vWbYNmbmp1entauXesrCVKeJJs/ymq10qpVKwDdH9WVnRFCiP99cvfyOko5gUEpT1u3bqWkpASA48ePM2HCBB599FGqqqo0nFIbko065ZFYX3zxBdHR0UyfPp369etTVVVFgwYNeOaZZ3C5XHz44YcaTxp8sjd/jZQndXrNxuVyBfw5MRgMmM1mAPLz80lOTvZ9XZyk150RQoj/dVK66ygpT+okm9rl5OTQsmVL32nkytGUxo0bExsbS2FhoZbjaUL2xp+UJ3WSjT/lZ2jZsmXMmzePpUuXsnLlStavX8/WrVvZtWsXGRkZOJ1Ojh07RsOGDTWeOLhkZ4QQou6S85LqOClP6iQbf0bjyc/h+vTpw3vvvUd8fDw9evQgIiKCqKgosrOzycrKom/fvhpPqh3Zm5PlyWg0smzZMoqLi0lKSsJisfj+ExERgdlspkWLFrorT5KNOqUk/vTTT2RkZOD1enE6ncDJJyaYzWYiIiKoV68ehw8fpkGDBtX+d3WV7IwQQtR9UrrrKClP6iQbdcqb2/79+3Po0CE+//xz1q1bR1hYGE6nk23btjFo0CBd3p1b9uYUKU/qJBt1yt9x1qxZvq/ZbDZKSkooKiqioKCAgoIC8vPzadasGampqdX+d3WV7IwQQtR9cvfyOq6yspJ58+axefNmLBaLX3l69NFHdXvzJ8mmdr/99hs7d+4kPz+f0NBQ+vbtS8uWLXX9Zk/2JjC18lRSUsLIkSN1+exyhWQj/izZGSGEqFukdOuElCd1kk1gP//8M263m/j4eGJiYoiMjMRkMhEREaH1aP8KsjdCCCGEEOKPkNKtA1Ke1Ek2gc2cOZM1a9YQExODy+XC6/ViNBoxm82EhoayePFirUfUlOyNEEIIIYT4o+Sa7jpOypM6ySawwsJCFi5cyH//+18aNmxIZWUl5eXl2Gw2ysrKfHcg1ivZGyGEEEII8WdI6a7DpDypk2zU5efnExcXx6BBg7Qe5V9H9kYIIYQQQvxZUrrrMClP6iQbdY0aNWLw4MF8//33XHzxxb67dgvZGyGEEEII8efJNd11WFlZGZ9++ilNmzaV8nQGycaf8qzYH3/8kZEjR+L1eunfvz8NGzYkKSmJ5ORkoqOjSU1NJSkpSetxNSF7I4QQQggh/iw50l0HKeVp9+7dzJkzR8rTaSQbdUqBjIqKYsCAAXi9Xg4fPsyOHTsoLS2loqICm83GoEGDmDhxosbTBpfsjRBCCCGE+KvkSHcdtmvXLj755BNfeSoqKtJ9eVJINn9Nbm4ukZGRunwONcjeCCGEEEKIP09Kt07pvTzVRM/ZeL1eDAYDBw8eZM+ePURHRxMTE0NMTAytWrViwoQJXHTRRVx99dVaj/qvo+e9EUIIIYQQ6uT08jqqtvI0e/Zs3ZYnySYwJZfvv/+et99+G5fLxdGjR4mJicFoNGK1WiktLeWaa67RelRNyN4IIYQQQoi/Qkp3HSTlSZ1ko07JZt68eXTt2pWbbrqJe++9l1tuuQWTycSqVat4+eWX6dmzp9ajBp3sjRBCCCGE+KukdNdBUp7USTbqDAYDAIcOHWLRokWYzWbcbjd33HEHUVFRpKSk8O2339KlSxciIyM1nja4ZG+EEEIIIcRfJaW7DpLypE6yUadkk5SUxIoVKxg4cCChoaFkZ2fTpk0bBgwYwLPPPsuYMWM0njT4ZG+EEEIIIcRfJQ+ZrYPOLE8ej8dXngAGDBjAqlWrdPmMYcmmdkOHDuX999+nqqqK3r17M2XKFLZs2cK7776LyWQiPDxc6xGDTvZGCCGEEEL8VfIOsQ6T8qROslF31VVXMWLECMxmM4MHD8Zms/HQQw/x7rvvMnbsWK3H05TsjRBCCCGE+LPkkWF1WEVFBZs3b+ayyy4jIyOD8ePHc/DgQaKjoxkzZgzXX3+91iNqRrI5xel0UlJSQlRUFGazGZPJ/6oTq9VKTEyMBtP9u8jeCCGEEEKIP0tKdx0i5UmdZKPum2++4dFHHyUuLo6IiAhiYmKIj48nJSWFhIQEUlJSSElJISoqimbNmpGamqr1yEEjeyOEEEIIIf4uKd11iJQndZKNOpvNRkZGBjabjfz8fI4fP05ubi75+fkUFhZSXFyM3W6npKSEm266icmTJ2s9ctDI3gghhBBCiL9LSncdIuVJnWTz9xUUFBAWFkZ0dLTWowSN7I0QQgghhPi7pHTrkB7L0x8l2cD27dvZunUr8fHxJCQkEBcXR5s2bXjiiSe44IILuP/++7Ue8V9H9kYIIYQQQqiR53TXYWrl6emnn9Z9eZJsqvN4PBiNRlatWsV7771HaGgoO3bsICYmBqPRSEFBAdHR0QwePFjrUTUleyOEEEIIIf4sKd11jJQndZKNOuWEl4ULF3LZZZdx3333ce211/LQQw/RsGFD3nnnHR588EG6d++u8aTBJ3sjhBBCCCH+DinddYyUJ3WSjTqDwQBATk4OgwYNIjw8HIfDQa9evahfvz5xcXG88cYbtGjRgsTERI2nDS7ZGyGEEEII8XdI6a5jpDypk2zUGY1GAJo1a8abb77J2LFjiYqKYv/+/dSvX5+2bduyadMmQkJCNJ40+GRvhBBCCCHE32HUegDxzzqzPHm9Xl95AnRdniSb2o0cOZIdO3bgcrkYMGAAU6ZMYeHChTz//PPExsYSFxen9YhBJ3sjhBBCCCH+DjnSXUeNHDmSadOmVStPR44c4ejRo7otTwrJJjCv10uXLl146qmnMJvN3HnnneTm5vLee+8RGRnJ888/r/WImpK9EUIIIYQQf4U8MqwO8nq9uN1u9u7dS8eOHcnLy2P69Ols376dyMhIxo0bR8+ePbUeUxOSjfgrZG+EEEIIIcRfJaVbCEF6ejpvvfUWsbGxDB06FIvFwvz58zl69CjNmzfnhhtuoE2bNlqPKYQQQgghxP8cKd11jJQndZJNdV6vF4PBwK5du3j11VcxmUyEhYVhsVho3LgxGzZs4Pzzz+fHH38kJiaGOXPmUK9ePa3HDjrZGyGEEEII8XdI6a4DpDypk2zUKdnMmTOHAwcO8MILL2C1Whk7dizx8fFMmzYNs9mM0+lk3LhxtGnThgcffFDrsYNC9kYIIYQQQvxT5EZqdciGDRtITEysVp6cTifLli2rVp6WL1+um/KkkGz8KcXy8OHDtGrViqioKKKiokhJSSElJQWz2UxZWRnR0dFERUVhs9m0HjnoZG+EEEIIIcTfJY8MqwOUkxVOL08NGzYkJSWF1NRUX3kym826K0+SjTolm9zcXBo3buz7eklJCS1atADAYrEAcPz4cV0dyZW9EUIIIYQQ/xQp3XWAlCd1ko06g8EAgM1m48iRI/z6669kZWWRnZ2N1+uloqKCgoICAPLz82nQoIGW4waV7I0QQgghhPinyOnldUCg8pSQkFCtPFmtVpKTk3VXniSb2oWGhrJkyRJWrVpFeHg4mZmZLFmyhO+//56wsDBSU1PZv38/DRs21HrUoJG9EUIIIYQQ/xQp3XWIlCd1ko0/o/HkiS6LFi2isLCQoqIi8vPzsdvtZGVlkZ+fT1FRETt27KBVq1a6ykYheyOEEEIIIf4uuXt5HeJwOGosTyUlJRQVFbFo0SLi4+O1HjeoJBvxV8jeCCGEEEKIv0tKtxBCCCGEEEIIcZbIjdSEEEIIIYQQQoizREq3EEIIIYQQQghxlkjpFkIIIYQQQgghzhIp3UIIIYQQQgghxFkipVsIIYQQQgghhDhLpHQLIYQQQgghhBBniZRuIYQQQgghhBDiLJHSLYQQQgghhBBCnCVSuoUQQgghhBBCiLPk/wFLvdKOBVI6MwAAAABJRU5ErkJggg==", + "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": null, + "id": "66277bb4", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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3jz961//UmZmpiRp/fr1GjJkiA4fPqzbb79dUVFReumllwo1V+izzz6rBQsWqEyZMnr88ccVFRWlxYsXq0uXLsrOzlaDBg1Uu3ZtSVK5cuXUrVs3VatWrcCxvv76a/Xr1087d+5U48aNVbt2bf3000/q3Lmz5Y1rUSUnJ0uSvLy8FBoaKkkaN26cRo8eraNHj6pFixaqWrWqFi5cqN69e1u2W7hwod577z2dPHlSd955p8qXL68XXnghX6M9z7Zt27RgwQI9/PDDqlq1qsxms55++ml98sknMgxDbdq0kZeXl959911Nnjy50I+fJA0ePFgffvihfHx8FB8fr7vvvls//PCDOnfufMW53F566SWNHz9ex48ft8wxt3nzZj377LMFzhP/+uuv648//lCDBg2UlZWlxYsXX/d59wEA7uPvv/9Wt27dtGrVKkVGRurhhx/WgQMHNHjwYP3www+SLk3T0q1bNy1btkyRkZF6/PHHVa5cOX366aeWs8ObN2+uyMhISVJkZKS6deum8uXLF7jPyZMna9iwYTp06JBatGihW2+9VcuWLVPnzp116tQpq3Vr1qypJk2aKDU1VTNmzLB5P3788Uc999xz+vXXX1WvXj01atRIGzZsUNeuXXX27FmVL19e8fHxlvXj4+PVvHlzux47AACul9zcXL3wwgtKT09XvXr1lJqaqo8++sgy1aokvfzyy5o6darOnTun1q1bq1SpUpozZ46GDh1qWee9997TuHHjdOrUKbVp00ZlypTRsmXL9MILLyg3N9dqn/98f1yQ2267TZJ0+vRptW3bVlOmTNGWLVuUmZmpsLAwmUwmy7qRkZG64447JP1v+hbp0ofgubm5uuWWW3T33XdLkn799VdJ0u23366SJUvm2+8dd9yhp556SpUrVy7S4wi4OqZzAewwduxYZWZmKi4uTrNmzZLJZNKJEyf04IMP6o8//tCiRYvUuXNnffzxx5IuvdlMSEiQp6envvvuO7344otXHD85OVm7du2SJE2bNk1hYWGSpJkzZ8owDKWmpqp169Y6ePCg/vvf/1pN4ZJ3JncewzAsDduePXvqpZdeknSpEfzTTz/p008/1bBhwwp933Nzc7V3717Nnj1bkvTggw/K29tbZ86csdzfiRMnqmHDhpKkjh07avPmzdq0aZPq1aunefPmSbp0ttnUqVMlSbNnz9a4ceMK3N+RI0e0dOlSywuBH374QYmJifL399cXX3yh0NBQpaSkqHHjxvrwww/Vo0cPZWZmXvXxK1WqlNatWydJGjlypOVr7QkJCTp79qzOnz9v2e5yGzdu1Pfffy9J+vDDDxUbGyvpf9PbjB8/Xm3atJGHx/8+qwwNDdW0adNkMplUoUIFzZo1SytXrsz3dTkAwI1pw4YNysjIyLc8JiamwK+HHz9+XM2bN5eXl5def/11eXp6ytvbWwsWLNCKFSssrzdOnjypgIAAzZ49Wx4eHsrNzdXEiRMVFBSkzMxM/etf/9LOnTu1b98+xcTEWF4r5L0JznP+/HnNmjVL0qUPfp944gkZhqFOnTopKSlJX3/9tdW0LYZhaMCAAVqzZo0++eQTm835iRMnyjAM9ejRw/L6491339WMGTP06aefqm/fvurdu7flm3m9e/dmTnQAgFupWbOm3njjDUnSgAEDtHz5cq1cuVLdu3fX7t27Le8d58+fr4iICGVnZ+uhhx7S119/rd69eys8PFweHh564okn1LRpUzVu3Fh///234uLi9Pvvv+vQoUOKiIiw7O+f748L8thjj2np0qXavn27Dh8+rPfff1+S5O3trfvuu0/PPvus7rrrLsv6rVu31u7du/Xjjz9aToDLm8qlZcuWlqb733//LenSSXzAzYQmOnCNLly4oG3btkm69LWmvEApX768YmNjtWnTJm3evFmdO3e2TJfStGlTy5zhzZs3l5+fny5cuGBzH4GBgSpdurROnz6t9u3bq2nTpoqNjVX79u0L/MT3Sg4cOKCTJ09Kkpo0aWJZ/u677xZ6jOnTpxf4VbG6detaGvCJiYmWr4r98MMPWrNmjSRZpi1JTEzUnXfeaTnz/aGHHrKM065dO5tN9IiICKsXCHmPfYkSJazmWfPx8dG5c+f0xx9/qEaNGoV6/CIiIrRr1y49//zzeuCBBxQbG6smTZpc8UXBhg0bJEmVKlWyNNClSy8uPvvsM506dUoHDhywnPknSW3atLH8O7n77rs1a9asfGf1AQBuXImJiUpMTMy3/NFHHy2wiV6nTh1VrFhR33//vSZMmKDs7GzLV6zz3sCWL19evr6+Sk9PV5s2bXT//fcrNjZWvXr1KvLFSn/77TfLt+jyXiuYTCYlJCTY3CYqKkpt2rTR4sWL9f7772vUqFFWt6elpSkpKUmStHfvXo0cOVLSpSloJBX4eAAA4G7atm1r+fvdd9+t5cuXW97r5b139fb21meffWZZL++94Y4dOxQeHq4XX3xR69at044dO7Rhw4Z805Re3kT/5/vjgvj6+uqTTz7RsmXLtGLFCm3dulXnz59Xdna21qxZo/Xr12vmzJmWKVpbtWqlcePGadeuXTp58qRKliyp9evXS/rfVC6X1202m4v+QAFujCY6cI1SU1MtX6n6Z0M77+e8+UHzpgS5/IxmDw8PhYSEXLGJ7u3trdmzZ+vNN9/U9u3b9fHHH+vjjz+Wj4+PunXrpldeeaXQ9eZNuyJJwcHBhd7ucjExMapTp44kafv27dqxY4eqVKmijz76SF5el36dnD9/3rJ+QW+6T548qeTkZMsLgssfuyt9MPDPs8Hz9pOcnGw58/1yJ06cUExMTKEev/fff1/Dhg3T+vXrtWDBAi1YsECenp5q06aN3n77bas53vLkPZ62jr2kfPPilypVyvJ3Pz8/Scr3tTwAwI3rueeeK9KFRbdu3aqnn35aWVlZNtcpXbq0pk+frlGjRmnv3r36448/JEkBAQHq16+fnnrqqULv71pfK/Tr10/Lly/X4sWL9cwzz1jddvm1P3766ad82544caLQ+wEAwFVd/n71n+/18t67ZmdnF/je9eTJk8rNzVWfPn2splK53OUN9X/u70q8vLzUrl07tWvXToZh6I8//tCSJUs0Z84c5eTkaPbs2ZYmerly5XT33Xdr8+bN+vHHH1WhQgVlZGSoatWqlqleJKlChQqWuoGbCU104BoFBwdbvjKdd8GOPHk/5wVbaGioTp06ZXXRrdzc3KtehEuSatSooYSEBP3999/avn27NmzYoK+++kqzZ89WzZo11bJly0LXm+fyN8np6ek6f/68vLy8VLp06SuOcd9991ne/B85ckSPPPKIDh06pDlz5li+3h0SEmJZf8uWLQW+Cb/84imXPwZXmn/88mlRLr8/t99+u7755hub2xXm8atUqZI+/PBDpaSkaPv27dq8ebMSEhL09ddfKzIyUj179sw3bl6z/J81nz592vL3y5vmAAAU1bvvvqusrCxFR0dr6tSpKlu2rMaPH2+ZciVP/fr1tXTpUh09elTbt2/XmjVrtGzZMo0ePVqxsbGWa6dczeWZnZKSYpmaJTU1VRkZGfLx8SnwTXvFihXVqVMnzZs3TxMmTJC/v79l2pqgoCCZTCYZhqEpU6aoWbNm1/pwAADglvLyNSgoSFu3bi1wnY0bN1oa6GPHjlXLli1lGIZlutF/+uf744KcOHFCf/zxh+644w6VKlVKJpNJ1atX18svv6y0tDR9/vnnOnbsmNU2bdq00ebNm7V27VrL1GqXn4UuSffee68WLFhgmWamSpUqVrfv3LlTI0eOVOfOndWqVatC1Qq4A/4lA9fIz8/PMn/Yt99+a/lk+OjRo5arV+fNB169enVJl87Ayvs0esWKFVc8C12S9u3bp3fffVdz585V2bJl1bx5c7355pu67777LPuS/vd1qsvP9vqnqlWrWt74Xv7p9htvvKH777/f8vXqwgoPD7c0l6dMmaLDhw9LkqKjoy1nbv/888+W9T/77DPNnTtX+/btk4+Pj2699VZJ0n/+8x/LOgVdjNOWvMd+3759+uuvvyRJGRkZmjp1qj799FOdP3++UI/fyZMn9f777+u9995TaGiomjRpokGDBlmuUm7r4q95n9YfO3ZM27dvtyz/9ttvJV2a5uWfLyYAACiKvA+a69Spo7Jly+rixYuWs7nzPpD+73//q7Fjx2rx4sWqVKmSWrdurfHjx1umE8t7c5z3WqGgOdnzxMTEWDI8L58Nw1CvXr10//33a86cOTa3fe655xQQEKBVq1ZZfVju7++vGjVqSJLlGiSStGbNGs2ePVsbN260qu9qNQIA4G7y3rueP39e//3vfyVdOqlu1qxZmj9/vk6ePGn1LeYmTZrIx8dHK1assCy7PFsL4+LFi2rXrp169Oih8ePHW53JbhiGjhw5Ikn53rM2b95c3t7e2rx5c4FTuUhSs2bNVLlyZRmGoVGjRll9Yy4lJUVvvPGGtm3bpi+++IIGOm4onIkOXMHnn3+u5cuX51t+33336c0339Qrr7yirl276ueff1aXLl106623as2aNcrOzlZsbKwlbDp37qz169frv//9rzp16qSIiAj9/PPPCg4OVmpqqs39BwYGav78+bpw4YK2bNmiW265RSdPntS6devk6+urxo0bS/rfBT12796tgQMH6pFHHpG/v7/VWJ6enurfv7+GDRumjz76SMeOHbO8Gff19dWzzz5b5MenV69e+uabb3T48GHLuGFhYerSpYvmzp2r1157TatWrVJycrLWr1+vMmXKqFWrVpbHZNSoUfr+++/19NNPKzQ0VDt37iz0vhs3bqzo6Gjt2LFDHTp0UMOGDbVz507t3btX9erVU+fOnZWRkXHVxy8kJEQLFy7U33//rcTERFWtWlUpKSn64Ycf5OHhYTVn++XuvfdePfjgg/rhhx/Uq1cvNWvWTCdOnNCGDRvk6empwYMHWzUEAACwdWFR6dIFuv8pJiZGf/75pxYtWqQLFy5o+/btqlq1qv7880/t2rVLb7zxhp544gl9/PHHMplMWrdunUJDQ3Xo0CH9+eefCgsL0z333CNJKlu2rKRLH+i/9tpr6tChQ779hYWFqXv37po5c6bGjBmj3377TSdOnND27dsVFhamrl272rxvYWFheuaZZ/T+++9bro2Sp3fv3urTp48SEhJ07NgxBQUF6ccff7R8jVy6NC2Nl5eXcnJy9OqrryouLq5IU98AAGCvq73/v1a333675b1jz5491aRJEx06dEjbt29XtWrV1L59e9WqVcuSgy+88ILKli2rTZs2qW7dutq8ebMmTZpUpDnIfXx8NGjQIA0ePFiLFi3Szp07FRMTI5PJpP/+97/au3evfH191adPH6vtgoOD1ahRI61atUppaWmKjo7O12j38fHRpEmT9Mwzz2j16tVq3ry57rvvPmVlZWnDhg06e/asKleurHfeeeeaHzPAFfGREHAF586d0+HDh/P9ybtASO3atfX555+rcePGlrnF/P399dxzz2nOnDmWs7maNWum1157TWXLltXu3bu1b98+TZo0yTJXWkFzbkuXmuOffPKJGjdurK1btyohIUHbtm1To0aNNHfuXMsZ7q1atVLDhg3l7e2tn3/+2eY0MR07dtSECRN0xx13aPXq1dq0aZPi4uL02Wef6fbbby/y4+Pj46MhQ4ZIutQYyDuTfNCgQXrllVdUvnx5ff/999qxY4cefvhhffbZZypTpowkqWvXrurVq5dKliypX3/9VadPn7ZcLfxKj0keT09PzZkzRx07dpRhGPrmm2+UkpKip59+WlOnTpXJZCrU4+fr66vPPvtMLVu2VFJSkhYsWKD169erdu3amj59uuWM84K899576t+/v8LCwrR06VLt2LFDDRs21Lx58/TAAw8U+fEEANzYEhMTLdfn+Oef3bt351v/5Zdftkx/snr1aj344IOaNGmS5UP6TZs2qVatWpo1a5buuusu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A87lytlNEx0AAAAAAAAAABv4/hAAAAAAAAAAADbQRAcAAAAAAAAAwAaa6AAAAAAAAAAA2EATHQAAAAAAAAAAG2iiAwAAAAAAAABgA010AAAAAAAAAABsoIkOAAAAAAAAAIANNNEBAAAAAAAAALCBJjpwE+jatavGjx/vtP3v27dPzZs3V+3atXXmzJlrGuPo0aOKiorSvn37JEnR0dFav369I8sEAMBlkN0AALgXshu4sdFEB66zpk2bqlGjRsrIyLBavmnTJjVt2tRJVRWvhQsXKjAwUL/++qtKlSpV4Dr79u3TgAEDdN9996l27dpq2rSpRowYoZSUlALX37Fjhxo0aOCQ+j766CPl5OQ4ZCwAwI2H7Ca7AQDuhewmuwFHo4kOOMHFixc1depUZ5dRZIZhKDc3t8jbnTt3TpUrV5aXl1eBt//+++9q3769ypcvryVLlmj79u2aPn26/vzzT3Xq1EmZmZn2lm7T2bNnNXbsWJnN5mLbBwDA/ZHd1shuAICrI7utkd2AfWiiA07Qt29fffrppzpw4ECBt//zK1SSNH78eHXt2lWStGHDBt15551atWqVGjdurNjYWE2cOFG7du1S69atFRsbq/79+1t9ypuZmamXXnpJsbGxat68udatW2e57fjx43ruuecUGxurRo0aaejQoUpPT5d06ZP62NhYzZ8/X3feeae2bduWr97c3FxNmTJFDz74oO666y517NhRiYmJkqR///vfWrx4sVasWKHo6GidPn063/ZvvfWWGjZsqEGDBql06dLy8PBQ9erVNWXKFNWpU0d///13vm2ioqK0du1aSZdeHL311luqV6+e6tatqx49eujw4cOSpJycHEVFRWnlypXq2LGj6tSpo7Zt2yopKUmnT59Wo0aNZBiG7r77bi1atEinT59W7969Va9ePd1555166qmndOTIkSsfUADADY/stkZ2AwBcHdltjewG7EMTHXCCatWqqUOHDhoxYsQ1be/p6akLFy5o48aNWrFihYYNG6bp06dr+vTpmjdvnhYuXKj//Oc/VoG9ZMkStW7dWps2bVLbtm3Vv39/paWlSZJeeuklVapUSRs2bNDXX3+tQ4cO6Z133rFsm52drUOHDumXX37RXXfdla+eTz/9VF9++aUmT56sDRs2qFmzZnrqqad09uxZvfPOO2rbtq1atGihHTt2qHTp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R1Hnfxx/bxqkbUJCDx2UQwgQpIhUQQVRRHNSBLGAIP5EFFBRDhERRUROULCgIuX0sBxNBER6KNICBlCCRy8CISSE9GQzvz9yWVnJQsJu2N3wej4ePo7Mzn73s5nbvGc+O/OdAwe0detW7dq1S+fOndOTTz7p2gJd6NLfzaVatmx5/Yu5iqysLN1+++266667PLJBAwDu5tIMyMrK0s8//6yffvpJGzdu1H/+8x/VrVvXtQW6qYCAAD300EPWn5OSkrR69WrNmTNHW7du1XfffSdfX18XVug6f/3dFChbtqwLqrm6V199Vd98843i4+NdXQoAwI7GjRurf//+mjNnjt566y21bdtWZcuWVV5ensaNGyfDMNSpUyd16tRJhmHolVde0cKFC+Xl5aXWrVurWrVqOnLkiLUHsHTpUn322WcKDAy87HXceb+IzMKNjiY6cB3cfvvtGj58uM2y119/XV999ZVmzpypJ554Qt7e3i6qzrUK+924q9WrVys1NdXVZQBAqfHXDEhNTdUdd9yhlJQULV68WCNGjHBhde4rODj4squ14uLi1LNnT+3fv19r167V3Xff7aLqXKuw3427ys7O1o8//ujqMgAARfD8889r1apVOnnypD755BM999xzmj9/vvbt2yd/f3+NGTNGkvTVV19p4cKF8vX11SeffKI2bdpYx9iwYYP+7//+T7Gxsfr88881bNgwm9dw5/0iMgtgTnTAZQrC9MKFCzp//rwkKSMjQ++9957uvvtuNWnSRB07dtRrr72m5ORk6/Nefvll1a9fXx988IGWLVumrl27qlGjRrr//vu1Z88em9dYuHCh7r77bkVGRqpHjx7auHFjobWcP39e48ePV8eOHdWoUSO1bt1azz//vP773/9a19m6davq16+vTp066dChQ+rTp48aN26se+65R5s2bVJCQoIGDhyoJk2a6I477lBMTIzTflfbtm3TwIED1bx5czVq1EhdunTR+++/r6ysrMt+L1OnTtWrr76qqKgoLVmyRJJ05swZjRo1Sp07d1ZkZKTuueceffvttzavceTIEY0cOVIdOnRQZGSk7rjjDo0fP14pKSmSpP79+1t3aBYuXKj69etr69atTnuPAAApKChI1atXlyRdvHjRurwo+ViYEydO6IUXXlCHDh3UuHFjde3aVZ999pny8vKs63Tq1En169fXli1bNHXqVLVt21aNGzfWkCFDlJiYaDPejz/+qJ49e6pJkyZq3bq1hgwZot9++81mnV27dunJJ59UmzZt1KRJEz388MPauXOnzToFl4U3btxYnTp10hdffHEtvy4bjRs3ltlslpSfaQVWrVqlPn36qEWLFmrRooX69+9vU8+l+X7y5EkNHDhQTZs2VevWrTVz5kyb1zh8+LAGDBigJk2aqG3btpoyZYosFkuh9Xz77beKjo5WkyZN1LRpU/Xs2VOLFi2yWafgd79582ZNmDBBzZs3V6tWrfTOO+9YL5lv3bq1oqKi9Oabb9p9reIqzn7PHXfcodWrV6tDhw4aMGCAJCkvL0+zZ8/WAw88oKioKLVu3Vpjxoyx7jNIUm5urj766CPde++9ioqKUqtWrTRw4EDrlDILFixQZGSkLly4IEmqX7++Xn75Zae8PwCA8wUEBOi1116TJH322Wf65ZdfNHXqVEnS008/rYiICEmyZnqvXr1sGuiS1L59e40ePVrTp0/XkCFDrvqa9vaLJOmnn35S3759FRUVpcaNG+v+++/X7NmzbfZxpKIdS5NZQNHQRAdcJCkpSZLk4+Oj0NBQSdI//vEPffzxx8rKytIDDzwgPz8/zZ8/v9CA2rhxoyZNmqSoqCiVL19e8fHxeuqpp5SZmSlJ2rJli15++WUdPXpUzZo1U7NmzTRq1KjLGg4pKSnq3bu3vvzyS3l5een+++9XxYoVtXz5cvXs2dPmgFLKb/q/8MILqlGjhsqVK6dDhw5p5MiReuGFFxQYGKiIiAidOnVKzz//vNLS0hz+Pa1atUqPP/64Nm7cqIYNG+ree+/V+fPnNWPGDA0ZMkSGYdis/8MPP2jLli3q3r27qlSpotTUVPXt21eLFi1ScHCwevToodTUVI0ZM0YLFiyQlH+p3KOPPqqlS5eqbt26euihh1SpUiV9+eWXGjx4sCSpS5cu1svn6tatq0cffVSVK1d2+P0BAP6UmpqqY8eOScpvChcoTj4WyMrK0mOPPabvv/9eFSpUUI8ePZSQkKDJkydrzpw5l60/bdo0rVu3Trfffru8vLy0du1amzOaFy5cqGHDhmnv3r3q2LGjmjRporVr16pv377WrNy7d68effRRxcTEqEGDBurSpYv27dunAQMGWNe5cOGCBgwYoN27dysiIkKdOnXSV1995fCXz5mZmUpPT5ckhYeHS8o/423o0KH65Zdf1KFDBzVr1kzbtm3ToEGDdOrUKZvnX7hwQU8//bSCgoIUGRmp8+fPa8qUKVq5cqWk/APsJ598Ups2bVK5cuXUtWtXbdiw4bIvpSVp0qRJGjNmjA4cOKBOnTqpXbt22rt3r0aNGlXovOXvvfeefv/9d91yyy1KTk7W559/rhEjRmjFihVq1qyZ0tPTNXfuXOuX444o7n7PxYsXNW7cOLVs2VK33XabJGny5MmaOHGiTpw4oa5du6pOnTr69ttv9cwzz1ifN2XKFE2dOlWZmZm6//771a5dO23dulWPP/64fv/9d9WrV09dunSxrv/oo49e1mwBALiXDh066L777lN2drb69++vCxcuqE6dOtYvWf/44w8dP35ckuz+Te/bt6/uuusumznR7bG3X/Svf/1LQ4cOVWxsrFq1aqW77rpLR44c0cSJE232XYp6LE1mAUXDdC7AdZaXl6cDBw7os88+kyTddddd8vX1lcViUWhoqHr37q3o6Gg1bdpUu3btUp8+fbRhwwZlZmbazOe5f/9+rVixQlWqVNHBgwfVrVs3JSYmKjY2Vrfffru1QdCsWTPNnj1bJpNJ7du3v+wb7y+++ELHjh1TeHi4Fi9erODgYOXk5Oihhx7S/v37NX36dOs37FJ+kPfr109///vftX37dj3yyCNKSkpSzZo1NX78eP3xxx+64447lJqaqtjYWLVr1+6af1eGYeitt96SxWJR7969NX78eEl/XrK+efNmbdiwQR06dLA+JyEhQWvWrFFYWJgkafbs2Tpx4oQiIiL0zTffyM/PT4cPH1bXrl01ffp0RUdH6/fff9eZM2cUGBiozz77TF5eXsrLy9PUqVMVHByszMxMPfLII9q7d68OHjyoxo0be8yl4gDgzjZv3mxt/BbM/Zmamqr7779f3bt3l6Ri52OBP/74w3qAN2LECOsNvf75z3/qxx9/1BNPPGGzflZWlr799lv5+voqKipK48aN07p165SdnS1fX1+99957kqRBgwZZL6ceMWKE1q5dqy+//FKvvfaaZsyYoezsbN13332aMmWKJOnWW2/V2LFj9dlnn+ntt9/WggULdPHiRQUEBGj+/PkKCQnR008/rc6dO1/z7zEpKUnTpk1Tbm6uAgICdMcdd0jKP+O6V69eqlOnjh5//HFJUteuXXX48GHF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kvLvtVGnTh3r2VLFzcfGjRtr/fr1iomJ0csvv6zff/9dFStWlLe3txISEvTSSy/plVdeKVKd3t7eeu6556yZffLkSWVnZ2vt2rUqW7asnnrqKUnSU089pXXr1mnDhg3q27evIiIitG7dOqWkpOjtt9+WlJ9LM2bMUGZmpnr16qWWLVtqw4YNqlq1qo4ePXpNv8datWppwIAB+vjjjzVjxgx17dpVNWvWVPXq1XX8+HG98847WrNmjdatW6f27dtr1apVWrx4scLDw9WgQYMivcZdd92lChUqKCEhQU888YTat2+vnTt3KiQkxGa/okaNGnr00Uc1e/ZsvfLKK1q/fr3S0tKs23XkyJHWuc5dwdH9nrCwMPXr18/6/lavXq2kpCRt2rRJFSpU0L333qugoCD99NNP+vXXX7V3715FRkYqLS3N+gVMwRzrlSpVso47ZMgQderU6bIb3gIAXC89PV2vv/66pPyru2655RZJUkREhJ566ilNnTpVH3zwge6991717t1bv/zyixYsWKChQ4eqVatWql27tpKTk7V9+3brlVmFXVFXlP0iSXr++ef11ltvaerUqdq3b598fHysXwY//vjjql69uiQV+ViazAKKhjPRARfw8/PTmDFjJOUH5aJFi9SiRQu9+OKLqlSpkrZu3aqcnBx9/vnneuaZZxQaGqo9e/YUOk+nPZ06ddIrr7yiihUrat++fTp48KCmTZt22V3CQ0JCNH/+fPXq1UsZGRnWedYeeOABfffdd4XerO16uuuuuzRr1iy1bNlSO3fu1LJly1S1alW9+OKLl93QrTBBQUH66quvdN999+nChQtasmSJLBaLhg8fbr1DeaNGjfTpp5/q1ltv1YYNG/T111/rwIED6tatm+bNm6cKFSpIkvr27aumTZvKMAxt2LDhsrP6AQDFExcXp7lz52ru3LmaN2+eNm3apHr16umVV17RjBkzJOma83HgwIF68MEHFRAQoDVr1qhhw4Z677339MQTT6hMmTLaunWrzU2/rqZPnz6aMmWKbrnlFq1bt05bt25Vu3bt9NVXX+lvf/ubpPyzlOfMmaNWrVrpt99+0/Lly1WtWjVNmzZNDz74oCSpfPny+uijj3TzzTfr9OnT2rZtm5555hmbG5Zei6effloRERHKysrS2LFjJeXf+DQyMlJnz55VbGys/vGPf+iNN97Q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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.7577 ± 0.0196 [0.7390, 0.7780]\n", + " 5 clients: 0.7694 ± 0.0413 [0.6970, 0.7960]\n", + " 10 clients: 0.7258 ± 0.0275 [0.6920, 0.7750]\n", + " 20 clients: 0.7255 ± 0.0637 [0.5980, 0.8580]\n", + " Performance degradation (3→20 clients): 4.25%\n", + "\n", + "ElasticNet:\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", + "Linear SVC:\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", + "Random Forest:\n", + "----------------------------------------\n", + " 3 clients: 0.5263 ± 0.0106 [0.5150, 0.5360]\n", + " 5 clients: 0.5116 ± 0.0135 [0.4980, 0.5260]\n", + " 10 clients: 0.5000 ± 0.0000 [0.5000, 0.5000]\n", + " 20 clients: 0.5000 ± 0.0000 [0.5000, 0.5000]\n", + " Performance degradation (3→20 clients): 5.00%\n", + "\n", + "Balanced Random Forest:\n", + "----------------------------------------\n", + " 3 clients: 0.7713 ± 0.0156 [0.7570, 0.7880]\n", + " 5 clients: 0.7636 ± 0.0297 [0.7190, 0.8010]\n", + " 10 clients: 0.7269 ± 0.0254 [0.6940, 0.7800]\n", + " 20 clients: 0.7177 ± 0.0685 [0.5650, 0.8360]\n", + " Performance degradation (3→20 clients): 6.95%\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.7713)\n", + "Best at 5 clients: Logistic Regression (balanced_accuracy: 0.7694)\n", + "Best at 10 clients: Balanced Random Forest (balanced_accuracy: 0.7269)\n", + "Best at 20 clients: Logistic Regression (balanced_accuracy: 0.7255)\n", + "\n", + "Overall best model: Logistic Regression (Avg balanced_accuracy: 0.7339)\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 = \"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", + "# Prepare data for boxplot\n", + "# boxplot_data = []\n", + "# client_labels = []\n", + "x_positions = clients\n", + "\n", + "# for client_idx, client in enumerate(clients):\n", + "# client_data = model_data[model_data['n_clients'] == client][metric]\n", + "# if len(client_data) > 0:\n", + "# boxplot_data.append(client_data)\n", + "# # Use actual client number as x-position\n", + "# client_labels.append(f'{client}')\n", + " \n", + "# print(box_positions)\n", + "# x\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", + " \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", + " # 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.4, 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": 34, + "id": "add792d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 6 experiments\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat10\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat20\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat35\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat40\n", + "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal FeatN\n", + "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat10\n", + "\n", + "Weighted Average Metrics Table:\n", + "\n", + "Model Balanced Accuracy Auroc Round Time [S]\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", + "\n", + "LaTeX Table:\n", + "\n", + "\\begin{tabular}{lccc}\n", + "Model & Balanced Accuracy & Auroc & Round Time [S] \\\\\n", + "\\hline\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", + "\\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 +} From 6678ff3ebae308e8fbdb5629e2b8663448c770f4 Mon Sep 17 00:00:00 2001 From: faildeny Date: Wed, 4 Feb 2026 12:59:51 +0100 Subject: [PATCH 13/26] Fix for AUROC in LSVC models since they do not output probabilites --- flcore/metrics.py | 5 +++-- flcore/models/linear_models/client.py | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/flcore/metrics.py b/flcore/metrics.py index 5de33bc..ad33faf 100644 --- a/flcore/metrics.py +++ b/flcore/metrics.py @@ -71,8 +71,9 @@ def calculate_metrics(y_true, y_pred_proba, task_type="binary", threshold=0.5): if not torch.is_tensor(y_pred_proba): y_pred_proba = torch.tensor(y_pred_proba.tolist()) - # Extract probabilities for the positive class - y_pred_proba = y_pred_proba[:, 1] + # Extract probabilities for the positive class if shape>1 + if y_pred_proba.ndim > 1: + y_pred_proba = y_pred_proba[:, 1] metrics_collection.update(y_pred_proba, y_true) diff --git a/flcore/models/linear_models/client.py b/flcore/models/linear_models/client.py index a4cd1ac..e73fb87 100644 --- a/flcore/models/linear_models/client.py +++ b/flcore/models/linear_models/client.py @@ -67,7 +67,11 @@ 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_proba = self.model.predict_proba(self.X_test) + # 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_test) + else: + y_pred_proba = self.model.predict_proba(self.X_test) metrics = calculate_metrics(self.y_test, y_pred_proba) print(f"Client {self.client_id} Evaluation just after local training: {metrics['balanced_accuracy']}") # Add 'personalized' to the metrics to identify them @@ -82,7 +86,10 @@ def fit(self, parameters, config): # type: ignore local_model = utils.get_model(self.model_name, local=True) # utils.set_initial_params(local_model,self.n_features) local_model.fit(self.X_train, self.y_train) - y_pred_proba = local_model.predict_proba(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) local_metrics = calculate_metrics(self.y_test, y_pred_proba) #Add 'local' to the metrics to identify them local_metrics = {f"local {key}": local_metrics[key] for key in local_metrics} @@ -95,10 +102,16 @@ def evaluate(self, parameters, config): # type: ignore utils.set_model_params(self.model, parameters) # Calculate validation set metrics - y_pred_proba = self.model.predict_proba(self.X_val) + 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) val_metrics = calculate_metrics(self.y_val, y_pred_proba) - y_pred_proba = self.model.predict_proba(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)): From 848f627325720eaed76d60c362baa2ef3ec4208c Mon Sep 17 00:00:00 2001 From: faildeny Date: Wed, 4 Feb 2026 13:00:46 +0100 Subject: [PATCH 14/26] Add much faster tests with lower config parameters --- flcore/models/random_forest/aggregatorRF.py | 2 +- flcore/models/random_forest/client.py | 2 +- flcore/models/random_forest/server.py | 2 +- flcore/models/random_forest/utils.py | 6 +++--- tests/test_models.py | 8 +++++++- 5 files changed, 13 insertions(+), 7 deletions(-) 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 e4e1595..ef1e758 100644 --- a/flcore/models/random_forest/client.py +++ b/flcore/models/random_forest/client.py @@ -31,7 +31,7 @@ def __init__(self, data,client_id,config): (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 = True if config['model'] == 'balanced_random_forest' else False - self.model = utils.get_model(self.bal_RF) + self.model = utils.get_model(self.bal_RF, config['random_forest']['tree_num']) self.round_time = 0 # 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/random_forest/server.py b/flcore/models/random_forest/server.py index 06b538c..e863a52 100644 --- a/flcore/models/random_forest/server.py +++ b/flcore/models/random_forest/server.py @@ -34,7 +34,7 @@ def fit_round( server_round: int ) -> Dict: def get_server_and_strategy(config): bal_RF = True if config['model'] == 'balanced_random_forest' else False - model = get_model(bal_RF) + 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 diff --git a/flcore/models/random_forest/utils.py b/flcore/models/random_forest/utils.py index 426e9f7..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=300,max_depth=10) + model = BalancedRandomForestClassifier(n_estimators=tree_num,max_depth=10) else: - model = RandomForestClassifier(n_estimators=300,max_depth=10,class_weight= "balanced_subsample") + model = RandomForestClassifier(n_estimators=tree_num,max_depth=10,class_weight= "balanced_subsample") return model diff --git a/tests/test_models.py b/tests/test_models.py index feb2cc2..3a02568 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -40,7 +40,13 @@ 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["xgb"]["tree_num"] = 5 + self.config["xgb"]["num_iterations"] = 2 @pytest.mark.parametrize( From 2aadb58d3a33e2f7cecfa5aab9df23cceaf23752 Mon Sep 17 00:00:00 2001 From: faildeny Date: Wed, 4 Feb 2026 13:02:14 +0100 Subject: [PATCH 15/26] Add tree number parameter in config for RF --- config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config.yaml b/config.yaml index 1319554..448da96 100644 --- a/config.yaml +++ b/config.yaml @@ -119,6 +119,7 @@ dirichlet_alpha: Null # Random Forest random_forest: balanced_rf: true + tree_num: 300 # Weighted Random Forest weighted_random_forest: From c4935c0a2a13644194c602e3c89668fe0b361d1d Mon Sep 17 00:00:00 2001 From: faildeny Date: Wed, 4 Feb 2026 13:25:56 +0100 Subject: [PATCH 16/26] Update preprocessing aggregation method in config --- config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.yaml b/config.yaml index 448da96..d17bc02 100644 --- a/config.yaml +++ b/config.yaml @@ -73,8 +73,8 @@ experiment: # "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" +data_preprocessing_method: "equal_aggregate" +# data_preprocessing_method: "reference" # Toggle data normalization (Standard scaler) based on largest center (global) or local client data_normalization: "local" From 7d6504511c932ffb8f7fa870aa79568f6a68bab3 Mon Sep 17 00:00:00 2001 From: faildeny Date: Wed, 4 Feb 2026 14:24:38 +0100 Subject: [PATCH 17/26] Remove legacy dataset preparation function --- flcore/datasets.py | 121 --------------------------------------------- 1 file changed, 121 deletions(-) diff --git a/flcore/datasets.py b/flcore/datasets.py index f82a776..32a5ed9 100644 --- a/flcore/datasets.py +++ b/flcore/datasets.py @@ -426,127 +426,6 @@ def aggregate_preprocessing_params(preprocessing_params_list, center_sizes, meth return aggregated -def prepare_dataset_old(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') - 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") - 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) - - # 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) - 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 aggregation_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 - ) - print("Calculated global preprocessing parameters using reference center") - elif aggregation_method == 'weighted_aggregate': - # 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) - print("Calculated global preprocessing parameters using weighted aggregation") - 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' or 'weighted_aggregate'") - - 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=42, 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) - - # shuffle the training data - X_train_processed, y_train = shuffle(X_train_processed, y_train) - - return X_train_processed, y_train, X_test_processed, y_test - def prepare_dataset(X, y, center_id, config, center_indices=None): """ Load and preprocess raw dataset for federated learning with feature selection From 50994038b0344377b9134f2287fd2a110884a1cf Mon Sep 17 00:00:00 2001 From: faildeny Date: Thu, 5 Feb 2026 17:55:47 +0100 Subject: [PATCH 18/26] Add minimum num of samples in dirichlet partitioning --- flcore/datasets.py | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/flcore/datasets.py b/flcore/datasets.py index 32a5ed9..3d699b6 100644 --- a/flcore/datasets.py +++ b/flcore/datasets.py @@ -244,9 +244,15 @@ def apply_preprocessing(subset_data, preprocessing_params, normalization="global return data_copy, data_copy.columns.tolist() -def partition_data_dirichlet(labels, num_centers, alpha=1.0): +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) @@ -281,10 +287,32 @@ def partition_data_dirichlet(labels, num_centers, alpha=1.0): # Calculate number of samples for each center center_samples = (proportions * n_class_samples).astype(int) - # Adjust for rounding errors - diff = n_class_samples - center_samples.sum() + # 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: - center_samples[np.random.choice(num_centers, diff, replace=True)] += 1 + # 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) @@ -448,6 +476,7 @@ def prepare_dataset(X, y, center_id, config, center_indices=None): 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") @@ -463,7 +492,7 @@ def prepare_dataset(X, y, center_id, config, center_indices=None): if not center_indices: # Partition data using Dirichlet distribution - all_center_indices = partition_data_dirichlet(y_binary.values, num_centers, alpha) + all_center_indices = partition_data_dirichlet(y_binary.values, num_centers, alpha, min_samples_per_class) else: all_center_indices = center_indices @@ -475,7 +504,7 @@ def prepare_dataset(X, y, center_id, config, center_indices=None): 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': From 9005e97d14e0bcadd6dc640bc4277dc57b37697a Mon Sep 17 00:00:00 2001 From: faildeny Date: Thu, 5 Feb 2026 18:00:58 +0100 Subject: [PATCH 19/26] Add threshold fine tuning on validation set for all models --- config.yaml | 2 +- flcore/metrics.py | 26 +++++++-- flcore/models/linear_models/client.py | 21 +++++--- flcore/models/random_forest/client.py | 39 +++++++++++--- flcore/models/xgb/client.py | 58 +++++++++++++++----- flcore/models/xgb/cnn.py | 76 +++++++++++++++------------ 6 files changed, 158 insertions(+), 64 deletions(-) diff --git a/config.yaml b/config.yaml index d17bc02..917fdc1 100644 --- a/config.yaml +++ b/config.yaml @@ -77,7 +77,7 @@ data_preprocessing_method: "equal_aggregate" # data_preprocessing_method: "reference" # Toggle data normalization (Standard scaler) based on largest center (global) or local client -data_normalization: "local" +data_normalization: "global" # Determine target for feature selection number n_features: Null diff --git a/flcore/metrics.py b/flcore/metrics.py index ad33faf..9bbcb89 100644 --- a/flcore/metrics.py +++ b/flcore/metrics.py @@ -67,12 +67,18 @@ def get_metrics_collection(task_type="binary", device="cpu", threshold=0.5): 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 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): - y_pred_proba = torch.tensor(y_pred_proba.tolist()) + 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: + 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) @@ -97,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 e73fb87..06a6eed 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 @@ -86,11 +86,17 @@ def fit(self, parameters, config): # type: ignore local_model = utils.get_model(self.model_name, local=True) # utils.set_initial_params(local_model,self.n_features) local_model.fit(self.X_train, self.y_train) + # 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) + 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) @@ -106,7 +112,8 @@ def evaluate(self, parameters, config): # type: ignore y_pred_proba = self.model.decision_function(self.X_val) else: y_pred_proba = self.model.predict_proba(self.X_val) - val_metrics = calculate_metrics(self.y_val, y_pred_proba) + 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) if self.model_name == 'lsvc': y_pred_proba = self.model.decision_function(self.X_test) @@ -119,13 +126,13 @@ 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_proba) + 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) diff --git a/flcore/models/random_forest/client.py b/flcore/models/random_forest/client.py index ef1e758..307096c 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, @@ -32,7 +32,9 @@ def __init__(self, data,client_id,config): self.splits_nested = datasets.split_partitions(n_folds_out,0.2, 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.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 @@ -59,9 +61,9 @@ def fit(self, ins: FitIns): # , parameters, config type: ignore 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,:] + self.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.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) @@ -69,8 +71,8 @@ def fit(self, ins: FitIns): # , parameters, config type: ignore #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_proba = self.model.predict_proba(X_val) - metrics = calculate_metrics(y_val, y_pred_proba) + y_pred_proba = self.model.predict_proba(self.X_val) + metrics = calculate_metrics(self.y_val, y_pred_proba) # print(f"Accuracy client in fit: {accuracy}") # print(f"Sensitivity client in fit: {sensitivity}") # print(f"Specificity client in fit: {specificity}") @@ -85,6 +87,21 @@ def fit(self, ins: FitIns): # , parameters, config type: ignore 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, self.y_train) + + 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) @@ -104,12 +121,20 @@ 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_prob) + 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}") diff --git a/flcore/models/xgb/client.py b/flcore/models/xgb/client.py index d2e358e..4e9f492 100644 --- a/flcore/models/xgb/client.py +++ b/flcore/models/xgb/client.py @@ -22,6 +22,7 @@ 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 ( @@ -34,14 +35,15 @@ 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, @@ -52,9 +54,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 +65,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=42, 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) @@ -126,7 +137,7 @@ def fit(self, fit_params: FitIns) -> FitRes: else: print("Client " + self.cid + ": only had its own tree") - # Don't prepare dataloaders if they number of clients didn't change + # 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( @@ -143,6 +154,13 @@ 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") @@ -166,6 +184,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, device=self.device) + 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": @@ -174,7 +208,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( @@ -200,8 +234,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, ) @@ -270,8 +305,7 @@ def get_client(config, data, client_id) -> fl.client.Client: client = FL_Client( task_type, - trainloader, - valloader, + data, client_tree_num, client_num, cid, diff --git a/flcore/models/xgb/cnn.py b/flcore/models/xgb/cnn.py index 849efc3..3a5331b 100644 --- a/flcore/models/xgb/cnn.py +++ b/flcore/models/xgb/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") From 67f20da50e3ea28c3edd83941f9ed8e02b926293 Mon Sep 17 00:00:00 2001 From: faildeny Date: Thu, 5 Feb 2026 18:01:30 +0100 Subject: [PATCH 20/26] Fix missing metrics from XGBoost model in distributed fit --- flcore/models/xgb/server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flcore/models/xgb/server.py b/flcore/models/xgb/server.py index 046fc2d..4b5a748 100644 --- a/flcore/models/xgb/server.py +++ b/flcore/models/xgb/server.py @@ -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) From 183ab758b5f41beb875327cfdb38f1455e73d67e Mon Sep 17 00:00:00 2001 From: faildeny Date: Mon, 9 Feb 2026 12:00:07 +0100 Subject: [PATCH 21/26] Fix xgb training with device argument --- flcore/models/xgb/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flcore/models/xgb/client.py b/flcore/models/xgb/client.py index 4e9f492..fc9ae6b 100644 --- a/flcore/models/xgb/client.py +++ b/flcore/models/xgb/client.py @@ -188,7 +188,7 @@ def fit(self, fit_params: FitIns) -> FitRes: if self.first_round: #Get best threshold based on validation set - y_pred_proba_val = self.tree.predict_proba(self.X_val, device=self.device) + 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) From d594349b87535645737045afee76a9f66dd6a504 Mon Sep 17 00:00:00 2001 From: faildeny Date: Mon, 9 Feb 2026 16:12:20 +0100 Subject: [PATCH 22/26] Add usage of seed from config for reproducibility --- flcore/compile_results.py | 23 +++++++++++++++-------- flcore/datasets.py | 8 ++------ flcore/models/linear_models/client.py | 10 +++++++--- flcore/models/random_forest/client.py | 22 +++++----------------- flcore/models/random_forest/server.py | 1 + flcore/models/xgb/client.py | 6 ++++-- server.py | 10 +++++----- 7 files changed, 39 insertions(+), 41 deletions(-) diff --git a/flcore/compile_results.py b/flcore/compile_results.py index 4e7f0c3..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 = {} @@ -49,9 +50,11 @@ def compile_results(experiment_dir: str): # Read history.yaml 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'] + 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(): @@ -128,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): diff --git a/flcore/datasets.py b/flcore/datasets.py index 3d699b6..d0c16ed 100644 --- a/flcore/datasets.py +++ b/flcore/datasets.py @@ -482,7 +482,7 @@ def prepare_dataset(X, y, center_id, config, center_indices=None): feature_selection_method = config.get("feature_selection_method", "mutual_info") normalization_method = config.get("data_normalization", "global") - np.random.seed(42) + np.random.seed(42) # For reproducibility of partitioning and reference selection # Convert target to binary classification if needed if y.nunique() > 2: @@ -563,7 +563,7 @@ def prepare_dataset(X, y, center_id, config, center_indices=None): # 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=42, stratify=y_center + X_center, y_center, test_size=0.2, random_state=config['seed'], stratify=y_center ) else: X_train, y_train = X_center, y_center @@ -573,9 +573,6 @@ def prepare_dataset(X, y, center_id, config, center_indices=None): 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) - # shuffle the training data - X_train_processed, y_train = shuffle(X_train_processed, y_train) - return X_train_processed, y_train, X_test_processed, y_test def load_mnist(center_id=None, num_splits=5): @@ -711,7 +708,6 @@ def load_kaggle_hf(data_path, center_id, config) -> Dataset: elif center_id == 3: center_id_mapped = 3 # switzerland else: - # print(f"Invalid center id: {center_id}", type(center_id)) raise ValueError(f"Invalid center id: {center_id}") # Create center_indices diff --git a/flcore/models/linear_models/client.py b/flcore/models/linear_models/client.py index 06a6eed..b0dd88b 100644 --- a/flcore/models/linear_models/client.py +++ b/flcore/models/linear_models/client.py @@ -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) @@ -68,12 +68,16 @@ def fit(self, parameters, config): # type: ignore # 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)]) # 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) - print(f"Client {self.client_id} Evaluation just after local training: {metrics['balanced_accuracy']}") + 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) diff --git a/flcore/models/random_forest/client.py b/flcore/models/random_forest/client.py index 307096c..e53984b 100644 --- a/flcore/models/random_forest/client.py +++ b/flcore/models/random_forest/client.py @@ -26,10 +26,9 @@ 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.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 @@ -60,37 +59,26 @@ 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, :] + self.X_train_2 = self.X_train.iloc[train_idx, :] self.X_val = self.X_train.iloc[val_idx,:] - y_train_2 = self.y_train.iloc[train_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) + self.model.fit(self.X_train_2, self.y_train_2) elapsed_time = (time.time() - start_time) - #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_proba = self.model.predict_proba(self.X_val) metrics = calculate_metrics(self.y_val, y_pred_proba) - # 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}") 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, self.y_train) + 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") diff --git a/flcore/models/random_forest/server.py b/flcore/models/random_forest/server.py index e863a52..97a1373 100644 --- a/flcore/models/random_forest/server.py +++ b/flcore/models/random_forest/server.py @@ -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/xgb/client.py b/flcore/models/xgb/client.py index fc9ae6b..515f94b 100644 --- a/flcore/models/xgb/client.py +++ b/flcore/models/xgb/client.py @@ -47,6 +47,7 @@ def __init__( client_tree_num: int, client_num: int, cid: str, + config, log_progress: bool = False, ): """ @@ -75,7 +76,7 @@ def __init__( (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=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) 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)) @@ -309,6 +310,7 @@ def get_client(config, data, client_id) -> fl.client.Client: client_tree_num, client_num, cid, - log_progress=False, + config, + log_progress=False ) return client diff --git a/server.py b/server.py index 5149c1e..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,12 +101,12 @@ 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]])) + 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) + # 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 From 3aca0546ce5a5999bcdfe78a2fa6f4e2850fefba Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 10 Feb 2026 11:48:08 +0100 Subject: [PATCH 23/26] Update benchmarking parameters --- benchmark.py | 33 ++- plots.ipynb | 702 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 628 insertions(+), 107 deletions(-) diff --git a/benchmark.py b/benchmark.py index 3f78ef0..f5e8dc9 100644 --- a/benchmark.py +++ b/benchmark.py @@ -51,15 +51,32 @@ # 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] +# # 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 = [10, 20, 35, 40, None] +n_features = [None] os.makedirs(benchmark_dir, exist_ok=True) diff --git a/plots.ipynb b/plots.ipynb index 7078efd..a5c008d 100644 --- a/plots.ipynb +++ b/plots.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "9f05d536", "metadata": {}, "outputs": [], @@ -20,15 +20,12 @@ "\n", "logs_dir = \"logs\"\n", "logs_dir = \"benchmark_results\"\n", - "\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", - "\n", "results_file = \"per_center_results.csv\"\n", - "\n", "keywords = [\n", " experiment_name,\n", " dataset_name,\n", @@ -40,16 +37,6 @@ " \"aNone\"\n", " ]\n", "\n", - "# 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", - "\n", "def load_data(logs_dir, experiment_name, keywords, results_file=\"per_center_results.csv\"):\n", " data = {}\n", "\n", @@ -57,8 +44,8 @@ " 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(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", @@ -97,37 +84,27 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "29bb08b0", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Logistic Regression C10 AN NormN FeatN: 0.6632\n", - "Logistic Regression C10 AN Normglobal FeatN: 0.7546\n", - "Logistic Regression C10 AN Normlocal FeatN: 0.7586\n" - ] - } - ], + "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", + "# 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)" + "# # Sort results alphabetically by model name\n", + "# results.sort()\n", + "# for result in results:\n", + "# print(result)" ] }, { @@ -191,15 +168,531 @@ "# 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": "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3jz961//UmZmpiRp/fr1GjJkiA4fPqzbb79dUVFReumllwo1V+izzz6rBQsWqEyZMnr88ccVFRWlxYsXq0uXLsrOzlaDBg1Uu3ZtSVK5cuXUrVs3VatWrcCxvv76a/Xr1087d+5U48aNVbt2bf3000/q3Lmz5Y1rUSUnJ0uSvLy8FBoaKkkaN26cRo8eraNHj6pFixaqWrWqFi5cqN69e1u2W7hwod577z2dPHlSd955p8qXL68XXnghX6M9z7Zt27RgwQI9/PDDqlq1qsxms55++ml98sknMgxDbdq0kZeXl959911Nnjy50I+fJA0ePFgffvihfHx8FB8fr7vvvls//PCDOnfufMW53F566SWNHz9ex48ft8wxt3nzZj377LMFzhP/+uuv648//lCDBg2UlZWlxYsXX/d59wEA7uPvv/9Wt27dtGrVKkVGRurhhx/WgQMHNHjwYP3www+SLk3T0q1bNy1btkyRkZF6/PHHVa5cOX366aeWs8ObN2+uyMhISVJkZKS6deum8uXLF7jPyZMna9iwYTp06JBatGihW2+9VcuWLVPnzp116tQpq3Vr1qypJk2aKDU1VTNmzLB5P3788Uc999xz+vXXX1WvXj01atRIGzZsUNeuXXX27FmVL19e8fHxlvXj4+PVvHlzux47AACul9zcXL3wwgtKT09XvXr1lJqaqo8++sgy1aokvfzyy5o6darOnTun1q1bq1SpUpozZ46GDh1qWee9997TuHHjdOrUKbVp00ZlypTRsmXL9MILLyg3N9dqn/98f1yQ2267TZJ0+vRptW3bVlOmTNGWLVuUmZmpsLAwmUwmy7qRkZG64447JP1v+hbp0ofgubm5uuWWW3T33XdLkn799VdJ0u23366SJUvm2+8dd9yhp556SpUrVy7S4wi4OqZzAewwduxYZWZmKi4uTrNmzZLJZNKJEyf04IMP6o8//tCiRYvUuXNnffzxx5IuvdlMSEiQp6envvvuO7344otXHD85OVm7du2SJE2bNk1hYWGSpJkzZ8owDKWmpqp169Y6ePCg/vvf/1pN4ZJ3JncewzAsDduePXvqpZdeknSpEfzTTz/p008/1bBhwwp933Nzc7V3717Nnj1bkvTggw/K29tbZ86csdzfiRMnqmHDhpKkjh07avPmzdq0aZPq1aunefPmSbp0ttnUqVMlSbNnz9a4ceMK3N+RI0e0dOlSywuBH374QYmJifL399cXX3yh0NBQpaSkqHHjxvrwww/Vo0cPZWZmXvXxK1WqlNatWydJGjlypOVr7QkJCTp79qzOnz9v2e5yGzdu1Pfffy9J+vDDDxUbGyvpf9PbjB8/Xm3atJGHx/8+qwwNDdW0adNkMplUoUIFzZo1SytXrsz3dTkAwI1pw4YNysjIyLc8JiamwK+HHz9+XM2bN5eXl5def/11eXp6ytvbWwsWLNCKFSssrzdOnjypgIAAzZ49Wx4eHsrNzdXEiRMVFBSkzMxM/etf/9LOnTu1b98+xcTEWF4r5L0JznP+/HnNmjVL0qUPfp944gkZhqFOnTopKSlJX3/9tdW0LYZhaMCAAVqzZo0++eQTm835iRMnyjAM9ejRw/L6491339WMGTP06aefqm/fvurdu7flm3m9e/dmTnQAgFupWbOm3njjDUnSgAEDtHz5cq1cuVLdu3fX7t27Le8d58+fr4iICGVnZ+uhhx7S119/rd69eys8PFweHh564okn1LRpUzVu3Fh///234uLi9Pvvv+vQoUOKiIiw7O+f748L8thjj2np0qXavn27Dh8+rPfff1+S5O3trfvuu0/PPvus7rrrLsv6rVu31u7du/Xjjz9aToDLm8qlZcuWlqb733//LenSSXzAzYQmOnCNLly4oG3btkm69LWmvEApX768YmNjtWnTJm3evFmdO3e2TJfStGlTy5zhzZs3l5+fny5cuGBzH4GBgSpdurROnz6t9u3bq2nTpoqNjVX79u0L/MT3Sg4cOKCTJ09Kkpo0aWJZ/u677xZ6jOnTpxf4VbG6detaGvCJiYmWr4r98MMPWrNmjSRZpi1JTEzUnXfeaTnz/aGHHrKM065dO5tN9IiICKsXCHmPfYkSJazmWfPx8dG5c+f0xx9/qEaNGoV6/CIiIrRr1y49//zzeuCBBxQbG6smTZpc8UXBhg0bJEmVKlWyNNClSy8uPvvsM506dUoHDhywnPknSW3atLH8O7n77rs1a9asfGf1AQBuXImJiUpMTMy3/NFHHy2wiV6nTh1VrFhR33//vSZMmKDs7GzLV6zz3sCWL19evr6+Sk9PV5s2bXT//fcrNjZWvXr1KvLFSn/77TfLt+jyXiuYTCYlJCTY3CYqKkpt2rTR4sWL9f7772vUqFFWt6elpSkpKUmStHfvXo0cOVLSpSloJBX4eAAA4G7atm1r+fvdd9+t5cuXW97r5b139fb21meffWZZL++94Y4dOxQeHq4XX3xR69at044dO7Rhw4Z805Re3kT/5/vjgvj6+uqTTz7RsmXLtGLFCm3dulXnz59Xdna21qxZo/Xr12vmzJmWKVpbtWqlcePGadeuXTp58qRKliyp9evXS/rfVC6X1202m4v+QAFujCY6cI1SU1MtX6n6Z0M77+e8+UHzpgS5/IxmDw8PhYSEXLGJ7u3trdmzZ+vNN9/U9u3b9fHHH+vjjz+Wj4+PunXrpldeeaXQ9eZNuyJJwcHBhd7ucjExMapTp44kafv27dqxY4eqVKmijz76SF5el36dnD9/3rJ+QW+6T548qeTkZMsLgssfuyt9MPDPs8Hz9pOcnGw58/1yJ06cUExMTKEev/fff1/Dhg3T+vXrtWDBAi1YsECenp5q06aN3n77bas53vLkPZ62jr2kfPPilypVyvJ3Pz8/Scr3tTwAwI3rueeeK9KFRbdu3aqnn35aWVlZNtcpXbq0pk+frlGjRmnv3r36448/JEkBAQHq16+fnnrqqULv71pfK/Tr10/Lly/X4sWL9cwzz1jddvm1P3766ad82544caLQ+wEAwFVd/n71n+/18t67ZmdnF/je9eTJk8rNzVWfPn2splK53OUN9X/u70q8vLzUrl07tWvXToZh6I8//tCSJUs0Z84c5eTkaPbs2ZYmerly5XT33Xdr8+bN+vHHH1WhQgVlZGSoatWqlqleJKlChQqWuoGbCU104BoFBwdbvjKdd8GOPHk/5wVbaGioTp06ZXXRrdzc3KtehEuSatSooYSEBP3999/avn27NmzYoK+++kqzZ89WzZo11bJly0LXm+fyN8np6ek6f/68vLy8VLp06SuOcd9991ne/B85ckSPPPKIDh06pDlz5li+3h0SEmJZf8uWLQW+Cb/84imXPwZXmn/88mlRLr8/t99+u7755hub2xXm8atUqZI+/PBDpaSkaPv27dq8ebMSEhL09ddfKzIyUj179sw3bl6z/J81nz592vL3y5vmAAAU1bvvvqusrCxFR0dr6tSpKlu2rMaPH2+ZciVP/fr1tXTpUh09elTbt2/XmjVrtGzZMo0ePVqxsbGWa6dczeWZnZKSYpmaJTU1VRkZGfLx8SnwTXvFihXVqVMnzZs3TxMmTJC/v79l2pqgoCCZTCYZhqEpU6aoWbNm1/pwAADglvLyNSgoSFu3bi1wnY0bN1oa6GPHjlXLli1lGIZlutF/+uf744KcOHFCf/zxh+644w6VKlVKJpNJ1atX18svv6y0tDR9/vnnOnbsmNU2bdq00ebNm7V27VrL1GqXn4UuSffee68WLFhgmWamSpUqVrfv3LlTI0eOVOfOndWqVatC1Qq4A/4lA9fIz8/PMn/Yt99+a/lk+OjRo5arV+fNB169enVJl87Ayvs0esWKFVc8C12S9u3bp3fffVdz585V2bJl1bx5c7355pu67777LPuS/vd1qsvP9vqnqlWrWt74Xv7p9htvvKH777/f8vXqwgoPD7c0l6dMmaLDhw9LkqKjoy1nbv/888+W9T/77DPNnTtX+/btk4+Pj2699VZJ0n/+8x/LOgVdjNOWvMd+3759+uuvvyRJGRkZmjp1qj799FOdP3++UI/fyZMn9f777+u9995TaGiomjRpokGDBlmuUm7r4q95n9YfO3ZM27dvtyz/9ttvJV2a5uWfLyYAACiKvA+a69Spo7Jly+rixYuWs7nzPpD+73//q7Fjx2rx4sWqVKmSWrdurfHjx1umE8t7c5z3WqGgOdnzxMTEWDI8L58Nw1CvXr10//33a86cOTa3fe655xQQEKBVq1ZZfVju7++vGjVqSJLlGiSStGbNGs2ePVsbN260qu9qNQIA4G7y3rueP39e//3vfyVdOqlu1qxZmj9/vk6ePGn1LeYmTZrIx8dHK1assCy7PFsL4+LFi2rXrp169Oih8ePHW53JbhiGjhw5Ikn53rM2b95c3t7e2rx5c4FTuUhSs2bNVLlyZRmGoVGjRll9Yy4lJUVvvPGGtm3bpi+++IIGOm4onIkOXMHnn3+u5cuX51t+33336c0339Qrr7yirl276ueff1aXLl106623as2aNcrOzlZsbKwlbDp37qz169frv//9rzp16qSIiAj9/PPPCg4OVmpqqs39BwYGav78+bpw4YK2bNmiW265RSdPntS6devk6+urxo0bS/rfBT12796tgQMH6pFHHpG/v7/VWJ6enurfv7+GDRumjz76SMeOHbO8Gff19dWzzz5b5MenV69e+uabb3T48GHLuGFhYerSpYvmzp2r1157TatWrVJycrLWr1+vMmXKqFWrVpbHZNSoUfr+++/19NNPKzQ0VDt37iz0vhs3bqzo6Gjt2LFDHTp0UMOGDbVz507t3btX9erVU+fOnZWRkXHVxy8kJEQLFy7U33//rcTERFWtWlUpKSn64Ycf5OHhYTVn++XuvfdePfjgg/rhhx/Uq1cvNWvWTCdOnNCGDRvk6empwYMHWzUEAACwdWFR6dIFuv8pJiZGf/75pxYtWqQLFy5o+/btqlq1qv7880/t2rVLb7zxhp544gl9/PHHMplMWrdunUJDQ3Xo0CH9+eefCgsL0z333CNJKlu2rKRLH+i/9tpr6tChQ779hYWFqXv37po5c6bGjBmj3377TSdOnND27dsVFhamrl272rxvYWFheuaZZ/T+++9bro2Sp3fv3urTp48SEhJ07NgxBQUF6ccff7R8jVy6NC2Nl5eXcnJy9OqrryouLq5IU98AAGCvq73/v1a333675b1jz5491aRJEx06dEjbt29XtWrV1L59e9WqVcuSgy+88ILKli2rTZs2qW7dutq8ebMmTZpUpDnIfXx8NGjQIA0ePFiLFi3Szp07FRMTI5PJpP/+97/au3evfH191adPH6vtgoOD1ahRI61atUppaWmKjo7O12j38fHRpEmT9Mwzz2j16tVq3ry57rvvPmVlZWnDhg06e/asKleurHfeeeeaHzPAFfGREHAF586d0+HDh/P9ybtASO3atfX555+rcePGlrnF/P399dxzz2nOnDmWs7maNWum1157TWXLltXu3bu1b98+TZo0yTJXWkFzbkuXmuOffPKJGjdurK1btyohIUHbtm1To0aNNHfuXMsZ7q1atVLDhg3l7e2tn3/+2eY0MR07dtSECRN0xx13aPXq1dq0aZPi4uL02Wef6fbbby/y4+Pj46MhQ4ZIutQYyDuTfNCgQXrllVdUvnx5ff/999qxY4cefvhhffbZZypTpowkqWvXrurVq5dKliypX3/9VadPn7ZcLfxKj0keT09PzZkzRx07dpRhGPrmm2+UkpKip59+WlOnTpXJZCrU4+fr66vPPvtMLVu2VFJSkhYsWKD169erdu3amj59uuWM84K899576t+/v8LCwrR06VLt2LFDDRs21Lx58/TAAw8U+fEEANzYEhMTLdfn+Oef3bt351v/5Zdftkx/snr1aj344IOaNGmS5UP6TZs2qVatWpo1a5buuusu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A87lytlNEx0AAAAAAAAAABv4/hAAAAAAAAAAADbQRAcAAAAAAAAAwAaa6AAAAAAAAAAA2EATHQAAAAAAAAAAG2iiAwAAAAAAAABgA010AAAAAAAAAABsoIkOAAAAAAAAAIANNNEBAAAAAAAAALCBJjpwE+jatavGjx/vtP3v27dPzZs3V+3atXXmzJlrGuPo0aOKiorSvn37JEnR0dFav369I8sEAMBlkN0AALgXshu4sdFEB66zpk2bqlGjRsrIyLBavmnTJjVt2tRJVRWvhQsXKjAwUL/++qtKlSpV4Dr79u3TgAEDdN9996l27dpq2rSpRowYoZSUlALX37Fjhxo0aOCQ+j766CPl5OQ4ZCwAwI2H7Ca7AQDuhewmuwFHo4kOOMHFixc1depUZ5dRZIZhKDc3t8jbnTt3TpUrV5aXl1eBt//+++9q3769ypcvryVLlmj79u2aPn26/vzzT3Xq1EmZmZn2lm7T2bNnNXbsWJnN5mLbBwDA/ZHd1shuAICrI7utkd2AfWiiA07Qt29fffrppzpw4ECBt//zK1SSNH78eHXt2lWStGHDBt15551atWqVGjdurNjYWE2cOFG7du1S69atFRsbq/79+1t9ypuZmamXXnpJsbGxat68udatW2e57fjx43ruuecUGxurRo0aaejQoUpPT5d06ZP62NhYzZ8/X3feeae2bduWr97c3FxNmTJFDz74oO666y517NhRiYmJkqR///vfWrx4sVasWKHo6GidPn063/ZvvfWWGjZsqEGDBql06dLy8PBQ9erVNWXKFNWpU0d///13vm2ioqK0du1aSZdeHL311luqV6+e6tatqx49eujw4cOSpJycHEVFRWnlypXq2LGj6tSpo7Zt2yopKUmnT59Wo0aNZBiG7r77bi1atEinT59W7969Va9ePd1555166qmndOTIkSsfUADADY/stkZ2AwBcHdltjewG7EMTHXCCatWqqUOHDhoxYsQ1be/p6akLFy5o48aNWrFihYYNG6bp06dr+vTpmjdvnhYuXKj//Oc/VoG9ZMkStW7dWps2bVLbtm3Vv39/paWlSZJeeuklVapUSRs2bNDXX3+tQ4cO6Z133rFsm52drUOHDumXX37RXXfdla+eTz/9VF9++aUmT56sDRs2qFmzZnrqqad09uxZvfPOO2rbtq1atGihHTt2qHTp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R1Hnfxx/bxqkbUJCDx2UQwgQpIhUQQVRRHNSBLGAIP5EFFBRDhERRUROULCgIuX0sBxNBER6KNICBlCCRy8CISSE9GQzvz9yWVnJQsJu2N3wej4ePo7Mzn73s5nbvGc+O/OdAwe0detW7dq1S+fOndOTTz7p2gJd6NLfzaVatmx5/Yu5iqysLN1+++266667PLJBAwDu5tIMyMrK0s8//6yffvpJGzdu1H/+8x/VrVvXtQW6qYCAAD300EPWn5OSkrR69WrNmTNHW7du1XfffSdfX18XVug6f/3dFChbtqwLqrm6V199Vd98843i4+NdXQoAwI7GjRurf//+mjNnjt566y21bdtWZcuWVV5ensaNGyfDMNSpUyd16tRJhmHolVde0cKFC+Xl5aXWrVurWrVqOnLkiLUHsHTpUn322WcKDAy87HXceb+IzMKNjiY6cB3cfvvtGj58uM2y119/XV999ZVmzpypJ554Qt7e3i6qzrUK+924q9WrVys1NdXVZQBAqfHXDEhNTdUdd9yhlJQULV68WCNGjHBhde4rODj4squ14uLi1LNnT+3fv19r167V3Xff7aLqXKuw3427ys7O1o8//ujqMgAARfD8889r1apVOnnypD755BM999xzmj9/vvbt2yd/f3+NGTNGkvTVV19p4cKF8vX11SeffKI2bdpYx9iwYYP+7//+T7Gxsfr88881bNgwm9dw5/0iMgtgTnTAZQrC9MKFCzp//rwkKSMjQ++9957uvvtuNWnSRB07dtRrr72m5ORk6/Nefvll1a9fXx988IGWLVumrl27qlGjRrr//vu1Z88em9dYuHCh7r77bkVGRqpHjx7auHFjobWcP39e48ePV8eOHdWoUSO1bt1azz//vP773/9a19m6davq16+vTp066dChQ+rTp48aN26se+65R5s2bVJCQoIGDhyoJk2a6I477lBMTIzTflfbtm3TwIED1bx5czVq1EhdunTR+++/r6ysrMt+L1OnTtWrr76qqKgoLVmyRJJ05swZjRo1Sp07d1ZkZKTuueceffvttzavceTIEY0cOVIdOnRQZGSk7rjjDo0fP14pKSmSpP79+1t3aBYuXKj69etr69atTnuPAAApKChI1atXlyRdvHjRurwo+ViYEydO6IUXXlCHDh3UuHFjde3aVZ999pny8vKs63Tq1En169fXli1bNHXqVLVt21aNGzfWkCFDlJiYaDPejz/+qJ49e6pJkyZq3bq1hgwZot9++81mnV27dunJJ59UmzZt1KRJEz388MPauXOnzToFl4U3btxYnTp10hdffHEtvy4bjRs3ltlslpSfaQVWrVqlPn36qEWLFmrRooX69+9vU8+l+X7y5EkNHDhQTZs2VevWrTVz5kyb1zh8+LAGDBigJk2aqG3btpoyZYosFkuh9Xz77beKjo5WkyZN1LRpU/Xs2VOLFi2yWafgd79582ZNmDBBzZs3V6tWrfTOO+9YL5lv3bq1oqKi9Oabb9p9reIqzn7PHXfcodWrV6tDhw4aMGCAJCkvL0+zZ8/WAw88oKioKLVu3Vpjxoyx7jNIUm5urj766CPde++9ioqKUqtWrTRw4EDrlDILFixQZGSkLly4IEmqX7++Xn75Zae8PwCA8wUEBOi1116TJH322Wf65ZdfNHXqVEnS008/rYiICEmyZnqvXr1sGuiS1L59e40ePVrTp0/XkCFDrvqa9vaLJOmnn35S3759FRUVpcaNG+v+++/X7NmzbfZxpKIdS5NZQNHQRAdcJCkpSZLk4+Oj0NBQSdI//vEPffzxx8rKytIDDzwgPz8/zZ8/v9CA2rhxoyZNmqSoqCiVL19e8fHxeuqpp5SZmSlJ2rJli15++WUdPXpUzZo1U7NmzTRq1KjLGg4pKSnq3bu3vvzyS3l5een+++9XxYoVtXz5cvXs2dPmgFLKb/q/8MILqlGjhsqVK6dDhw5p5MiReuGFFxQYGKiIiAidOnVKzz//vNLS0hz+Pa1atUqPP/64Nm7cqIYNG+ree+/V+fPnNWPGDA0ZMkSGYdis/8MPP2jLli3q3r27qlSpotTUVPXt21eLFi1ScHCwevToodTUVI0ZM0YLFiyQlH+p3KOPPqqlS5eqbt26euihh1SpUiV9+eWXGjx4sCSpS5cu1svn6tatq0cffVSVK1d2+P0BAP6UmpqqY8eOScpvChcoTj4WyMrK0mOPPabvv/9eFSpUUI8ePZSQkKDJkydrzpw5l60/bdo0rVu3Trfffru8vLy0du1amzOaFy5cqGHDhmnv3r3q2LGjmjRporVr16pv377WrNy7d68effRRxcTEqEGDBurSpYv27dunAQMGWNe5cOGCBgwYoN27dysiIkKdOnXSV1995fCXz5mZmUpPT5ckhYeHS8o/423o0KH65Zdf1KFDBzVr1kzbtm3ToEGDdOrUKZvnX7hwQU8//bSCgoIUGRmp8+fPa8qUKVq5cqWk/APsJ598Ups2bVK5cuXUtWtXbdiw4bIvpSVp0qRJGjNmjA4cOKBOnTqpXbt22rt3r0aNGlXovOXvvfeefv/9d91yyy1KTk7W559/rhEjRmjFihVq1qyZ0tPTNXfuXOuX444o7n7PxYsXNW7cOLVs2VK33XabJGny5MmaOHGiTpw4oa5du6pOnTr69ttv9cwzz1ifN2XKFE2dOlWZmZm6//771a5dO23dulWPP/64fv/9d9WrV09dunSxrv/oo49e1mwBALiXDh066L777lN2drb69++vCxcuqE6dOtYvWf/44w8dP35ckuz+Te/bt6/uuusumznR7bG3X/Svf/1LQ4cOVWxsrFq1aqW77rpLR44c0cSJE232XYp6LE1mAUXDdC7AdZaXl6cDBw7os88+kyTddddd8vX1lcViUWhoqHr37q3o6Gg1bdpUu3btUp8+fbRhwwZlZmbazOe5f/9+rVixQlWqVNHBgwfVrVs3JSYmKjY2Vrfffru1QdCsWTPNnj1bJpNJ7du3v+wb7y+++ELHjh1TeHi4Fi9erODgYOXk5Oihhx7S/v37NX36dOs37FJ+kPfr109///vftX37dj3yyCNKSkpSzZo1NX78eP3xxx+64447lJqaqtjYWLVr1+6af1eGYeitt96SxWJR7969NX78eEl/XrK+efNmbdiwQR06dLA+JyEhQWvWrFFYWJgkafbs2Tpx4oQiIiL0zTffyM/PT4cPH1bXrl01ffp0RUdH6/fff9eZM2cUGBiozz77TF5eXsrLy9PUqVMVHByszMxMPfLII9q7d68OHjyoxo0be8yl4gDgzjZv3mxt/BbM/Zmamqr7779f3bt3l6Ri52OBP/74w3qAN2LECOsNvf75z3/qxx9/1BNPPGGzflZWlr799lv5+voqKipK48aN07p165SdnS1fX1+99957kqRBgwZZL6ceMWKE1q5dqy+//FKvvfaaZsyYoezsbN13332aMmWKJOnWW2/V2LFj9dlnn+ntt9/WggULdPHiRQUEBGj+/PkKCQnR008/rc6dO1/z7zEpKUnTpk1Tbm6uAgICdMcdd0jKP+O6V69eqlOnjh5//HFJUteuXXX48GHF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kvLvtVGnTh3r2VLFzcfGjRtr/fr1iomJ0csvv6zff/9dFStWlLe3txISEvTSSy/plVdeKVKd3t7eeu6556yZffLkSWVnZ2vt2rUqW7asnnrqKUnSU089pXXr1mnDhg3q27evIiIitG7dOqWkpOjtt9+WlJ9LM2bMUGZmpnr16qWWLVtqw4YNqlq1qo4ePXpNv8datWppwIAB+vjjjzVjxgx17dpVNWvWVPXq1XX8+HG98847WrNmjdatW6f27dtr1apVWrx4scLDw9WgQYMivcZdd92lChUqKCEhQU888YTat2+vnTt3KiQkxGa/okaNGnr00Uc1e/ZsvfLKK1q/fr3S0tKs23XkyJHWuc5dwdH9nrCwMPXr18/6/lavXq2kpCRt2rRJFSpU0L333qugoCD99NNP+vXXX7V3715FRkYqLS3N+gVMwRzrlSpVso47ZMgQderU6bIb3gIAXC89PV2vv/66pPyru2655RZJUkREhJ566ilNnTpVH3zwge6991717t1bv/zyixYsWKChQ4eqVatWql27tpKTk7V9+3brlVmFXVFXlP0iSXr++ef11ltvaerUqdq3b598fHysXwY//vjjql69uiQV+ViazAKKhjPRARfw8/PTmDFjJOUH5aJFi9SiRQu9+OKLqlSpkrZu3aqcnBx9/vnneuaZZxQaGqo9e/YUOk+nPZ06ddIrr7yiihUrat++fTp48KCmTZt22V3CQ0JCNH/+fPXq1UsZGRnWedYeeOABfffdd4XerO16uuuuuzRr1iy1bNlSO3fu1LJly1S1alW9+OKLl93QrTBBQUH66quvdN999+nChQtasmSJLBaLhg8fbr1DeaNGjfTpp5/q1ltv1YYNG/T111/rwIED6tatm+bNm6cKFSpIkvr27aumTZvKMAxt2LDhsrP6AQDFExcXp7lz52ru3LmaN2+eNm3apHr16umVV17RjBkzJOma83HgwIF68MEHFRAQoDVr1qhhw4Z677339MQTT6hMmTLaunWrzU2/rqZPnz6aMmWKbrnlFq1bt05bt25Vu3bt9NVXX+lvf/ubpPyzlOfMmaNWrVrpt99+0/Lly1WtWjVNmzZNDz74oCSpfPny+uijj3TzzTfr9OnT2rZtm5555hmbG5Zei6effloRERHKysrS2LFjJeXf+DQyMlJnz55VbGys/vGPf+iNN97Q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", + "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": [ "
" ] @@ -216,43 +709,43 @@ "\n", "Logistic Regression:\n", "----------------------------------------\n", - " 3 clients: 0.7577 ± 0.0196 [0.7390, 0.7780]\n", - " 5 clients: 0.7694 ± 0.0413 [0.6970, 0.7960]\n", - " 10 clients: 0.7258 ± 0.0275 [0.6920, 0.7750]\n", - " 20 clients: 0.7255 ± 0.0637 [0.5980, 0.8580]\n", - " Performance degradation (3→20 clients): 4.25%\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: 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", + " 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: 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", + " 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.5263 ± 0.0106 [0.5150, 0.5360]\n", - " 5 clients: 0.5116 ± 0.0135 [0.4980, 0.5260]\n", - " 10 clients: 0.5000 ± 0.0000 [0.5000, 0.5000]\n", - " 20 clients: 0.5000 ± 0.0000 [0.5000, 0.5000]\n", - " Performance degradation (3→20 clients): 5.00%\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.7713 ± 0.0156 [0.7570, 0.7880]\n", - " 5 clients: 0.7636 ± 0.0297 [0.7190, 0.8010]\n", - " 10 clients: 0.7269 ± 0.0254 [0.6940, 0.7800]\n", - " 20 clients: 0.7177 ± 0.0685 [0.5650, 0.8360]\n", - " Performance degradation (3→20 clients): 6.95%\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", @@ -265,12 +758,12 @@ "================================================================================\n", "COMPARATIVE ANALYSIS:\n", "================================================================================\n", - "Best at 3 clients: Balanced Random Forest (balanced_accuracy: 0.7713)\n", - "Best at 5 clients: Logistic Regression (balanced_accuracy: 0.7694)\n", - "Best at 10 clients: Balanced Random Forest (balanced_accuracy: 0.7269)\n", - "Best at 20 clients: Logistic Regression (balanced_accuracy: 0.7255)\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: Logistic Regression (Avg balanced_accuracy: 0.7339)\n" + "Overall best model: Random Forest (Avg balanced_accuracy: 0.7441)\n" ] } ], @@ -291,7 +784,9 @@ "# clients = [3, 5, 10] # Only these client numbers\n", "\n", "extracted_data = []\n", - "metric = \"balanced_accuracy\"\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", @@ -309,7 +804,7 @@ " 'alpha': alpha,\n", " metric: score\n", " })\n", - "\n", + " \n", "# Convert to DataFrame\n", "df = pd.DataFrame(extracted_data)\n", "\n", @@ -330,27 +825,15 @@ " 'MLP': '#8c564b'\n", "}\n", "\n", - "# Prepare data for boxplot\n", - "# boxplot_data = []\n", - "# client_labels = []\n", "x_positions = clients\n", "\n", - "# for client_idx, client in enumerate(clients):\n", - "# client_data = model_data[model_data['n_clients'] == client][metric]\n", - "# if len(client_data) > 0:\n", - "# boxplot_data.append(client_data)\n", - "# # Use actual client number as x-position\n", - "# client_labels.append(f'{client}')\n", - " \n", - "# print(box_positions)\n", - "# x\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", - " \n", + " print(model)\n", + " print(model_data)\n", " # Prepare data for boxplot\n", " boxplot_data = []\n", " client_labels = []\n", @@ -393,6 +876,7 @@ " 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", @@ -401,7 +885,7 @@ " \n", " # Set consistent y-axis across all subplots\n", " # ax.set_ylim(0.5, 0.78)\n", - " ax.set_ylim(0.4, 0.85)\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", @@ -608,7 +1092,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 37, "id": "add792d5", "metadata": {}, "outputs": [ @@ -616,35 +1100,55 @@ "name": "stdout", "output_type": "stream", "text": [ - "Found 6 experiments\n", - "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat10\n", - "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat20\n", - "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat35\n", - "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal Feat40\n", - "Ukbb Cvd Balanced Random Forest C5 A0.7 Normglobal FeatN\n", - "Ukbb Cvd Balanced Random Forest C5 AN Normglobal Feat10\n", + "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" ] } From ad77488123e65615fded264f1a723c52a6d48fdf Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 10 Feb 2026 13:40:51 +0100 Subject: [PATCH 24/26] Rename old XGBoost implementation to xgblr --- flcore/client_selector.py | 6 +++--- flcore/datasets.py | 2 +- flcore/models/xgb/__init__.py | 4 ---- flcore/models/xgblr/__init__.py | 4 ++++ flcore/models/{xgb => xgblr}/client.py | 4 ++-- flcore/models/{xgb => xgblr}/cnn.py | 0 flcore/models/{xgb => xgblr}/fed_custom_strategy.py | 0 flcore/models/{xgb => xgblr}/server.py | 8 ++++---- flcore/models/{xgb => xgblr}/utils.py | 0 flcore/server_selector.py | 6 +++--- 10 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 flcore/models/xgb/__init__.py create mode 100644 flcore/models/xgblr/__init__.py rename flcore/models/{xgb => xgblr}/client.py (99%) rename flcore/models/{xgb => xgblr}/cnn.py (100%) rename flcore/models/{xgb => xgblr}/fed_custom_strategy.py (100%) rename flcore/models/{xgb => xgblr}/server.py (99%) rename flcore/models/{xgb => xgblr}/utils.py (100%) diff --git a/flcore/client_selector.py b/flcore/client_selector.py index 3f92915..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 @@ -17,8 +17,8 @@ def get_model_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/datasets.py b/flcore/datasets.py index d0c16ed..68d048f 100644 --- a/flcore/datasets.py +++ b/flcore/datasets.py @@ -22,7 +22,7 @@ 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] 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 99% rename from flcore/models/xgb/client.py rename to flcore/models/xgblr/client.py index 515f94b..197e1a9 100644 --- a/flcore/models/xgb/client.py +++ b/flcore/models/xgblr/client.py @@ -24,8 +24,8 @@ 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, diff --git a/flcore/models/xgb/cnn.py b/flcore/models/xgblr/cnn.py similarity index 100% rename from flcore/models/xgb/cnn.py rename to flcore/models/xgblr/cnn.py diff --git a/flcore/models/xgb/fed_custom_strategy.py b/flcore/models/xgblr/fed_custom_strategy.py similarity index 100% rename from flcore/models/xgb/fed_custom_strategy.py rename to flcore/models/xgblr/fed_custom_strategy.py diff --git a/flcore/models/xgb/server.py b/flcore/models/xgblr/server.py similarity index 99% rename from flcore/models/xgb/server.py rename to flcore/models/xgblr/server.py index 4b5a748..156844b 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, 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/server_selector.py b/flcore/server_selector.py index 8c5e010..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 @@ -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}") From 90a42cdce623513ada32b6820c4e0159068dd3f4 Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 10 Feb 2026 14:18:42 +0100 Subject: [PATCH 25/26] Rename old XGBoost implementation to xgblr tests --- flcore/models/xgblr/client.py | 4 ++-- flcore/models/xgblr/server.py | 6 +++--- tests/test_models.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flcore/models/xgblr/client.py b/flcore/models/xgblr/client.py index 197e1a9..2a1d65a 100644 --- a/flcore/models/xgblr/client.py +++ b/flcore/models/xgblr/client.py @@ -272,9 +272,9 @@ 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 diff --git a/flcore/models/xgblr/server.py b/flcore/models/xgblr/server.py index 156844b..4312d5d 100644 --- a/flcore/models/xgblr/server.py +++ b/flcore/models/xgblr/server.py @@ -410,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/tests/test_models.py b/tests/test_models.py index 3a02568..f5969f7 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -18,7 +18,7 @@ "random_forest", "balanced_random_forest", # # "weighted_random_forest", - "xgb" + "xgblr" ] datasets = [ @@ -45,8 +45,8 @@ def setup_class(self): # To speed up tests, reduce number of trees in xgboost and random forest self.config["random_forest"]["tree_num"] = 5 - self.config["xgb"]["tree_num"] = 5 - self.config["xgb"]["num_iterations"] = 2 + self.config["xgblr"]["tree_num"] = 5 + self.config["xgblr"]["num_iterations"] = 2 @pytest.mark.parametrize( From 5e80ec53643914639085e42ee4d4135c7c1e2bfa Mon Sep 17 00:00:00 2001 From: faildeny Date: Tue, 10 Feb 2026 14:19:35 +0100 Subject: [PATCH 26/26] Update gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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