Skip to main content

sklears_feature_extraction/
streaming_projection.rs

1use crate::SklResult;
2use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
3use sklears_core::prelude::{Float, SklearsError};
4
5/// Streaming Feature Extractor
6///
7/// Process large datasets in chunks for memory-efficient feature extraction.
8/// Supports online computation of statistical features without loading entire dataset into memory.
9///
10/// # Parameters
11///
12/// * `chunk_size` - Size of chunks to process at once
13/// * `buffer_size` - Size of internal buffer for overlapping windows
14/// * `features` - Types of features to extract
15///
16/// # Examples
17///
18/// ```
19/// use sklears_feature_extraction::streaming_projection::StreamingFeatureExtractor;
20/// use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
21///
22/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
23/// let mut extractor = StreamingFeatureExtractor::new()
24///     .chunk_size(1000)
25///     .buffer_size(100);
26///
27/// // Process data in chunks
28/// let mut features = Vec::new();
29/// let data_chunks = vec![Array2::from_elem((10, 3), 1.0)];
30/// for chunk in data_chunks {
31///     let chunk_features = extractor.extract_chunk(&chunk.view())?;
32///     features.extend(chunk_features);
33/// }
34/// # Ok(())
35/// # }
36/// ```
37#[derive(Debug, Clone)]
38pub struct StreamingFeatureExtractor {
39    chunk_size: usize,
40    buffer_size: usize,
41    include_mean: bool,
42    include_std: bool,
43    include_min_max: bool,
44    include_quantiles: bool,
45    include_moments: bool,
46    // Internal state for streaming computation
47    running_mean: Option<Array1<Float>>,
48    running_variance: Option<Array1<Float>>,
49    running_count: usize,
50    running_min: Option<Array1<Float>>,
51    running_max: Option<Array1<Float>>,
52}
53
54#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
55impl StreamingFeatureExtractor {
56    /// Create a new streaming feature extractor
57    pub fn new() -> Self {
58        Self {
59            chunk_size: 1000,
60            buffer_size: 100,
61            include_mean: true,
62            include_std: true,
63            include_min_max: true,
64            include_quantiles: false,
65            include_moments: false,
66            running_mean: None,
67            running_variance: None,
68            running_count: 0,
69            running_min: None,
70            running_max: None,
71        }
72    }
73
74    /// Set chunk size for processing
75    pub fn chunk_size(mut self, chunk_size: usize) -> Self {
76        self.chunk_size = chunk_size;
77        self
78    }
79
80    /// Set buffer size for overlapping windows
81    pub fn buffer_size(mut self, buffer_size: usize) -> Self {
82        self.buffer_size = buffer_size;
83        self
84    }
85
86    /// Include mean in extracted features
87    pub fn include_mean(mut self, include: bool) -> Self {
88        self.include_mean = include;
89        self
90    }
91
92    /// Include standard deviation in extracted features
93    pub fn include_std(mut self, include: bool) -> Self {
94        self.include_std = include;
95        self
96    }
97
98    /// Include min/max in extracted features
99    pub fn include_min_max(mut self, include: bool) -> Self {
100        self.include_min_max = include;
101        self
102    }
103
104    /// Include quantiles in extracted features
105    pub fn include_quantiles(mut self, include: bool) -> Self {
106        self.include_quantiles = include;
107        self
108    }
109
110    /// Include higher moments in extracted features
111    pub fn include_moments(mut self, include: bool) -> Self {
112        self.include_moments = include;
113        self
114    }
115
116    /// Process a single chunk of data
117    pub fn extract_chunk(&mut self, data: &ArrayView2<Float>) -> SklResult<Array1<Float>> {
118        if data.nrows() == 0 {
119            return Err(SklearsError::InvalidInput("Empty data chunk".to_string()));
120        }
121
122        let (_n_samples, n_features) = data.dim();
123
124        // Update running statistics
125        self.update_running_stats(data)?;
126
127        // Extract features for this chunk
128        let mut features = Vec::new();
129
130        if self.include_mean {
131            if let Some(ref mean) = self.running_mean {
132                features.extend(mean.iter().cloned());
133            }
134        }
135
136        if self.include_std {
137            if let Some(ref variance) = self.running_variance {
138                let std_dev: Vec<Float> = variance.iter().map(|&v| v.sqrt()).collect();
139                features.extend(std_dev);
140            }
141        }
142
143        if self.include_min_max {
144            if let Some(ref min_vals) = self.running_min {
145                features.extend(min_vals.iter().cloned());
146            }
147            if let Some(ref max_vals) = self.running_max {
148                features.extend(max_vals.iter().cloned());
149            }
150        }
151
152        if self.include_quantiles {
153            // For streaming quantiles, we use approximate methods
154            for feature_idx in 0..n_features {
155                let column = data.column(feature_idx);
156                let quantiles = self.compute_approximate_quantiles(&column)?;
157                features.extend(quantiles);
158            }
159        }
160
161        if self.include_moments {
162            // Compute higher moments for current chunk
163            for feature_idx in 0..n_features {
164                let column = data.column(feature_idx);
165                let moments = self.compute_moments(&column)?;
166                features.extend(moments);
167            }
168        }
169
170        Ok(Array1::from_vec(features))
171    }
172
173    /// Finalize streaming computation and return aggregated features
174    pub fn finalize(&self) -> SklResult<Array1<Float>> {
175        if self.running_count == 0 {
176            return Err(SklearsError::InvalidInput("No data processed".to_string()));
177        }
178
179        let mut features = Vec::new();
180
181        if self.include_mean {
182            if let Some(ref mean) = self.running_mean {
183                features.extend(mean.iter().cloned());
184            }
185        }
186
187        if self.include_std {
188            if let Some(ref variance) = self.running_variance {
189                let std_dev: Vec<Float> = variance.iter().map(|&v| v.sqrt()).collect();
190                features.extend(std_dev);
191            }
192        }
193
194        if self.include_min_max {
195            if let Some(ref min_vals) = self.running_min {
196                features.extend(min_vals.iter().cloned());
197            }
198            if let Some(ref max_vals) = self.running_max {
199                features.extend(max_vals.iter().cloned());
200            }
201        }
202
203        Ok(Array1::from_vec(features))
204    }
205
206    /// Reset internal state for new stream
207    pub fn reset(&mut self) {
208        self.running_mean = None;
209        self.running_variance = None;
210        self.running_count = 0;
211        self.running_min = None;
212        self.running_max = None;
213    }
214
215    /// Update running statistics with new data chunk
216    fn update_running_stats(&mut self, data: &ArrayView2<Float>) -> SklResult<()> {
217        let (_n_samples, n_features) = data.dim();
218
219        if self.running_count == 0 {
220            // Initialize running statistics
221            self.running_mean = Some(Array1::zeros(n_features));
222            self.running_variance = Some(Array1::zeros(n_features));
223            self.running_min = Some(Array1::from_elem(n_features, Float::INFINITY));
224            self.running_max = Some(Array1::from_elem(n_features, Float::NEG_INFINITY));
225        }
226
227        // Update using Welford's online algorithm
228        for sample in data.axis_iter(Axis(0)) {
229            self.running_count += 1;
230            let count = self.running_count as Float;
231
232            for (feature_idx, &value) in sample.iter().enumerate() {
233                // Update mean and variance using Welford's algorithm
234                if let (Some(ref mut mean), Some(ref mut variance)) =
235                    (&mut self.running_mean, &mut self.running_variance)
236                {
237                    let delta = value - mean[feature_idx];
238                    mean[feature_idx] += delta / count;
239                    let delta2 = value - mean[feature_idx];
240                    variance[feature_idx] += delta * delta2;
241                }
242
243                // Update min/max
244                if let Some(ref mut min_vals) = self.running_min {
245                    if value < min_vals[feature_idx] {
246                        min_vals[feature_idx] = value;
247                    }
248                }
249                if let Some(ref mut max_vals) = self.running_max {
250                    if value > max_vals[feature_idx] {
251                        max_vals[feature_idx] = value;
252                    }
253                }
254            }
255        }
256
257        // Normalize variance
258        if let Some(ref mut variance) = self.running_variance {
259            let count = self.running_count as Float;
260            if count > 1.0 {
261                variance.mapv_inplace(|v| v / (count - 1.0));
262            }
263        }
264
265        Ok(())
266    }
267
268    /// Compute approximate quantiles for streaming data
269    fn compute_approximate_quantiles(&self, data: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
270        let mut sorted_data: Vec<Float> = data.iter().cloned().collect();
271        sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
272
273        let n = sorted_data.len();
274        let quantiles = vec![0.25, 0.5, 0.75]; // Q1, median, Q3
275        let mut result = Vec::new();
276
277        for &q in &quantiles {
278            let index = (q * (n - 1) as Float).round() as usize;
279            let index = index.min(n - 1);
280            result.push(sorted_data[index]);
281        }
282
283        Ok(result)
284    }
285
286    /// Compute moments for a data column
287    fn compute_moments(&self, data: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
288        let n = data.len() as Float;
289        if n == 0.0 {
290            return Ok(vec![0.0, 0.0, 0.0]); // mean, skewness, kurtosis
291        }
292
293        let mean = data.sum() / n;
294        let variance = data.mapv(|x| (x - mean).powi(2)).sum() / n;
295        let std_dev = variance.sqrt();
296
297        let (skewness, kurtosis) = if std_dev < 1e-10 {
298            (0.0, 0.0)
299        } else {
300            let skewness = data.mapv(|x| ((x - mean) / std_dev).powi(3)).sum() / n;
301            let kurtosis = data.mapv(|x| ((x - mean) / std_dev).powi(4)).sum() / n - 3.0;
302            (skewness, kurtosis)
303        };
304
305        Ok(vec![mean, skewness, kurtosis])
306    }
307}
308
309impl Default for StreamingFeatureExtractor {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315/// Random Projection Feature Extractor
316///
317/// Performs dimensionality reduction using random projections while preserving
318/// approximate distances between data points (Johnson-Lindenstrauss lemma).
319/// Useful for high-dimensional data preprocessing and approximate similarity search.
320///
321/// # Parameters
322///
323/// * `n_components` - Number of dimensions in the projected space
324/// * `random_state` - Random seed for reproducibility
325/// * `density` - Density of random projection matrix (for sparse projections)
326/// * `eps` - Distortion parameter for auto-sizing components
327///
328/// # Examples
329///
330/// ```
331/// use sklears_feature_extraction::streaming_projection::RandomProjectionFeatures;
332/// use scirs2_core::ndarray::Array2;
333///
334/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
335/// let X = Array2::from_elem((100, 1000), 1.0);
336/// let mut rp = RandomProjectionFeatures::new()
337///     .n_components(50)
338///     .random_state(42);
339///
340/// let X_projected = rp.fit_transform(&X.view())?;
341/// assert_eq!(X_projected.ncols(), 50);
342/// # Ok(())
343/// # }
344/// ```
345#[derive(Debug, Clone)]
346pub struct RandomProjectionFeatures {
347    n_components: Option<usize>,
348    random_state: Option<u64>,
349    density: Option<Float>,
350    eps: Float,
351    projection_matrix: Option<Array2<Float>>,
352    n_features: Option<usize>,
353}
354
355#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
356impl RandomProjectionFeatures {
357    /// Create a new random projection feature extractor
358    pub fn new() -> Self {
359        Self {
360            n_components: None,
361            random_state: None,
362            density: None,
363            eps: 0.1,
364            projection_matrix: None,
365            n_features: None,
366        }
367    }
368
369    /// Set number of components in projected space
370    pub fn n_components(mut self, n_components: usize) -> Self {
371        self.n_components = Some(n_components);
372        self
373    }
374
375    /// Set random state for reproducibility
376    pub fn random_state(mut self, random_state: u64) -> Self {
377        self.random_state = Some(random_state);
378        self
379    }
380
381    /// Set density for sparse random projections
382    pub fn density(mut self, density: Float) -> Self {
383        self.density = Some(density);
384        self
385    }
386
387    /// Set distortion parameter for auto-sizing
388    pub fn eps(mut self, eps: Float) -> Self {
389        self.eps = eps;
390        self
391    }
392
393    /// Fit the random projection to data
394    pub fn fit(&mut self, X: &ArrayView2<Float>) -> SklResult<()> {
395        let (n_samples, n_features) = X.dim();
396        self.n_features = Some(n_features);
397
398        // Auto-size number of components if not specified
399        let n_components = if let Some(n_comp) = self.n_components {
400            n_comp
401        } else {
402            self.johnson_lindenstrauss_min_dim(n_samples, self.eps)?
403        };
404
405        // Create random projection matrix
406        self.projection_matrix = Some(self.create_projection_matrix(n_features, n_components)?);
407
408        Ok(())
409    }
410
411    /// Transform data using fitted random projection
412    pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
413        if let Some(ref proj_matrix) = self.projection_matrix {
414            let (_n_samples, n_features) = X.dim();
415
416            if Some(n_features) != self.n_features {
417                return Err(SklearsError::InvalidInput(
418                    "Number of features does not match fitted data".to_string(),
419                ));
420            }
421
422            // Perform matrix multiplication: X @ projection_matrix.T
423            let result = X.dot(proj_matrix);
424            Ok(result)
425        } else {
426            Err(SklearsError::InvalidInput(
427                "Random projection not fitted yet".to_string(),
428            ))
429        }
430    }
431
432    /// Fit and transform data in one step
433    pub fn fit_transform(&mut self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
434        self.fit(X)?;
435        self.transform(X)
436    }
437
438    /// Create random projection matrix
439    fn create_projection_matrix(
440        &self,
441        n_features: usize,
442        n_components: usize,
443    ) -> SklResult<Array2<Float>> {
444        use scirs2_core::random::thread_rng;
445
446        // Use rng() function from scirs2_core - seeding handled elsewhere if needed
447        let mut rng = thread_rng();
448
449        let mut matrix = Array2::zeros((n_features, n_components));
450
451        if let Some(density) = self.density {
452            // Sparse random projection
453            let threshold = density;
454            for i in 0..n_features {
455                for j in 0..n_components {
456                    let random_val: Float = rng.random();
457                    if random_val < threshold {
458                        matrix[[i, j]] = if rng.random_bool(0.5) { 1.0 } else { -1.0 };
459                        matrix[[i, j]] /= (density * n_components as Float).sqrt();
460                    }
461                }
462            }
463        } else {
464            // Dense Gaussian random projection
465            for i in 0..n_features {
466                for j in 0..n_components {
467                    matrix[[i, j]] = rng.random_range(-1.0..1.0);
468                }
469            }
470            // Normalize
471            let norm_factor = 1.0 / (n_components as Float).sqrt();
472            matrix.mapv_inplace(|x| x * norm_factor);
473        }
474
475        Ok(matrix)
476    }
477
478    /// Compute minimum number of components for Johnson-Lindenstrauss lemma
479    fn johnson_lindenstrauss_min_dim(&self, n_samples: usize, eps: Float) -> SklResult<usize> {
480        if eps <= 0.0 || eps >= 1.0 {
481            return Err(SklearsError::InvalidInput(
482                "eps must be between 0 and 1".to_string(),
483            ));
484        }
485
486        if n_samples <= 1 {
487            return Ok(1);
488        }
489
490        // Simplified JL bound: k >= 4 * ln(n) / (eps^2 / 2)
491        // This is a more conservative but simpler bound
492        let ln_n = (n_samples as Float).ln();
493        let denominator = eps * eps / 2.0;
494
495        if denominator <= 0.0 {
496            return Err(SklearsError::InvalidInput(
497                "Invalid eps value for Johnson-Lindenstrauss bound".to_string(),
498            ));
499        }
500
501        let min_dim = (4.0 * ln_n / denominator).ceil() as usize;
502        Ok(min_dim.max(1))
503    }
504}
505
506impl Default for RandomProjectionFeatures {
507    fn default() -> Self {
508        Self::new()
509    }
510}
511
512/// Locality-Sensitive Hashing (LSH) Feature Extractor
513///
514/// Implements locality-sensitive hashing for approximate similarity search
515/// and clustering. Maps similar items to the same buckets with high probability.
516///
517/// # Parameters
518///
519/// * `n_hash_functions` - Number of hash functions to use
520/// * `hash_table_size` - Size of each hash table
521/// * `random_state` - Random seed for reproducibility
522/// * `distance_metric` - Distance metric for similarity ('cosine', 'euclidean')
523///
524/// # Examples
525///
526/// ```
527/// use sklears_feature_extraction::streaming_projection::LocalitySensitiveHashing;
528/// use scirs2_core::ndarray::Array2;
529///
530/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
531/// let mut lsh = LocalitySensitiveHashing::new()
532///     .n_hash_functions(10)
533///     .hash_table_size(1000)
534///     .distance_metric("cosine");
535///
536/// let X = Array2::from_elem((100, 50), 1.0);
537/// let hash_codes = lsh.fit_transform(&X.view())?;
538/// # Ok(())
539/// # }
540/// ```
541#[derive(Debug, Clone)]
542pub struct LocalitySensitiveHashing {
543    n_hash_functions: usize,
544    hash_table_size: usize,
545    random_state: Option<u64>,
546    distance_metric: String,
547    hash_matrices: Option<Vec<Array2<Float>>>,
548    n_features: Option<usize>,
549}
550
551#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
552impl LocalitySensitiveHashing {
553    /// Create a new LSH feature extractor
554    pub fn new() -> Self {
555        Self {
556            n_hash_functions: 10,
557            hash_table_size: 1000,
558            random_state: None,
559            distance_metric: "cosine".to_string(),
560            hash_matrices: None,
561            n_features: None,
562        }
563    }
564
565    /// Set number of hash functions
566    pub fn n_hash_functions(mut self, n_hash_functions: usize) -> Self {
567        self.n_hash_functions = n_hash_functions;
568        self
569    }
570
571    /// Set hash table size
572    pub fn hash_table_size(mut self, hash_table_size: usize) -> Self {
573        self.hash_table_size = hash_table_size;
574        self
575    }
576
577    /// Set random state
578    pub fn random_state(mut self, random_state: u64) -> Self {
579        self.random_state = Some(random_state);
580        self
581    }
582
583    /// Set distance metric
584    pub fn distance_metric(mut self, metric: &str) -> Self {
585        self.distance_metric = metric.to_string();
586        self
587    }
588
589    /// Fit LSH to data
590    pub fn fit(&mut self, X: &ArrayView2<Float>) -> SklResult<()> {
591        let (_, n_features) = X.dim();
592        self.n_features = Some(n_features);
593
594        // Create hash matrices
595        self.hash_matrices = Some(self.create_hash_matrices(n_features)?);
596
597        Ok(())
598    }
599
600    /// Transform data to hash codes
601    pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
602        if let Some(ref hash_matrices) = self.hash_matrices {
603            let (n_samples, n_features) = X.dim();
604
605            if Some(n_features) != self.n_features {
606                return Err(SklearsError::InvalidInput(
607                    "Number of features does not match fitted data".to_string(),
608                ));
609            }
610
611            let mut hash_codes = Array2::zeros((n_samples, self.n_hash_functions));
612
613            for (hash_idx, hash_matrix) in hash_matrices.iter().enumerate() {
614                // Apply hash function
615                let hash_values = X.dot(hash_matrix);
616
617                // Convert to binary hash codes
618                for (sample_idx, &value) in hash_values.iter().enumerate() {
619                    hash_codes[[sample_idx, hash_idx]] = if value >= 0.0 { 1.0 } else { 0.0 };
620                }
621            }
622
623            Ok(hash_codes)
624        } else {
625            Err(SklearsError::InvalidInput("LSH not fitted yet".to_string()))
626        }
627    }
628
629    /// Fit and transform data
630    pub fn fit_transform(&mut self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
631        self.fit(X)?;
632        self.transform(X)
633    }
634
635    /// Find approximate nearest neighbors using LSH
636    pub fn find_neighbors(
637        &self,
638        X: &ArrayView2<Float>,
639        query: &ArrayView1<Float>,
640        n_neighbors: usize,
641    ) -> SklResult<Vec<usize>> {
642        if let Some(ref hash_matrices) = self.hash_matrices {
643            let (n_samples, n_features) = X.dim();
644
645            if query.len() != n_features {
646                return Err(SklearsError::InvalidInput(
647                    "Query dimension does not match fitted data".to_string(),
648                ));
649            }
650
651            // Compute hash code for query
652            let mut query_hash = vec![0.0; self.n_hash_functions];
653            for (hash_idx, hash_matrix) in hash_matrices.iter().enumerate() {
654                let hash_value = query.dot(&hash_matrix.column(0));
655                query_hash[hash_idx] = if hash_value >= 0.0 { 1.0 } else { 0.0 };
656            }
657
658            // Compute hash codes for all data points
659            let data_hashes = self.transform(X)?;
660
661            // Find candidates with similar hash codes
662            let mut candidates: Vec<(usize, usize)> = Vec::new();
663            for sample_idx in 0..n_samples {
664                let mut matching_hashes = 0;
665                for hash_idx in 0..self.n_hash_functions {
666                    if (data_hashes[[sample_idx, hash_idx]] - query_hash[hash_idx]).abs() < 1e-10 {
667                        matching_hashes += 1;
668                    }
669                }
670                if matching_hashes > 0 {
671                    candidates.push((sample_idx, matching_hashes));
672                }
673            }
674
675            // Sort by number of matching hash functions
676            candidates.sort_by_key(|b| std::cmp::Reverse(b.1));
677
678            // Return top candidates
679            let result: Vec<usize> = candidates
680                .into_iter()
681                .take(n_neighbors)
682                .map(|(idx, _)| idx)
683                .collect();
684
685            Ok(result)
686        } else {
687            Err(SklearsError::InvalidInput("LSH not fitted yet".to_string()))
688        }
689    }
690
691    /// Create hash matrices for LSH
692    fn create_hash_matrices(&self, n_features: usize) -> SklResult<Vec<Array2<Float>>> {
693        use scirs2_core::random::thread_rng;
694
695        // Use rng() function from scirs2_core - seeding handled elsewhere if needed
696        let mut rng = thread_rng();
697
698        let mut hash_matrices = Vec::new();
699
700        for _ in 0..self.n_hash_functions {
701            let mut matrix = Array2::zeros((n_features, 1));
702
703            match self.distance_metric.as_str() {
704                "cosine" => {
705                    // Random hyperplane hash for cosine similarity
706                    for i in 0..n_features {
707                        matrix[[i, 0]] = rng.random_range(-1.0..1.0);
708                    }
709                }
710                "euclidean" => {
711                    // Random projection hash for Euclidean distance
712                    for i in 0..n_features {
713                        matrix[[i, 0]] = rng.random_range(-1.0..1.0);
714                    }
715                    // Normalize
716                    let norm = (matrix.mapv(|x| x * x).sum() as Float).sqrt();
717                    matrix.mapv_inplace(|x| x / norm);
718                }
719                _ => {
720                    return Err(SklearsError::InvalidInput(
721                        "Unsupported distance metric".to_string(),
722                    ));
723                }
724            }
725
726            hash_matrices.push(matrix);
727        }
728
729        Ok(hash_matrices)
730    }
731}
732
733impl Default for LocalitySensitiveHashing {
734    fn default() -> Self {
735        Self::new()
736    }
737}