Skip to main content

sklears_feature_extraction/
statistical_features.rs

1//! Statistical feature extraction
2//!
3//! This module provides transformers for extracting statistical features from data
4//! including moments, distribution properties, and entropy-based measures.
5
6use crate::*;
7// use rayon::prelude::*;
8use scirs2_core::ndarray::{s, Array1, ArrayView1};
9use sklears_core::prelude::SklearsError;
10
11/// Statistical Moments Extractor
12///
13/// Extracts statistical moments and moment-based features from data including
14/// raw moments, central moments, cumulants, and higher-order statistics.
15///
16/// # Parameters
17///
18/// * `max_order` - Maximum order of moments to compute
19/// * `normalize` - Whether to normalize moments
20/// * `include_cumulants` - Whether to include cumulant features
21/// * `robust` - Whether to use robust estimators
22///
23/// # Examples
24///
25/// ```
26/// use sklears_feature_extraction::statistical_features::StatisticalMomentsExtractor;
27/// use scirs2_core::ndarray::array;
28///
29/// let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
30/// let extractor = StatisticalMomentsExtractor::new()
31///     .max_order(4)
32///     .normalize(true)
33///     .include_cumulants(true);
34///
35/// let features = extractor.extract_features(&data.view()).expect("valid data produces features");
36/// ```
37#[derive(Debug, Clone)]
38pub struct StatisticalMomentsExtractor {
39    max_order: usize,
40    normalize: bool,
41    include_cumulants: bool,
42    robust: bool,
43}
44
45impl StatisticalMomentsExtractor {
46    /// Create a new StatisticalMomentsExtractor
47    pub fn new() -> Self {
48        Self {
49            max_order: 4,
50            normalize: true,
51            include_cumulants: true,
52            robust: false,
53        }
54    }
55
56    /// Set the maximum order of moments
57    pub fn max_order(mut self, max_order: usize) -> Self {
58        self.max_order = max_order;
59        self
60    }
61
62    /// Set whether to normalize moments
63    pub fn normalize(mut self, normalize: bool) -> Self {
64        self.normalize = normalize;
65        self
66    }
67
68    /// Set whether to include cumulant features
69    pub fn include_cumulants(mut self, include_cumulants: bool) -> Self {
70        self.include_cumulants = include_cumulants;
71        self
72    }
73
74    /// Set whether to use robust estimators
75    pub fn robust(mut self, robust: bool) -> Self {
76        self.robust = robust;
77        self
78    }
79
80    /// Extract statistical moments from data
81    pub fn extract_features(&self, data: &ArrayView1<Float>) -> SklResult<Array1<Float>> {
82        let n = data.len();
83        if n == 0 {
84            return Err(SklearsError::InvalidInput(
85                "Data cannot be empty".to_string(),
86            ));
87        }
88        if n < 2 {
89            return Err(SklearsError::InvalidInput(
90                "Data must have at least 2 points".to_string(),
91            ));
92        }
93
94        let mut features = Vec::new();
95
96        // Basic statistics
97        let mean = if self.robust {
98            self.robust_mean(data)
99        } else {
100            data.sum() / n as Float
101        };
102        features.push(mean);
103
104        let variance = if self.robust {
105            self.robust_variance(data, mean)
106        } else {
107            data.mapv(|x| (x - mean).powi(2)).sum() / (n - 1) as Float
108        };
109        features.push(variance);
110
111        let std_dev = variance.sqrt();
112        features.push(std_dev);
113
114        // Raw moments
115        for order in 1..=self.max_order {
116            let raw_moment = data.mapv(|x| x.powi(order as i32)).sum() / n as Float;
117            features.push(raw_moment);
118        }
119
120        // Central moments
121        for order in 2..=self.max_order {
122            let central_moment = data.mapv(|x| (x - mean).powi(order as i32)).sum() / n as Float;
123            features.push(central_moment);
124        }
125
126        // Standardized moments (normalized by standard deviation)
127        if self.normalize && std_dev > 1e-10 {
128            for order in 3..=self.max_order {
129                let standardized_moment = data
130                    .mapv(|x| ((x - mean) / std_dev).powi(order as i32))
131                    .sum()
132                    / n as Float;
133                features.push(standardized_moment);
134            }
135        }
136
137        // Cumulants (if requested)
138        if self.include_cumulants {
139            let cumulants = self.compute_cumulants(data, mean, variance)?;
140            features.extend_from_slice(&cumulants);
141        }
142
143        // Shape statistics
144        let skewness = if std_dev > 1e-10 {
145            data.mapv(|x| ((x - mean) / std_dev).powi(3)).sum() / n as Float
146        } else {
147            0.0
148        };
149        features.push(skewness);
150
151        let kurtosis = if std_dev > 1e-10 {
152            data.mapv(|x| ((x - mean) / std_dev).powi(4)).sum() / n as Float - 3.0
153        // Excess kurtosis
154        } else {
155            0.0
156        };
157        features.push(kurtosis);
158
159        // Additional moment-based features
160        let coefficient_of_variation = if mean.abs() > 1e-10 {
161            std_dev / mean.abs()
162        } else {
163            0.0
164        };
165        features.push(coefficient_of_variation);
166
167        Ok(Array1::from_vec(features))
168    }
169
170    fn robust_mean(&self, data: &ArrayView1<Float>) -> Float {
171        // Use median as robust mean estimator
172        let mut sorted_data = data.to_vec();
173        sorted_data.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
174        let n = sorted_data.len();
175
176        if n % 2 == 1 {
177            sorted_data[n / 2]
178        } else {
179            (sorted_data[n / 2 - 1] + sorted_data[n / 2]) / 2.0
180        }
181    }
182
183    fn robust_variance(&self, data: &ArrayView1<Float>, center: Float) -> Float {
184        // Use median absolute deviation (MAD) scaled to match variance
185        let mad_values: Vec<Float> = data.iter().map(|&x| (x - center).abs()).collect();
186        let mut sorted_mad = mad_values;
187        sorted_mad.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
188        let n = sorted_mad.len();
189
190        let mad = if n % 2 == 1 {
191            sorted_mad[n / 2]
192        } else {
193            (sorted_mad[n / 2 - 1] + sorted_mad[n / 2]) / 2.0
194        };
195
196        // Scale MAD to match standard deviation for normal distribution
197        (1.4826 * mad).powi(2)
198    }
199
200    fn compute_cumulants(
201        &self,
202        data: &ArrayView1<Float>,
203        mean: Float,
204        variance: Float,
205    ) -> SklResult<Vec<Float>> {
206        let n = data.len() as Float;
207        let mut cumulants = Vec::new();
208
209        // First cumulant is the mean
210        cumulants.push(mean);
211
212        // Second cumulant is the variance
213        cumulants.push(variance);
214
215        // Third cumulant (related to skewness)
216        let third_moment = data.mapv(|x| (x - mean).powi(3)).sum() / n;
217        cumulants.push(third_moment);
218
219        // Fourth cumulant (related to kurtosis)
220        let fourth_moment = data.mapv(|x| (x - mean).powi(4)).sum() / n;
221        let fourth_cumulant = fourth_moment - 3.0 * variance.powi(2);
222        cumulants.push(fourth_cumulant);
223
224        Ok(cumulants)
225    }
226}
227
228impl Default for StatisticalMomentsExtractor {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234/// Distribution-Based Feature Extractor
235///
236/// Extracts features based on the empirical distribution of data including
237/// quantiles, percentiles, histogram features, and distribution shape measures.
238///
239/// # Parameters
240///
241/// * `n_quantiles` - Number of quantiles to compute
242/// * `n_bins` - Number of histogram bins
243/// * `include_shape_features` - Whether to include distribution shape features
244/// * `density_estimation` - Method for density estimation ("histogram", "kde")
245///
246/// # Examples
247///
248/// ```
249/// use sklears_feature_extraction::statistical_features::DistributionFeaturesExtractor;
250/// use scirs2_core::ndarray::array;
251///
252/// let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
253/// let extractor = DistributionFeaturesExtractor::new()
254///     .n_quantiles(5)
255///     .n_bins(10)
256///     .include_shape_features(true);
257///
258/// let features = extractor.extract_features(&data.view()).expect("valid data produces features");
259/// ```
260#[derive(Debug, Clone)]
261pub struct DistributionFeaturesExtractor {
262    n_quantiles: usize,
263    n_bins: usize,
264    include_shape_features: bool,
265    density_estimation: String,
266}
267
268impl DistributionFeaturesExtractor {
269    /// Create a new DistributionFeaturesExtractor
270    pub fn new() -> Self {
271        Self {
272            n_quantiles: 5,
273            n_bins: 10,
274            include_shape_features: true,
275            density_estimation: "histogram".to_string(),
276        }
277    }
278
279    /// Set the number of quantiles
280    pub fn n_quantiles(mut self, n_quantiles: usize) -> Self {
281        self.n_quantiles = n_quantiles;
282        self
283    }
284
285    /// Set the number of histogram bins
286    pub fn n_bins(mut self, n_bins: usize) -> Self {
287        self.n_bins = n_bins;
288        self
289    }
290
291    /// Set whether to include shape features
292    pub fn include_shape_features(mut self, include_shape_features: bool) -> Self {
293        self.include_shape_features = include_shape_features;
294        self
295    }
296
297    /// Set the density estimation method
298    pub fn density_estimation(mut self, method: String) -> Self {
299        self.density_estimation = method;
300        self
301    }
302
303    /// Extract distribution-based features from data
304    pub fn extract_features(&self, data: &ArrayView1<Float>) -> SklResult<Array1<Float>> {
305        let n = data.len();
306        if n == 0 {
307            return Err(SklearsError::InvalidInput(
308                "Data cannot be empty".to_string(),
309            ));
310        }
311
312        let mut features = Vec::new();
313
314        // Sort data for quantile computation
315        let mut sorted_data = data.to_vec();
316        sorted_data.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
317
318        // Quantile features
319        for i in 1..=self.n_quantiles {
320            let quantile = i as Float / (self.n_quantiles + 1) as Float;
321            let quantile_value = self.compute_quantile(&sorted_data, quantile);
322            features.push(quantile_value);
323        }
324
325        // Additional percentiles (5th, 25th, 50th, 75th, 95th)
326        let percentiles = [0.05, 0.25, 0.5, 0.75, 0.95];
327        for &percentile in &percentiles {
328            let percentile_value = self.compute_quantile(&sorted_data, percentile);
329            features.push(percentile_value);
330        }
331
332        // Interquartile range
333        let q25 = self.compute_quantile(&sorted_data, 0.25);
334        let q75 = self.compute_quantile(&sorted_data, 0.75);
335        let iqr = q75 - q25;
336        features.push(iqr);
337
338        // Range
339        let range = sorted_data[n - 1] - sorted_data[0];
340        features.push(range);
341
342        // Histogram features
343        let histogram = self.compute_histogram(&sorted_data)?;
344        features.extend_from_slice(&histogram);
345
346        // Shape features
347        if self.include_shape_features {
348            let shape_features = self.compute_shape_features(&sorted_data)?;
349            features.extend_from_slice(&shape_features);
350        }
351
352        Ok(Array1::from_vec(features))
353    }
354
355    fn compute_quantile(&self, sorted_data: &[Float], quantile: Float) -> Float {
356        let n = sorted_data.len();
357        let index = quantile * (n - 1) as Float;
358        let lower_index = index.floor() as usize;
359        let upper_index = index.ceil() as usize;
360
361        if lower_index == upper_index {
362            sorted_data[lower_index]
363        } else {
364            let weight = index - lower_index as Float;
365            sorted_data[lower_index] * (1.0 - weight) + sorted_data[upper_index] * weight
366        }
367    }
368
369    fn compute_histogram(&self, sorted_data: &[Float]) -> SklResult<Vec<Float>> {
370        if sorted_data.is_empty() {
371            return Ok(vec![0.0; self.n_bins]);
372        }
373
374        let min_val = sorted_data[0];
375        let max_val = sorted_data[sorted_data.len() - 1];
376
377        if (max_val - min_val).abs() < 1e-10 {
378            // All values are the same
379            let mut hist = vec![0.0; self.n_bins];
380            hist[0] = sorted_data.len() as Float;
381            return Ok(hist);
382        }
383
384        let bin_width = (max_val - min_val) / self.n_bins as Float;
385        let mut histogram = vec![0.0; self.n_bins];
386
387        for &value in sorted_data {
388            let bin_index = ((value - min_val) / bin_width).floor() as usize;
389            let bin_index = bin_index.min(self.n_bins - 1); // Handle edge case
390            histogram[bin_index] += 1.0;
391        }
392
393        // Normalize to get density
394        let total_count = sorted_data.len() as Float;
395        for count in &mut histogram {
396            *count /= total_count;
397        }
398
399        Ok(histogram)
400    }
401
402    fn compute_shape_features(&self, sorted_data: &[Float]) -> SklResult<Vec<Float>> {
403        let mut features = Vec::new();
404
405        // Trimmed mean (10% trim)
406        let trim_amount = (sorted_data.len() as Float * 0.1).floor() as usize;
407        if trim_amount > 0 && trim_amount * 2 < sorted_data.len() {
408            let trimmed_slice = &sorted_data[trim_amount..sorted_data.len() - trim_amount];
409            let trimmed_mean = trimmed_slice.iter().sum::<Float>() / trimmed_slice.len() as Float;
410            features.push(trimmed_mean);
411        } else {
412            features.push(sorted_data.iter().sum::<Float>() / sorted_data.len() as Float);
413        }
414
415        // Mode approximation (most frequent bin in histogram)
416        let histogram = self.compute_histogram(sorted_data)?;
417        let mode_bin = histogram
418            .iter()
419            .enumerate()
420            .max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
421            .map(|(i, _)| i)
422            .unwrap_or(0);
423
424        let min_val = sorted_data[0];
425        let max_val = sorted_data[sorted_data.len() - 1];
426        let bin_width = (max_val - min_val) / self.n_bins as Float;
427        let mode_estimate = min_val + (mode_bin as Float + 0.5) * bin_width;
428        features.push(mode_estimate);
429
430        // Peak count (local maxima in histogram)
431        let mut peak_count = 0;
432        for i in 1..histogram.len() - 1 {
433            if histogram[i] > histogram[i - 1] && histogram[i] > histogram[i + 1] {
434                peak_count += 1;
435            }
436        }
437        features.push(peak_count as Float);
438
439        // Tail weight (sum of probabilities in tails)
440        let tail_size = (self.n_bins as Float * 0.1).max(1.0) as usize;
441        let left_tail: Float = histogram[0..tail_size].iter().sum();
442        let right_tail: Float = histogram[histogram.len() - tail_size..].iter().sum();
443        features.push(left_tail + right_tail);
444
445        Ok(features)
446    }
447}
448
449impl Default for DistributionFeaturesExtractor {
450    fn default() -> Self {
451        Self::new()
452    }
453}
454
455/// Entropy-Based Feature Extractor
456///
457/// Extracts entropy-based features from data including Shannon entropy,
458/// Rényi entropy, sample entropy, and approximate entropy measures.
459///
460/// # Parameters
461///
462/// * `n_bins` - Number of bins for histogram-based entropy
463/// * `include_conditional_entropy` - Whether to include conditional entropy features
464/// * `window_size` - Window size for sample entropy computation
465/// * `tolerance` - Tolerance for approximate entropy
466///
467/// # Examples
468///
469/// ```
470/// use sklears_feature_extraction::statistical_features::EntropyFeaturesExtractor;
471/// use scirs2_core::ndarray::array;
472///
473/// let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
474/// let extractor = EntropyFeaturesExtractor::new()
475///     .n_bins(8)
476///     .window_size(2)
477///     .tolerance(0.1);
478///
479/// let features = extractor.extract_features(&data.view()).expect("valid data produces features");
480/// ```
481#[derive(Debug, Clone)]
482pub struct EntropyFeaturesExtractor {
483    n_bins: usize,
484    include_conditional_entropy: bool,
485    window_size: usize,
486    tolerance: Float,
487}
488
489impl EntropyFeaturesExtractor {
490    /// Create a new EntropyFeaturesExtractor
491    pub fn new() -> Self {
492        Self {
493            n_bins: 10,
494            include_conditional_entropy: true,
495            window_size: 2,
496            tolerance: 0.2,
497        }
498    }
499
500    /// Set the number of bins for histogram-based entropy
501    pub fn n_bins(mut self, n_bins: usize) -> Self {
502        self.n_bins = n_bins;
503        self
504    }
505
506    /// Set whether to include conditional entropy features
507    pub fn include_conditional_entropy(mut self, include_conditional_entropy: bool) -> Self {
508        self.include_conditional_entropy = include_conditional_entropy;
509        self
510    }
511
512    /// Set the window size for sample entropy
513    pub fn window_size(mut self, window_size: usize) -> Self {
514        self.window_size = window_size;
515        self
516    }
517
518    /// Set the tolerance for approximate entropy
519    pub fn tolerance(mut self, tolerance: Float) -> Self {
520        self.tolerance = tolerance;
521        self
522    }
523
524    /// Extract entropy-based features from data
525    pub fn extract_features(&self, data: &ArrayView1<Float>) -> SklResult<Array1<Float>> {
526        let n = data.len();
527        if n == 0 {
528            return Err(SklearsError::InvalidInput(
529                "Data cannot be empty".to_string(),
530            ));
531        }
532
533        let mut features = Vec::new();
534
535        // Shannon entropy from histogram
536        let shannon_entropy = self.compute_shannon_entropy(data)?;
537        features.push(shannon_entropy);
538
539        // Rényi entropy (order 2)
540        let renyi_entropy = self.compute_renyi_entropy(data, 2.0)?;
541        features.push(renyi_entropy);
542
543        // Sample entropy
544        if n >= 2 * self.window_size {
545            let sample_entropy = self.compute_sample_entropy(data)?;
546            features.push(sample_entropy);
547        } else {
548            features.push(0.0); // Not enough data for sample entropy
549        }
550
551        // Approximate entropy
552        if n > self.window_size {
553            let approximate_entropy = self.compute_approximate_entropy(data)?;
554            features.push(approximate_entropy);
555        } else {
556            features.push(0.0); // Not enough data for approximate entropy
557        }
558
559        // Differential entropy estimate
560        let differential_entropy = self.compute_differential_entropy(data)?;
561        features.push(differential_entropy);
562
563        // Conditional entropy (if requested)
564        if self.include_conditional_entropy && n >= 2 {
565            let conditional_entropy = self.compute_conditional_entropy(data)?;
566            features.push(conditional_entropy);
567        }
568
569        Ok(Array1::from_vec(features))
570    }
571
572    fn compute_shannon_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
573        let histogram = self.compute_normalized_histogram(data)?;
574
575        let mut entropy = 0.0;
576        for &prob in &histogram {
577            if prob > 1e-10 {
578                entropy -= prob * prob.ln();
579            }
580        }
581
582        Ok(entropy)
583    }
584
585    fn compute_renyi_entropy(&self, data: &ArrayView1<Float>, alpha: Float) -> SklResult<Float> {
586        if (alpha - 1.0).abs() < 1e-10 {
587            return self.compute_shannon_entropy(data); // Rényi entropy converges to Shannon entropy as α → 1
588        }
589
590        let histogram = self.compute_normalized_histogram(data)?;
591
592        let mut sum = 0.0;
593        for &prob in &histogram {
594            if prob > 1e-10 {
595                sum += prob.powf(alpha);
596            }
597        }
598
599        let entropy = if sum > 1e-10 {
600            sum.ln() / (1.0 - alpha)
601        } else {
602            0.0
603        };
604
605        Ok(entropy)
606    }
607
608    fn compute_sample_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
609        let n = data.len();
610        let m = self.window_size;
611        let r = self.tolerance;
612
613        if n < m + 1 {
614            return Ok(0.0);
615        }
616
617        let mut a = 0;
618        let mut b = 0;
619
620        for i in 0..(n - m) {
621            for j in 0..(n - m) {
622                if i != j {
623                    let mut match_m = true;
624                    let mut _match_m1 = true;
625
626                    // Check for matches of length m
627                    for k in 0..m {
628                        if (data[i + k] - data[j + k]).abs() > r {
629                            match_m = false;
630                            _match_m1 = false;
631                            break;
632                        }
633                    }
634
635                    if match_m {
636                        b += 1;
637
638                        // Check for matches of length m+1
639                        if i < n - m && j < n - m {
640                            if (data[i + m] - data[j + m]).abs() <= r {
641                                a += 1;
642                            } else {
643                                _match_m1 = false;
644                            }
645                        } else {
646                            _match_m1 = false;
647                        }
648                    }
649                }
650            }
651        }
652
653        let sample_entropy = if a > 0 && b > 0 {
654            -(a as Float / b as Float).ln()
655        } else {
656            std::f64::consts::E // Natural logarithm base for undefined case
657        };
658
659        Ok(sample_entropy)
660    }
661
662    fn compute_approximate_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
663        let n = data.len();
664        let m = self.window_size;
665        let r = self.tolerance;
666
667        if n < m + 1 {
668            return Ok(0.0);
669        }
670
671        let phi_m = self.compute_phi(data, m, r)?;
672        let phi_m1 = self.compute_phi(data, m + 1, r)?;
673
674        Ok(phi_m - phi_m1)
675    }
676
677    fn compute_phi(&self, data: &ArrayView1<Float>, m: usize, r: Float) -> SklResult<Float> {
678        let n = data.len();
679        let mut phi = 0.0;
680
681        for i in 0..(n - m + 1) {
682            let mut pattern_count = 0;
683
684            for j in 0..(n - m + 1) {
685                let mut matches = true;
686                for k in 0..m {
687                    if (data[i + k] - data[j + k]).abs() > r {
688                        matches = false;
689                        break;
690                    }
691                }
692                if matches {
693                    pattern_count += 1;
694                }
695            }
696
697            if pattern_count > 0 {
698                phi += (pattern_count as Float / (n - m + 1) as Float).ln();
699            }
700        }
701
702        phi /= (n - m + 1) as Float;
703        Ok(phi)
704    }
705
706    fn compute_differential_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
707        // Estimate using Gaussian assumption: h(X) = 0.5 * log(2πeσ²)
708        let mean = data.sum() / data.len() as Float;
709        let variance = data.mapv(|x| (x - mean).powi(2)).sum() / (data.len() - 1) as Float;
710
711        let differential_entropy =
712            0.5 * (2.0 * std::f64::consts::PI * std::f64::consts::E * variance).ln();
713        Ok(differential_entropy)
714    }
715
716    fn compute_conditional_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
717        // Approximate conditional entropy H(X_{t+1} | X_t)
718        let n = data.len();
719        if n < 2 {
720            return Ok(0.0);
721        }
722
723        // Create pairs (X_t, X_{t+1})
724        let pairs: Vec<(Float, Float)> = (0..n - 1).map(|i| (data[i], data[i + 1])).collect();
725
726        // Use histogram approach for simplicity
727        let joint_entropy = self.compute_joint_entropy(&pairs)?;
728        let marginal_entropy = self.compute_shannon_entropy(&data.slice(s![0..n - 1]))?;
729
730        Ok(joint_entropy - marginal_entropy)
731    }
732
733    fn compute_joint_entropy(&self, pairs: &[(Float, Float)]) -> SklResult<Float> {
734        // Simple binning approach for joint entropy
735        let n_bins = self.n_bins;
736
737        if pairs.is_empty() {
738            return Ok(0.0);
739        }
740
741        // Find ranges for both dimensions
742        let x_min = pairs
743            .iter()
744            .map(|(x, _)| *x)
745            .fold(f64::INFINITY, |a, b| a.min(b));
746        let x_max = pairs
747            .iter()
748            .map(|(x, _)| *x)
749            .fold(f64::NEG_INFINITY, |a, b| a.max(b));
750        let y_min = pairs
751            .iter()
752            .map(|(_, y)| *y)
753            .fold(f64::INFINITY, |a, b| a.min(b));
754        let y_max = pairs
755            .iter()
756            .map(|(_, y)| *y)
757            .fold(f64::NEG_INFINITY, |a, b| a.max(b));
758
759        let x_range = if (x_max - x_min).abs() < 1e-10 {
760            1.0
761        } else {
762            x_max - x_min
763        };
764        let y_range = if (y_max - y_min).abs() < 1e-10 {
765            1.0
766        } else {
767            y_max - y_min
768        };
769
770        let mut joint_hist = vec![vec![0.0; n_bins]; n_bins];
771
772        for &(x, y) in pairs {
773            let x_bin = ((x - x_min) / x_range * n_bins as Float).floor() as usize;
774            let y_bin = ((y - y_min) / y_range * n_bins as Float).floor() as usize;
775            let x_bin = x_bin.min(n_bins - 1);
776            let y_bin = y_bin.min(n_bins - 1);
777            joint_hist[x_bin][y_bin] += 1.0;
778        }
779
780        // Normalize and compute entropy
781        let total = pairs.len() as Float;
782        let mut entropy = 0.0;
783
784        for row in joint_hist.iter().take(n_bins) {
785            for &count in row.iter().take(n_bins) {
786                let prob = count / total;
787                if prob > 1e-10 {
788                    entropy -= prob * prob.ln();
789                }
790            }
791        }
792
793        Ok(entropy)
794    }
795
796    fn compute_normalized_histogram(&self, data: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
797        let n = data.len();
798        if n == 0 {
799            return Ok(vec![0.0; self.n_bins]);
800        }
801
802        let min_val = data.iter().cloned().fold(f64::INFINITY, |a, b| a.min(b));
803        let max_val = data
804            .iter()
805            .cloned()
806            .fold(f64::NEG_INFINITY, |a, b| a.max(b));
807
808        if (max_val - min_val).abs() < 1e-10 {
809            // All values are the same
810            let mut hist = vec![0.0; self.n_bins];
811            hist[0] = 1.0;
812            return Ok(hist);
813        }
814
815        let bin_width = (max_val - min_val) / self.n_bins as Float;
816        let mut histogram = vec![0.0; self.n_bins];
817
818        for &value in data.iter() {
819            let bin_index = ((value - min_val) / bin_width).floor() as usize;
820            let bin_index = bin_index.min(self.n_bins - 1);
821            histogram[bin_index] += 1.0;
822        }
823
824        // Normalize to get probabilities
825        let total_count = n as Float;
826        for count in &mut histogram {
827            *count /= total_count;
828        }
829
830        Ok(histogram)
831    }
832}
833
834impl Default for EntropyFeaturesExtractor {
835    fn default() -> Self {
836        Self::new()
837    }
838}