surrogates_interface 4.0.1__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.
- surrogates_interface/__init__.py +3 -0
- surrogates_interface/domains.py +107 -0
- surrogates_interface/openmdao.py +389 -0
- surrogates_interface/surrogates.py +1027 -0
- surrogates_interface/test/__init__.py +0 -0
- surrogates_interface/test/test_domains.py +44 -0
- surrogates_interface/test/test_init.py +12 -0
- surrogates_interface/test/test_pca_jacobian.py +137 -0
- surrogates_interface/test/test_smt.py +258 -0
- surrogates_interface/test/test_surrogates.py +22 -0
- surrogates_interface/test/test_tensorflow_and_openmdao.py +1220 -0
- surrogates_interface/test/test_transformers.py +617 -0
- surrogates_interface/transformers.py +816 -0
- surrogates_interface-4.0.1.dist-info/LICENSE +25 -0
- surrogates_interface-4.0.1.dist-info/METADATA +66 -0
- surrogates_interface-4.0.1.dist-info/RECORD +17 -0
- surrogates_interface-4.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
The classes contained in this module allow to transform the input and output, so that its is more suitable for training a surrogate model.
|
|
4
|
+
They are heavily inspired by the equivalent ones in `scikit-learn`. The main differences are that they provide the analytical gradient
|
|
5
|
+
and a serialization in HDF5 format.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
|
|
10
|
+
import h5py
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
# String type for h5py.
|
|
14
|
+
H5_STR = h5py.special_dtype(vlen=str)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Transformer(ABC):
|
|
18
|
+
"""Base class for all transformers."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def fields(self):
|
|
23
|
+
"List of fields in sklearn_scaler, hdf5 group and Transformer object."
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def transform(self, x, inplace):
|
|
27
|
+
"""Apply the direct transformation."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def inverse_transform(self, x, inplace):
|
|
31
|
+
"""Apply the inverse transformation."""
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def transform_gradient(self, x):
|
|
35
|
+
"""Compute the gradient of the direct transformation."""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def inverse_transform_gradient(self, x):
|
|
39
|
+
"""Compute the gradient of the inverse transformation."""
|
|
40
|
+
|
|
41
|
+
def _save_h5(self, group):
|
|
42
|
+
"""Save this transformer to a h5py `Group`.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
group : h5py.Group
|
|
47
|
+
Group where this object will be saved.
|
|
48
|
+
"""
|
|
49
|
+
group.create_dataset("type", data=self.__class__.__name__, dtype=H5_STR)
|
|
50
|
+
for f in self.fields:
|
|
51
|
+
group.create_dataset(f, data=getattr(self, f))
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def _load_h5(cls, group):
|
|
55
|
+
"""
|
|
56
|
+
Loads this transformer from a h5py `Group`.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
group : h5py.Group
|
|
61
|
+
Group where this object has been saved.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
obj : Transformer
|
|
66
|
+
The saved transformer.
|
|
67
|
+
"""
|
|
68
|
+
# Protected because this function should be called by SurrogateModel.load_h5.
|
|
69
|
+
return cls(*[group[f][:] for f in cls.fields])
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def from_sklearn(sklearn_obj):
|
|
73
|
+
"""
|
|
74
|
+
Create a new transformer, based on a scikit-learn scaler/transformer that has already been fitted.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
sklearn_scaler : {sklearn.preprocessing.MinMaxScaler,
|
|
79
|
+
sklearn.preprocessing.StandardScaler,
|
|
80
|
+
sklearn.preprocessing.PowerTransformer,
|
|
81
|
+
sklearn.decomposition.PCA}
|
|
82
|
+
A Scaler object from scikit-learn, that has already been fitted.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
if getattr(sklearn_obj, "clip", False):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"clip is not supported, because it cannot be differentiated."
|
|
88
|
+
)
|
|
89
|
+
cls = globals().get(sklearn_obj.__class__.__name__, None)
|
|
90
|
+
if cls is None:
|
|
91
|
+
raise NotImplementedError(f"{sklearn_obj.__class__.__name__} not supported")
|
|
92
|
+
return cls(*[getattr(sklearn_obj, f) for f in cls.fields])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class GainOffsetScaler(Transformer):
|
|
96
|
+
"""
|
|
97
|
+
Transform features by gain and offset
|
|
98
|
+
|
|
99
|
+
transformed = untransformed * gain + offset
|
|
100
|
+
|
|
101
|
+
Base class for `MinMaxScaler` and `StandardScaler`.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, gain, offset):
|
|
105
|
+
"""Create a new GainOffsetScaler, based on the given gain and offset.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
gain : array-like of shape (n_features)
|
|
110
|
+
Per feature relative scaling of the data. Equivalent to `(max - min) / (X.max(axis=0) - X.min(axis=0))`
|
|
111
|
+
offset : array-like of shape (n_features)
|
|
112
|
+
Per feature adjustment for minimum. Equivalent to `min - X.min(axis=0) * self.gain_`
|
|
113
|
+
"""
|
|
114
|
+
self.gain_ = np.asarray(gain)
|
|
115
|
+
self.inv_gain_ = (
|
|
116
|
+
1.0 / self.gain_
|
|
117
|
+
) # Saved because multiplying is faster than dividing.
|
|
118
|
+
self.offset_ = np.asarray(offset)
|
|
119
|
+
|
|
120
|
+
def transform(self, x, inplace=True):
|
|
121
|
+
"""Apply the direct transformation.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
x : array-like of shape (n_points, n_features)
|
|
126
|
+
Input data that will be transformed.
|
|
127
|
+
inplace : bool
|
|
128
|
+
`True` if the transformation must happen inplace. Same behavior of the `copy` attribute in scikit-learn.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
y : array-like of shape (n_points, n_features)
|
|
133
|
+
Transformed data.
|
|
134
|
+
"""
|
|
135
|
+
if inplace:
|
|
136
|
+
x *= self.gain_
|
|
137
|
+
x += self.offset_
|
|
138
|
+
else:
|
|
139
|
+
y = x * self.gain_ + self.offset_
|
|
140
|
+
return y
|
|
141
|
+
|
|
142
|
+
def inverse_transform(self, x, inplace=True):
|
|
143
|
+
"""Apply the inverse transformation.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
x : array-like of shape (n_points, n_features)
|
|
148
|
+
Input data that will be transformed.
|
|
149
|
+
inplace : bool
|
|
150
|
+
True if the transformation must happen inplace. Same behavior of the `copy` attribute in scikit-learn.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
y : array-like of shape (n_points, n_features)
|
|
155
|
+
Transformed data.
|
|
156
|
+
"""
|
|
157
|
+
if inplace:
|
|
158
|
+
x -= self.offset_
|
|
159
|
+
x *= self.inv_gain_
|
|
160
|
+
else:
|
|
161
|
+
y = (x - self.offset_) * self.inv_gain_
|
|
162
|
+
return y
|
|
163
|
+
|
|
164
|
+
def transform_gradient(self, x):
|
|
165
|
+
"""Compute the gradient of the direct transformation.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
x : array-like of shape (n_points, n_features)
|
|
170
|
+
Input data.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
y : array-like of shape (n_points, n_features)
|
|
175
|
+
Gradient of the direct transformation evaluated on the input data.
|
|
176
|
+
Warning: a read-only view is returned.
|
|
177
|
+
"""
|
|
178
|
+
y = np.broadcast_to(self.gain_, x.shape)
|
|
179
|
+
return y
|
|
180
|
+
|
|
181
|
+
def inverse_transform_gradient(self, x):
|
|
182
|
+
"""Compute the gradient of the inverse transformation.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
x : array-like of shape (n_points, n_features)
|
|
187
|
+
Input data.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
y : array-like of shape (n_points, n_features)
|
|
192
|
+
Gradient of the inverse transformation evaluated on the input data.
|
|
193
|
+
Warning: a read-only view is returned.
|
|
194
|
+
"""
|
|
195
|
+
y = np.broadcast_to(self.inv_gain_, x.shape)
|
|
196
|
+
return y
|
|
197
|
+
|
|
198
|
+
def __eq__(self, other):
|
|
199
|
+
if isinstance(other, self.__class__):
|
|
200
|
+
test_gain = np.array_equal(self.gain_, other.gain_)
|
|
201
|
+
test_offset = np.array_equal(self.offset_, other.offset_)
|
|
202
|
+
return test_gain and test_offset
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class MinMaxScaler(GainOffsetScaler):
|
|
207
|
+
"""
|
|
208
|
+
Transform features by scaling each feature to a given range.
|
|
209
|
+
|
|
210
|
+
This class is heavily inspired by the equivalent one in scikit-learn, which should be used for its construction.
|
|
211
|
+
It does not provide a function to fit, but allows to transform, and provides the analytical gradient.
|
|
212
|
+
|
|
213
|
+
transformed = untransformed * scale + min
|
|
214
|
+
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
fields = ("scale_", "min_")
|
|
218
|
+
|
|
219
|
+
def __init__(self, scale, min):
|
|
220
|
+
"""
|
|
221
|
+
Create a new min-max scaler, based on the given scale and min.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
scale : array-like of shape (n_features)
|
|
226
|
+
Per feature relative scaling of the data. Equivalent to `(max - min) / (X.max(axis=0) - X.min(axis=0))`.
|
|
227
|
+
min : array-like of shape (n_features)
|
|
228
|
+
Per feature adjustment for minimum. Equivalent to `min - X.min(axis=0) * self.gain_`.
|
|
229
|
+
"""
|
|
230
|
+
GainOffsetScaler.__init__(self, scale, min)
|
|
231
|
+
|
|
232
|
+
scale_ = property(lambda self: self.gain_)
|
|
233
|
+
min_ = property(lambda self: self.offset_)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class StandardScaler(GainOffsetScaler):
|
|
237
|
+
"""
|
|
238
|
+
Standardize features by removing the mean and scaling to unit variance.
|
|
239
|
+
|
|
240
|
+
This class is heavily inspired by the equivalent one in scikit-learn, which should be used for its construction.
|
|
241
|
+
It does not provide a function to fit, but allows to transform, and provides the analytical gradient.
|
|
242
|
+
|
|
243
|
+
transformed = (untransformed - mean) / scale
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
fields = ("scale_", "mean_")
|
|
247
|
+
|
|
248
|
+
def __init__(self, scale, mean):
|
|
249
|
+
"""
|
|
250
|
+
Create a new standard scaler, based on the given scale and mean.
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
scale : array-like of shape (n_features)
|
|
255
|
+
Per feature relative scaling of the data to achieve zero mean and unit variance.
|
|
256
|
+
Generally this is calculated using `np.sqrt(var_)`. If a variance is zero, we
|
|
257
|
+
cannot achieve unit variance, and the data is left as-is, giving a scaling factor of 1.
|
|
258
|
+
mean : array-like of shape (n_features)
|
|
259
|
+
The mean value for each feature in the training set.
|
|
260
|
+
"""
|
|
261
|
+
GainOffsetScaler.__init__(
|
|
262
|
+
self, 1 / np.asarray(scale), -np.asarray(mean) / scale
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
scale_ = property(lambda self: self.inv_gain_)
|
|
266
|
+
inv_scale_ = property(lambda self: self.gain_)
|
|
267
|
+
mean_ = property(lambda self: -self.offset_ * self.scale_)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class PCA(Transformer):
|
|
271
|
+
"""
|
|
272
|
+
Principal component analysis (PCA).
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
fields = ("components_", "mean_", "whiten", "explained_variance_")
|
|
276
|
+
|
|
277
|
+
def __init__(self, components, mean=None, whiten=False, explained_variance=None):
|
|
278
|
+
"""
|
|
279
|
+
Create a new Principal component analysis (PCA).
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
components_ : ndarray of shape (n_components, n_features)
|
|
284
|
+
Principal axes in feature space, representing the directions of maximum variance in the data.
|
|
285
|
+
Equivalently, the right singular vectors of the centered input data, parallel to its eigenvectors.
|
|
286
|
+
The components are sorted by decreasing explained_variance_.
|
|
287
|
+
mean : ndarray of shape (n_features,), optional
|
|
288
|
+
Per-feature empirical mean, estimated from the training set. The default is `None`.
|
|
289
|
+
whiten : bool, optional
|
|
290
|
+
When `True` the `components_` vectors are multiplied by the square root of `n_samples` and then
|
|
291
|
+
divided by the singular values to ensure uncorrelated outputs with unit component-wise variances.
|
|
292
|
+
Whitening will remove some information from the transformed signal (the relative variance scales
|
|
293
|
+
of the components) but can sometime improve the predictive accuracy of the downstream estimators
|
|
294
|
+
by making their data respect some hard-wired assumptions. The default is `False`.
|
|
295
|
+
explained_variance : ndarray of shape (n_components,), optional
|
|
296
|
+
The amount of variance explained by each of the selected components.
|
|
297
|
+
The variance estimation uses `n_samples - 1` degrees of freedom.
|
|
298
|
+
Equal to `n_components` largest eigenvalues of the covariance matrix of `x`. The default is `None`.
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
None.
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
# Save the fields.
|
|
306
|
+
self.components_ = np.asarray(components)
|
|
307
|
+
self.mean_ = np.asarray(mean)
|
|
308
|
+
self.whiten = np.array(
|
|
309
|
+
[whiten]
|
|
310
|
+
) # Encapsulating will allow _load_h5() to work without changes.
|
|
311
|
+
self.explained_variance_ = np.asarray(
|
|
312
|
+
explained_variance
|
|
313
|
+
) # Needed by _save_h5().
|
|
314
|
+
|
|
315
|
+
# Update fields.
|
|
316
|
+
self.n_components = components.shape[0]
|
|
317
|
+
self.n_features = components.shape[1]
|
|
318
|
+
self.explained_std_ = np.sqrt(explained_variance)
|
|
319
|
+
if whiten:
|
|
320
|
+
self.components_direct_ = (
|
|
321
|
+
self.components_ / self.explained_std_[:, np.newaxis]
|
|
322
|
+
)
|
|
323
|
+
self.components_inverse_ = (
|
|
324
|
+
self.components_ * self.explained_std_[:, np.newaxis]
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
self.components_direct_ = self.components_
|
|
328
|
+
self.components_inverse_ = self.components_
|
|
329
|
+
|
|
330
|
+
def transform(self, x, inplace=False):
|
|
331
|
+
r"""
|
|
332
|
+
Apply the direct transformation.
|
|
333
|
+
|
|
334
|
+
The transformation is given by
|
|
335
|
+
|
|
336
|
+
.. math::
|
|
337
|
+
y = C^T (x - m) / \sigma
|
|
338
|
+
|
|
339
|
+
with :math:`x` the input data, :math:`m` the mean, :math:`C` the components
|
|
340
|
+
and :math:`\sigma` the explained standard deviation.
|
|
341
|
+
|
|
342
|
+
Parameters
|
|
343
|
+
----------
|
|
344
|
+
x : ndarray of shape (n_points, n_features)
|
|
345
|
+
Input data that will be transformed.
|
|
346
|
+
inplace : bool, optional
|
|
347
|
+
Must be `False`. Although it would be possible to to update the first `n_components` columns of `x`,
|
|
348
|
+
we choose to not do it, so that `predict_output()` can be supported more easily.
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
y : ndarray of shape (n_points, n_components)
|
|
353
|
+
Projection of `x` in the first principal components.
|
|
354
|
+
"""
|
|
355
|
+
if inplace:
|
|
356
|
+
raise ValueError("The PCA transformation cannot be done inplace.")
|
|
357
|
+
# if self.mean_ is not None:
|
|
358
|
+
# x -= self.mean_
|
|
359
|
+
# # Multiply by the PCA basis and rewrite the first columns.
|
|
360
|
+
# np.matmul(x, self.components_direct_.T, out=x[:, : self.n_components])
|
|
361
|
+
else:
|
|
362
|
+
if self.mean_ is not None:
|
|
363
|
+
y = x - self.mean_
|
|
364
|
+
y = y @ self.components_direct_.T
|
|
365
|
+
return y
|
|
366
|
+
|
|
367
|
+
def inverse_transform(self, x, inplace):
|
|
368
|
+
r"""
|
|
369
|
+
Apply the inverse transformation.
|
|
370
|
+
|
|
371
|
+
The inverse transformation is given by
|
|
372
|
+
|
|
373
|
+
.. math::
|
|
374
|
+
y = C (\sigma x) + m
|
|
375
|
+
|
|
376
|
+
with :math:`x` the input data, :math:`m` the mean, :math:`C` the components
|
|
377
|
+
and :math:`\sigma` the explained standard deviation.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
x : array-like of shape (n_points, n_components)
|
|
382
|
+
Input data that will be inverse-transformed.
|
|
383
|
+
inplace : bool
|
|
384
|
+
Since `n_features >= n_components`, it is not possible to
|
|
385
|
+
apply the inverse transformation inplace. Therefore, this parameter must be `False`.
|
|
386
|
+
|
|
387
|
+
Raises
|
|
388
|
+
------
|
|
389
|
+
ValueError
|
|
390
|
+
If `inplace=True`.
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
y : ndarray of shape (n_samples, n_features)
|
|
395
|
+
Input data after applying the inverse PCA.
|
|
396
|
+
|
|
397
|
+
"""
|
|
398
|
+
if inplace:
|
|
399
|
+
raise ValueError("The PCA inverse transformation cannot be done inplace.")
|
|
400
|
+
else:
|
|
401
|
+
y = x @ self.components_inverse_
|
|
402
|
+
if self.mean_ is not None:
|
|
403
|
+
y += self.mean_
|
|
404
|
+
return y
|
|
405
|
+
|
|
406
|
+
def transform_gradient(self, x):
|
|
407
|
+
"""Compute the gradient of the direct transformation.
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
x : array-like of shape (n_points, n_features)
|
|
412
|
+
Input data.
|
|
413
|
+
|
|
414
|
+
Returns
|
|
415
|
+
-------
|
|
416
|
+
y : array-like of shape (n_points, n_components, n_features)
|
|
417
|
+
Gradient of the direct transformation evaluated on the input data.
|
|
418
|
+
Warning: a read-only view is returned.
|
|
419
|
+
"""
|
|
420
|
+
return np.broadcast_to(
|
|
421
|
+
self.components_direct_, (x.shape[0], self.n_components, self.n_features)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def inverse_transform_gradient(self, x):
|
|
425
|
+
"""Compute the gradient of the inverse transformation.
|
|
426
|
+
|
|
427
|
+
Parameters
|
|
428
|
+
----------
|
|
429
|
+
x : array-like of shape (n_points, n_components)
|
|
430
|
+
Input data.
|
|
431
|
+
|
|
432
|
+
Returns
|
|
433
|
+
-------
|
|
434
|
+
y : array-like of shape (n_points, n_features, n_components)
|
|
435
|
+
Gradient of the inverse transformation evaluated on the input data.
|
|
436
|
+
Warning: a read-only view is returned.
|
|
437
|
+
"""
|
|
438
|
+
return np.broadcast_to(
|
|
439
|
+
self.components_inverse_.T, (x.shape[0], self.n_features, self.n_components)
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def __eq__(self, other):
|
|
443
|
+
if isinstance(other, self.__class__):
|
|
444
|
+
test = np.array_equal(self.components_, other.components_)
|
|
445
|
+
test = test and np.array_equal(self.mean_, other.mean_)
|
|
446
|
+
test = test and np.array_equal(
|
|
447
|
+
self.explained_variance_, other.explained_variance_
|
|
448
|
+
)
|
|
449
|
+
test = test and (self.whiten == other.whiten)
|
|
450
|
+
return test
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class PowerTransformer:
|
|
455
|
+
"""
|
|
456
|
+
Factory to build power transformers.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
# The fields are used by `Transformer.from_sklearn()`.
|
|
460
|
+
fields = ("lambdas_", "method", "standardize")
|
|
461
|
+
|
|
462
|
+
def __new__(cls, lambdas, method="yeo-johnson", standardize=False):
|
|
463
|
+
"""
|
|
464
|
+
Create a new power transformer with the given parameters.
|
|
465
|
+
|
|
466
|
+
Parameters
|
|
467
|
+
----------
|
|
468
|
+
lambdas_ : array-like of shape (n_features)
|
|
469
|
+
The parameters of the power transformation for the selected features.
|
|
470
|
+
method : str
|
|
471
|
+
The power transform method. Available methods are:
|
|
472
|
+
- `"yeo-johnson"`, works with positive and negative values.
|
|
473
|
+
- `"box-cox"`, only works with strictly positive values. Not implemented yet.
|
|
474
|
+
standardize : bool
|
|
475
|
+
The `standardize` option is not available. Please add a :py:class:`StandardScaler`.
|
|
476
|
+
"""
|
|
477
|
+
if standardize:
|
|
478
|
+
raise NotImplementedError(
|
|
479
|
+
"The standardize option is not supported. Please add a StandardScaler."
|
|
480
|
+
)
|
|
481
|
+
if method == "yeo-johnson":
|
|
482
|
+
return PowerTransformerYeoJohnson(lambdas)
|
|
483
|
+
|
|
484
|
+
# elif method == "box-cox":
|
|
485
|
+
else:
|
|
486
|
+
raise NotImplementedError()
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class PowerTransformerYeoJohnson(Transformer):
|
|
490
|
+
"""
|
|
491
|
+
Power transformer with the yeo-johnson method.
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
fields = ("lambdas_",)
|
|
495
|
+
|
|
496
|
+
def __init__(self, lambdas):
|
|
497
|
+
"""
|
|
498
|
+
Create a new power transformer with the given parameters.
|
|
499
|
+
|
|
500
|
+
Parameters
|
|
501
|
+
----------
|
|
502
|
+
lambdas_ : array-like of shape (n_features)
|
|
503
|
+
The parameters of the power transformation for all features.
|
|
504
|
+
"""
|
|
505
|
+
self.lambdas_ = np.asarray(lambdas)
|
|
506
|
+
|
|
507
|
+
# lambdas_ = property(lambda self: self.lambdas_)
|
|
508
|
+
|
|
509
|
+
def transform(self, x, inplace=True):
|
|
510
|
+
r"""Apply the direct transformation.
|
|
511
|
+
|
|
512
|
+
The transformation can be found in `In-Kwon Yeo and Richard A. Johnson, A new family of power transformations
|
|
513
|
+
to Improve normality or symmetry <https://doi.org/10.1093/biomet/87.4.954>`_, Eq. (2.1), here reported for convenience.
|
|
514
|
+
|
|
515
|
+
.. math::
|
|
516
|
+
y = \begin{cases}
|
|
517
|
+
\ln(x + 1) & \text{if $x \ge 0$ and $\lambda = 0$} \\
|
|
518
|
+
((x + 1)^\lambda - 1) / \lambda & \text{if $x \ge 0$ and $\lambda \neq 0$} \\
|
|
519
|
+
-((-x + 1)^{2 - \lambda} - 1) / (2 - \lambda) & \text{if $x < 0$ and $\lambda \neq 2$} \\
|
|
520
|
+
-\ln(-x + 1) & \text{if $x < 0$ and $\lambda = 2$} \\
|
|
521
|
+
\end{cases}
|
|
522
|
+
|
|
523
|
+
where :math:`\lambda` is a feature-dependent coefficient.
|
|
524
|
+
|
|
525
|
+
Parameters
|
|
526
|
+
----------
|
|
527
|
+
x : array-like of shape (n_points, n_features)
|
|
528
|
+
Input data that will be transformed.
|
|
529
|
+
inplace : bool
|
|
530
|
+
`True` if the transformation must happen inplace. Same behavior of the `copy` attribute in scikit-learn.
|
|
531
|
+
|
|
532
|
+
Returns
|
|
533
|
+
-------
|
|
534
|
+
y : array-like of shape (n_points, n_features)
|
|
535
|
+
Transformed data.
|
|
536
|
+
"""
|
|
537
|
+
# Inspired from https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/preprocessing/_data.py#L3322
|
|
538
|
+
if inplace:
|
|
539
|
+
# Loop over the features.
|
|
540
|
+
for i in range(x.shape[1]):
|
|
541
|
+
# Get lambda for current feature.
|
|
542
|
+
lmbda = self.lambdas_[i]
|
|
543
|
+
|
|
544
|
+
# When x >= 0.
|
|
545
|
+
if np.abs(lmbda) < np.spacing(1.0):
|
|
546
|
+
# Binary mask.
|
|
547
|
+
pos = x[:, i] >= 0.0
|
|
548
|
+
np.log1p(x[:, i], out=x[:, i], where=pos)
|
|
549
|
+
|
|
550
|
+
else: # lambdas_[i] != 0
|
|
551
|
+
# Binary mask.
|
|
552
|
+
pos = x[:, i] >= 0.0
|
|
553
|
+
x[pos, i] += 1.0
|
|
554
|
+
np.power(x[:, i], lmbda, out=x[:, i], where=pos)
|
|
555
|
+
x[pos, i] -= 1.0
|
|
556
|
+
x[pos, i] /= lmbda
|
|
557
|
+
|
|
558
|
+
# When x < 0.
|
|
559
|
+
if np.abs(lmbda - 2.0) > np.spacing(1.0):
|
|
560
|
+
# Binary mask.
|
|
561
|
+
neg = x[:, i] < 0.0
|
|
562
|
+
np.negative(x[:, i], out=x[:, i], where=neg)
|
|
563
|
+
x[neg, i] += 1.0
|
|
564
|
+
np.power(x[:, i], 2.0 - lmbda, out=x[:, i], where=neg)
|
|
565
|
+
x[neg, i] -= 1.0
|
|
566
|
+
x[neg, i] /= 2.0 - lmbda
|
|
567
|
+
np.negative(x[:, i], out=x[:, i], where=neg)
|
|
568
|
+
|
|
569
|
+
else: # lmbda == 2
|
|
570
|
+
# Binary mask.
|
|
571
|
+
neg = x[:, i] < 0.0
|
|
572
|
+
np.negative(x[:, i], out=x[:, i], where=neg)
|
|
573
|
+
np.log1p(x[:, i], out=x[:, i], where=neg)
|
|
574
|
+
np.negative(x[:, i], out=x[:, i], where=neg)
|
|
575
|
+
|
|
576
|
+
else: # not inplace
|
|
577
|
+
y = np.zeros_like(x)
|
|
578
|
+
|
|
579
|
+
# Loop over the features.
|
|
580
|
+
for i in range(x.shape[1]):
|
|
581
|
+
# Get lambda for current feature.
|
|
582
|
+
lmbda = self.lambdas_[i]
|
|
583
|
+
|
|
584
|
+
# When x >= 0.
|
|
585
|
+
if np.abs(lmbda) < np.spacing(1.0):
|
|
586
|
+
# Binary mask.
|
|
587
|
+
pos = x[:, i] >= 0.0
|
|
588
|
+
y[pos, i] = np.log1p(x[pos, i])
|
|
589
|
+
|
|
590
|
+
else: # lmbda != 0
|
|
591
|
+
# Binary mask.
|
|
592
|
+
pos = x[:, i] >= 0.0
|
|
593
|
+
y[pos, i] = (np.power(x[pos, i] + 1.0, lmbda) - 1.0) / lmbda
|
|
594
|
+
|
|
595
|
+
# When x < 0.
|
|
596
|
+
if np.abs(lmbda - 2.0) > np.spacing(1.0):
|
|
597
|
+
# Binary mask.
|
|
598
|
+
neg = x[:, i] < 0.0
|
|
599
|
+
y[neg, i] = -(np.power(-x[neg, i] + 1.0, 2.0 - lmbda) - 1.0) / (
|
|
600
|
+
2.0 - lmbda
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
else: # lmbda == 2
|
|
604
|
+
# Binary mask.
|
|
605
|
+
neg = x[:, i] < 0.0
|
|
606
|
+
y[neg, i] = -np.log1p(-x[neg, i])
|
|
607
|
+
|
|
608
|
+
return y
|
|
609
|
+
|
|
610
|
+
def inverse_transform(self, x, inplace):
|
|
611
|
+
r"""Apply the inverse transformation.
|
|
612
|
+
|
|
613
|
+
The inverse transformation is:
|
|
614
|
+
|
|
615
|
+
.. math::
|
|
616
|
+
y = \begin{cases}
|
|
617
|
+
e^x - 1 & \text{if $x \ge 0$ and $\lambda = 0$} \\
|
|
618
|
+
\sqrt[\lambda]{\lambda x + 1} - 1 & \text{if $x \ge 0$ and $\lambda \neq 0$} \\
|
|
619
|
+
1 - (1 + x (\lambda - 2))^{1/(2-\lambda)} & \text{if $x < 0$ and $\lambda \neq 2$} \\
|
|
620
|
+
1 - e^{-x} & \text{if $x < 0$ and $\lambda = 2$} \\
|
|
621
|
+
\end{cases}
|
|
622
|
+
|
|
623
|
+
where :math:`\lambda` is a feature-dependent coefficient.
|
|
624
|
+
|
|
625
|
+
Parameters
|
|
626
|
+
----------
|
|
627
|
+
x : array-like of shape (n_points, n_features)
|
|
628
|
+
Input data that will be transformed.
|
|
629
|
+
inplace : bool
|
|
630
|
+
True if the transformation must happen inplace. Same behavior of the `copy` attribute in scikit-learn.
|
|
631
|
+
|
|
632
|
+
Returns
|
|
633
|
+
-------
|
|
634
|
+
y : array-like of shape (n_points, n_features)
|
|
635
|
+
Transformed data.
|
|
636
|
+
"""
|
|
637
|
+
# Inspired from https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/preprocessing/_data.py#L3301
|
|
638
|
+
if inplace:
|
|
639
|
+
# Loop over the features.
|
|
640
|
+
for i in range(x.shape[1]):
|
|
641
|
+
# Get lambda for current feature.
|
|
642
|
+
lmbda = self.lambdas_[i]
|
|
643
|
+
|
|
644
|
+
# When x >= 0
|
|
645
|
+
if np.abs(lmbda) < np.spacing(1.0):
|
|
646
|
+
# Binary mask.
|
|
647
|
+
pos = x[:, i] >= 0.0
|
|
648
|
+
np.expm1(x[:, i], out=x[:, i], where=pos)
|
|
649
|
+
|
|
650
|
+
else: # lmbda != 0
|
|
651
|
+
# Binary mask.
|
|
652
|
+
pos = x[:, i] >= 0.0
|
|
653
|
+
x[pos, i] *= lmbda
|
|
654
|
+
x[pos, i] += 1.0
|
|
655
|
+
np.power(x[:, i], 1.0 / lmbda, out=x[:, i], where=pos)
|
|
656
|
+
x[pos, i] -= 1.0
|
|
657
|
+
|
|
658
|
+
# When x < 0
|
|
659
|
+
if np.abs(lmbda - 2.0) > np.spacing(1.0):
|
|
660
|
+
# Binary mask.
|
|
661
|
+
neg = x[:, i] < 0.0
|
|
662
|
+
x[neg, i] *= -(2.0 - lmbda)
|
|
663
|
+
x[neg, i] += 1.0
|
|
664
|
+
np.power(x[:, i], 1.0 / (2.0 - lmbda), out=x[:, i], where=neg)
|
|
665
|
+
np.negative(x[:, i], out=x[:, i], where=neg)
|
|
666
|
+
x[neg, i] += 1.0
|
|
667
|
+
|
|
668
|
+
else: # lmbda == 2
|
|
669
|
+
# Binary mask.
|
|
670
|
+
neg = x[:, i] < 0.0
|
|
671
|
+
np.negative(x[:, i], out=x[:, i], where=neg)
|
|
672
|
+
np.expm1(x[:, i], out=x[:, i], where=neg)
|
|
673
|
+
np.negative(x[:, i], out=x[:, i], where=neg)
|
|
674
|
+
|
|
675
|
+
else: # not inplace
|
|
676
|
+
y = np.zeros_like(x)
|
|
677
|
+
|
|
678
|
+
# Loop over the features.
|
|
679
|
+
for i in range(x.shape[1]):
|
|
680
|
+
# Get lambda for current feature.
|
|
681
|
+
lmbda = self.lambdas_[i]
|
|
682
|
+
|
|
683
|
+
# When x >= 0
|
|
684
|
+
if np.abs(lmbda) < np.spacing(1.0):
|
|
685
|
+
# Binary mask.
|
|
686
|
+
pos = x[:, i] >= 0.0
|
|
687
|
+
y[pos, i] = np.expm1(x[pos, i])
|
|
688
|
+
else: # lmbda != 0
|
|
689
|
+
# Binary mask.
|
|
690
|
+
pos = x[:, i] >= 0.0
|
|
691
|
+
y[pos, i] = np.power(x[pos, i] * lmbda + 1.0, 1.0 / lmbda) - 1.0
|
|
692
|
+
|
|
693
|
+
# When x < 0
|
|
694
|
+
if np.abs(lmbda - 2.0) > np.spacing(1.0):
|
|
695
|
+
# Binary mask.
|
|
696
|
+
neg = x[:, i] < 0.0
|
|
697
|
+
y[neg, i] = 1.0 - np.power(
|
|
698
|
+
-(2.0 - lmbda) * x[neg, i] + 1.0, 1.0 / (2.0 - lmbda)
|
|
699
|
+
)
|
|
700
|
+
else: # lmbda == 2
|
|
701
|
+
# Binary mask.
|
|
702
|
+
neg = x[:, i] < 0.0
|
|
703
|
+
y[neg, i] = -np.expm1(-x[neg, i])
|
|
704
|
+
|
|
705
|
+
return y
|
|
706
|
+
|
|
707
|
+
def transform_gradient(self, x):
|
|
708
|
+
r"""Compute the gradient of the Yeo Johnson direct transformation.
|
|
709
|
+
|
|
710
|
+
.. math::
|
|
711
|
+
y = \begin{cases}
|
|
712
|
+
1 / (1 + x) & \text{if $x \ge 0$ and $\lambda = 0$} \\
|
|
713
|
+
(x + 1)^{\lambda - 1} & \text{if $x \ge 0$ and $\lambda \neq 0$} \\
|
|
714
|
+
(1 - x)^{1 - \lambda} & \text{if $x < 0$ and $\lambda \neq 2$} \\
|
|
715
|
+
1 / (1 - x) & \text{if $x < 0$ and $\lambda = 2$} \\
|
|
716
|
+
\end{cases}
|
|
717
|
+
|
|
718
|
+
where :math:`\lambda` is a feature-dependent coefficient.
|
|
719
|
+
|
|
720
|
+
Parameters
|
|
721
|
+
----------
|
|
722
|
+
x : array-like of shape (n_points, n_features)
|
|
723
|
+
Input data.
|
|
724
|
+
|
|
725
|
+
Returns
|
|
726
|
+
-------
|
|
727
|
+
y : array-like of shape (n_points, n_features)
|
|
728
|
+
Gradient of the transformation evaluated on the input data.
|
|
729
|
+
"""
|
|
730
|
+
y = np.zeros_like(x)
|
|
731
|
+
|
|
732
|
+
# Loop over the features.
|
|
733
|
+
for i in range(x.shape[1]):
|
|
734
|
+
# Get lambda for current feature.
|
|
735
|
+
lmbda = self.lambdas_[i]
|
|
736
|
+
|
|
737
|
+
# When x >= 0.
|
|
738
|
+
if np.abs(lmbda) < np.spacing(1.0):
|
|
739
|
+
# Binary mask.
|
|
740
|
+
pos = x[:, i] >= 0.0
|
|
741
|
+
y[pos, i] = 1.0 / (1.0 + x[pos, i])
|
|
742
|
+
|
|
743
|
+
else: # lmbda != 0
|
|
744
|
+
# Binary mask.
|
|
745
|
+
pos = x[:, i] >= 0.0
|
|
746
|
+
y[pos, i] = np.power(x[pos, i] + 1.0, lmbda - 1.0)
|
|
747
|
+
|
|
748
|
+
# When x < 0.
|
|
749
|
+
if np.abs(lmbda - 2.0) > np.spacing(1.0):
|
|
750
|
+
# Binary mask.
|
|
751
|
+
neg = x[:, i] < 0.0
|
|
752
|
+
y[neg, i] = np.power(1.0 - x[neg, i], 1.0 - lmbda)
|
|
753
|
+
|
|
754
|
+
else: # lmbda == 2
|
|
755
|
+
# Binary mask.
|
|
756
|
+
neg = x[:, i] < 0.0
|
|
757
|
+
y[neg, i] = 1.0 / (1.0 - x[neg, i])
|
|
758
|
+
|
|
759
|
+
return y
|
|
760
|
+
|
|
761
|
+
def inverse_transform_gradient(self, x):
|
|
762
|
+
r"""Compute the gradient of the inverse Yeo Johnson direct transformation.
|
|
763
|
+
|
|
764
|
+
.. math::
|
|
765
|
+
y = \begin{cases}
|
|
766
|
+
e^x & \text{if $x \ge 0$ and $\lambda = 0$} \\
|
|
767
|
+
(\lambda x + 1)^{1/\lambda - 1} & \text{if $x \ge 0$ and $\lambda \neq 0$} \\
|
|
768
|
+
(1 + x (\lambda - 2))^{1/(2-\lambda) - 1} & \text{if $x < 0$ and $\lambda \neq 2$} \\
|
|
769
|
+
e^{-x} & \text{if $x < 0$ and $\lambda = 2$} \\
|
|
770
|
+
\end{cases}
|
|
771
|
+
|
|
772
|
+
where :math:`\lambda` is a feature-dependent coefficient.
|
|
773
|
+
|
|
774
|
+
Parameters
|
|
775
|
+
----------
|
|
776
|
+
x : array-like of shape (n_points, n_features)
|
|
777
|
+
Input data.
|
|
778
|
+
|
|
779
|
+
Returns
|
|
780
|
+
-------
|
|
781
|
+
y : array-like of shape (n_points, n_features)
|
|
782
|
+
Gradient of the inverse transformation evaluated on the input data.
|
|
783
|
+
"""
|
|
784
|
+
y = np.zeros_like(x)
|
|
785
|
+
|
|
786
|
+
# Loop over the features.
|
|
787
|
+
for i in range(x.shape[1]):
|
|
788
|
+
# Get lambda for current feature.
|
|
789
|
+
lmbda = self.lambdas_[i]
|
|
790
|
+
|
|
791
|
+
# When x >= 0
|
|
792
|
+
if np.abs(lmbda) < np.spacing(1.0):
|
|
793
|
+
# Binary mask.
|
|
794
|
+
pos = x[:, i] >= 0.0
|
|
795
|
+
y[pos, i] = np.exp(x[pos, i])
|
|
796
|
+
else: # lmbda != 0
|
|
797
|
+
# Binary mask.
|
|
798
|
+
pos = x[:, i] >= 0.0
|
|
799
|
+
y[pos, i] = np.power(x[pos, i] * lmbda + 1.0, 1.0 / lmbda - 1.0)
|
|
800
|
+
|
|
801
|
+
# When x < 0
|
|
802
|
+
if np.abs(lmbda - 2.0) > np.spacing(1.0):
|
|
803
|
+
neg = x[:, i] < 0.0
|
|
804
|
+
y[neg, i] = np.power(
|
|
805
|
+
1.0 + (lmbda - 2.0) * x[neg, i], 1.0 / (2.0 - lmbda) - 1.0
|
|
806
|
+
)
|
|
807
|
+
else: # lmbda == 2
|
|
808
|
+
neg = x[:, i] < 0.0
|
|
809
|
+
y[neg, i] = np.exp(-x[neg, i])
|
|
810
|
+
|
|
811
|
+
return y
|
|
812
|
+
|
|
813
|
+
def __eq__(self, other):
|
|
814
|
+
if isinstance(other, self.__class__):
|
|
815
|
+
return np.array_equal(self.lambdas_, other.lambdas_)
|
|
816
|
+
return False
|