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.
@@ -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