sklears_feature_extraction/
streaming_projection.rs1use crate::SklResult;
2use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
3use sklears_core::prelude::{Float, SklearsError};
4
5#[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 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)] impl StreamingFeatureExtractor {
56 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 pub fn chunk_size(mut self, chunk_size: usize) -> Self {
76 self.chunk_size = chunk_size;
77 self
78 }
79
80 pub fn buffer_size(mut self, buffer_size: usize) -> Self {
82 self.buffer_size = buffer_size;
83 self
84 }
85
86 pub fn include_mean(mut self, include: bool) -> Self {
88 self.include_mean = include;
89 self
90 }
91
92 pub fn include_std(mut self, include: bool) -> Self {
94 self.include_std = include;
95 self
96 }
97
98 pub fn include_min_max(mut self, include: bool) -> Self {
100 self.include_min_max = include;
101 self
102 }
103
104 pub fn include_quantiles(mut self, include: bool) -> Self {
106 self.include_quantiles = include;
107 self
108 }
109
110 pub fn include_moments(mut self, include: bool) -> Self {
112 self.include_moments = include;
113 self
114 }
115
116 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 self.update_running_stats(data)?;
126
127 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 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 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 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 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 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 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 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 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 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 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 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]; 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 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]); }
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#[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)] impl RandomProjectionFeatures {
357 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 pub fn n_components(mut self, n_components: usize) -> Self {
371 self.n_components = Some(n_components);
372 self
373 }
374
375 pub fn random_state(mut self, random_state: u64) -> Self {
377 self.random_state = Some(random_state);
378 self
379 }
380
381 pub fn density(mut self, density: Float) -> Self {
383 self.density = Some(density);
384 self
385 }
386
387 pub fn eps(mut self, eps: Float) -> Self {
389 self.eps = eps;
390 self
391 }
392
393 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 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 self.projection_matrix = Some(self.create_projection_matrix(n_features, n_components)?);
407
408 Ok(())
409 }
410
411 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 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 pub fn fit_transform(&mut self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
434 self.fit(X)?;
435 self.transform(X)
436 }
437
438 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 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 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 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 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 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 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#[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)] impl LocalitySensitiveHashing {
553 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 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 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 pub fn random_state(mut self, random_state: u64) -> Self {
579 self.random_state = Some(random_state);
580 self
581 }
582
583 pub fn distance_metric(mut self, metric: &str) -> Self {
585 self.distance_metric = metric.to_string();
586 self
587 }
588
589 pub fn fit(&mut self, X: &ArrayView2<Float>) -> SklResult<()> {
591 let (_, n_features) = X.dim();
592 self.n_features = Some(n_features);
593
594 self.hash_matrices = Some(self.create_hash_matrices(n_features)?);
596
597 Ok(())
598 }
599
600 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 let hash_values = X.dot(hash_matrix);
616
617 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 pub fn fit_transform(&mut self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
631 self.fit(X)?;
632 self.transform(X)
633 }
634
635 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 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 let data_hashes = self.transform(X)?;
660
661 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 candidates.sort_by_key(|b| std::cmp::Reverse(b.1));
677
678 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 fn create_hash_matrices(&self, n_features: usize) -> SklResult<Vec<Array2<Float>>> {
693 use scirs2_core::random::thread_rng;
694
695 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 for i in 0..n_features {
707 matrix[[i, 0]] = rng.random_range(-1.0..1.0);
708 }
709 }
710 "euclidean" => {
711 for i in 0..n_features {
713 matrix[[i, 0]] = rng.random_range(-1.0..1.0);
714 }
715 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}