MEDfl 2.0.10.dev1__tar.gz → 2.0.10.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/__init__.py +15 -0
  2. medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/client.py +328 -0
  3. medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/config.py +63 -0
  4. medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/data.py +122 -0
  5. medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/metrics.py +192 -0
  6. medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/serialization.py +52 -0
  7. medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/server.py +262 -0
  8. medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/strategy.py +576 -0
  9. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/PKG-INFO +1 -1
  10. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/medfl.egg-info/PKG-INFO +1 -1
  11. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/medfl.egg-info/SOURCES.txt +8 -0
  12. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/setup.py +1 -1
  13. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/LICENSE +0 -0
  14. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/__init__.py +0 -0
  15. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/client.py +0 -0
  16. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/dynamicModal.py +0 -0
  17. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/federated_dataset.py +0 -0
  18. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/flpipeline.py +0 -0
  19. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/model.py +0 -0
  20. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/params.yaml +0 -0
  21. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/params_optimiser.py +0 -0
  22. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/plot.py +0 -0
  23. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/server.py +0 -0
  24. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/strategy.py +0 -0
  25. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/LearningManager/utils.py +0 -0
  26. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/NetManager/__init__.py +0 -0
  27. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/NetManager/database_connector.py +0 -0
  28. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/NetManager/dataset.py +0 -0
  29. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/NetManager/flsetup.py +0 -0
  30. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/NetManager/net_helper.py +0 -0
  31. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/NetManager/net_manager_queries.py +0 -0
  32. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/NetManager/network.py +0 -0
  33. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/NetManager/node.py +0 -0
  34. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/__init__.py +0 -0
  35. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/__init__.py +0 -0
  36. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/client.py +0 -0
  37. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/model.py +0 -0
  38. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/server.py +0 -0
  39. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/strategy.py +0 -0
  40. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/__init__.py +0 -0
  41. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/client.py +0 -0
  42. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/config.py +0 -0
  43. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/data.py +0 -0
  44. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/metrics.py +0 -0
  45. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/serialization.py +0 -0
  46. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/strategy.py +0 -0
  47. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/scripts/__init__.py +0 -0
  48. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/scripts/base.py +0 -0
  49. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/MEDfl/scripts/create_db.py +0 -0
  50. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/README.md +0 -0
  51. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/alembic/__init__.py +0 -0
  52. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/alembic/env.py +0 -0
  53. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/medfl.egg-info/dependency_links.txt +0 -0
  54. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/medfl.egg-info/requires.txt +0 -0
  55. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/medfl.egg-info/top_level.txt +0 -0
  56. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/setup.cfg +0 -0
  57. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/tests/test_dataset.py +0 -0
  58. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/tests/test_federated_dataset.py +0 -0
  59. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/tests/test_flpipeline.py +0 -0
  60. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/tests/test_flsetup.py +0 -0
  61. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/tests/test_network.py +0 -0
  62. {medfl-2.0.10.dev1 → medfl-2.0.10.dev2}/tests/test_node.py +0 -0
@@ -0,0 +1,15 @@
1
+ """
2
+ Federated XGBoost backend for MEDfl simulation mode.
3
+ """
4
+
5
+ from .client import XGBoostSimulationClient
6
+ from .config import XGBoostConfig
7
+ from .server import XGBoostSimulationServer
8
+ from .strategy import XGBoostSimulationStrategy
9
+
10
+ __all__ = [
11
+ "XGBoostSimulationClient",
12
+ "XGBoostConfig",
13
+ "XGBoostSimulationServer",
14
+ "XGBoostSimulationStrategy",
15
+ ]
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import logging
4
+ import sys
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import flwr as fl
8
+ import xgboost as xgb
9
+ from flwr.common import (
10
+ Code,
11
+ EvaluateIns,
12
+ EvaluateRes,
13
+ FitIns,
14
+ FitRes,
15
+ GetParametersIns,
16
+ GetParametersRes,
17
+ GetPropertiesIns,
18
+ GetPropertiesRes,
19
+ Status,
20
+ )
21
+ from torch.utils.data import DataLoader
22
+
23
+ from .data import SimulationXGBoostDataModule
24
+ from .metrics import evaluate_predictions
25
+ from .serialization import (
26
+ booster_to_parameters,
27
+ parameters_to_booster,
28
+ )
29
+
30
+
31
+ def _get_logger() -> logging.Logger:
32
+ logger = logging.getLogger(
33
+ "MEDfl.simulation.xgboost.client"
34
+ )
35
+
36
+ if not logger.handlers:
37
+ handler = logging.StreamHandler(sys.stdout)
38
+ handler.setFormatter(
39
+ logging.Formatter(
40
+ "[%(asctime)s][%(name)s][%(levelname)s] "
41
+ "%(message)s",
42
+ datefmt="%H:%M:%S",
43
+ )
44
+ )
45
+
46
+ logger.addHandler(handler)
47
+ logger.setLevel(logging.INFO)
48
+ logger.propagate = False
49
+
50
+ return logger
51
+
52
+
53
+ class XGBoostSimulationClient(fl.client.Client):
54
+ def __init__(
55
+ self,
56
+ cid: str,
57
+ trainloader: DataLoader,
58
+ valloader: Optional[DataLoader] = None,
59
+ testloader: Optional[DataLoader] = None,
60
+ task: str = "binary",
61
+ xgb_params: Optional[Dict[str, Any]] = None,
62
+ local_num_boost_round: int = 10,
63
+ threshold: float = 0.5,
64
+ feature_names: Optional[List[str]] = None,
65
+ ):
66
+ self.cid = str(cid)
67
+ self.task = task
68
+ self.xgb_params = xgb_params or {}
69
+ self.local_num_boost_round = int(
70
+ local_num_boost_round
71
+ )
72
+ self.threshold = float(threshold)
73
+ self.feature_names = feature_names
74
+
75
+ self.log = _get_logger()
76
+ self.booster: Optional[xgb.Booster] = None
77
+
78
+ data_module = SimulationXGBoostDataModule(
79
+ trainloader=trainloader,
80
+ valloader=valloader,
81
+ testloader=testloader,
82
+ feature_names=feature_names,
83
+ )
84
+
85
+ self.dtrain = data_module.dtrain
86
+ self.dval = data_module.dval
87
+ self.dtest = data_module.dtest
88
+ self.metadata = data_module.metadata
89
+
90
+ self._validate()
91
+
92
+ self.log.info(
93
+ "[Client %s] initialized: "
94
+ "train=%s, val=%s, test=%s, features=%s",
95
+ self.cid,
96
+ self.metadata["num_train"],
97
+ self.metadata["num_val"],
98
+ self.metadata["num_test"],
99
+ self.metadata["num_features"],
100
+ )
101
+
102
+ def _validate(self) -> None:
103
+ if self.task not in {
104
+ "binary",
105
+ "regression",
106
+ "multiclass",
107
+ }:
108
+ raise ValueError(
109
+ f"Unsupported task: {self.task}"
110
+ )
111
+
112
+ if self.dtrain is None:
113
+ raise ValueError(
114
+ f"Client {self.cid} has no training data."
115
+ )
116
+
117
+ if self.local_num_boost_round <= 0:
118
+ raise ValueError(
119
+ "local_num_boost_round must be greater than zero."
120
+ )
121
+
122
+ def get_parameters(
123
+ self,
124
+ ins: GetParametersIns,
125
+ ) -> GetParametersRes:
126
+ return GetParametersRes(
127
+ status=Status(
128
+ code=Code.OK,
129
+ message="Success",
130
+ ),
131
+ parameters=booster_to_parameters(
132
+ self.booster
133
+ ),
134
+ )
135
+
136
+ def fit(self, ins: FitIns) -> FitRes:
137
+ self.log.info(
138
+ "[Client %s] starting local XGBoost fit",
139
+ self.cid,
140
+ )
141
+
142
+ received_booster = parameters_to_booster(
143
+ ins.parameters
144
+ )
145
+
146
+ if received_booster is not None:
147
+ self.booster = received_booster
148
+
149
+ evals = []
150
+
151
+ if self.dval is not None:
152
+ evals.append(
153
+ (self.dval, "validation")
154
+ )
155
+
156
+ self.booster = xgb.train(
157
+ params=self.xgb_params,
158
+ dtrain=self.dtrain,
159
+ num_boost_round=self.local_num_boost_round,
160
+ xgb_model=self.booster,
161
+ evals=evals,
162
+ verbose_eval=False,
163
+ )
164
+
165
+ train_predictions = self.booster.predict(
166
+ self.dtrain
167
+ )
168
+
169
+ train_metrics = evaluate_predictions(
170
+ task=self.task,
171
+ y_true=self.dtrain.get_label(),
172
+ y_pred_or_prob=train_predictions,
173
+ threshold=self.threshold,
174
+ num_features=int(
175
+ self.metadata["num_features"]
176
+ ),
177
+ )
178
+
179
+ metrics = {
180
+ "backend": "xgboost",
181
+ "task": self.task,
182
+ "cid": self.cid,
183
+ "num_train": int(
184
+ self.metadata["num_train"]
185
+ ),
186
+ }
187
+
188
+ for key, value in train_metrics.items():
189
+ metrics[f"train_{key}"] = value
190
+
191
+ self.log.info(
192
+ "[Client %s] fit completed: %s",
193
+ self.cid,
194
+ metrics,
195
+ )
196
+
197
+ return FitRes(
198
+ status=Status(
199
+ code=Code.OK,
200
+ message="Success",
201
+ ),
202
+ parameters=booster_to_parameters(
203
+ self.booster
204
+ ),
205
+ num_examples=int(
206
+ self.metadata["num_train"]
207
+ ),
208
+ metrics=metrics,
209
+ )
210
+
211
+ def evaluate(
212
+ self,
213
+ ins: EvaluateIns,
214
+ ) -> EvaluateRes:
215
+ received_booster = parameters_to_booster(
216
+ ins.parameters
217
+ )
218
+
219
+ if received_booster is not None:
220
+ self.booster = received_booster
221
+
222
+ evaluation_matrix = (
223
+ self.dtest
224
+ if self.dtest is not None
225
+ else self.dval
226
+ )
227
+
228
+ if (
229
+ self.booster is None
230
+ or evaluation_matrix is None
231
+ ):
232
+ return EvaluateRes(
233
+ status=Status(
234
+ code=Code.EVALUATE_NOT_IMPLEMENTED,
235
+ message=(
236
+ "No booster or evaluation data."
237
+ ),
238
+ ),
239
+ loss=0.0,
240
+ num_examples=0,
241
+ metrics={},
242
+ )
243
+
244
+ predictions = self.booster.predict(
245
+ evaluation_matrix
246
+ )
247
+
248
+ eval_metrics = evaluate_predictions(
249
+ task=self.task,
250
+ y_true=evaluation_matrix.get_label(),
251
+ y_pred_or_prob=predictions,
252
+ threshold=self.threshold,
253
+ num_features=int(
254
+ self.metadata["num_features"]
255
+ ),
256
+ )
257
+
258
+ metrics = {
259
+ "backend": "xgboost",
260
+ "task": self.task,
261
+ "cid": self.cid,
262
+ }
263
+
264
+ for key, value in eval_metrics.items():
265
+ metrics[f"eval_{key}"] = value
266
+
267
+ if self.task == "binary":
268
+ loss = float(
269
+ eval_metrics.get("logloss", 0.0)
270
+ )
271
+ elif self.task == "regression":
272
+ loss = float(
273
+ eval_metrics.get("rmse", 0.0)
274
+ )
275
+ else:
276
+ loss = float(
277
+ eval_metrics.get("mlogloss", 0.0)
278
+ )
279
+
280
+ self.log.info(
281
+ "[Client %s] evaluation completed: "
282
+ "loss=%s metrics=%s",
283
+ self.cid,
284
+ loss,
285
+ metrics,
286
+ )
287
+
288
+ return EvaluateRes(
289
+ status=Status(
290
+ code=Code.OK,
291
+ message="Success",
292
+ ),
293
+ loss=loss,
294
+ num_examples=int(
295
+ evaluation_matrix.num_row()
296
+ ),
297
+ metrics=metrics,
298
+ )
299
+
300
+ def get_properties(
301
+ self,
302
+ ins: GetPropertiesIns,
303
+ ) -> GetPropertiesRes:
304
+ properties = {
305
+ "backend": "xgboost",
306
+ "cid": self.cid,
307
+ "task": self.task,
308
+ "num_train": int(
309
+ self.metadata["num_train"]
310
+ ),
311
+ "num_val": int(
312
+ self.metadata["num_val"]
313
+ ),
314
+ "num_test": int(
315
+ self.metadata["num_test"]
316
+ ),
317
+ "num_features": int(
318
+ self.metadata["num_features"]
319
+ ),
320
+ }
321
+
322
+ return GetPropertiesRes(
323
+ status=Status(
324
+ code=Code.OK,
325
+ message="Success",
326
+ ),
327
+ properties=properties,
328
+ )
@@ -0,0 +1,63 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict, Optional
3
+
4
+
5
+ @dataclass
6
+ class XGBoostConfig:
7
+ task: str = "binary"
8
+ mode: str = "bagging"
9
+ local_num_boost_round: int = 10
10
+ params: Dict[str, Any] = field(default_factory=dict)
11
+ num_classes: Optional[int] = None
12
+
13
+ def build_params(self) -> Dict[str, Any]:
14
+ if self.task == "binary":
15
+ defaults = {
16
+ "objective": "binary:logistic",
17
+ "eval_metric": "auc",
18
+ "tree_method": "hist",
19
+ "max_depth": 6,
20
+ "eta": 0.1,
21
+ "subsample": 1.0,
22
+ "colsample_bytree": 1.0,
23
+ }
24
+
25
+ elif self.task == "regression":
26
+ defaults = {
27
+ "objective": "reg:squarederror",
28
+ "eval_metric": "rmse",
29
+ "tree_method": "hist",
30
+ "max_depth": 6,
31
+ "eta": 0.1,
32
+ "subsample": 1.0,
33
+ "colsample_bytree": 1.0,
34
+ }
35
+
36
+ elif self.task == "multiclass":
37
+ if self.num_classes is None or self.num_classes < 2:
38
+ raise ValueError(
39
+ "num_classes must be provided and >= 2 "
40
+ "for multiclass XGBoost."
41
+ )
42
+
43
+ defaults = {
44
+ "objective": "multi:softprob",
45
+ "num_class": self.num_classes,
46
+ "eval_metric": "mlogloss",
47
+ "tree_method": "hist",
48
+ "max_depth": 6,
49
+ "eta": 0.1,
50
+ "subsample": 1.0,
51
+ "colsample_bytree": 1.0,
52
+ }
53
+
54
+ else:
55
+ raise ValueError(
56
+ f"Unsupported XGBoost task: {self.task}. "
57
+ "Expected binary, regression, or multiclass."
58
+ )
59
+
60
+ merged = defaults.copy()
61
+ merged.update(self.params or {})
62
+
63
+ return merged
@@ -0,0 +1,122 @@
1
+ from typing import List, Optional, Tuple
2
+
3
+ import numpy as np
4
+ import torch
5
+ import xgboost as xgb
6
+ from torch.utils.data import DataLoader
7
+
8
+
9
+ def dataloader_to_numpy(
10
+ loader: DataLoader,
11
+ ) -> Tuple[np.ndarray, np.ndarray]:
12
+ if loader is None:
13
+ raise ValueError("DataLoader cannot be None.")
14
+
15
+ x_batches = []
16
+ y_batches = []
17
+
18
+ for x_batch, y_batch in loader:
19
+ if isinstance(x_batch, torch.Tensor):
20
+ x_batch = x_batch.detach().cpu().numpy()
21
+
22
+ if isinstance(y_batch, torch.Tensor):
23
+ y_batch = y_batch.detach().cpu().numpy()
24
+
25
+ x_batch = np.asarray(x_batch)
26
+ y_batch = np.asarray(y_batch).reshape(-1)
27
+
28
+ x_batches.append(x_batch)
29
+ y_batches.append(y_batch)
30
+
31
+ if not x_batches:
32
+ raise ValueError(
33
+ "Cannot convert an empty DataLoader to DMatrix."
34
+ )
35
+
36
+ x = np.concatenate(x_batches, axis=0)
37
+ y = np.concatenate(y_batches, axis=0)
38
+
39
+ if x.ndim != 2:
40
+ raise ValueError(
41
+ "XGBoost tabular simulation expects a two-dimensional "
42
+ f"feature matrix. Received shape={x.shape}."
43
+ )
44
+
45
+ return x, y
46
+
47
+
48
+ def dataloader_to_dmatrix(
49
+ loader: Optional[DataLoader],
50
+ feature_names: Optional[List[str]] = None,
51
+ ) -> Optional[xgb.DMatrix]:
52
+ if loader is None:
53
+ return None
54
+
55
+ try:
56
+ if len(loader.dataset) == 0:
57
+ return None
58
+ except TypeError:
59
+ pass
60
+
61
+ x, y = dataloader_to_numpy(loader)
62
+
63
+ if feature_names is not None:
64
+ if len(feature_names) != x.shape[1]:
65
+ raise ValueError(
66
+ "feature_names length does not match the number "
67
+ f"of features: {len(feature_names)} != {x.shape[1]}"
68
+ )
69
+
70
+ return xgb.DMatrix(
71
+ data=x,
72
+ label=y,
73
+ feature_names=feature_names,
74
+ )
75
+
76
+
77
+ class SimulationXGBoostDataModule:
78
+ def __init__(
79
+ self,
80
+ trainloader: DataLoader,
81
+ valloader: Optional[DataLoader] = None,
82
+ testloader: Optional[DataLoader] = None,
83
+ feature_names: Optional[List[str]] = None,
84
+ ):
85
+ self.dtrain = dataloader_to_dmatrix(
86
+ trainloader,
87
+ feature_names,
88
+ )
89
+
90
+ self.dval = dataloader_to_dmatrix(
91
+ valloader,
92
+ feature_names,
93
+ )
94
+
95
+ self.dtest = dataloader_to_dmatrix(
96
+ testloader,
97
+ feature_names,
98
+ )
99
+
100
+ self.metadata = {
101
+ "num_train": (
102
+ self.dtrain.num_row()
103
+ if self.dtrain is not None
104
+ else 0
105
+ ),
106
+ "num_val": (
107
+ self.dval.num_row()
108
+ if self.dval is not None
109
+ else 0
110
+ ),
111
+ "num_test": (
112
+ self.dtest.num_row()
113
+ if self.dtest is not None
114
+ else 0
115
+ ),
116
+ "num_features": (
117
+ self.dtrain.num_col()
118
+ if self.dtrain is not None
119
+ else 0
120
+ ),
121
+ "feature_names": feature_names or [],
122
+ }
@@ -0,0 +1,192 @@
1
+ import math
2
+ from typing import Any, Dict
3
+
4
+ import numpy as np
5
+ from sklearn.metrics import (
6
+ accuracy_score,
7
+ f1_score,
8
+ log_loss,
9
+ mean_absolute_error,
10
+ mean_squared_error,
11
+ r2_score,
12
+ roc_auc_score,
13
+ )
14
+
15
+
16
+ def _safe_float(value: Any) -> float:
17
+ try:
18
+ value = float(value)
19
+
20
+ if math.isnan(value) or math.isinf(value):
21
+ return float("nan")
22
+
23
+ return value
24
+ except Exception:
25
+ return float("nan")
26
+
27
+
28
+ def binary_metrics(
29
+ y_true,
30
+ y_prob,
31
+ threshold: float = 0.5,
32
+ ) -> Dict[str, float]:
33
+ y_true = np.asarray(y_true)
34
+ y_prob = np.asarray(y_prob)
35
+ y_pred = (y_prob >= threshold).astype(int)
36
+
37
+ metrics = {
38
+ "accuracy": _safe_float(
39
+ accuracy_score(y_true, y_pred)
40
+ ),
41
+ }
42
+
43
+ try:
44
+ metrics["auc"] = _safe_float(
45
+ roc_auc_score(y_true, y_prob)
46
+ )
47
+ except Exception:
48
+ metrics["auc"] = float("nan")
49
+
50
+ try:
51
+ metrics["logloss"] = _safe_float(
52
+ log_loss(y_true, y_prob)
53
+ )
54
+ except Exception:
55
+ metrics["logloss"] = float("nan")
56
+
57
+ return metrics
58
+
59
+
60
+ def regression_metrics(
61
+ y_true,
62
+ y_pred,
63
+ num_features: int = 0,
64
+ ) -> Dict[str, float]:
65
+ y_true = np.asarray(y_true)
66
+ y_pred = np.asarray(y_pred)
67
+
68
+ rmse = mean_squared_error(
69
+ y_true,
70
+ y_pred,
71
+ squared=False,
72
+ )
73
+ mae = mean_absolute_error(y_true, y_pred)
74
+ r2 = r2_score(y_true, y_pred)
75
+
76
+ n = len(y_true)
77
+ p = int(num_features)
78
+
79
+ if n > p + 1:
80
+ adjusted_r2 = (
81
+ 1.0
82
+ - (1.0 - r2)
83
+ * ((n - 1.0) / (n - p - 1.0))
84
+ )
85
+ else:
86
+ adjusted_r2 = float("nan")
87
+
88
+ return {
89
+ "rmse": _safe_float(rmse),
90
+ "mae": _safe_float(mae),
91
+ "r2": _safe_float(r2),
92
+ "adjusted_r2": _safe_float(adjusted_r2),
93
+ }
94
+
95
+
96
+ def multiclass_metrics(
97
+ y_true,
98
+ y_prob,
99
+ ) -> Dict[str, float]:
100
+ y_true = np.asarray(y_true)
101
+ y_prob = np.asarray(y_prob)
102
+ y_pred = np.argmax(y_prob, axis=1)
103
+
104
+ metrics = {
105
+ "accuracy": _safe_float(
106
+ accuracy_score(y_true, y_pred)
107
+ ),
108
+ }
109
+
110
+ try:
111
+ metrics["macro_f1"] = _safe_float(
112
+ f1_score(
113
+ y_true,
114
+ y_pred,
115
+ average="macro",
116
+ )
117
+ )
118
+ except Exception:
119
+ metrics["macro_f1"] = float("nan")
120
+
121
+ try:
122
+ metrics["mlogloss"] = _safe_float(
123
+ log_loss(y_true, y_prob)
124
+ )
125
+ except Exception:
126
+ metrics["mlogloss"] = float("nan")
127
+
128
+ return metrics
129
+
130
+
131
+ def evaluate_predictions(
132
+ task: str,
133
+ y_true,
134
+ y_pred_or_prob,
135
+ threshold: float = 0.5,
136
+ num_features: int = 0,
137
+ ) -> Dict[str, float]:
138
+ if task == "binary":
139
+ return binary_metrics(
140
+ y_true,
141
+ y_pred_or_prob,
142
+ threshold=threshold,
143
+ )
144
+
145
+ if task == "regression":
146
+ return regression_metrics(
147
+ y_true,
148
+ y_pred_or_prob,
149
+ num_features=num_features,
150
+ )
151
+
152
+ if task == "multiclass":
153
+ return multiclass_metrics(
154
+ y_true,
155
+ y_pred_or_prob,
156
+ )
157
+
158
+ raise ValueError(f"Unsupported task: {task}")
159
+
160
+
161
+ def aggregate_weighted_metrics(results):
162
+ aggregated = {}
163
+
164
+ keys = set()
165
+ for _, metrics in results:
166
+ keys.update(metrics.keys())
167
+
168
+ for key in keys:
169
+ weighted_sum = 0.0
170
+ contributing_examples = 0
171
+
172
+ for num_examples, metrics in results:
173
+ if key not in metrics:
174
+ continue
175
+
176
+ try:
177
+ value = float(metrics[key])
178
+ except Exception:
179
+ continue
180
+
181
+ if math.isnan(value) or math.isinf(value):
182
+ continue
183
+
184
+ weighted_sum += num_examples * value
185
+ contributing_examples += num_examples
186
+
187
+ if contributing_examples > 0:
188
+ aggregated[key] = (
189
+ weighted_sum / contributing_examples
190
+ )
191
+
192
+ return aggregated