scirs2_transform/
scaling.rs

1//! Advanced scaling and transformation methods
2//!
3//! This module provides sophisticated scaling methods that go beyond basic normalization,
4//! including quantile transformations and robust scaling methods.
5
6use ndarray::{Array1, Array2, ArrayBase, Data, Ix2};
7use num_traits::{Float, NumCast};
8
9use crate::error::{Result, TransformError};
10
11// Define a small value to use for comparison with zero
12const EPSILON: f64 = 1e-10;
13
14/// QuantileTransformer for non-linear transformations
15///
16/// This transformer transforms features to follow a uniform or normal distribution
17/// using quantiles information. This method reduces the impact of outliers.
18pub struct QuantileTransformer {
19    /// Number of quantiles to estimate
20    n_quantiles: usize,
21    /// Output distribution ('uniform' or 'normal')
22    output_distribution: String,
23    /// Whether to clip transformed values to bounds [0, 1] for uniform distribution
24    clip: bool,
25    /// The quantiles for each feature
26    quantiles: Option<Array2<f64>>,
27    /// References values for each quantile
28    references: Option<Array1<f64>>,
29}
30
31impl QuantileTransformer {
32    /// Creates a new QuantileTransformer
33    ///
34    /// # Arguments
35    /// * `n_quantiles` - Number of quantiles to estimate (default: 1000)
36    /// * `output_distribution` - Target distribution ('uniform' or 'normal')
37    /// * `clip` - Whether to clip transformed values
38    ///
39    /// # Returns
40    /// * A new QuantileTransformer instance
41    pub fn new(n_quantiles: usize, output_distribution: &str, clip: bool) -> Result<Self> {
42        if n_quantiles < 2 {
43            return Err(TransformError::InvalidInput(
44                "n_quantiles must be at least 2".to_string(),
45            ));
46        }
47
48        if output_distribution != "uniform" && output_distribution != "normal" {
49            return Err(TransformError::InvalidInput(
50                "output_distribution must be 'uniform' or 'normal'".to_string(),
51            ));
52        }
53
54        Ok(QuantileTransformer {
55            n_quantiles,
56            output_distribution: output_distribution.to_string(),
57            clip,
58            quantiles: None,
59            references: None,
60        })
61    }
62
63    /// Fits the QuantileTransformer to the input data
64    ///
65    /// # Arguments
66    /// * `x` - The input data, shape (n_samples, n_features)
67    ///
68    /// # Returns
69    /// * `Result<()>` - Ok if successful, Err otherwise
70    pub fn fit<S>(&mut self, x: &ArrayBase<S, Ix2>) -> Result<()>
71    where
72        S: Data,
73        S::Elem: Float + NumCast,
74    {
75        let x_f64 = x.mapv(|x| num_traits::cast::<S::Elem, f64>(x).unwrap_or(0.0));
76
77        let n_samples = x_f64.shape()[0];
78        let n_features = x_f64.shape()[1];
79
80        if n_samples == 0 || n_features == 0 {
81            return Err(TransformError::InvalidInput("Empty input data".to_string()));
82        }
83
84        if self.n_quantiles > n_samples {
85            return Err(TransformError::InvalidInput(format!(
86                "n_quantiles ({}) cannot be greater than n_samples ({})",
87                self.n_quantiles, n_samples
88            )));
89        }
90
91        // Compute quantiles for each feature
92        let mut quantiles = Array2::zeros((n_features, self.n_quantiles));
93
94        for j in 0..n_features {
95            // Extract feature data and sort it
96            let mut feature_data: Vec<f64> = x_f64.column(j).to_vec();
97            feature_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
98
99            // Compute quantiles
100            for i in 0..self.n_quantiles {
101                let q = i as f64 / (self.n_quantiles - 1) as f64;
102                let idx = (q * (feature_data.len() - 1) as f64).round() as usize;
103                quantiles[[j, i]] = feature_data[idx];
104            }
105        }
106
107        // Generate reference distribution
108        let references = if self.output_distribution == "uniform" {
109            // Uniform distribution references
110            Array1::from_shape_fn(self.n_quantiles, |i| {
111                i as f64 / (self.n_quantiles - 1) as f64
112            })
113        } else {
114            // Normal distribution references (using inverse normal CDF approximation)
115            Array1::from_shape_fn(self.n_quantiles, |i| {
116                let u = i as f64 / (self.n_quantiles - 1) as f64;
117                // Clamp u to avoid extreme values
118                let u_clamped = u.clamp(1e-7, 1.0 - 1e-7);
119                inverse_normal_cdf(u_clamped)
120            })
121        };
122
123        self.quantiles = Some(quantiles);
124        self.references = Some(references);
125
126        Ok(())
127    }
128
129    /// Transforms the input data using the fitted QuantileTransformer
130    ///
131    /// # Arguments
132    /// * `x` - The input data, shape (n_samples, n_features)
133    ///
134    /// # Returns
135    /// * `Result<Array2<f64>>` - The transformed data
136    pub fn transform<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>>
137    where
138        S: Data,
139        S::Elem: Float + NumCast,
140    {
141        let x_f64 = x.mapv(|x| num_traits::cast::<S::Elem, f64>(x).unwrap_or(0.0));
142
143        let n_samples = x_f64.shape()[0];
144        let n_features = x_f64.shape()[1];
145
146        if self.quantiles.is_none() || self.references.is_none() {
147            return Err(TransformError::TransformationError(
148                "QuantileTransformer has not been fitted".to_string(),
149            ));
150        }
151
152        let quantiles = self.quantiles.as_ref().unwrap();
153        let references = self.references.as_ref().unwrap();
154
155        if n_features != quantiles.shape()[0] {
156            return Err(TransformError::InvalidInput(format!(
157                "x has {} features, but QuantileTransformer was fitted with {} features",
158                n_features,
159                quantiles.shape()[0]
160            )));
161        }
162
163        let mut transformed = Array2::zeros((n_samples, n_features));
164
165        for i in 0..n_samples {
166            for j in 0..n_features {
167                let value = x_f64[[i, j]];
168
169                // Find the position of the value in the quantiles
170                let feature_quantiles = quantiles.row(j);
171
172                // Find the index where value would be inserted
173                let mut lower_idx = 0;
174                let mut upper_idx = self.n_quantiles - 1;
175
176                // Handle edge cases
177                if value <= feature_quantiles[0] {
178                    transformed[[i, j]] = references[0];
179                    continue;
180                }
181                if value >= feature_quantiles[self.n_quantiles - 1] {
182                    transformed[[i, j]] = references[self.n_quantiles - 1];
183                    continue;
184                }
185
186                // Binary search to find the interval
187                while upper_idx - lower_idx > 1 {
188                    let mid = (lower_idx + upper_idx) / 2;
189                    if value <= feature_quantiles[mid] {
190                        upper_idx = mid;
191                    } else {
192                        lower_idx = mid;
193                    }
194                }
195
196                // Linear interpolation between reference values
197                let lower_quantile = feature_quantiles[lower_idx];
198                let upper_quantile = feature_quantiles[upper_idx];
199                let lower_ref = references[lower_idx];
200                let upper_ref = references[upper_idx];
201
202                if (upper_quantile - lower_quantile).abs() < EPSILON {
203                    transformed[[i, j]] = lower_ref;
204                } else {
205                    let ratio = (value - lower_quantile) / (upper_quantile - lower_quantile);
206                    transformed[[i, j]] = lower_ref + ratio * (upper_ref - lower_ref);
207                }
208            }
209        }
210
211        // Apply clipping if requested and output distribution is uniform
212        if self.clip && self.output_distribution == "uniform" {
213            for i in 0..n_samples {
214                for j in 0..n_features {
215                    transformed[[i, j]] = transformed[[i, j]].clamp(0.0, 1.0);
216                }
217            }
218        }
219
220        Ok(transformed)
221    }
222
223    /// Fits the QuantileTransformer to the input data and transforms it
224    ///
225    /// # Arguments
226    /// * `x` - The input data, shape (n_samples, n_features)
227    ///
228    /// # Returns
229    /// * `Result<Array2<f64>>` - The transformed data
230    pub fn fit_transform<S>(&mut self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>>
231    where
232        S: Data,
233        S::Elem: Float + NumCast,
234    {
235        self.fit(x)?;
236        self.transform(x)
237    }
238
239    /// Returns the quantiles for each feature
240    ///
241    /// # Returns
242    /// * `Option<&Array2<f64>>` - The quantiles, shape (n_features, n_quantiles)
243    pub fn quantiles(&self) -> Option<&Array2<f64>> {
244        self.quantiles.as_ref()
245    }
246}
247
248/// Approximation of the inverse normal cumulative distribution function
249///
250/// This uses the Beasley-Springer-Moro algorithm for approximating the inverse normal CDF
251fn inverse_normal_cdf(u: f64) -> f64 {
252    // Constants for the Beasley-Springer-Moro algorithm
253    const A0: f64 = 2.50662823884;
254    const A1: f64 = -18.61500062529;
255    const A2: f64 = 41.39119773534;
256    const A3: f64 = -25.44106049637;
257    const B1: f64 = -8.47351093090;
258    const B2: f64 = 23.08336743743;
259    const B3: f64 = -21.06224101826;
260    const B4: f64 = 3.13082909833;
261    const C0: f64 = 0.3374754822726147;
262    const C1: f64 = 0.9761690190917186;
263    const C2: f64 = 0.1607979714918209;
264    const C3: f64 = 0.0276438810333863;
265    const C4: f64 = 0.0038405729373609;
266    const C5: f64 = 0.0003951896511919;
267    const C6: f64 = 0.0000321767881768;
268    const C7: f64 = 0.0000002888167364;
269    const C8: f64 = 0.0000003960315187;
270
271    let y = u - 0.5;
272
273    if y.abs() < 0.42 {
274        // Central region
275        let r = y * y;
276        y * (((A3 * r + A2) * r + A1) * r + A0) / ((((B4 * r + B3) * r + B2) * r + B1) * r + 1.0)
277    } else {
278        // Tail region
279        let r = if y > 0.0 { 1.0 - u } else { u };
280        let r = (-r.ln()).ln();
281
282        let result = C0
283            + r * (C1 + r * (C2 + r * (C3 + r * (C4 + r * (C5 + r * (C6 + r * (C7 + r * C8)))))));
284
285        if y < 0.0 {
286            -result
287        } else {
288            result
289        }
290    }
291}
292
293/// MaxAbsScaler for scaling features by their maximum absolute value
294///
295/// This scaler scales each feature individually such that the maximal absolute value
296/// of each feature in the training set will be 1.0. It does not shift/center the data,
297/// and thus does not destroy any sparsity.
298pub struct MaxAbsScaler {
299    /// Maximum absolute values for each feature (learned during fit)
300    max_abs_: Option<Array1<f64>>,
301    /// Scale factors for each feature (1 / max_abs_)
302    scale_: Option<Array1<f64>>,
303}
304
305impl MaxAbsScaler {
306    /// Creates a new MaxAbsScaler
307    ///
308    /// # Returns
309    /// * A new MaxAbsScaler instance
310    ///
311    /// # Examples
312    /// ```
313    /// use scirs2_transform::scaling::MaxAbsScaler;
314    ///
315    /// let scaler = MaxAbsScaler::new();
316    /// ```
317    pub fn new() -> Self {
318        MaxAbsScaler {
319            max_abs_: None,
320            scale_: None,
321        }
322    }
323
324    /// Creates a MaxAbsScaler with default settings (same as new())
325    pub fn with_defaults() -> Self {
326        Self::new()
327    }
328
329    /// Fits the MaxAbsScaler to the input data
330    ///
331    /// # Arguments
332    /// * `x` - The input data, shape (n_samples, n_features)
333    ///
334    /// # Returns
335    /// * `Result<()>` - Ok if successful, Err otherwise
336    pub fn fit<S>(&mut self, x: &ArrayBase<S, Ix2>) -> Result<()>
337    where
338        S: Data,
339        S::Elem: Float + NumCast,
340    {
341        let x_f64 = x.mapv(|x| num_traits::cast::<S::Elem, f64>(x).unwrap_or(0.0));
342
343        let n_samples = x_f64.shape()[0];
344        let n_features = x_f64.shape()[1];
345
346        if n_samples == 0 || n_features == 0 {
347            return Err(TransformError::InvalidInput("Empty input data".to_string()));
348        }
349
350        // Compute maximum absolute value for each feature
351        let mut max_abs = Array1::zeros(n_features);
352
353        for j in 0..n_features {
354            let feature_data = x_f64.column(j);
355            let max_abs_value = feature_data
356                .iter()
357                .map(|&x| x.abs())
358                .fold(0.0, |acc, x| acc.max(x));
359
360            max_abs[j] = max_abs_value;
361        }
362
363        // Compute scale factors (avoid division by zero)
364        let scale = max_abs.mapv(|max_abs_val| {
365            if max_abs_val > EPSILON {
366                1.0 / max_abs_val
367            } else {
368                1.0 // If max_abs is 0, don't scale (feature is constant zero)
369            }
370        });
371
372        self.max_abs_ = Some(max_abs);
373        self.scale_ = Some(scale);
374
375        Ok(())
376    }
377
378    /// Transforms the input data using the fitted MaxAbsScaler
379    ///
380    /// # Arguments
381    /// * `x` - The input data, shape (n_samples, n_features)
382    ///
383    /// # Returns
384    /// * `Result<Array2<f64>>` - The scaled data
385    pub fn transform<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>>
386    where
387        S: Data,
388        S::Elem: Float + NumCast,
389    {
390        let x_f64 = x.mapv(|x| num_traits::cast::<S::Elem, f64>(x).unwrap_or(0.0));
391
392        let n_samples = x_f64.shape()[0];
393        let n_features = x_f64.shape()[1];
394
395        if self.scale_.is_none() {
396            return Err(TransformError::TransformationError(
397                "MaxAbsScaler has not been fitted".to_string(),
398            ));
399        }
400
401        let scale = self.scale_.as_ref().unwrap();
402
403        if n_features != scale.len() {
404            return Err(TransformError::InvalidInput(format!(
405                "x has {} features, but MaxAbsScaler was fitted with {} features",
406                n_features,
407                scale.len()
408            )));
409        }
410
411        let mut transformed = Array2::zeros((n_samples, n_features));
412
413        // Scale each feature by its scale factor
414        for i in 0..n_samples {
415            for j in 0..n_features {
416                transformed[[i, j]] = x_f64[[i, j]] * scale[j];
417            }
418        }
419
420        Ok(transformed)
421    }
422
423    /// Fits the MaxAbsScaler to the input data and transforms it
424    ///
425    /// # Arguments
426    /// * `x` - The input data, shape (n_samples, n_features)
427    ///
428    /// # Returns
429    /// * `Result<Array2<f64>>` - The scaled data
430    pub fn fit_transform<S>(&mut self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>>
431    where
432        S: Data,
433        S::Elem: Float + NumCast,
434    {
435        self.fit(x)?;
436        self.transform(x)
437    }
438
439    /// Inverse transforms the scaled data back to original scale
440    ///
441    /// # Arguments
442    /// * `x` - The scaled data, shape (n_samples, n_features)
443    ///
444    /// # Returns
445    /// * `Result<Array2<f64>>` - The data in original scale
446    pub fn inverse_transform<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>>
447    where
448        S: Data,
449        S::Elem: Float + NumCast,
450    {
451        let x_f64 = x.mapv(|x| num_traits::cast::<S::Elem, f64>(x).unwrap_or(0.0));
452
453        let n_samples = x_f64.shape()[0];
454        let n_features = x_f64.shape()[1];
455
456        if self.max_abs_.is_none() {
457            return Err(TransformError::TransformationError(
458                "MaxAbsScaler has not been fitted".to_string(),
459            ));
460        }
461
462        let max_abs = self.max_abs_.as_ref().unwrap();
463
464        if n_features != max_abs.len() {
465            return Err(TransformError::InvalidInput(format!(
466                "x has {} features, but MaxAbsScaler was fitted with {} features",
467                n_features,
468                max_abs.len()
469            )));
470        }
471
472        let mut transformed = Array2::zeros((n_samples, n_features));
473
474        // Scale back by multiplying with max_abs values
475        for i in 0..n_samples {
476            for j in 0..n_features {
477                transformed[[i, j]] = x_f64[[i, j]] * max_abs[j];
478            }
479        }
480
481        Ok(transformed)
482    }
483
484    /// Returns the maximum absolute values for each feature
485    ///
486    /// # Returns
487    /// * `Option<&Array1<f64>>` - The maximum absolute values
488    pub fn max_abs(&self) -> Option<&Array1<f64>> {
489        self.max_abs_.as_ref()
490    }
491
492    /// Returns the scale factors for each feature
493    ///
494    /// # Returns
495    /// * `Option<&Array1<f64>>` - The scale factors (1 / max_abs)
496    pub fn scale(&self) -> Option<&Array1<f64>> {
497        self.scale_.as_ref()
498    }
499}
500
501impl Default for MaxAbsScaler {
502    fn default() -> Self {
503        Self::new()
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use approx::assert_abs_diff_eq;
511    use ndarray::Array;
512
513    #[test]
514    fn test_quantile_transformer_uniform() {
515        // Create test data with different distributions
516        let data = Array::from_shape_vec(
517            (6, 2),
518            vec![
519                1.0, 10.0, 2.0, 20.0, 3.0, 30.0, 4.0, 40.0, 5.0, 50.0, 100.0, 1000.0,
520            ], // Last row has outliers
521        )
522        .unwrap();
523
524        let mut transformer = QuantileTransformer::new(5, "uniform", true).unwrap();
525        let transformed = transformer.fit_transform(&data).unwrap();
526
527        // Check that the shape is preserved
528        assert_eq!(transformed.shape(), &[6, 2]);
529
530        // For uniform distribution, values should be between 0 and 1
531        for i in 0..6 {
532            for j in 0..2 {
533                assert!(
534                    transformed[[i, j]] >= 0.0 && transformed[[i, j]] <= 1.0,
535                    "Value at [{}, {}] = {} is not in [0, 1]",
536                    i,
537                    j,
538                    transformed[[i, j]]
539                );
540            }
541        }
542
543        // The smallest value should map to 0 and largest to 1
544        assert_abs_diff_eq!(transformed[[0, 0]], 0.0, epsilon = 1e-10); // min of column 0
545        assert_abs_diff_eq!(transformed[[5, 0]], 1.0, epsilon = 1e-10); // max of column 0
546        assert_abs_diff_eq!(transformed[[0, 1]], 0.0, epsilon = 1e-10); // min of column 1
547        assert_abs_diff_eq!(transformed[[5, 1]], 1.0, epsilon = 1e-10); // max of column 1
548    }
549
550    #[test]
551    fn test_quantile_transformer_normal() {
552        // Create test data
553        let data = Array::from_shape_vec((5, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
554
555        let mut transformer = QuantileTransformer::new(5, "normal", false).unwrap();
556        let transformed = transformer.fit_transform(&data).unwrap();
557
558        // Check that the shape is preserved
559        assert_eq!(transformed.shape(), &[5, 1]);
560
561        // The middle value should be close to 0 (median of normal distribution)
562        assert_abs_diff_eq!(transformed[[2, 0]], 0.0, epsilon = 1e-10);
563    }
564
565    #[test]
566    fn test_quantile_transformer_errors() {
567        // Test invalid n_quantiles
568        assert!(QuantileTransformer::new(1, "uniform", true).is_err());
569
570        // Test invalid output_distribution
571        assert!(QuantileTransformer::new(100, "invalid", true).is_err());
572
573        // Test fitting with insufficient data
574        let small_data = Array::from_shape_vec((2, 1), vec![1.0, 2.0]).unwrap();
575        let mut transformer = QuantileTransformer::new(10, "uniform", true).unwrap();
576        assert!(transformer.fit(&small_data).is_err());
577    }
578
579    #[test]
580    fn test_inverse_normal_cdf() {
581        // Test some known values
582        assert_abs_diff_eq!(inverse_normal_cdf(0.5), 0.0, epsilon = 1e-6);
583        assert!(inverse_normal_cdf(0.1) < 0.0); // Should be negative
584        assert!(inverse_normal_cdf(0.9) > 0.0); // Should be positive
585    }
586
587    #[test]
588    fn test_max_abs_scaler_basic() {
589        // Create test data with different ranges
590        // Feature 0: [-4, -2, 0, 2, 4] -> max_abs = 4
591        // Feature 1: [-10, -5, 0, 5, 10] -> max_abs = 10
592        let data = Array::from_shape_vec(
593            (5, 2),
594            vec![-4.0, -10.0, -2.0, -5.0, 0.0, 0.0, 2.0, 5.0, 4.0, 10.0],
595        )
596        .unwrap();
597
598        let mut scaler = MaxAbsScaler::new();
599        let scaled = scaler.fit_transform(&data).unwrap();
600
601        // Check that the shape is preserved
602        assert_eq!(scaled.shape(), &[5, 2]);
603
604        // Check the maximum absolute values
605        let max_abs = scaler.max_abs().unwrap();
606        assert_abs_diff_eq!(max_abs[0], 4.0, epsilon = 1e-10);
607        assert_abs_diff_eq!(max_abs[1], 10.0, epsilon = 1e-10);
608
609        // Check the scale factors
610        let scale = scaler.scale().unwrap();
611        assert_abs_diff_eq!(scale[0], 0.25, epsilon = 1e-10); // 1/4
612        assert_abs_diff_eq!(scale[1], 0.1, epsilon = 1e-10); // 1/10
613
614        // Check that the maximum absolute value in each feature is 1.0
615        for j in 0..2 {
616            let feature_max = scaled
617                .column(j)
618                .iter()
619                .map(|&x| x.abs())
620                .fold(0.0, f64::max);
621            assert_abs_diff_eq!(feature_max, 1.0, epsilon = 1e-10);
622        }
623
624        // Check specific scaled values
625        assert_abs_diff_eq!(scaled[[0, 0]], -1.0, epsilon = 1e-10); // -4 / 4 = -1
626        assert_abs_diff_eq!(scaled[[0, 1]], -1.0, epsilon = 1e-10); // -10 / 10 = -1
627        assert_abs_diff_eq!(scaled[[2, 0]], 0.0, epsilon = 1e-10); // 0 / 4 = 0
628        assert_abs_diff_eq!(scaled[[2, 1]], 0.0, epsilon = 1e-10); // 0 / 10 = 0
629        assert_abs_diff_eq!(scaled[[4, 0]], 1.0, epsilon = 1e-10); // 4 / 4 = 1
630        assert_abs_diff_eq!(scaled[[4, 1]], 1.0, epsilon = 1e-10); // 10 / 10 = 1
631    }
632
633    #[test]
634    fn test_max_abs_scaler_positive_only() {
635        // Test with positive-only data
636        let data = Array::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 6.0, 5.0, 10.0]).unwrap();
637
638        let mut scaler = MaxAbsScaler::new();
639        let scaled = scaler.fit_transform(&data).unwrap();
640
641        // Check maximum absolute values
642        let max_abs = scaler.max_abs().unwrap();
643        assert_abs_diff_eq!(max_abs[0], 5.0, epsilon = 1e-10);
644        assert_abs_diff_eq!(max_abs[1], 10.0, epsilon = 1e-10);
645
646        // Check scaled values
647        assert_abs_diff_eq!(scaled[[0, 0]], 0.2, epsilon = 1e-10); // 1 / 5
648        assert_abs_diff_eq!(scaled[[0, 1]], 0.2, epsilon = 1e-10); // 2 / 10
649        assert_abs_diff_eq!(scaled[[2, 0]], 1.0, epsilon = 1e-10); // 5 / 5
650        assert_abs_diff_eq!(scaled[[2, 1]], 1.0, epsilon = 1e-10); // 10 / 10
651    }
652
653    #[test]
654    fn test_max_abs_scaler_inverse_transform() {
655        let data = Array::from_shape_vec((3, 2), vec![-6.0, 8.0, 0.0, -4.0, 3.0, 12.0]).unwrap();
656
657        let mut scaler = MaxAbsScaler::new();
658        let scaled = scaler.fit_transform(&data).unwrap();
659        let inverse = scaler.inverse_transform(&scaled).unwrap();
660
661        // Check that inverse transform recovers original data
662        assert_eq!(inverse.shape(), data.shape());
663        for i in 0..3 {
664            for j in 0..2 {
665                assert_abs_diff_eq!(inverse[[i, j]], data[[i, j]], epsilon = 1e-10);
666            }
667        }
668    }
669
670    #[test]
671    fn test_max_abs_scaler_constant_feature() {
672        // Test with a constant feature (all zeros)
673        let data = Array::from_shape_vec((3, 2), vec![0.0, 5.0, 0.0, 10.0, 0.0, 15.0]).unwrap();
674
675        let mut scaler = MaxAbsScaler::new();
676        let scaled = scaler.fit_transform(&data).unwrap();
677
678        // Constant zero feature should remain zero
679        for i in 0..3 {
680            assert_abs_diff_eq!(scaled[[i, 0]], 0.0, epsilon = 1e-10);
681        }
682
683        // Second feature should be scaled normally
684        assert_abs_diff_eq!(scaled[[0, 1]], 1.0 / 3.0, epsilon = 1e-10); // 5 / 15
685        assert_abs_diff_eq!(scaled[[2, 1]], 1.0, epsilon = 1e-10); // 15 / 15
686    }
687
688    #[test]
689    fn test_max_abs_scaler_errors() {
690        // Test with empty data
691        let empty_data = Array::<f64, _>::zeros((0, 2));
692        let mut scaler = MaxAbsScaler::new();
693        assert!(scaler.fit(&empty_data).is_err());
694
695        // Test transform before fit
696        let data = Array::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
697        let unfitted_scaler = MaxAbsScaler::new();
698        assert!(unfitted_scaler.transform(&data).is_err());
699        assert!(unfitted_scaler.inverse_transform(&data).is_err());
700
701        // Test feature dimension mismatch
702        let train_data = Array::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
703        let test_data = Array::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
704
705        let mut scaler = MaxAbsScaler::new();
706        scaler.fit(&train_data).unwrap();
707        assert!(scaler.transform(&test_data).is_err());
708        assert!(scaler.inverse_transform(&test_data).is_err());
709    }
710
711    #[test]
712    fn test_max_abs_scaler_single_feature() {
713        // Test with single feature
714        let data = Array::from_shape_vec((4, 1), vec![-8.0, -2.0, 4.0, 6.0]).unwrap();
715
716        let mut scaler = MaxAbsScaler::new();
717        let scaled = scaler.fit_transform(&data).unwrap();
718
719        // Maximum absolute value should be 8.0
720        let max_abs = scaler.max_abs().unwrap();
721        assert_abs_diff_eq!(max_abs[0], 8.0, epsilon = 1e-10);
722
723        // Check scaled values
724        assert_abs_diff_eq!(scaled[[0, 0]], -1.0, epsilon = 1e-10); // -8 / 8
725        assert_abs_diff_eq!(scaled[[1, 0]], -0.25, epsilon = 1e-10); // -2 / 8
726        assert_abs_diff_eq!(scaled[[2, 0]], 0.5, epsilon = 1e-10); // 4 / 8
727        assert_abs_diff_eq!(scaled[[3, 0]], 0.75, epsilon = 1e-10); // 6 / 8
728    }
729
730    #[test]
731    fn test_max_abs_scaler_sparse_preservation() {
732        // Test that zero values remain zero (sparsity preservation)
733        let data = Array::from_shape_vec(
734            (4, 3),
735            vec![
736                0.0, 5.0, 0.0, // Row with zeros
737                10.0, 0.0, -8.0, // Another row with zeros
738                0.0, 0.0, 4.0, // Row with multiple zeros
739                -5.0, 10.0, 0.0, // Row with zero at end
740            ],
741        )
742        .unwrap();
743
744        let mut scaler = MaxAbsScaler::new();
745        let scaled = scaler.fit_transform(&data).unwrap();
746
747        // Check that zeros remain zeros
748        assert_abs_diff_eq!(scaled[[0, 0]], 0.0, epsilon = 1e-10);
749        assert_abs_diff_eq!(scaled[[0, 2]], 0.0, epsilon = 1e-10);
750        assert_abs_diff_eq!(scaled[[1, 1]], 0.0, epsilon = 1e-10);
751        assert_abs_diff_eq!(scaled[[2, 0]], 0.0, epsilon = 1e-10);
752        assert_abs_diff_eq!(scaled[[2, 1]], 0.0, epsilon = 1e-10);
753        assert_abs_diff_eq!(scaled[[3, 2]], 0.0, epsilon = 1e-10);
754
755        // Check that non-zero values are scaled correctly
756        // Feature 0: max_abs = 10, Feature 1: max_abs = 10, Feature 2: max_abs = 8
757        assert_abs_diff_eq!(scaled[[0, 1]], 0.5, epsilon = 1e-10); // 5 / 10
758        assert_abs_diff_eq!(scaled[[1, 0]], 1.0, epsilon = 1e-10); // 10 / 10
759        assert_abs_diff_eq!(scaled[[1, 2]], -1.0, epsilon = 1e-10); // -8 / 8
760        assert_abs_diff_eq!(scaled[[2, 2]], 0.5, epsilon = 1e-10); // 4 / 8
761        assert_abs_diff_eq!(scaled[[3, 0]], -0.5, epsilon = 1e-10); // -5 / 10
762        assert_abs_diff_eq!(scaled[[3, 1]], 1.0, epsilon = 1e-10); // 10 / 10
763    }
764}