autoemulate 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autoemulate/__init__.py +0 -0
- autoemulate/compare.py +503 -0
- autoemulate/cross_validate.py +97 -0
- autoemulate/data_splitting.py +30 -0
- autoemulate/datasets.py +48 -0
- autoemulate/emulators/__init__.py +27 -0
- autoemulate/emulators/gaussian_process.py +129 -0
- autoemulate/emulators/gaussian_process_mogp.py +90 -0
- autoemulate/emulators/gradient_boosting.py +138 -0
- autoemulate/emulators/light_gbm.py +144 -0
- autoemulate/emulators/neural_net_sk.py +163 -0
- autoemulate/emulators/neural_net_torch.py +192 -0
- autoemulate/emulators/neural_networks/__init__.py +2 -0
- autoemulate/emulators/neural_networks/base.py +36 -0
- autoemulate/emulators/neural_networks/get_module.py +20 -0
- autoemulate/emulators/neural_networks/mlp.py +72 -0
- autoemulate/emulators/neural_networks/rbf.py +346 -0
- autoemulate/emulators/polynomials.py +96 -0
- autoemulate/emulators/random_forest.py +157 -0
- autoemulate/emulators/rbf.py +152 -0
- autoemulate/emulators/support_vector_machines.py +168 -0
- autoemulate/experimental_design.py +91 -0
- autoemulate/hyperparam_searching.py +174 -0
- autoemulate/logging_config.py +75 -0
- autoemulate/metrics.py +36 -0
- autoemulate/model_processing.py +148 -0
- autoemulate/plotting.py +340 -0
- autoemulate/printing.py +156 -0
- autoemulate/save.py +107 -0
- autoemulate/simulations/__init__.py +0 -0
- autoemulate/simulations/epidemic.py +55 -0
- autoemulate/simulations/projectile.py +223 -0
- autoemulate/utils.py +473 -0
- autoemulate-0.1.0.dist-info/LICENSE +21 -0
- autoemulate-0.1.0.dist-info/METADATA +115 -0
- autoemulate-0.1.0.dist-info/RECORD +37 -0
- autoemulate-0.1.0.dist-info/WHEEL +4 -0
autoemulate/__init__.py
ADDED
|
File without changes
|
autoemulate/compare.py
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from sklearn.base import BaseEstimator
|
|
5
|
+
from sklearn.decomposition import PCA
|
|
6
|
+
from sklearn.metrics import make_scorer
|
|
7
|
+
from sklearn.model_selection import cross_validate
|
|
8
|
+
from sklearn.model_selection import KFold
|
|
9
|
+
from sklearn.model_selection import PredefinedSplit
|
|
10
|
+
from sklearn.model_selection import train_test_split
|
|
11
|
+
from sklearn.pipeline import Pipeline
|
|
12
|
+
from sklearn.preprocessing import StandardScaler
|
|
13
|
+
from sklearn.utils.validation import check_is_fitted
|
|
14
|
+
from sklearn.utils.validation import check_X_y
|
|
15
|
+
from tqdm.autonotebook import tqdm
|
|
16
|
+
|
|
17
|
+
from autoemulate.cross_validate import _run_cv
|
|
18
|
+
from autoemulate.cross_validate import _update_scores_df
|
|
19
|
+
from autoemulate.data_splitting import _split_data
|
|
20
|
+
from autoemulate.emulators import MODEL_REGISTRY
|
|
21
|
+
from autoemulate.hyperparam_searching import _optimize_params
|
|
22
|
+
from autoemulate.logging_config import _configure_logging
|
|
23
|
+
from autoemulate.metrics import METRIC_REGISTRY
|
|
24
|
+
from autoemulate.model_processing import _get_and_process_models
|
|
25
|
+
from autoemulate.plotting import _plot_model
|
|
26
|
+
from autoemulate.plotting import _plot_results
|
|
27
|
+
from autoemulate.printing import _print_cv_results
|
|
28
|
+
from autoemulate.printing import _print_model_names
|
|
29
|
+
from autoemulate.printing import _print_setup
|
|
30
|
+
from autoemulate.save import ModelSerialiser
|
|
31
|
+
from autoemulate.utils import _get_full_model_name
|
|
32
|
+
from autoemulate.utils import _get_model_names_dict
|
|
33
|
+
from autoemulate.utils import _redirect_warnings
|
|
34
|
+
from autoemulate.utils import get_mean_scores
|
|
35
|
+
from autoemulate.utils import get_model_name
|
|
36
|
+
from autoemulate.utils import get_short_model_name
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AutoEmulate:
|
|
40
|
+
"""
|
|
41
|
+
The AutoEmulate class is the main class of the AutoEmulate package. It is used to set up and compare
|
|
42
|
+
different emulator models on a given dataset. It can also be used to save and load models, and to
|
|
43
|
+
print and plot the results of the comparison.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
"""Initializes an AutoEmulate object."""
|
|
48
|
+
self.X = None
|
|
49
|
+
self.y = None
|
|
50
|
+
self.is_set_up = False
|
|
51
|
+
|
|
52
|
+
def setup(
|
|
53
|
+
self,
|
|
54
|
+
X,
|
|
55
|
+
y,
|
|
56
|
+
param_search=False,
|
|
57
|
+
param_search_type="random",
|
|
58
|
+
param_search_iters=20,
|
|
59
|
+
test_set_size=0.2,
|
|
60
|
+
scale=True,
|
|
61
|
+
scaler=StandardScaler(),
|
|
62
|
+
reduce_dim=False,
|
|
63
|
+
dim_reducer=PCA(),
|
|
64
|
+
cross_validator=KFold(n_splits=5, shuffle=True),
|
|
65
|
+
n_jobs=None,
|
|
66
|
+
model_subset=None,
|
|
67
|
+
verbose=0,
|
|
68
|
+
log_to_file=False,
|
|
69
|
+
):
|
|
70
|
+
"""Sets up the automatic emulation.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
X : array-like, shape (n_samples, n_features)
|
|
75
|
+
Simulation input.
|
|
76
|
+
y : array-like, shape (n_samples, n_outputs)
|
|
77
|
+
Simulation output.
|
|
78
|
+
param_search : bool
|
|
79
|
+
Whether to perform hyperparameter search over predifined parameter grids.
|
|
80
|
+
param_search_type : str
|
|
81
|
+
Type of hyperparameter search to perform. Can be "grid", "random", or "bayes".
|
|
82
|
+
param_search_iters : int
|
|
83
|
+
Number of parameter settings that are sampled. Only used if
|
|
84
|
+
param_search=True and param_search_type="random".
|
|
85
|
+
scale : bool, default=True
|
|
86
|
+
Whether to scale the data before fitting the models using a scaler.
|
|
87
|
+
scaler : sklearn.preprocessing.StandardScaler
|
|
88
|
+
Scaler to use. Defaults to StandardScaler. Can be any sklearn scaler.
|
|
89
|
+
reduce_dim : bool, default=False
|
|
90
|
+
Whether to reduce the dimensionality of the data before fitting the models.
|
|
91
|
+
dim_reducer : sklearn.decomposition object
|
|
92
|
+
Dimensionality reduction method to use. Can be any method in `sklearn.decomposition`
|
|
93
|
+
with an `n_components` parameter. Defaults to PCA. Specify n_components like so:
|
|
94
|
+
`dim_reducer=PCA(n_components=2)` for choosing 2 principal components, or
|
|
95
|
+
`dim_reducer=PCA(n_components=0.95)` for choosing the number of components that
|
|
96
|
+
explain 95% of the variance. Other methods can have slightly different n_components
|
|
97
|
+
parameter inputs, see the sklearn documentation for more details. Dimension reduction
|
|
98
|
+
is always performed after scaling.
|
|
99
|
+
cross_validator : sklearn.model_selection object
|
|
100
|
+
Cross-validation strategy to use. Defaults to KFold with 5 splits and shuffle=True.
|
|
101
|
+
Can be any object in `sklearn.model_selection` that generates train/test indices.
|
|
102
|
+
n_jobs : int
|
|
103
|
+
Number of jobs to run in parallel. `None` means 1, `-1` means using all processors.
|
|
104
|
+
model_subset : list
|
|
105
|
+
List of models to use. If None, uses all models in MODEL_REGISTRY.
|
|
106
|
+
verbose : int
|
|
107
|
+
Verbosity level. Can be 0, 1, or 2.
|
|
108
|
+
log_to_file : bool
|
|
109
|
+
Whether to log to file.
|
|
110
|
+
"""
|
|
111
|
+
self.X, self.y = self._check_input(X, y)
|
|
112
|
+
self.train_idxs, self.test_idxs = _split_data(
|
|
113
|
+
self.X, test_size=test_set_size, random_state=42
|
|
114
|
+
)
|
|
115
|
+
self.model_names = _get_model_names_dict(MODEL_REGISTRY, model_subset)
|
|
116
|
+
self.models = _get_and_process_models(
|
|
117
|
+
MODEL_REGISTRY,
|
|
118
|
+
model_subset=list(self.model_names.keys()),
|
|
119
|
+
y=self.y,
|
|
120
|
+
scale=scale,
|
|
121
|
+
scaler=scaler,
|
|
122
|
+
reduce_dim=reduce_dim,
|
|
123
|
+
dim_reducer=dim_reducer,
|
|
124
|
+
)
|
|
125
|
+
self.metrics = self._get_metrics(METRIC_REGISTRY)
|
|
126
|
+
self.cross_validator = cross_validator
|
|
127
|
+
self.param_search = param_search
|
|
128
|
+
self.search_type = param_search_type
|
|
129
|
+
self.param_search_iters = param_search_iters
|
|
130
|
+
self.scale = scale
|
|
131
|
+
self.scaler = scaler
|
|
132
|
+
self.n_jobs = n_jobs
|
|
133
|
+
self.logger = _configure_logging(log_to_file, verbose)
|
|
134
|
+
self.is_set_up = True
|
|
135
|
+
self.dim_reducer = dim_reducer
|
|
136
|
+
self.reduce_dim = reduce_dim
|
|
137
|
+
self.cv_results = {}
|
|
138
|
+
|
|
139
|
+
self.print_setup()
|
|
140
|
+
|
|
141
|
+
def _check_input(self, X, y):
|
|
142
|
+
"""Checks and possibly converts the input data.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
X : array-like, shape (n_samples, n_features)
|
|
147
|
+
Simulation input.
|
|
148
|
+
y : array-like, shape (n_samples, n_outputs)
|
|
149
|
+
Simulation output.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
X_converted : array-like, shape (n_samples, n_features)
|
|
154
|
+
Simulation input.
|
|
155
|
+
y_converted : array-like, shape (n_samples, n_outputs)
|
|
156
|
+
Simulation output.
|
|
157
|
+
"""
|
|
158
|
+
X, y = check_X_y(X, y, multi_output=True, y_numeric=True, dtype="float32")
|
|
159
|
+
y = y.astype("float32") # needed for pytorch models
|
|
160
|
+
return X, y
|
|
161
|
+
|
|
162
|
+
def _get_metrics(self, METRIC_REGISTRY):
|
|
163
|
+
"""
|
|
164
|
+
Get metrics from REGISTRY
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
METRIC_REGISTRY : dict
|
|
169
|
+
Registry of metrics.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
List[Callable]
|
|
174
|
+
List of metric functions.
|
|
175
|
+
"""
|
|
176
|
+
return [metric for metric in METRIC_REGISTRY.values()]
|
|
177
|
+
|
|
178
|
+
def compare(self):
|
|
179
|
+
"""Compares the emulator models on the data. self.setup() must be run first.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
scores_df : pandas.DataFrame
|
|
184
|
+
Dataframe containing the scores for each model, metric and fold.
|
|
185
|
+
"""
|
|
186
|
+
if not self.is_set_up:
|
|
187
|
+
raise RuntimeError("Must run setup() before compare()")
|
|
188
|
+
|
|
189
|
+
# Freshly initialise scores dataframe when running compare()
|
|
190
|
+
self.scores_df = pd.DataFrame(
|
|
191
|
+
columns=["model", "short", "metric", "fold", "score"]
|
|
192
|
+
).astype(
|
|
193
|
+
{
|
|
194
|
+
"model": "object",
|
|
195
|
+
"short": "object",
|
|
196
|
+
"metric": "object",
|
|
197
|
+
"fold": "int64",
|
|
198
|
+
"score": "float64",
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if self.param_search:
|
|
203
|
+
pb_text = "Optimising and cross-validating"
|
|
204
|
+
else:
|
|
205
|
+
pb_text = "Cross-validating"
|
|
206
|
+
|
|
207
|
+
with tqdm(total=len(self.models), desc="Initializing") as pbar:
|
|
208
|
+
for i, model in enumerate(self.models):
|
|
209
|
+
model_name = get_model_name(model)
|
|
210
|
+
pbar.set_description(f"{pb_text} {model_name}")
|
|
211
|
+
|
|
212
|
+
with _redirect_warnings(self.logger):
|
|
213
|
+
try:
|
|
214
|
+
# hyperparameter search
|
|
215
|
+
if self.param_search:
|
|
216
|
+
self.models[i] = _optimize_params(
|
|
217
|
+
X=self.X[self.train_idxs],
|
|
218
|
+
y=self.y[self.train_idxs],
|
|
219
|
+
cv=self.cross_validator,
|
|
220
|
+
model=model,
|
|
221
|
+
search_type=self.search_type,
|
|
222
|
+
niter=self.param_search_iters,
|
|
223
|
+
param_space=None,
|
|
224
|
+
n_jobs=self.n_jobs,
|
|
225
|
+
logger=self.logger,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# run cross validation
|
|
229
|
+
fitted_model, cv_results = _run_cv(
|
|
230
|
+
X=self.X[self.train_idxs],
|
|
231
|
+
y=self.y[self.train_idxs],
|
|
232
|
+
cv=self.cross_validator,
|
|
233
|
+
model=model,
|
|
234
|
+
metrics=self.metrics,
|
|
235
|
+
n_jobs=self.n_jobs,
|
|
236
|
+
logger=self.logger,
|
|
237
|
+
)
|
|
238
|
+
except Exception:
|
|
239
|
+
self.logger.exception(
|
|
240
|
+
f"Error cross-validating model {model_name}"
|
|
241
|
+
)
|
|
242
|
+
continue
|
|
243
|
+
finally:
|
|
244
|
+
pbar.update(1)
|
|
245
|
+
|
|
246
|
+
self.models[i] = fitted_model
|
|
247
|
+
self.cv_results[model_name] = cv_results
|
|
248
|
+
|
|
249
|
+
# update scores dataframe
|
|
250
|
+
self.scores_df = _update_scores_df(
|
|
251
|
+
self.scores_df,
|
|
252
|
+
model_name,
|
|
253
|
+
self.cv_results[model_name],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# get best model
|
|
257
|
+
best_model_name, self.best_model = self.get_model(
|
|
258
|
+
rank=1, metric="r2", name=True
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return self.best_model
|
|
262
|
+
|
|
263
|
+
def get_model(self, rank=1, metric="r2", name=False):
|
|
264
|
+
"""Get a fitted model based on it's rank in the comparison.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
rank : int
|
|
269
|
+
Rank of the model to return. Defaults to 1, which is the best model, 2 is the second best, etc.
|
|
270
|
+
metric : str
|
|
271
|
+
Metric to use for determining the best model.
|
|
272
|
+
name : bool
|
|
273
|
+
If True, returns tuple of model name and model. If False, returns only the model.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
model : object
|
|
278
|
+
Model fitted on full data.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
if not hasattr(self, "scores_df"):
|
|
282
|
+
raise RuntimeError("Must run compare() before get_model()")
|
|
283
|
+
|
|
284
|
+
# get average scores across folds
|
|
285
|
+
means = get_mean_scores(self.scores_df, metric)
|
|
286
|
+
# get model by rank
|
|
287
|
+
if (rank > len(means)) or (rank < 1):
|
|
288
|
+
raise RuntimeError(f"Rank must be >= 1 and <= {len(means)}")
|
|
289
|
+
chosen_model_name = means.iloc[rank - 1]["model"]
|
|
290
|
+
|
|
291
|
+
# get best model:
|
|
292
|
+
for model in self.models:
|
|
293
|
+
if get_model_name(model) == chosen_model_name:
|
|
294
|
+
chosen_model = model
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
# check whether the model is fitted
|
|
298
|
+
check_is_fitted(chosen_model)
|
|
299
|
+
|
|
300
|
+
if name:
|
|
301
|
+
return chosen_model_name, chosen_model
|
|
302
|
+
return chosen_model
|
|
303
|
+
|
|
304
|
+
def refit_model(self, model):
|
|
305
|
+
"""Refits a model on the full data.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
model : object
|
|
310
|
+
Usually a fitted model.
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
model : object
|
|
315
|
+
Refitted model.
|
|
316
|
+
"""
|
|
317
|
+
if not hasattr(self, "X"):
|
|
318
|
+
raise RuntimeError("Must run setup() before refit_model()")
|
|
319
|
+
|
|
320
|
+
model.fit(self.X, self.y)
|
|
321
|
+
return model
|
|
322
|
+
|
|
323
|
+
def refit_models(self):
|
|
324
|
+
"""(Re-) fits all models on the full data.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
models : dict
|
|
329
|
+
dict with refitted models.
|
|
330
|
+
"""
|
|
331
|
+
if not hasattr(self, "X"):
|
|
332
|
+
raise RuntimeError("Must run setup() before refit_models()")
|
|
333
|
+
for i in range(len(self.models)):
|
|
334
|
+
self.models[i] = self.refit_model(self.models[i])
|
|
335
|
+
return self.models
|
|
336
|
+
|
|
337
|
+
def save_model(self, model=None, path=None):
|
|
338
|
+
"""Saves model to disk.
|
|
339
|
+
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
model : object, optional
|
|
343
|
+
Model to save. If None, saves the best model.
|
|
344
|
+
If "all", saves all models.
|
|
345
|
+
path : str
|
|
346
|
+
Path to save the model.
|
|
347
|
+
"""
|
|
348
|
+
if not hasattr(self, "best_model"):
|
|
349
|
+
raise RuntimeError("Must run compare() before save_model()")
|
|
350
|
+
serialiser = ModelSerialiser()
|
|
351
|
+
|
|
352
|
+
if model is None or not isinstance(model, (Pipeline, BaseEstimator)):
|
|
353
|
+
raise ValueError(
|
|
354
|
+
"Model must be provided and should be a scikit-learn pipeline or model"
|
|
355
|
+
)
|
|
356
|
+
serialiser._save_model(model, path)
|
|
357
|
+
|
|
358
|
+
def save_models(self, path=None):
|
|
359
|
+
"""Saves all models to disk.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
path : str
|
|
364
|
+
Directory to save the models.
|
|
365
|
+
If None, saves to the current working directory.
|
|
366
|
+
"""
|
|
367
|
+
if not hasattr(self, "best_model"):
|
|
368
|
+
raise RuntimeError("Must run compare() before save_models()")
|
|
369
|
+
serialiser = ModelSerialiser()
|
|
370
|
+
serialiser._save_models(self.models, path)
|
|
371
|
+
|
|
372
|
+
def load_model(self, path=None):
|
|
373
|
+
"""Loads a model from disk."""
|
|
374
|
+
serialiser = ModelSerialiser()
|
|
375
|
+
if path is None:
|
|
376
|
+
raise ValueError("Filepath must be provided")
|
|
377
|
+
|
|
378
|
+
return serialiser._load_model(path)
|
|
379
|
+
|
|
380
|
+
def print_model_names(self):
|
|
381
|
+
"""Print available models"""
|
|
382
|
+
_print_model_names(self)
|
|
383
|
+
|
|
384
|
+
def print_setup(self):
|
|
385
|
+
"""Print the setup of the AutoEmulate object."""
|
|
386
|
+
_print_setup(self)
|
|
387
|
+
|
|
388
|
+
def print_results(self, model=None, sort_by="r2"):
|
|
389
|
+
"""Print cv results.
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
model : str, optional
|
|
394
|
+
The name of the model to print. If None, the best fold from each model will be printed.
|
|
395
|
+
If a model name is provided, the scores for that model across all folds will be printed.
|
|
396
|
+
sort_by : str, optional
|
|
397
|
+
The metric to sort by. Default is "r2", can also be "rmse".
|
|
398
|
+
"""
|
|
399
|
+
model_name = (
|
|
400
|
+
_get_full_model_name(model, self.model_names) if model is not None else None
|
|
401
|
+
)
|
|
402
|
+
_print_cv_results(
|
|
403
|
+
self.models,
|
|
404
|
+
self.scores_df,
|
|
405
|
+
model_name=model_name,
|
|
406
|
+
sort_by=sort_by,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def plot_results(
|
|
410
|
+
self,
|
|
411
|
+
model=None,
|
|
412
|
+
plot="standard",
|
|
413
|
+
n_cols=3,
|
|
414
|
+
figsize=None,
|
|
415
|
+
output_index=0,
|
|
416
|
+
):
|
|
417
|
+
"""Plots the results of the cross-validation.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
model : str
|
|
422
|
+
Name of the model to plot. If None, plots best folds of each models.
|
|
423
|
+
If a model name is specified, plots all folds of that model.
|
|
424
|
+
plot_type : str, optional
|
|
425
|
+
The type of plot to draw:
|
|
426
|
+
“standard” draws the observed values (y-axis) vs. the predicted values (x-axis) (default).
|
|
427
|
+
“residual” draws the residuals, i.e. difference between observed and predicted values, (y-axis) vs. the predicted values (x-axis).
|
|
428
|
+
n_cols : int
|
|
429
|
+
Number of columns in the plot grid.
|
|
430
|
+
figsize : tuple, optional
|
|
431
|
+
Overrides the default figure size.
|
|
432
|
+
output_index : int
|
|
433
|
+
Index of the output to plot. Default is 0.
|
|
434
|
+
"""
|
|
435
|
+
model_name = (
|
|
436
|
+
_get_full_model_name(model, self.model_names) if model is not None else None
|
|
437
|
+
)
|
|
438
|
+
_plot_results(
|
|
439
|
+
self.cv_results,
|
|
440
|
+
self.X,
|
|
441
|
+
self.y,
|
|
442
|
+
model_name=model_name,
|
|
443
|
+
n_cols=n_cols,
|
|
444
|
+
plot=plot,
|
|
445
|
+
figsize=figsize,
|
|
446
|
+
output_index=output_index,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def evaluate_model(self, model=None):
|
|
450
|
+
"""
|
|
451
|
+
Evaluates the model on the hold-out set.
|
|
452
|
+
|
|
453
|
+
Parameters
|
|
454
|
+
----------
|
|
455
|
+
model : object
|
|
456
|
+
Fitted model.
|
|
457
|
+
|
|
458
|
+
Returns
|
|
459
|
+
-------
|
|
460
|
+
scores_df : pandas.DataFrame
|
|
461
|
+
Dataframe containing the model scores on the test set.
|
|
462
|
+
"""
|
|
463
|
+
if model is None:
|
|
464
|
+
raise ValueError("Model must be provided")
|
|
465
|
+
|
|
466
|
+
y_pred = model.predict(self.X[self.test_idxs])
|
|
467
|
+
scores = {}
|
|
468
|
+
for metric in self.metrics:
|
|
469
|
+
scores[metric.__name__] = metric(self.y[self.test_idxs], y_pred)
|
|
470
|
+
|
|
471
|
+
scores_df = pd.concat(
|
|
472
|
+
[
|
|
473
|
+
pd.DataFrame({"model": [get_model_name(model)]}),
|
|
474
|
+
pd.DataFrame({"short": [get_short_model_name(model)]}),
|
|
475
|
+
pd.DataFrame(scores, index=[0]),
|
|
476
|
+
],
|
|
477
|
+
axis=1,
|
|
478
|
+
).round(3)
|
|
479
|
+
|
|
480
|
+
return scores_df
|
|
481
|
+
|
|
482
|
+
def plot_model(self, model, plot="standard", n_cols=2, figsize=None):
|
|
483
|
+
"""Plots the model predictions vs. the true values.
|
|
484
|
+
|
|
485
|
+
Parameters
|
|
486
|
+
----------
|
|
487
|
+
model : object
|
|
488
|
+
Fitted model.
|
|
489
|
+
plot : str, optional
|
|
490
|
+
The type of plot to draw:
|
|
491
|
+
“standard” draws the observed values (y-axis) vs. the predicted values (x-axis) (default).
|
|
492
|
+
“residual” draws the residuals, i.e. difference between observed and predicted values, (y-axis) vs. the predicted values (x-axis).
|
|
493
|
+
n_cols : int, optional
|
|
494
|
+
Number of columns in the plot grid for multi-output. Default is 2.
|
|
495
|
+
"""
|
|
496
|
+
_plot_model(
|
|
497
|
+
model,
|
|
498
|
+
self.X[self.test_idxs],
|
|
499
|
+
self.y[self.test_idxs],
|
|
500
|
+
plot,
|
|
501
|
+
n_cols,
|
|
502
|
+
figsize,
|
|
503
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from sklearn.metrics import make_scorer
|
|
6
|
+
from sklearn.model_selection import cross_validate
|
|
7
|
+
from sklearn.model_selection import PredefinedSplit
|
|
8
|
+
from sklearn.model_selection import train_test_split
|
|
9
|
+
|
|
10
|
+
from autoemulate.utils import get_model_name
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run_cv(X, y, cv, model, metrics, n_jobs, logger):
|
|
14
|
+
"""Runs cross-validation on a model.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
X : array-like
|
|
19
|
+
Features.
|
|
20
|
+
y : array-like
|
|
21
|
+
Target variable.
|
|
22
|
+
cv : int, cross-validation generator or an iterable, default=None
|
|
23
|
+
Determines the cross-validation splitting strategy.
|
|
24
|
+
model : scikit-learn model
|
|
25
|
+
Model to cross-validate.
|
|
26
|
+
metrics : list
|
|
27
|
+
List of metrics to use for cross-validation.
|
|
28
|
+
n_jobs : int
|
|
29
|
+
Number of jobs to run in parallel.
|
|
30
|
+
logger : logging.Logger
|
|
31
|
+
Logger object.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
fitted_model : scikit-learn model
|
|
36
|
+
Fitted model.
|
|
37
|
+
cv_results : dict
|
|
38
|
+
Results of the cross-validation.
|
|
39
|
+
"""
|
|
40
|
+
# The metrics we want to use for cross-validation
|
|
41
|
+
scorers = {metric.__name__: make_scorer(metric) for metric in metrics}
|
|
42
|
+
|
|
43
|
+
logger.info(f"Cross-validating {get_model_name(model)}...")
|
|
44
|
+
logger.info(f"Parameters: {model.named_steps['model'].get_params()}")
|
|
45
|
+
|
|
46
|
+
cv_results = None
|
|
47
|
+
try:
|
|
48
|
+
cv_results = cross_validate(
|
|
49
|
+
model,
|
|
50
|
+
X,
|
|
51
|
+
y,
|
|
52
|
+
cv=cv,
|
|
53
|
+
scoring=scorers,
|
|
54
|
+
n_jobs=n_jobs,
|
|
55
|
+
return_estimator=True,
|
|
56
|
+
return_indices=True,
|
|
57
|
+
)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.exception(f"Failed to cross-validate {get_model_name(model)}")
|
|
60
|
+
|
|
61
|
+
# refit the model on the whole dataset
|
|
62
|
+
fitted_model = model.fit(X, y)
|
|
63
|
+
|
|
64
|
+
return fitted_model, cv_results
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _update_scores_df(scores_df, model_name, cv_results):
|
|
68
|
+
"""Updates the scores dataframe with the results of the cross-validation.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
scores_df : pandas.DataFrame
|
|
73
|
+
DataFrame with columns "model", "metric", "fold", "score".
|
|
74
|
+
model_name : str
|
|
75
|
+
Name of the model.
|
|
76
|
+
cv_results : dict
|
|
77
|
+
Results of the cross-validation.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
None
|
|
82
|
+
Modifies the self.scores_df DataFrame in-place.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
# Gather scores from each metric
|
|
86
|
+
# Initialise scores dataframe
|
|
87
|
+
for key in cv_results.keys():
|
|
88
|
+
if key.startswith("test_"):
|
|
89
|
+
for fold, score in enumerate(cv_results[key]):
|
|
90
|
+
scores_df.loc[len(scores_df.index)] = {
|
|
91
|
+
"model": model_name,
|
|
92
|
+
"short": "".join(re.findall(r"[A-Z]", model_name)).lower(),
|
|
93
|
+
"metric": key.split("test_", 1)[1],
|
|
94
|
+
"fold": fold,
|
|
95
|
+
"score": score,
|
|
96
|
+
}
|
|
97
|
+
return scores_df
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from sklearn.model_selection import train_test_split
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _split_data(X, test_size=0.2, random_state=None):
|
|
6
|
+
"""Splits the data into training and testing sets.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
X : array-like, shape (n_samples, n_features)
|
|
11
|
+
Simulation input.
|
|
12
|
+
test_size : float, default=0.2
|
|
13
|
+
Proportion of the dataset to include in the test split.
|
|
14
|
+
random_state : int, RandomState instance or None, default=None
|
|
15
|
+
Controls the shuffling applied to the data before applying the split.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
train_idx : array-like
|
|
20
|
+
Indices of the training set.
|
|
21
|
+
test_idx : array-like
|
|
22
|
+
Indices of the testing set.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
idxs = np.arange(X.shape[0])
|
|
26
|
+
train_idxs, test_idxs = train_test_split(
|
|
27
|
+
idxs, test_size=test_size, random_state=random_state
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return train_idxs, test_idxs
|
autoemulate/datasets.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from sklearn.model_selection import train_test_split
|
|
6
|
+
|
|
7
|
+
data_dir = Path(__file__).parent.parent / "data"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fetch_data(dataset, split=False, test_size=0.2, random_state=42):
|
|
11
|
+
"""
|
|
12
|
+
Fetch a dataset by name.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
dataset : str
|
|
17
|
+
Dataset to load. Can be any of the following strings:
|
|
18
|
+
|
|
19
|
+
- **cardiac1**: ionic atrial cell model data from LH sampling.
|
|
20
|
+
- **cardiac2**: isotonic contraction ventricular cell model, no LH sampling.
|
|
21
|
+
- **cardiac3**: CircAdapt: four-chamber pressure and volume CircAdapt ODE model from LH sampling.
|
|
22
|
+
- **cardiac4**: four chamber: 3D-0D four-chamber electromechanics model to predict pressure and volume biomarkers for cardiac function.
|
|
23
|
+
- **cardiac5**: passive mechanics: inflated volumes and mean atrial and ventricular fiber strains for a passive inflation.
|
|
24
|
+
- **cardiac6**: tissue electrophysiology: predict total atrial and ventricular activation times with an Eikonal model.
|
|
25
|
+
- **climate1**: GENIE model: predict climate variables SAT, ACC, VEGC, SOILC, MAXPMOC, OCN_O2, fCaCO3, SIAREA_S.
|
|
26
|
+
- **engineering1**: Cantilever truss simulation.
|
|
27
|
+
split : bool, optional
|
|
28
|
+
Whether to split the data into training and testing sets. Default is False.
|
|
29
|
+
test_size : float, optional
|
|
30
|
+
The proportion of the dataset to include in the test split. Default is 0.2.
|
|
31
|
+
random_state : int, optional
|
|
32
|
+
Controls the shuffling applied to the data before applying the split. Default is 42.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
X : array-like
|
|
37
|
+
Simulation parameters / inputs.
|
|
38
|
+
y : array-like
|
|
39
|
+
Simulation outputs.
|
|
40
|
+
"""
|
|
41
|
+
data_dir_dataset = data_dir / dataset / "processed"
|
|
42
|
+
X = pd.read_csv(data_dir_dataset / "parameters.csv").to_numpy()
|
|
43
|
+
y = pd.read_csv(data_dir_dataset / "outputs.csv").to_numpy()
|
|
44
|
+
|
|
45
|
+
if split:
|
|
46
|
+
return train_test_split(X, y, test_size=test_size, random_state=random_state)
|
|
47
|
+
else:
|
|
48
|
+
return X, y
|