Skip to main content

scirs2/
transform.rs

1//! Python bindings for scirs2-transform
2//!
3//! This module provides Python bindings for data transformation operations,
4//! including normalization, feature engineering, dimensionality reduction,
5//! categorical encoding, imputation, and preprocessing pipelines.
6
7use pyo3::exceptions::{PyRuntimeError, PyValueError};
8use pyo3::prelude::*;
9
10// NumPy types for Python array interface
11use scirs2_numpy::{IntoPyArray, PyArray1, PyArray2, PyArrayMethods};
12
13// Direct imports from scirs2-transform
14use scirs2_transform::{
15    // Encoding
16    encoding::{OneHotEncoder, OrdinalEncoder},
17    // Features
18    features::{
19        binarize, discretize_equal_frequency, discretize_equal_width, log_transform,
20        power_transform, PolynomialFeatures, PowerTransformer,
21    },
22    // Imputation
23    impute::{ImputeStrategy, KNNImputer, SimpleImputer},
24    // Normalization
25    normalize::{normalize_array, normalize_vector, NormalizationMethod, Normalizer},
26    // Reduction
27    reduction::{PCA, TSNE, UMAP},
28    // Scaling
29    scaling::{MaxAbsScaler, QuantileTransformer},
30};
31
32// ========================================
33// NORMALIZATION
34// ========================================
35
36/// Normalize array using specified method
37#[pyfunction]
38#[pyo3(signature = (array, method="zscore", axis=0))]
39fn normalize_array_py(
40    py: Python,
41    array: &Bound<'_, PyArray2<f64>>,
42    method: &str,
43    axis: usize,
44) -> PyResult<Py<PyArray2<f64>>> {
45    let binding = array.readonly();
46    let arr = binding.as_array();
47
48    let norm_method = parse_normalization_method(method)?;
49
50    let result = normalize_array(&arr, norm_method, axis)
51        .map_err(|e| PyRuntimeError::new_err(format!("Normalization failed: {}", e)))?;
52
53    Ok(result.into_pyarray(py).unbind())
54}
55
56/// Normalize vector using specified method
57#[pyfunction]
58#[pyo3(signature = (array, method="zscore"))]
59fn normalize_vector_py(
60    py: Python,
61    array: &Bound<'_, PyArray1<f64>>,
62    method: &str,
63) -> PyResult<Py<PyArray1<f64>>> {
64    let binding = array.readonly();
65    let arr = binding.as_array();
66
67    let norm_method = parse_normalization_method(method)?;
68
69    let result = normalize_vector(&arr, norm_method)
70        .map_err(|e| PyRuntimeError::new_err(format!("Normalization failed: {}", e)))?;
71
72    Ok(result.into_pyarray(py).unbind())
73}
74
75/// Normalizer class for fit/transform pattern
76#[pyclass(name = "Normalizer")]
77pub struct PyNormalizer {
78    inner: Normalizer,
79}
80
81#[pymethods]
82impl PyNormalizer {
83    #[new]
84    #[pyo3(signature = (method="zscore", axis=0))]
85    fn new(method: &str, axis: usize) -> PyResult<Self> {
86        let norm_method = parse_normalization_method(method)?;
87        Ok(Self {
88            inner: Normalizer::new(norm_method, axis),
89        })
90    }
91
92    fn fit(&mut self, array: &Bound<'_, PyArray2<f64>>) -> PyResult<()> {
93        let binding = array.readonly();
94        let arr = binding.as_array();
95        self.inner
96            .fit(&arr)
97            .map_err(|e| PyRuntimeError::new_err(format!("Fit failed: {}", e)))
98    }
99
100    fn transform(
101        &self,
102        py: Python,
103        array: &Bound<'_, PyArray2<f64>>,
104    ) -> PyResult<Py<PyArray2<f64>>> {
105        let binding = array.readonly();
106        let arr = binding.as_array();
107        let result = self
108            .inner
109            .transform(&arr)
110            .map_err(|e| PyRuntimeError::new_err(format!("Transform failed: {}", e)))?;
111        Ok(result.into_pyarray(py).unbind())
112    }
113
114    fn fit_transform(
115        &mut self,
116        py: Python,
117        array: &Bound<'_, PyArray2<f64>>,
118    ) -> PyResult<Py<PyArray2<f64>>> {
119        let binding = array.readonly();
120        let arr = binding.as_array();
121        let result = self
122            .inner
123            .fit_transform(&arr)
124            .map_err(|e| PyRuntimeError::new_err(format!("Fit transform failed: {}", e)))?;
125        Ok(result.into_pyarray(py).unbind())
126    }
127}
128
129// Helper to parse normalization method
130fn parse_normalization_method(method: &str) -> PyResult<NormalizationMethod> {
131    match method.to_lowercase().as_str() {
132        "minmax" => Ok(NormalizationMethod::MinMax),
133        "zscore" => Ok(NormalizationMethod::ZScore),
134        "maxabs" => Ok(NormalizationMethod::MaxAbs),
135        "l1" => Ok(NormalizationMethod::L1),
136        "l2" => Ok(NormalizationMethod::L2),
137        "robust" => Ok(NormalizationMethod::Robust),
138        _ => Err(PyValueError::new_err(format!(
139            "Unknown normalization method: {}",
140            method
141        ))),
142    }
143}
144
145// ========================================
146// FEATURE ENGINEERING
147// ========================================
148
149/// Polynomial Features generator
150#[pyclass(name = "PolynomialFeatures")]
151pub struct PyPolynomialFeatures {
152    inner: PolynomialFeatures,
153}
154
155#[pymethods]
156impl PyPolynomialFeatures {
157    #[new]
158    #[pyo3(signature = (degree=2, interaction_only=false, include_bias=true))]
159    fn new(degree: usize, interaction_only: bool, include_bias: bool) -> Self {
160        Self {
161            inner: PolynomialFeatures::new(degree, interaction_only, include_bias),
162        }
163    }
164
165    fn n_output_features(&self, n_features: usize) -> usize {
166        self.inner.n_output_features(n_features)
167    }
168
169    fn transform(
170        &self,
171        py: Python,
172        array: &Bound<'_, PyArray2<f64>>,
173    ) -> PyResult<Py<PyArray2<f64>>> {
174        let binding = array.readonly();
175        let arr = binding.as_array();
176        let result = self
177            .inner
178            .transform(&arr)
179            .map_err(|e| PyRuntimeError::new_err(format!("Polynomial transform failed: {}", e)))?;
180        Ok(result.into_pyarray(py).unbind())
181    }
182}
183
184/// Power Transformer (Box-Cox, Yeo-Johnson)
185#[pyclass(name = "PowerTransformer")]
186pub struct PyPowerTransformer {
187    inner: PowerTransformer,
188}
189
190#[pymethods]
191impl PyPowerTransformer {
192    #[new]
193    #[pyo3(signature = (method="yeo-johnson", standardize=true))]
194    fn new(method: &str, standardize: bool) -> PyResult<Self> {
195        let transformer = PowerTransformer::new(method, standardize).map_err(|e| {
196            PyRuntimeError::new_err(format!("PowerTransformer creation failed: {}", e))
197        })?;
198        Ok(Self { inner: transformer })
199    }
200
201    fn fit(&mut self, array: &Bound<'_, PyArray2<f64>>) -> PyResult<()> {
202        let binding = array.readonly();
203        let arr = binding.as_array();
204        self.inner
205            .fit(&arr)
206            .map_err(|e| PyRuntimeError::new_err(format!("Fit failed: {}", e)))
207    }
208
209    fn transform(
210        &self,
211        py: Python,
212        array: &Bound<'_, PyArray2<f64>>,
213    ) -> PyResult<Py<PyArray2<f64>>> {
214        let binding = array.readonly();
215        let arr = binding.as_array();
216        let result = self
217            .inner
218            .transform(&arr)
219            .map_err(|e| PyRuntimeError::new_err(format!("Transform failed: {}", e)))?;
220        Ok(result.into_pyarray(py).unbind())
221    }
222
223    fn inverse_transform(
224        &self,
225        py: Python,
226        array: &Bound<'_, PyArray2<f64>>,
227    ) -> PyResult<Py<PyArray2<f64>>> {
228        let binding = array.readonly();
229        let arr = binding.as_array();
230        let result = self
231            .inner
232            .inverse_transform(&arr)
233            .map_err(|e| PyRuntimeError::new_err(format!("Inverse transform failed: {}", e)))?;
234        Ok(result.into_pyarray(py).unbind())
235    }
236}
237
238/// Binarize array with threshold
239#[pyfunction]
240#[pyo3(signature = (array, threshold=0.0))]
241fn binarize_py(
242    py: Python,
243    array: &Bound<'_, PyArray2<f64>>,
244    threshold: f64,
245) -> PyResult<Py<PyArray2<f64>>> {
246    let binding = array.readonly();
247    let arr = binding.as_array();
248    let result = binarize(&arr, threshold)
249        .map_err(|e| PyRuntimeError::new_err(format!("Binarization failed: {}", e)))?;
250    Ok(result.into_pyarray(py).unbind())
251}
252
253/// Discretize with equal width bins
254#[pyfunction]
255#[pyo3(signature = (array, n_bins, encode="ordinal", axis=0))]
256fn discretize_equal_width_py(
257    py: Python,
258    array: &Bound<'_, PyArray2<f64>>,
259    n_bins: usize,
260    encode: &str,
261    axis: usize,
262) -> PyResult<Py<PyArray2<f64>>> {
263    let binding = array.readonly();
264    let arr = binding.as_array();
265    let result = discretize_equal_width(&arr, n_bins, encode, axis)
266        .map_err(|e| PyRuntimeError::new_err(format!("Discretization failed: {}", e)))?;
267    Ok(result.into_pyarray(py).unbind())
268}
269
270/// Discretize with equal frequency bins
271#[pyfunction]
272#[pyo3(signature = (array, n_bins, encode="ordinal", axis=0))]
273fn discretize_equal_frequency_py(
274    py: Python,
275    array: &Bound<'_, PyArray2<f64>>,
276    n_bins: usize,
277    encode: &str,
278    axis: usize,
279) -> PyResult<Py<PyArray2<f64>>> {
280    let binding = array.readonly();
281    let arr = binding.as_array();
282    let result = discretize_equal_frequency(&arr, n_bins, encode, axis)
283        .map_err(|e| PyRuntimeError::new_err(format!("Discretization failed: {}", e)))?;
284    Ok(result.into_pyarray(py).unbind())
285}
286
287/// Log transform
288#[pyfunction]
289#[pyo3(signature = (array, epsilon=1e-10))]
290fn log_transform_py(
291    py: Python,
292    array: &Bound<'_, PyArray2<f64>>,
293    epsilon: f64,
294) -> PyResult<Py<PyArray2<f64>>> {
295    let binding = array.readonly();
296    let arr = binding.as_array();
297    let result = log_transform(&arr, epsilon)
298        .map_err(|e| PyRuntimeError::new_err(format!("Log transform failed: {}", e)))?;
299    Ok(result.into_pyarray(py).unbind())
300}
301
302/// Power transform (Box-Cox or Yeo-Johnson)
303#[pyfunction]
304#[pyo3(signature = (array, method="yeo-johnson", standardize=true))]
305fn power_transform_py(
306    py: Python,
307    array: &Bound<'_, PyArray2<f64>>,
308    method: &str,
309    standardize: bool,
310) -> PyResult<Py<PyArray2<f64>>> {
311    let binding = array.readonly();
312    let arr = binding.as_array();
313    let result = power_transform(&arr, method, standardize)
314        .map_err(|e| PyRuntimeError::new_err(format!("Power transform failed: {}", e)))?;
315    Ok(result.into_pyarray(py).unbind())
316}
317
318// ========================================
319// DIMENSIONALITY REDUCTION
320// ========================================
321
322/// PCA dimensionality reduction
323#[pyclass(name = "PCA")]
324pub struct PyPCA {
325    inner: PCA,
326}
327
328#[pymethods]
329impl PyPCA {
330    #[new]
331    #[pyo3(signature = (n_components=2, center=true, scale=false))]
332    fn new(n_components: usize, center: bool, scale: bool) -> Self {
333        Self {
334            inner: PCA::new(n_components, center, scale),
335        }
336    }
337
338    fn fit(&mut self, array: &Bound<'_, PyArray2<f64>>) -> PyResult<()> {
339        let binding = array.readonly();
340        let arr = binding.as_array();
341        self.inner
342            .fit(&arr)
343            .map_err(|e| PyRuntimeError::new_err(format!("PCA fit failed: {}", e)))
344    }
345
346    fn transform(
347        &self,
348        py: Python,
349        array: &Bound<'_, PyArray2<f64>>,
350    ) -> PyResult<Py<PyArray2<f64>>> {
351        let binding = array.readonly();
352        let arr = binding.as_array();
353        let result = self
354            .inner
355            .transform(&arr)
356            .map_err(|e| PyRuntimeError::new_err(format!("PCA transform failed: {}", e)))?;
357        Ok(result.into_pyarray(py).unbind())
358    }
359
360    fn fit_transform(
361        &mut self,
362        py: Python,
363        array: &Bound<'_, PyArray2<f64>>,
364    ) -> PyResult<Py<PyArray2<f64>>> {
365        let binding = array.readonly();
366        let arr = binding.as_array();
367        let result = self
368            .inner
369            .fit_transform(&arr)
370            .map_err(|e| PyRuntimeError::new_err(format!("PCA fit_transform failed: {}", e)))?;
371        Ok(result.into_pyarray(py).unbind())
372    }
373
374    fn components(&self, py: Python) -> PyResult<Option<Py<PyArray2<f64>>>> {
375        match self.inner.components() {
376            Some(comp) => Ok(Some(comp.clone().into_pyarray(py).unbind())),
377            None => Ok(None),
378        }
379    }
380
381    fn explained_variance_ratio(&self, py: Python) -> PyResult<Option<Py<PyArray1<f64>>>> {
382        match self.inner.explained_variance_ratio() {
383            Some(evr) => Ok(Some(evr.clone().into_pyarray(py).unbind())),
384            None => Ok(None),
385        }
386    }
387}
388
389/// t-SNE dimensionality reduction
390#[pyclass(name = "TSNE")]
391pub struct PyTSNE {
392    inner: TSNE,
393}
394
395#[pymethods]
396impl PyTSNE {
397    #[new]
398    #[pyo3(signature = (n_components=2, perplexity=30.0, max_iter=1000))]
399    fn new(n_components: usize, perplexity: f64, max_iter: usize) -> Self {
400        Self {
401            inner: TSNE::new()
402                .with_n_components(n_components)
403                .with_perplexity(perplexity)
404                .with_max_iter(max_iter),
405        }
406    }
407
408    fn fit_transform(
409        &mut self,
410        py: Python,
411        array: &Bound<'_, PyArray2<f64>>,
412    ) -> PyResult<Py<PyArray2<f64>>> {
413        let binding = array.readonly();
414        let arr = binding.as_array();
415        let result = self
416            .inner
417            .fit_transform(&arr)
418            .map_err(|e| PyRuntimeError::new_err(format!("TSNE fit_transform failed: {}", e)))?;
419        Ok(result.into_pyarray(py).unbind())
420    }
421}
422
423/// UMAP dimensionality reduction
424#[pyclass(name = "UMAP")]
425pub struct PyUMAP {
426    inner: UMAP,
427}
428
429#[pymethods]
430impl PyUMAP {
431    #[new]
432    #[pyo3(signature = (n_components=2, n_neighbors=15, min_dist=0.1, learning_rate=1.0, n_epochs=200))]
433    fn new(
434        n_components: usize,
435        n_neighbors: usize,
436        min_dist: f64,
437        learning_rate: f64,
438        n_epochs: usize,
439    ) -> Self {
440        Self {
441            inner: UMAP::new(n_neighbors, n_components, min_dist, learning_rate, n_epochs),
442        }
443    }
444
445    fn fit_transform(
446        &mut self,
447        py: Python,
448        array: &Bound<'_, PyArray2<f64>>,
449    ) -> PyResult<Py<PyArray2<f64>>> {
450        let binding = array.readonly();
451        let arr = binding.as_array();
452        let result = self
453            .inner
454            .fit_transform(&arr)
455            .map_err(|e| PyRuntimeError::new_err(format!("UMAP fit_transform failed: {}", e)))?;
456        Ok(result.into_pyarray(py).unbind())
457    }
458}
459
460// ========================================
461// CATEGORICAL ENCODING
462// ========================================
463
464/// One-Hot Encoder
465#[pyclass(name = "OneHotEncoder")]
466pub struct PyOneHotEncoder {
467    inner: OneHotEncoder,
468}
469
470#[pymethods]
471impl PyOneHotEncoder {
472    #[new]
473    #[pyo3(signature = (drop=None, handle_unknown="error", sparse=false))]
474    fn new(drop: Option<String>, handle_unknown: &str, sparse: bool) -> PyResult<Self> {
475        let encoder = OneHotEncoder::new(drop, handle_unknown, sparse).map_err(|e| {
476            PyRuntimeError::new_err(format!("OneHotEncoder creation failed: {}", e))
477        })?;
478        Ok(Self { inner: encoder })
479    }
480
481    fn fit(&mut self, array: &Bound<'_, PyArray2<f64>>) -> PyResult<()> {
482        let binding = array.readonly();
483        let arr = binding.as_array();
484        self.inner
485            .fit(&arr)
486            .map_err(|e| PyRuntimeError::new_err(format!("Fit failed: {}", e)))
487    }
488
489    fn transform(
490        &self,
491        py: Python,
492        array: &Bound<'_, PyArray2<f64>>,
493    ) -> PyResult<Py<PyArray2<f64>>> {
494        let binding = array.readonly();
495        let arr = binding.as_array();
496        let result = self
497            .inner
498            .transform(&arr)
499            .map_err(|e| PyRuntimeError::new_err(format!("Transform failed: {}", e)))?;
500        Ok(result.to_dense().into_pyarray(py).unbind())
501    }
502
503    fn fit_transform(
504        &mut self,
505        py: Python,
506        array: &Bound<'_, PyArray2<f64>>,
507    ) -> PyResult<Py<PyArray2<f64>>> {
508        let binding = array.readonly();
509        let arr = binding.as_array();
510        let result = self
511            .inner
512            .fit_transform(&arr)
513            .map_err(|e| PyRuntimeError::new_err(format!("Fit transform failed: {}", e)))?;
514        Ok(result.to_dense().into_pyarray(py).unbind())
515    }
516}
517
518/// Ordinal Encoder
519#[pyclass(name = "OrdinalEncoder")]
520pub struct PyOrdinalEncoder {
521    inner: OrdinalEncoder,
522}
523
524#[pymethods]
525impl PyOrdinalEncoder {
526    #[new]
527    #[pyo3(signature = (handle_unknown="error", unknown_value=None))]
528    fn new(handle_unknown: &str, unknown_value: Option<f64>) -> PyResult<Self> {
529        let encoder = OrdinalEncoder::new(handle_unknown, unknown_value).map_err(|e| {
530            PyRuntimeError::new_err(format!("OrdinalEncoder creation failed: {}", e))
531        })?;
532        Ok(Self { inner: encoder })
533    }
534
535    fn fit(&mut self, array: &Bound<'_, PyArray2<f64>>) -> PyResult<()> {
536        let binding = array.readonly();
537        let arr = binding.as_array();
538        self.inner
539            .fit(&arr)
540            .map_err(|e| PyRuntimeError::new_err(format!("Fit failed: {}", e)))
541    }
542
543    fn transform(
544        &self,
545        py: Python,
546        array: &Bound<'_, PyArray2<f64>>,
547    ) -> PyResult<Py<PyArray2<f64>>> {
548        let binding = array.readonly();
549        let arr = binding.as_array();
550        let result = self
551            .inner
552            .transform(&arr)
553            .map_err(|e| PyRuntimeError::new_err(format!("Transform failed: {}", e)))?;
554        Ok(result.into_pyarray(py).unbind())
555    }
556
557    fn fit_transform(
558        &mut self,
559        py: Python,
560        array: &Bound<'_, PyArray2<f64>>,
561    ) -> PyResult<Py<PyArray2<f64>>> {
562        let binding = array.readonly();
563        let arr = binding.as_array();
564        let result = self
565            .inner
566            .fit_transform(&arr)
567            .map_err(|e| PyRuntimeError::new_err(format!("Fit transform failed: {}", e)))?;
568        Ok(result.into_pyarray(py).unbind())
569    }
570}
571
572// ========================================
573// IMPUTATION
574// ========================================
575
576/// Simple Imputer for missing values
577#[pyclass(name = "SimpleImputer")]
578pub struct PySimpleImputer {
579    inner: SimpleImputer,
580}
581
582#[pymethods]
583impl PySimpleImputer {
584    #[new]
585    #[pyo3(signature = (strategy="mean", missing_values=f64::NAN))]
586    fn new(strategy: &str, missing_values: f64) -> PyResult<Self> {
587        let impute_strategy = match strategy.to_lowercase().as_str() {
588            "mean" => ImputeStrategy::Mean,
589            "median" => ImputeStrategy::Median,
590            "most_frequent" => ImputeStrategy::MostFrequent,
591            _ => {
592                return Err(PyValueError::new_err(format!(
593                    "Unknown impute strategy: {}",
594                    strategy
595                )))
596            }
597        };
598
599        Ok(Self {
600            inner: SimpleImputer::new(impute_strategy, missing_values),
601        })
602    }
603
604    fn fit(&mut self, array: &Bound<'_, PyArray2<f64>>) -> PyResult<()> {
605        let binding = array.readonly();
606        let arr = binding.as_array();
607        self.inner
608            .fit(&arr)
609            .map_err(|e| PyRuntimeError::new_err(format!("Fit failed: {}", e)))
610    }
611
612    fn transform(
613        &self,
614        py: Python,
615        array: &Bound<'_, PyArray2<f64>>,
616    ) -> PyResult<Py<PyArray2<f64>>> {
617        let binding = array.readonly();
618        let arr = binding.as_array();
619        let result = self
620            .inner
621            .transform(&arr)
622            .map_err(|e| PyRuntimeError::new_err(format!("Transform failed: {}", e)))?;
623        Ok(result.into_pyarray(py).unbind())
624    }
625
626    fn fit_transform(
627        &mut self,
628        py: Python,
629        array: &Bound<'_, PyArray2<f64>>,
630    ) -> PyResult<Py<PyArray2<f64>>> {
631        let binding = array.readonly();
632        let arr = binding.as_array();
633        let result = self
634            .inner
635            .fit_transform(&arr)
636            .map_err(|e| PyRuntimeError::new_err(format!("Fit transform failed: {}", e)))?;
637        Ok(result.into_pyarray(py).unbind())
638    }
639}
640
641/// KNN Imputer for missing values
642#[pyclass(name = "KNNImputer")]
643pub struct PyKNNImputer {
644    inner: KNNImputer,
645}
646
647#[pymethods]
648impl PyKNNImputer {
649    #[new]
650    #[pyo3(signature = (n_neighbors=5, missing_values=f64::NAN))]
651    fn new(n_neighbors: usize, missing_values: f64) -> Self {
652        use scirs2_transform::impute::{DistanceMetric, WeightingScheme};
653        Self {
654            inner: KNNImputer::new(
655                n_neighbors,
656                DistanceMetric::Euclidean,
657                WeightingScheme::Uniform,
658                missing_values,
659            ),
660        }
661    }
662
663    fn fit(&mut self, array: &Bound<'_, PyArray2<f64>>) -> PyResult<()> {
664        let binding = array.readonly();
665        let arr = binding.as_array();
666        self.inner
667            .fit(&arr)
668            .map_err(|e| PyRuntimeError::new_err(format!("Fit failed: {}", e)))
669    }
670
671    fn transform(
672        &self,
673        py: Python,
674        array: &Bound<'_, PyArray2<f64>>,
675    ) -> PyResult<Py<PyArray2<f64>>> {
676        let binding = array.readonly();
677        let arr = binding.as_array();
678        let result = self
679            .inner
680            .transform(&arr)
681            .map_err(|e| PyRuntimeError::new_err(format!("Transform failed: {}", e)))?;
682        Ok(result.into_pyarray(py).unbind())
683    }
684}
685
686// ========================================
687// SCALING
688// ========================================
689
690/// Max Absolute Scaler
691#[pyclass(name = "MaxAbsScaler")]
692pub struct PyMaxAbsScaler {
693    inner: MaxAbsScaler,
694}
695
696#[pymethods]
697impl PyMaxAbsScaler {
698    #[new]
699    fn new() -> Self {
700        Self {
701            inner: MaxAbsScaler::new(),
702        }
703    }
704
705    fn fit(&mut self, array: &Bound<'_, PyArray2<f64>>) -> PyResult<()> {
706        let binding = array.readonly();
707        let arr = binding.as_array();
708        self.inner
709            .fit(&arr)
710            .map_err(|e| PyRuntimeError::new_err(format!("Fit failed: {}", e)))
711    }
712
713    fn transform(
714        &self,
715        py: Python,
716        array: &Bound<'_, PyArray2<f64>>,
717    ) -> PyResult<Py<PyArray2<f64>>> {
718        let binding = array.readonly();
719        let arr = binding.as_array();
720        let result = self
721            .inner
722            .transform(&arr)
723            .map_err(|e| PyRuntimeError::new_err(format!("Transform failed: {}", e)))?;
724        Ok(result.into_pyarray(py).unbind())
725    }
726
727    fn fit_transform(
728        &mut self,
729        py: Python,
730        array: &Bound<'_, PyArray2<f64>>,
731    ) -> PyResult<Py<PyArray2<f64>>> {
732        let binding = array.readonly();
733        let arr = binding.as_array();
734        let result = self
735            .inner
736            .fit_transform(&arr)
737            .map_err(|e| PyRuntimeError::new_err(format!("Fit transform failed: {}", e)))?;
738        Ok(result.into_pyarray(py).unbind())
739    }
740}
741
742/// Quantile Transformer
743#[pyclass(name = "QuantileTransformer")]
744pub struct PyQuantileTransformer {
745    inner: QuantileTransformer,
746}
747
748#[pymethods]
749impl PyQuantileTransformer {
750    #[new]
751    #[pyo3(signature = (n_quantiles=1000, output_distribution="uniform", clip=false))]
752    fn new(n_quantiles: usize, output_distribution: &str, clip: bool) -> PyResult<Self> {
753        let transformer = QuantileTransformer::new(n_quantiles, output_distribution, clip)
754            .map_err(|e| {
755                PyRuntimeError::new_err(format!("QuantileTransformer creation failed: {}", e))
756            })?;
757        Ok(Self { inner: transformer })
758    }
759
760    fn fit(&mut self, array: &Bound<'_, PyArray2<f64>>) -> PyResult<()> {
761        let binding = array.readonly();
762        let arr = binding.as_array();
763        self.inner
764            .fit(&arr)
765            .map_err(|e| PyRuntimeError::new_err(format!("Fit failed: {}", e)))
766    }
767
768    fn transform(
769        &self,
770        py: Python,
771        array: &Bound<'_, PyArray2<f64>>,
772    ) -> PyResult<Py<PyArray2<f64>>> {
773        let binding = array.readonly();
774        let arr = binding.as_array();
775        let result = self
776            .inner
777            .transform(&arr)
778            .map_err(|e| PyRuntimeError::new_err(format!("Transform failed: {}", e)))?;
779        Ok(result.into_pyarray(py).unbind())
780    }
781
782    fn fit_transform(
783        &mut self,
784        py: Python,
785        array: &Bound<'_, PyArray2<f64>>,
786    ) -> PyResult<Py<PyArray2<f64>>> {
787        let binding = array.readonly();
788        let arr = binding.as_array();
789        let result = self
790            .inner
791            .fit_transform(&arr)
792            .map_err(|e| PyRuntimeError::new_err(format!("Fit transform failed: {}", e)))?;
793        Ok(result.into_pyarray(py).unbind())
794    }
795}
796
797/// Python module registration
798pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
799    // Normalization
800    m.add_function(wrap_pyfunction!(normalize_array_py, m)?)?;
801    m.add_function(wrap_pyfunction!(normalize_vector_py, m)?)?;
802    m.add_class::<PyNormalizer>()?;
803
804    // Feature engineering
805    m.add_class::<PyPolynomialFeatures>()?;
806    m.add_class::<PyPowerTransformer>()?;
807    m.add_function(wrap_pyfunction!(binarize_py, m)?)?;
808    m.add_function(wrap_pyfunction!(discretize_equal_width_py, m)?)?;
809    m.add_function(wrap_pyfunction!(discretize_equal_frequency_py, m)?)?;
810    m.add_function(wrap_pyfunction!(log_transform_py, m)?)?;
811    m.add_function(wrap_pyfunction!(power_transform_py, m)?)?;
812
813    // Dimensionality reduction
814    m.add_class::<PyPCA>()?;
815    m.add_class::<PyTSNE>()?;
816    m.add_class::<PyUMAP>()?;
817
818    // Categorical encoding
819    m.add_class::<PyOneHotEncoder>()?;
820    m.add_class::<PyOrdinalEncoder>()?;
821
822    // Imputation
823    m.add_class::<PySimpleImputer>()?;
824    m.add_class::<PyKNNImputer>()?;
825
826    // Scaling
827    m.add_class::<PyMaxAbsScaler>()?;
828    m.add_class::<PyQuantileTransformer>()?;
829
830    Ok(())
831}