1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//! Stateful feature scalers that remember their training statistics
//!
//! Houses the `fit` and `transform` counterparts to the stateless [`standardize`] and
//! [`normalize`] functions, mirroring scikit-learn's `sklearn.preprocessing` scalers. Each
//! one learns its per-feature statistics once, on the training matrix. It reuses those
//! frozen numbers for every later batch: the test split, a validation fold, or a single
//! sample arriving at inference time. This keeps a train-test boundary honest. Rescaling a
//! test set by its own column statistics applies a different linear map than the one the
//! model was trained under.
//!
//! | Scaler | Maps each feature by | Reach for it when |
//! |---|---|---|
//! | [`StandardScaler`] | `(x - mean) / std` | the default for distance- and gradient-based models |
//! | [`MinMaxScaler`] | `(x - min) / (max - min)`, rescaled into a target range | you need values bounded to `[0, 1]` (or any range) |
//! | [`MaxAbsScaler`] | `x / max(\|x\|)` | zeros and signs must survive, for example sparse data |
//! | [`RobustScaler`] | `(x - median) / IQR` | outliers you do not want to remove |
//! | [`Normalizer`] | each *sample* divided by its own norm | only a sample's direction carries signal |
//!
//! [`Normalizer`] is the odd one out. It rescales samples (rows), not features (columns), so
//! it learns nothing beyond the feature count. It exists so row normalization uses the same
//! [`Fit`](crate::traits::Fit) and [`Transform`](crate::traits::Transform) contract as the
//! rest. The other 4 are per-feature and stateful in the full sense. All but
//! [`RobustScaler`] also support `partial_fit`. Their statistics merge exactly across batches,
//! but quantiles do not.
//!
//! Every scaler here is 2-D and per-feature, exactly like scikit-learn's. The free functions
//! handle row-wise or whole-array standardization, and N-D arrays.
//!
//! [`standardize`]: crate::utils::standardize::standardize
//! [`normalize`]: crate::utils::normalize::normalize
use crateError;
use crate;
use ;
use ;
/// Scale features by their maximum absolute value, preserving zeros and signs
/// Scale features to a `[0, 1]` (or custom) range
/// Scale each sample to unit norm
/// Scale features by their median and interquartile range, resisting outliers
/// Standardize features to zero mean and unit variance
pub use MaxAbsScaler;
pub use MinMaxScaler;
pub use Normalizer;
pub use RobustScaler;
pub use StandardScaler;
/// A divisor at or below this magnitude is treated as degenerate and replaced by `1.0`
///
/// [`normalize`](crate::utils::normalize::normalize) uses the same threshold for a near-zero
/// lane. It also matches scikit-learn's `_handle_zeros_in_scale` rule
const DEGENERATE_SCALE_THRESHOLD: f64 = 10.0 * f64EPSILON;
/// Replaces a degenerate divisor with `1.0`
///
/// A feature with no spread otherwise divides by about zero and blows up to `NaN` or `Inf`.
/// This happens for a constant column in [`MinMaxScaler`] or [`RobustScaler`], and for an
/// all-zero column in [`MaxAbsScaler`]. A divisor of `1.0` instead leaves the value unchanged
/// after centering
///
/// [`StandardScaler`] does not use this rule. It detects a constant feature from the variance
/// itself, a magnitude-relative bound. scikit-learn reserves this finer test for the standard
/// scaler too
/// Computes the per-feature `(min, max)` over the rows of `x`
///
/// Columns are folded independently, so the serial and parallel paths produce identical
/// results. The gate only decides who does the work
/// Computes the requested quantiles of every feature
///
/// `quantiles` holds fractions in `[0, 1]`. The returned vector has one entry per feature,
/// each listing that feature's quantiles in the order requested. The function copies each
/// column out, sorts it once, and reads it at every requested position. Asking for 3
/// quantiles this way costs one sort rather than 3 passes
///
/// Interpolates linearly between the 2 order statistics that bracket a fractional position.
/// This is NumPy's default `linear` method, the same one scikit-learn's `RobustScaler` uses,
/// so its quantiles line up with a ported pipeline
///
/// Each column is handled independently, so the serial and parallel paths produce identical
/// results. The gate only decides who does the work. Each worker copies only one column at a
/// time, so the extra memory is `threads * n_samples`, not a second copy of the matrix
/// Reads the `q`-quantile (a fraction in `[0, 1]`) out of an ascending slice
///
/// The position is `(n - 1) * q`. A fractional position interpolates linearly between its 2
/// neighbors. `sorted` must be non-empty
/// Applies `row_op` to every row of `x` in place, on rayon above the cheap-map gate
/// Rejects a matrix that is empty, featureless, or non-finite
///
/// # Errors
///
/// - [`Error::EmptyInput`] - If `x` has no rows or no columns
/// - [`Error::NonFinite`] - If any element is NaN or infinite
/// Validates a matrix handed to a `transform`-style method against the fitted feature count
///
/// # Errors
///
/// - [`Error::EmptyInput`] - If `x` has no rows or no columns
/// - [`Error::DimensionMismatch`] - If `x`'s feature count differs from `n_features`
/// - [`Error::NonFinite`] - If any element is NaN or infinite
/// Borrows a fitted statistic, or reports that the scaler has not been fitted
///
/// # Errors
///
/// - [`Error::NotFitted`] - If `stat` is `None`