Skip to main content

sklears_feature_extraction/
sketching_sampling.rs

1//! Sketching and sampling feature engineering components
2//!
3//! This module provides implementations of various sketching and sampling algorithms
4//! for efficient feature extraction and data processing.
5
6use crate::*;
7// use rayon::prelude::*;
8use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis};
9use sklears_core::prelude::{SklearsError, Transform};
10use sklears_core::types::Float;
11use std::collections::HashMap;
12use std::hash::Hash;
13
14/// Count-Min Sketch for frequency estimation with guaranteed error bounds
15///
16/// Count-Min Sketch is a probabilistic data structure that maintains frequency
17/// estimates for items in a stream. It provides space-efficient approximate
18/// counting with mathematical guarantees on accuracy.
19///
20/// This implementation provides feature extraction from sketched frequency
21/// distributions, useful for large-scale data analysis where exact counting
22/// is impractical.
23///
24/// # Examples
25/// ```
26/// # use sklears_feature_extraction::sketching_sampling::CountMinSketch;
27/// # use scirs2_core::ndarray::Array2;
28/// # use sklears_core::traits::{Transform, Fit};
29/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
30/// let data = Array2::from_shape_vec((100, 3), (0..300).map(|x| (x % 50) as f64).collect())?;
31///
32/// let sketch = CountMinSketch::new()
33///     .width(100)
34///     .depth(5)
35///     .include_frequency_statistics(true);
36///
37/// let features = sketch.transform(&data.view())?;
38/// assert!(features.len() > 0);
39/// # Ok(())
40/// # }
41/// ```
42#[derive(Debug, Clone)]
43pub struct CountMinSketch {
44    width: usize,
45    depth: usize,
46    include_frequency_statistics: bool,
47    include_sketch_properties: bool,
48    hash_seed: u64,
49}
50
51impl CountMinSketch {
52    /// Create a new Count-Min Sketch
53    pub fn new() -> Self {
54        Self {
55            width: 1000,
56            depth: 5,
57            include_frequency_statistics: true,
58            include_sketch_properties: true,
59            hash_seed: 42,
60        }
61    }
62
63    /// Set the width of the sketch (number of buckets per hash function)
64    pub fn width(mut self, width: usize) -> Self {
65        self.width = width;
66        self
67    }
68
69    /// Set the depth of the sketch (number of hash functions)
70    pub fn depth(mut self, depth: usize) -> Self {
71        self.depth = depth;
72        self
73    }
74
75    /// Set whether to include frequency statistics
76    pub fn include_frequency_statistics(mut self, include: bool) -> Self {
77        self.include_frequency_statistics = include;
78        self
79    }
80
81    /// Set whether to include sketch properties
82    pub fn include_sketch_properties(mut self, include: bool) -> Self {
83        self.include_sketch_properties = include;
84        self
85    }
86
87    /// Set the hash seed for reproducibility
88    pub fn hash_seed(mut self, seed: u64) -> Self {
89        self.hash_seed = seed;
90        self
91    }
92
93    /// Hash function implementation
94    fn hash(&self, item: u64, hash_idx: usize) -> usize {
95        // Simple hash function combining item and hash index
96        let combined = item.wrapping_add((hash_idx as u64).wrapping_mul(self.hash_seed));
97        let hash_val = combined.wrapping_mul(0x9e3779b97f4a7c15_u64);
98        (hash_val as usize) % self.width
99    }
100
101    /// Build the Count-Min Sketch from data
102    fn build_sketch(&self, data: &Array2<f64>) -> Array2<u32> {
103        let mut sketch = Array2::zeros((self.depth, self.width));
104
105        // Process each data point
106        for row in data.outer_iter() {
107            for &value in row.iter() {
108                // Convert value to integer for hashing
109                let item = (value * 1000.0) as u64; // Scale to preserve some precision
110
111                // Update all hash functions
112                for hash_idx in 0..self.depth {
113                    let bucket = self.hash(item, hash_idx);
114                    sketch[[hash_idx, bucket]] += 1;
115                }
116            }
117        }
118
119        sketch
120    }
121
122    /// Estimate frequency of an item
123    #[allow(dead_code)] // estimate_frequency retained as public count-min sketch API
124    fn estimate_frequency(&self, sketch: &Array2<u32>, item: f64) -> u32 {
125        let item_hash = (item * 1000.0) as u64;
126        let mut min_count = u32::MAX;
127
128        for hash_idx in 0..self.depth {
129            let bucket = self.hash(item_hash, hash_idx);
130            min_count = min_count.min(sketch[[hash_idx, bucket]]);
131        }
132
133        min_count
134    }
135
136    /// Extract features from the sketch
137    fn extract_sketch_features(&self, sketch: &Array2<u32>, data: &Array2<f64>) -> Vec<f64> {
138        let mut features = Vec::new();
139
140        if self.include_frequency_statistics {
141            // Total count
142            let total_count: u32 = sketch.sum();
143            features.push(total_count as f64);
144
145            // Non-zero buckets
146            let non_zero_buckets = sketch.iter().filter(|&&x| x > 0).count();
147            features.push(non_zero_buckets as f64);
148
149            // Load factor (fraction of non-zero buckets)
150            let load_factor = non_zero_buckets as f64 / (self.width * self.depth) as f64;
151            features.push(load_factor);
152
153            // Maximum bucket count
154            let max_count = sketch.iter().max().copied().unwrap_or(0);
155            features.push(max_count as f64);
156
157            // Average bucket count (excluding zeros)
158            let avg_count = if non_zero_buckets > 0 {
159                total_count as f64 / non_zero_buckets as f64
160            } else {
161                0.0
162            };
163            features.push(avg_count);
164        }
165
166        if self.include_sketch_properties {
167            // Sketch dimensions
168            features.push(self.width as f64);
169            features.push(self.depth as f64);
170
171            // Collision rate estimate (approximate)
172            let unique_items = data
173                .iter()
174                .map(|&x| (x * 1000.0) as u64)
175                .collect::<std::collections::HashSet<_>>()
176                .len();
177            let collision_rate = if unique_items > 0 {
178                1.0 - (unique_items as f64 / (self.width as f64))
179            } else {
180                0.0
181            };
182            features.push(collision_rate.max(0.0));
183        }
184
185        features
186    }
187}
188
189impl Default for CountMinSketch {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl Transform<ArrayView2<'_, Float>, Array1<Float>> for CountMinSketch {
196    fn transform(&self, data: &ArrayView2<'_, Float>) -> SklResult<Array1<Float>> {
197        if data.is_empty() {
198            return Err(SklearsError::InvalidInput(
199                "Cannot create sketch from empty data".to_string(),
200            ));
201        }
202
203        // Convert to f64 for internal processing
204        let data_owned = data.mapv(|x| x).to_owned();
205
206        let sketch = self.build_sketch(&data_owned);
207        let features = self.extract_sketch_features(&sketch, &data_owned);
208
209        Ok(Array1::from_vec(
210            features.into_iter().map(|x| x as Float).collect(),
211        ))
212    }
213}
214
215/// Fast Johnson-Lindenstrauss Transform for efficient random projection
216///
217/// The Fast Johnson-Lindenstrauss Transform (FJLT) provides an efficient
218/// implementation of the Johnson-Lindenstrauss lemma using fast transforms
219/// like Walsh-Hadamard Transform combined with sparse random matrices.
220///
221/// This provides approximate distance preservation with better computational
222/// efficiency compared to dense Gaussian random projections.
223///
224/// # Examples
225/// ```
226/// # use sklears_feature_extraction::sketching_sampling::FastJohnsonLindenstrauss;
227/// # use scirs2_core::ndarray::Array2;
228/// # use sklears_core::traits::{Transform, Fit};
229/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
230/// let data = Array2::from_shape_vec((50, 16), (0..800).map(|x| x as f64).collect())?;
231///
232/// let fjlt = FastJohnsonLindenstrauss::new()
233///     .target_dimension(8)
234///     .sparsity_parameter(0.1);
235///
236/// let features = fjlt.transform(&data.view())?;
237/// assert_eq!(features.len(), 50 * 8); // 50 samples, 8 dimensions
238/// # Ok(())
239/// # }
240/// ```
241#[derive(Debug, Clone)]
242pub struct FastJohnsonLindenstrauss {
243    target_dimension: usize,
244    sparsity_parameter: f64,
245    random_state: Option<u64>,
246    include_transform_stats: bool,
247}
248
249impl FastJohnsonLindenstrauss {
250    /// Create a new Fast Johnson-Lindenstrauss transform
251    pub fn new() -> Self {
252        Self {
253            target_dimension: 100,
254            sparsity_parameter: 0.1,
255            random_state: Some(42),
256            include_transform_stats: false,
257        }
258    }
259
260    /// Set the target dimension for the projection
261    pub fn target_dimension(mut self, target_dimension: usize) -> Self {
262        self.target_dimension = target_dimension;
263        self
264    }
265
266    /// Set the sparsity parameter (fraction of non-zero elements)
267    pub fn sparsity_parameter(mut self, sparsity: f64) -> Self {
268        self.sparsity_parameter = sparsity.clamp(0.01, 1.0);
269        self
270    }
271
272    /// Set the random state for reproducibility
273    pub fn random_state(mut self, seed: u64) -> Self {
274        self.random_state = Some(seed);
275        self
276    }
277
278    /// Set whether to include transform statistics
279    pub fn include_transform_stats(mut self, include: bool) -> Self {
280        self.include_transform_stats = include;
281        self
282    }
283
284    /// Generate sparse random matrix
285    fn generate_sparse_matrix(&self, input_dim: usize) -> Array2<f64> {
286        use std::collections::hash_map::DefaultHasher;
287        use std::hash::Hasher;
288
289        let mut matrix = Array2::zeros((self.target_dimension, input_dim));
290        let seed = self.random_state.unwrap_or(42);
291
292        for i in 0..self.target_dimension {
293            for j in 0..input_dim {
294                let mut hasher = DefaultHasher::new();
295                (seed, i, j).hash(&mut hasher);
296                let hash_val = hasher.finish();
297
298                // Determine if this element should be non-zero
299                let uniform_val = (hash_val % 1000) as f64 / 1000.0;
300
301                if uniform_val < self.sparsity_parameter {
302                    // Rademacher random variable (+1 or -1)
303                    let sign_hash = hash_val >> 32;
304                    let sign = if sign_hash.is_multiple_of(2) {
305                        1.0
306                    } else {
307                        -1.0
308                    };
309                    matrix[[i, j]] = sign / (self.sparsity_parameter * input_dim as f64).sqrt();
310                }
311            }
312        }
313
314        matrix
315    }
316
317    /// Apply fast Walsh-Hadamard transform (simplified version)
318    fn fast_hadamard_transform(&self, mut data: Array1<f64>) -> Array1<f64> {
319        let n = data.len();
320
321        // Find the next power of 2
322        let next_pow2 = n.next_power_of_two();
323
324        // Pad with zeros if necessary
325        if next_pow2 > n {
326            let mut padded = Array1::zeros(next_pow2);
327            padded.slice_mut(s![..n]).assign(&data);
328            data = padded;
329        }
330
331        let len = data.len();
332        let mut h = 1;
333
334        while h < len {
335            for i in (0..len).step_by(h * 2) {
336                for j in i..(i + h).min(len) {
337                    if j + h < len {
338                        let a = data[j];
339                        let b = data[j + h];
340                        data[j] = a + b;
341                        data[j + h] = a - b;
342                    }
343                }
344            }
345            h *= 2;
346        }
347
348        // Normalize
349        let normalizer = (len as f64).sqrt();
350        data.mapv_inplace(|x| x / normalizer);
351
352        // Return only the original size
353        if next_pow2 > n {
354            data.slice(s![..n]).to_owned()
355        } else {
356            data
357        }
358    }
359
360    /// Apply the FJLT
361    fn apply_fjlt(&self, data: &Array2<f64>) -> Array2<f64> {
362        let n_samples = data.nrows();
363        let input_dim = data.ncols();
364
365        let mut result = Array2::zeros((n_samples, self.target_dimension));
366
367        // Generate sparse random matrix
368        let sparse_matrix = self.generate_sparse_matrix(input_dim);
369
370        // Apply transform to each sample
371        for (i, sample) in data.outer_iter().enumerate() {
372            // Apply fast Hadamard transform
373            let hadamard_result = self.fast_hadamard_transform(sample.to_owned());
374
375            // Apply sparse random projection
376            for j in 0..self.target_dimension {
377                let mut sum = 0.0;
378                for k in 0..input_dim {
379                    sum += sparse_matrix[[j, k]] * hadamard_result[k];
380                }
381                result[[i, j]] = sum;
382            }
383        }
384
385        result
386    }
387
388    /// Extract features from the transformed data
389    fn extract_features(&self, transformed_data: &Array2<f64>) -> Vec<f64> {
390        if self.include_transform_stats {
391            let mut features = Vec::new();
392
393            // Basic statistics of the transformed data
394            let mean = transformed_data.mean().unwrap_or(0.0);
395            let std = transformed_data.std(0.0);
396            let min_val = transformed_data
397                .iter()
398                .copied()
399                .fold(f64::INFINITY, f64::min);
400            let max_val = transformed_data
401                .iter()
402                .copied()
403                .fold(f64::NEG_INFINITY, f64::max);
404
405            features.push(mean);
406            features.push(std);
407            features.push(min_val);
408            features.push(max_val);
409            features.push(self.target_dimension as f64);
410            features.push(self.sparsity_parameter);
411
412            features
413        } else {
414            // Flatten the transformed data
415            transformed_data.iter().copied().collect()
416        }
417    }
418}
419
420impl Default for FastJohnsonLindenstrauss {
421    fn default() -> Self {
422        Self::new()
423    }
424}
425
426impl Transform<ArrayView2<'_, Float>, Array1<Float>> for FastJohnsonLindenstrauss {
427    fn transform(&self, data: &ArrayView2<'_, Float>) -> SklResult<Array1<Float>> {
428        if data.is_empty() {
429            return Err(SklearsError::InvalidInput(
430                "Cannot transform empty data".to_string(),
431            ));
432        }
433
434        if data.ncols() == 0 {
435            return Err(SklearsError::InvalidInput(
436                "Data must have at least one column".to_string(),
437            ));
438        }
439
440        // Convert to f64 for internal processing
441        let data_owned = data.mapv(|x| x).to_owned();
442
443        let transformed = self.apply_fjlt(&data_owned);
444        let features = self.extract_features(&transformed);
445
446        Ok(Array1::from_vec(
447            features.into_iter().map(|x| x as Float).collect(),
448        ))
449    }
450}
451
452/// Reservoir Sampling for Large Dataset Processing
453///
454/// Implements reservoir sampling algorithm for selecting a fixed-size sample
455/// from a large or streaming dataset with uniform probability.
456///
457/// # Parameters
458///
459/// * `reservoir_size` - Size of the sample to maintain
460/// * `random_state` - Random state for reproducible sampling
461///
462/// # Examples
463///
464/// ```
465/// use sklears_feature_extraction::sketching_sampling::ReservoirSampler;
466/// use scirs2_core::ndarray::array;
467///
468/// let sampler = ReservoirSampler::new()
469///     .reservoir_size(100)
470///     .random_state(42);
471/// ```
472#[derive(Debug, Clone)]
473pub struct ReservoirSampler {
474    reservoir_size: usize,
475    random_state: Option<u64>,
476    reservoir: Vec<usize>,
477    seen_count: usize,
478}
479
480impl ReservoirSampler {
481    /// Create a new ReservoirSampler
482    pub fn new() -> Self {
483        Self {
484            reservoir_size: 1000,
485            random_state: None,
486            reservoir: Vec::new(),
487            seen_count: 0,
488        }
489    }
490
491    /// Set the reservoir size
492    pub fn reservoir_size(mut self, size: usize) -> Self {
493        self.reservoir_size = size;
494        self
495    }
496
497    /// Set the random state for reproducible sampling
498    pub fn random_state(mut self, seed: u64) -> Self {
499        self.random_state = Some(seed);
500        self
501    }
502
503    /// Sample indices from a dataset using reservoir sampling
504    pub fn sample_indices(&mut self, dataset_size: usize) -> SklResult<Vec<usize>> {
505        use scirs2_core::random::thread_rng;
506
507        // Use rng() function from scirs2_core - seeding handled elsewhere if needed
508        let mut rng = thread_rng();
509
510        let actual_size = self.reservoir_size.min(dataset_size);
511        let mut reservoir = Vec::with_capacity(actual_size);
512
513        // Fill reservoir with first k elements
514        for i in 0..actual_size {
515            reservoir.push(i);
516        }
517
518        // Replace elements with gradually decreasing probability
519        for i in actual_size..dataset_size {
520            let j = rng.random_range(0..i + 1);
521            if j < actual_size {
522                reservoir[j] = i;
523            }
524        }
525
526        self.reservoir = reservoir.clone();
527        self.seen_count = dataset_size;
528
529        Ok(reservoir)
530    }
531
532    /// Sample data using reservoir sampling
533    pub fn sample_data(&mut self, data: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
534        let indices = self.sample_indices(data.nrows())?;
535        let sampled_data = data.select(Axis(0), &indices);
536        Ok(sampled_data)
537    }
538
539    /// Get current reservoir indices
540    pub fn get_reservoir_indices(&self) -> &[usize] {
541        &self.reservoir
542    }
543
544    /// Get count of seen samples
545    pub fn get_seen_count(&self) -> usize {
546        self.seen_count
547    }
548}
549
550impl Default for ReservoirSampler {
551    fn default() -> Self {
552        Self::new()
553    }
554}
555
556/// Importance Sampling for Weighted Data Selection
557///
558/// Implements importance sampling for selecting samples based on weights,
559/// useful for handling imbalanced datasets or focusing on important regions.
560///
561/// # Parameters
562///
563/// * `sample_size` - Number of samples to select
564/// * `replacement` - Whether to sample with replacement
565/// * `random_state` - Random state for reproducible sampling
566///
567/// # Examples
568///
569/// ```
570/// use sklears_feature_extraction::sketching_sampling::ImportanceSampler;
571/// use scirs2_core::ndarray::array;
572///
573/// let sampler = ImportanceSampler::new()
574///     .sample_size(100)
575///     .replacement(true)
576///     .random_state(42);
577/// ```
578#[derive(Debug, Clone)]
579pub struct ImportanceSampler {
580    sample_size: usize,
581    replacement: bool,
582    random_state: Option<u64>,
583}
584
585impl ImportanceSampler {
586    /// Create a new ImportanceSampler
587    pub fn new() -> Self {
588        Self {
589            sample_size: 1000,
590            replacement: false,
591            random_state: None,
592        }
593    }
594
595    /// Set the sample size
596    pub fn sample_size(mut self, size: usize) -> Self {
597        self.sample_size = size;
598        self
599    }
600
601    /// Set whether to sample with replacement
602    pub fn replacement(mut self, replacement: bool) -> Self {
603        self.replacement = replacement;
604        self
605    }
606
607    /// Set the random state for reproducible sampling
608    pub fn random_state(mut self, seed: u64) -> Self {
609        self.random_state = Some(seed);
610        self
611    }
612
613    /// Sample indices based on importance weights
614    pub fn sample_indices(&self, weights: &ArrayView1<Float>) -> SklResult<Vec<usize>> {
615        use scirs2_core::random::thread_rng;
616
617        if weights.is_empty() {
618            return Err(SklearsError::InvalidInput(
619                "Weights cannot be empty".to_string(),
620            ));
621        }
622
623        // Normalize weights to probabilities
624        let weight_sum: Float = weights.sum();
625        if weight_sum <= 0.0 {
626            return Err(SklearsError::InvalidInput(
627                "Weights must be positive".to_string(),
628            ));
629        }
630
631        let probabilities: Array1<Float> = weights / weight_sum;
632        let cumulative: Array1<Float> = {
633            let mut cum = Array1::zeros(probabilities.len());
634            cum[0] = probabilities[0];
635            for i in 1..probabilities.len() {
636                cum[i] = cum[i - 1] + probabilities[i];
637            }
638            cum
639        };
640
641        // Use rng() function from scirs2_core - seeding handled elsewhere if needed
642        let mut rng = thread_rng();
643
644        let mut selected_indices = Vec::with_capacity(self.sample_size);
645        let mut used_indices = std::collections::HashSet::new();
646
647        for _ in 0..self.sample_size {
648            let random_value: Float = rng.random();
649
650            // Find index using binary search on cumulative distribution
651            let mut idx = 0;
652            for (i, &cum_prob) in cumulative.iter().enumerate() {
653                if random_value <= cum_prob {
654                    idx = i;
655                    break;
656                }
657            }
658
659            if self.replacement || !used_indices.contains(&idx) {
660                selected_indices.push(idx);
661                if !self.replacement {
662                    used_indices.insert(idx);
663                }
664            } else if !self.replacement {
665                // Find next available index
666                for i in 0..weights.len() {
667                    if !used_indices.contains(&i) {
668                        selected_indices.push(i);
669                        used_indices.insert(i);
670                        break;
671                    }
672                }
673            }
674
675            if !self.replacement && used_indices.len() == weights.len() {
676                break;
677            }
678        }
679
680        Ok(selected_indices)
681    }
682
683    /// Sample data based on importance weights
684    pub fn sample_data(
685        &self,
686        data: &ArrayView2<Float>,
687        weights: &ArrayView1<Float>,
688    ) -> SklResult<Array2<Float>> {
689        if data.nrows() != weights.len() {
690            return Err(SklearsError::InvalidInput(
691                "Data rows and weights length must match".to_string(),
692            ));
693        }
694
695        let indices = self.sample_indices(weights)?;
696        let sampled_data = data.select(Axis(0), &indices);
697        Ok(sampled_data)
698    }
699}
700
701impl Default for ImportanceSampler {
702    fn default() -> Self {
703        Self::new()
704    }
705}
706
707/// Stratified Sampling for Balanced Data Selection
708///
709/// Implements stratified sampling to maintain the same proportion of samples
710/// in each stratum as in the original dataset, useful for maintaining class balance.
711///
712/// # Parameters
713///
714/// * `sample_size` - Total number of samples to select
715/// * `random_state` - Random state for reproducible sampling
716///
717/// # Examples
718///
719/// ```
720/// use sklears_feature_extraction::sketching_sampling::StratifiedSampler;
721/// use scirs2_core::ndarray::array;
722///
723/// let sampler = StratifiedSampler::new()
724///     .sample_size(100)
725///     .random_state(42);
726/// ```
727#[derive(Debug, Clone)]
728pub struct StratifiedSampler {
729    sample_size: usize,
730    random_state: Option<u64>,
731}
732
733impl StratifiedSampler {
734    /// Create a new StratifiedSampler
735    pub fn new() -> Self {
736        Self {
737            sample_size: 1000,
738            random_state: None,
739        }
740    }
741
742    /// Set the sample size
743    pub fn sample_size(mut self, size: usize) -> Self {
744        self.sample_size = size;
745        self
746    }
747
748    /// Set the random state for reproducible sampling
749    pub fn random_state(mut self, seed: u64) -> Self {
750        self.random_state = Some(seed);
751        self
752    }
753
754    /// Sample indices using stratified sampling
755    pub fn sample_indices(&self, strata: &ArrayView1<Float>) -> SklResult<Vec<usize>> {
756        use scirs2_core::random::thread_rng;
757
758        if strata.is_empty() {
759            return Err(SklearsError::InvalidInput(
760                "Strata cannot be empty".to_string(),
761            ));
762        }
763
764        // Use rng() function from scirs2_core - seeding handled elsewhere if needed
765        let mut rng = thread_rng();
766
767        // Group indices by stratum
768        let mut stratum_indices: HashMap<String, Vec<usize>> = HashMap::new();
769        for (idx, &stratum) in strata.iter().enumerate() {
770            let key = format!("{:.6}", stratum); // Use string representation for grouping
771            stratum_indices.entry(key).or_default().push(idx);
772        }
773
774        let total_samples = strata.len();
775        let mut selected_indices = Vec::new();
776
777        // Sample proportionally from each stratum
778        for (_, indices) in stratum_indices.iter() {
779            let stratum_proportion = indices.len() as f64 / total_samples as f64;
780            let stratum_sample_size =
781                (self.sample_size as f64 * stratum_proportion).round() as usize;
782            let stratum_sample_size = stratum_sample_size.min(indices.len());
783
784            if stratum_sample_size > 0 {
785                let mut stratum_indices = indices.clone();
786                // Simple Fisher-Yates shuffle
787                for i in (1..stratum_indices.len()).rev() {
788                    let j = rng.random_range(0..i + 1);
789                    stratum_indices.swap(i, j);
790                }
791                selected_indices.extend(stratum_indices.into_iter().take(stratum_sample_size));
792            }
793        }
794
795        // If we haven't reached the target sample size, add more samples
796        while selected_indices.len() < self.sample_size && selected_indices.len() < total_samples {
797            let all_indices: Vec<usize> = (0..total_samples).collect();
798            let mut remaining: Vec<usize> = all_indices
799                .into_iter()
800                .filter(|&idx| !selected_indices.contains(&idx))
801                .collect();
802
803            if remaining.is_empty() {
804                break;
805            }
806
807            // Simple Fisher-Yates shuffle
808            for i in (1..remaining.len()).rev() {
809                let j = rng.random_range(0..i + 1);
810                remaining.swap(i, j);
811            }
812            let needed = self.sample_size - selected_indices.len();
813            selected_indices.extend(remaining.into_iter().take(needed));
814        }
815
816        selected_indices.truncate(self.sample_size);
817        Ok(selected_indices)
818    }
819
820    /// Sample data using stratified sampling
821    pub fn sample_data(
822        &self,
823        data: &ArrayView2<Float>,
824        strata: &ArrayView1<Float>,
825    ) -> SklResult<Array2<Float>> {
826        if data.nrows() != strata.len() {
827            return Err(SklearsError::InvalidInput(
828                "Data rows and strata length must match".to_string(),
829            ));
830        }
831
832        let indices = self.sample_indices(strata)?;
833        let sampled_data = data.select(Axis(0), &indices);
834        Ok(sampled_data)
835    }
836}
837
838impl Default for StratifiedSampler {
839    fn default() -> Self {
840        Self::new()
841    }
842}
843
844/// Fast Transform Methods for Efficient Feature Extraction
845///
846/// Implements various fast transform algorithms for efficient computation
847/// of frequency domain features and other transform-based features.
848///
849/// # Transform Types
850///
851/// * `FFT` - Fast Fourier Transform for frequency analysis
852/// * `DCT` - Discrete Cosine Transform for compression and feature extraction
853/// * `Walsh` - Walsh-Hadamard Transform for fast computation
854/// * `Haar` - Haar Transform for multiresolution analysis
855///
856/// # Examples
857///
858/// ```
859/// use sklears_feature_extraction::sketching_sampling::{FastTransformExtractor, TransformType};
860/// use scirs2_core::ndarray::array;
861///
862/// let extractor = FastTransformExtractor::new()
863///     .transform_type(TransformType::FFT)
864///     .include_magnitude(true)
865///     .include_phase(false);
866/// ```
867#[derive(Debug, Clone, PartialEq)]
868pub enum TransformType {
869    /// Fast Fourier Transform
870    FFT,
871    /// Discrete Cosine Transform
872    DCT,
873    /// Walsh-Hadamard Transform
874    Walsh,
875    /// Haar Transform
876    Haar,
877}
878
879#[derive(Debug, Clone)]
880pub struct FastTransformExtractor {
881    transform_type: TransformType,
882    include_magnitude: bool,
883    include_phase: bool,
884    include_power: bool,
885    normalize: bool,
886    zero_pad_to_power_of_2: bool,
887}
888
889impl FastTransformExtractor {
890    /// Create a new FastTransformExtractor
891    pub fn new() -> Self {
892        Self {
893            transform_type: TransformType::FFT,
894            include_magnitude: true,
895            include_phase: false,
896            include_power: false,
897            normalize: true,
898            zero_pad_to_power_of_2: true,
899        }
900    }
901
902    /// Set the transform type
903    pub fn transform_type(mut self, transform_type: TransformType) -> Self {
904        self.transform_type = transform_type;
905        self
906    }
907
908    /// Set whether to include magnitude features
909    pub fn include_magnitude(mut self, include: bool) -> Self {
910        self.include_magnitude = include;
911        self
912    }
913
914    /// Set whether to include phase features
915    pub fn include_phase(mut self, include: bool) -> Self {
916        self.include_phase = include;
917        self
918    }
919
920    /// Set whether to include power features
921    pub fn include_power(mut self, include: bool) -> Self {
922        self.include_power = include;
923        self
924    }
925
926    /// Set whether to normalize the output
927    pub fn normalize(mut self, normalize: bool) -> Self {
928        self.normalize = normalize;
929        self
930    }
931
932    /// Set whether to zero-pad to next power of 2
933    pub fn zero_pad_to_power_of_2(mut self, zero_pad: bool) -> Self {
934        self.zero_pad_to_power_of_2 = zero_pad;
935        self
936    }
937
938    /// Extract features using fast transform methods
939    pub fn extract_features(&self, data: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
940        if data.is_empty() {
941            return Err(SklearsError::InvalidInput(
942                "Input data cannot be empty".to_string(),
943            ));
944        }
945
946        let mut features_list = Vec::new();
947
948        for row in data.axis_iter(Axis(0)) {
949            let row_features = match self.transform_type {
950                TransformType::FFT => self.compute_fft(&row)?,
951                TransformType::DCT => self.compute_dct(&row)?,
952                TransformType::Walsh => self.compute_walsh(&row)?,
953                TransformType::Haar => self.compute_haar(&row)?,
954            };
955            features_list.push(row_features);
956        }
957
958        if features_list.is_empty() {
959            return Err(SklearsError::InvalidInput(
960                "No features extracted".to_string(),
961            ));
962        }
963
964        let n_features = features_list[0].len();
965        let n_samples = features_list.len();
966        let mut result = Array2::zeros((n_samples, n_features));
967
968        for (i, features) in features_list.into_iter().enumerate() {
969            for (j, &value) in features.iter().enumerate() {
970                result[[i, j]] = value;
971            }
972        }
973
974        Ok(result)
975    }
976
977    /// Compute Fast Fourier Transform features
978    fn compute_fft(&self, signal: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
979        let mut padded_signal = signal.to_vec();
980
981        // Zero-pad to next power of 2 if requested
982        if self.zero_pad_to_power_of_2 {
983            let next_power_of_2 = (padded_signal.len() as f64).log2().ceil() as u32;
984            let target_size = 2_usize.pow(next_power_of_2);
985            padded_signal.resize(target_size, 0.0);
986        }
987
988        // Simple DFT implementation (can be optimized with real FFT libraries)
989        let n = padded_signal.len();
990        let mut real_part = Vec::with_capacity(n / 2 + 1);
991        let mut imag_part = Vec::with_capacity(n / 2 + 1);
992
993        for k in 0..=n / 2 {
994            let mut real_sum = 0.0;
995            let mut imag_sum = 0.0;
996
997            #[allow(clippy::needless_range_loop)]
998            // j used in arithmetic angle computation, not just indexing
999            for j in 0..n {
1000                let angle = -2.0 * std::f64::consts::PI * (k as f64) * (j as f64) / (n as f64);
1001                real_sum += padded_signal[j] * angle.cos();
1002                imag_sum += padded_signal[j] * angle.sin();
1003            }
1004
1005            real_part.push(real_sum);
1006            imag_part.push(imag_sum);
1007        }
1008
1009        let mut features = Vec::new();
1010
1011        if self.include_magnitude {
1012            for i in 0..real_part.len() {
1013                let magnitude = (real_part[i] * real_part[i] + imag_part[i] * imag_part[i]).sqrt();
1014                features.push(magnitude);
1015            }
1016        }
1017
1018        if self.include_phase {
1019            for i in 0..real_part.len() {
1020                let phase = imag_part[i].atan2(real_part[i]);
1021                features.push(phase);
1022            }
1023        }
1024
1025        if self.include_power {
1026            for i in 0..real_part.len() {
1027                let power = real_part[i] * real_part[i] + imag_part[i] * imag_part[i];
1028                features.push(power);
1029            }
1030        }
1031
1032        if self.normalize && !features.is_empty() {
1033            let max_val = features.iter().fold(0.0 as Float, |a, &b| a.max(b.abs()));
1034            if max_val > 0.0 {
1035                for feature in features.iter_mut() {
1036                    *feature /= max_val;
1037                }
1038            }
1039        }
1040
1041        Ok(features)
1042    }
1043
1044    /// Compute Discrete Cosine Transform features
1045    fn compute_dct(&self, signal: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
1046        let n = signal.len();
1047        let mut dct_coeffs = Vec::with_capacity(n);
1048
1049        for k in 0..n {
1050            let mut sum = 0.0;
1051            for j in 0..n {
1052                let angle = std::f64::consts::PI * (k as f64) * (j as f64 + 0.5) / (n as f64);
1053                sum += signal[j] * angle.cos();
1054            }
1055
1056            // Apply normalization factors
1057            let coeff = if k == 0 {
1058                sum * (1.0 / n as f64).sqrt()
1059            } else {
1060                sum * (2.0 / n as f64).sqrt()
1061            };
1062
1063            dct_coeffs.push(coeff);
1064        }
1065
1066        if self.normalize && !dct_coeffs.is_empty() {
1067            let max_val = dct_coeffs.iter().fold(0.0 as Float, |a, &b| a.max(b.abs()));
1068            if max_val > 0.0 {
1069                for coeff in dct_coeffs.iter_mut() {
1070                    *coeff /= max_val;
1071                }
1072            }
1073        }
1074
1075        Ok(dct_coeffs)
1076    }
1077
1078    /// Compute Walsh-Hadamard Transform features
1079    fn compute_walsh(&self, signal: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
1080        let mut data = signal.to_vec();
1081        let n = data.len();
1082
1083        // Ensure length is power of 2
1084        let next_power_of_2 = (n as f64).log2().ceil() as u32;
1085        let target_size = 2_usize.pow(next_power_of_2);
1086        data.resize(target_size, 0.0);
1087
1088        // Fast Walsh-Hadamard Transform
1089        let mut step = 1;
1090        while step < target_size {
1091            for i in (0..target_size).step_by(step * 2) {
1092                for j in 0..step {
1093                    let u = data[i + j];
1094                    let v = data[i + j + step];
1095                    data[i + j] = u + v;
1096                    data[i + j + step] = u - v;
1097                }
1098            }
1099            step *= 2;
1100        }
1101
1102        if self.normalize && !data.is_empty() {
1103            let norm_factor = (target_size as f64).sqrt();
1104            for value in data.iter_mut() {
1105                *value /= norm_factor;
1106            }
1107        }
1108
1109        Ok(data)
1110    }
1111
1112    /// Compute Haar Transform features
1113    fn compute_haar(&self, signal: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
1114        let mut data = signal.to_vec();
1115        let n = data.len();
1116
1117        // Ensure length is power of 2
1118        let next_power_of_2 = (n as f64).log2().ceil() as u32;
1119        let target_size = 2_usize.pow(next_power_of_2);
1120        data.resize(target_size, 0.0);
1121
1122        let mut temp = vec![0.0; target_size];
1123        let mut length = target_size;
1124
1125        while length > 1 {
1126            let half_length = length / 2;
1127
1128            // Compute averages (scaling function)
1129            for i in 0..half_length {
1130                temp[i] = (data[2 * i] + data[2 * i + 1]) / 2.0_f64.sqrt();
1131            }
1132
1133            // Compute differences (wavelet function)
1134            for i in 0..half_length {
1135                temp[half_length + i] = (data[2 * i] - data[2 * i + 1]) / 2.0_f64.sqrt();
1136            }
1137
1138            // Copy back
1139            data[0..length].copy_from_slice(&temp[0..length]);
1140            length = half_length;
1141        }
1142
1143        if self.normalize && !data.is_empty() {
1144            let max_val = data.iter().fold(0.0 as Float, |a, &b| a.max(b.abs()));
1145            if max_val > 0.0 {
1146                for value in data.iter_mut() {
1147                    *value /= max_val;
1148                }
1149            }
1150        }
1151
1152        Ok(data)
1153    }
1154}
1155
1156impl Default for FastTransformExtractor {
1157    fn default() -> Self {
1158        Self::new()
1159    }
1160}
1161
1162#[allow(non_snake_case)]
1163#[cfg(test)]
1164mod tests {
1165    use super::*;
1166    use scirs2_core::ndarray::Array2;
1167
1168    #[test]
1169    fn test_count_min_sketch_basic() {
1170        let data = Array2::from_shape_vec((50, 3), (0..150).map(|x| (x % 20) as f64).collect())
1171            .expect("operation should succeed");
1172
1173        let sketch = CountMinSketch::new()
1174            .width(50)
1175            .depth(3)
1176            .include_frequency_statistics(true)
1177            .include_sketch_properties(true);
1178
1179        let features = sketch
1180            .transform(&data.view())
1181            .expect("operation should succeed");
1182
1183        // Should have 8 features: 5 frequency stats + 3 sketch properties
1184        assert_eq!(features.len(), 8);
1185
1186        // All features should be finite
1187        for &feature in &features {
1188            assert!(feature.is_finite(), "Feature should be finite");
1189        }
1190
1191        // Total count should be positive
1192        assert!(features[0] > 0.0, "Total count should be positive");
1193    }
1194
1195    #[test]
1196    fn test_count_min_sketch_configuration() {
1197        let data = Array2::from_shape_vec((20, 2), (0..40).map(|x| x as f64).collect())
1198            .expect("operation should succeed");
1199
1200        // Test with different configurations
1201        let sketch1 = CountMinSketch::new()
1202            .width(100)
1203            .depth(5)
1204            .include_frequency_statistics(true)
1205            .include_sketch_properties(false);
1206
1207        let features1 = sketch1
1208            .transform(&data.view())
1209            .expect("operation should succeed");
1210        assert_eq!(features1.len(), 5); // Only frequency stats
1211
1212        let sketch2 = CountMinSketch::new()
1213            .width(100)
1214            .depth(5)
1215            .include_frequency_statistics(false)
1216            .include_sketch_properties(true);
1217
1218        let features2 = sketch2
1219            .transform(&data.view())
1220            .expect("operation should succeed");
1221        assert_eq!(features2.len(), 3); // Only sketch properties
1222    }
1223
1224    #[test]
1225    fn test_count_min_sketch_empty_data() {
1226        let data = Array2::zeros((0, 2));
1227        let sketch = CountMinSketch::new();
1228
1229        let result = sketch.transform(&data.view());
1230        assert!(result.is_err(), "Should return error for empty data");
1231    }
1232
1233    #[test]
1234    fn test_fast_johnson_lindenstrauss_basic() {
1235        let data = Array2::from_shape_vec((10, 8), (0..80).map(|x| x as f64).collect())
1236            .expect("operation should succeed");
1237
1238        let fjlt = FastJohnsonLindenstrauss::new()
1239            .target_dimension(4)
1240            .sparsity_parameter(0.2)
1241            .include_transform_stats(false);
1242
1243        let features = fjlt
1244            .transform(&data.view())
1245            .expect("operation should succeed");
1246
1247        // Should have 10 * 4 = 40 features (flattened transformed data)
1248        assert_eq!(features.len(), 40);
1249
1250        // All features should be finite
1251        for &feature in &features {
1252            assert!(feature.is_finite(), "Feature should be finite");
1253        }
1254    }
1255
1256    #[test]
1257    fn test_fast_johnson_lindenstrauss_stats() {
1258        let data = Array2::from_shape_vec((5, 4), (0..20).map(|x| x as f64).collect())
1259            .expect("operation should succeed");
1260
1261        let fjlt = FastJohnsonLindenstrauss::new()
1262            .target_dimension(2)
1263            .include_transform_stats(true);
1264
1265        let features = fjlt
1266            .transform(&data.view())
1267            .expect("operation should succeed");
1268
1269        // Should have 6 features: mean, std, min, max, target_dim, sparsity
1270        assert_eq!(features.len(), 6);
1271
1272        // All features should be finite
1273        for &feature in &features {
1274            assert!(feature.is_finite(), "Feature should be finite");
1275        }
1276
1277        // Target dimension should match
1278        assert_eq!(features[4], 2.0);
1279    }
1280
1281    #[test]
1282    fn test_fast_johnson_lindenstrauss_empty_data() {
1283        let data = Array2::zeros((0, 4));
1284        let fjlt = FastJohnsonLindenstrauss::new();
1285
1286        let result = fjlt.transform(&data.view());
1287        assert!(result.is_err(), "Should return error for empty data");
1288    }
1289
1290    #[test]
1291    fn test_fast_johnson_lindenstrauss_zero_columns() {
1292        let data = Array2::zeros((5, 0));
1293        let fjlt = FastJohnsonLindenstrauss::new();
1294
1295        let result = fjlt.transform(&data.view());
1296        assert!(result.is_err(), "Should return error for zero columns");
1297    }
1298
1299    #[test]
1300    fn test_sketching_methods_consistency() {
1301        let data = Array2::from_shape_vec((30, 4), (0..120).map(|x| (x % 10) as f64).collect())
1302            .expect("operation should succeed");
1303
1304        // Test Count-Min Sketch with fixed seed
1305        let sketch1 = CountMinSketch::new().hash_seed(123);
1306        let sketch2 = CountMinSketch::new().hash_seed(123);
1307
1308        let features1 = sketch1
1309            .transform(&data.view())
1310            .expect("operation should succeed");
1311        let features2 = sketch2
1312            .transform(&data.view())
1313            .expect("operation should succeed");
1314
1315        // Should be identical with same seed
1316        assert_eq!(features1.len(), features2.len());
1317        for (f1, f2) in features1.iter().zip(features2.iter()) {
1318            assert!(
1319                (f1 - f2).abs() < 1e-10,
1320                "Features should be identical with same seed"
1321            );
1322        }
1323
1324        // Test FJLT with fixed seed
1325        let fjlt1 = FastJohnsonLindenstrauss::new()
1326            .random_state(456)
1327            .target_dimension(3);
1328        let fjlt2 = FastJohnsonLindenstrauss::new()
1329            .random_state(456)
1330            .target_dimension(3);
1331
1332        let fjlt_features1 = fjlt1
1333            .transform(&data.view())
1334            .expect("operation should succeed");
1335        let fjlt_features2 = fjlt2
1336            .transform(&data.view())
1337            .expect("operation should succeed");
1338
1339        // Should be identical with same seed
1340        assert_eq!(fjlt_features1.len(), fjlt_features2.len());
1341        for (f1, f2) in fjlt_features1.iter().zip(fjlt_features2.iter()) {
1342            assert!(
1343                (f1 - f2).abs() < 1e-10,
1344                "FJLT features should be identical with same seed"
1345            );
1346        }
1347    }
1348
1349    #[test]
1350    fn test_reservoir_sampler_basic() {
1351        let mut sampler = ReservoirSampler::new().reservoir_size(10).random_state(42);
1352
1353        let indices = sampler
1354            .sample_indices(20)
1355            .expect("operation should succeed");
1356        assert_eq!(indices.len(), 10);
1357        assert_eq!(sampler.get_seen_count(), 20);
1358
1359        // All indices should be valid
1360        for &idx in &indices {
1361            assert!(idx < 20);
1362        }
1363    }
1364
1365    #[test]
1366    fn test_importance_sampler_basic() {
1367        let weights = Array1::from_vec(vec![0.1, 0.3, 0.2, 0.4]);
1368        let sampler = ImportanceSampler::new().sample_size(3).random_state(42);
1369
1370        let indices = sampler
1371            .sample_indices(&weights.view())
1372            .expect("operation should succeed");
1373        assert_eq!(indices.len(), 3);
1374
1375        // All indices should be valid
1376        for &idx in &indices {
1377            assert!(idx < 4);
1378        }
1379    }
1380
1381    #[test]
1382    fn test_stratified_sampler_basic() {
1383        let strata = Array1::from_vec(vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
1384        let sampler = StratifiedSampler::new().sample_size(4).random_state(42);
1385
1386        let indices = sampler
1387            .sample_indices(&strata.view())
1388            .expect("operation should succeed");
1389        assert_eq!(indices.len(), 4);
1390
1391        // All indices should be valid
1392        for &idx in &indices {
1393            assert!(idx < 6);
1394        }
1395    }
1396
1397    #[test]
1398    fn test_fast_transform_extractor_fft() {
1399        let data = Array2::from_shape_vec((3, 8), (0..24).map(|x| x as f64).collect())
1400            .expect("operation should succeed");
1401
1402        let extractor = FastTransformExtractor::new()
1403            .transform_type(TransformType::FFT)
1404            .include_magnitude(true)
1405            .include_phase(false);
1406
1407        let features = extractor
1408            .extract_features(&data.view())
1409            .expect("operation should succeed");
1410        assert_eq!(features.nrows(), 3);
1411        assert!(features.ncols() > 0);
1412
1413        // All features should be finite
1414        for &value in features.iter() {
1415            assert!(value.is_finite());
1416        }
1417    }
1418
1419    #[test]
1420    fn test_fast_transform_extractor_dct() {
1421        let data = Array2::from_shape_vec((2, 4), (0..8).map(|x| x as f64).collect())
1422            .expect("operation should succeed");
1423
1424        let extractor = FastTransformExtractor::new()
1425            .transform_type(TransformType::DCT)
1426            .normalize(true);
1427
1428        let features = extractor
1429            .extract_features(&data.view())
1430            .expect("operation should succeed");
1431        assert_eq!(features.nrows(), 2);
1432        assert_eq!(features.ncols(), 4);
1433
1434        // All features should be finite
1435        for &value in features.iter() {
1436            assert!(value.is_finite());
1437        }
1438    }
1439}