sklears_feature_extraction/
pipelines.rs1use crate::feature_traits::{FeatureExtractor, FeatureUnion};
7use scirs2_core::ndarray::{Array2, Axis};
8use sklears_core::{error::Result as SklResult, prelude::SklearsError, types::Float};
9
10#[derive(Debug)]
29pub struct SequentialPipeline<T> {
30 stages: Vec<T>,
31 stage_names: Vec<String>,
32}
33
34impl<T> SequentialPipeline<T> {
35 pub fn new() -> Self {
37 Self {
38 stages: Vec::new(),
39 stage_names: Vec::new(),
40 }
41 }
42
43 pub fn add_stage(mut self, stage: T, name: String) -> Self {
45 self.stages.push(stage);
46 self.stage_names.push(name);
47 self
48 }
49
50 pub fn len(&self) -> usize {
52 self.stages.len()
53 }
54
55 pub fn is_empty(&self) -> bool {
57 self.stages.is_empty()
58 }
59}
60
61impl<T> Default for SequentialPipeline<T> {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67#[derive(Debug, Clone)]
96pub struct ParallelFeatureUnion<E> {
97 extractors: Vec<E>,
98 extractor_names: Vec<String>,
99}
100
101impl<E> ParallelFeatureUnion<E> {
102 pub fn new() -> Self {
104 Self {
105 extractors: Vec::new(),
106 extractor_names: Vec::new(),
107 }
108 }
109
110 pub fn add_extractor(mut self, extractor: E, name: String) -> Self {
112 self.extractors.push(extractor);
113 self.extractor_names.push(name);
114 self
115 }
116
117 pub fn len(&self) -> usize {
119 self.extractors.len()
120 }
121
122 pub fn is_empty(&self) -> bool {
124 self.extractors.is_empty()
125 }
126
127 pub fn extractor_names(&self) -> &[String] {
129 &self.extractor_names
130 }
131}
132
133impl<E> Default for ParallelFeatureUnion<E> {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl<E> FeatureUnion for ParallelFeatureUnion<E>
140where
141 E: FeatureExtractor<Input = String, Output = Array2<Float>>,
142{
143 type Input = String;
144 type Output = Array2<Float>;
145
146 fn extract_union(&self, input: &[Self::Input]) -> SklResult<Self::Output> {
147 if self.extractors.is_empty() {
148 return Err(SklearsError::InvalidInput(
149 "Feature union has no extractors".to_string(),
150 ));
151 }
152
153 if input.is_empty() {
154 return Err(SklearsError::InvalidInput("Empty input data".to_string()));
155 }
156
157 let mut feature_matrices: Vec<Array2<Float>> = Vec::new();
159
160 for extractor in &self.extractors {
161 let features = extractor.extract_features(input)?;
162 feature_matrices.push(features);
163 }
164
165 let n_samples = feature_matrices[0].nrows();
167 for (i, matrix) in feature_matrices.iter().enumerate() {
168 if matrix.nrows() != n_samples {
169 return Err(SklearsError::InvalidInput(format!(
170 "Extractor {} produced {} samples, expected {}",
171 i,
172 matrix.nrows(),
173 n_samples
174 )));
175 }
176 }
177
178 let mut combined = feature_matrices[0].clone();
180 for matrix in &feature_matrices[1..] {
181 combined =
182 scirs2_core::ndarray::concatenate(Axis(1), &[combined.view(), matrix.view()])
183 .map_err(|e| {
184 SklearsError::InvalidInput(format!("Concatenation failed: {}", e))
185 })?;
186 }
187
188 Ok(combined)
189 }
190
191 fn n_extractors(&self) -> usize {
192 self.extractors.len()
193 }
194
195 fn feature_splits(&self) -> Vec<usize> {
196 let mut splits = vec![0];
197 let mut cumsum = 0;
198
199 for extractor in &self.extractors {
200 if let Some(n_features) = extractor.n_features() {
201 cumsum += n_features;
202 splits.push(cumsum);
203 }
204 }
205
206 splits
207 }
208}
209
210#[derive(Debug, Clone)]
219pub struct WeightedFeatureUnion<E> {
220 extractors: Vec<E>,
221 weights: Vec<Float>,
222 extractor_names: Vec<String>,
223}
224
225impl<E> WeightedFeatureUnion<E> {
226 pub fn new() -> Self {
228 Self {
229 extractors: Vec::new(),
230 weights: Vec::new(),
231 extractor_names: Vec::new(),
232 }
233 }
234
235 pub fn add_weighted_extractor(mut self, extractor: E, weight: Float, name: String) -> Self {
237 self.extractors.push(extractor);
238 self.weights.push(weight);
239 self.extractor_names.push(name);
240 self
241 }
242
243 pub fn len(&self) -> usize {
245 self.extractors.len()
246 }
247
248 pub fn is_empty(&self) -> bool {
250 self.extractors.is_empty()
251 }
252
253 pub fn weights(&self) -> &[Float] {
255 &self.weights
256 }
257}
258
259impl<E> Default for WeightedFeatureUnion<E> {
260 fn default() -> Self {
261 Self::new()
262 }
263}
264
265impl<E> FeatureUnion for WeightedFeatureUnion<E>
266where
267 E: FeatureExtractor<Input = String, Output = Array2<Float>>,
268{
269 type Input = String;
270 type Output = Array2<Float>;
271
272 fn extract_union(&self, input: &[Self::Input]) -> SklResult<Self::Output> {
273 if self.extractors.is_empty() {
274 return Err(SklearsError::InvalidInput(
275 "Feature union has no extractors".to_string(),
276 ));
277 }
278
279 if input.is_empty() {
280 return Err(SklearsError::InvalidInput("Empty input data".to_string()));
281 }
282
283 let mut feature_matrices: Vec<Array2<Float>> = Vec::new();
285
286 for (extractor, weight) in self.extractors.iter().zip(&self.weights) {
287 let mut features = extractor.extract_features(input)?;
288 features *= *weight;
290 feature_matrices.push(features);
291 }
292
293 let n_samples = feature_matrices[0].nrows();
295 for (i, matrix) in feature_matrices.iter().enumerate() {
296 if matrix.nrows() != n_samples {
297 return Err(SklearsError::InvalidInput(format!(
298 "Extractor {} produced {} samples, expected {}",
299 i,
300 matrix.nrows(),
301 n_samples
302 )));
303 }
304 }
305
306 let mut combined = feature_matrices[0].clone();
308 for matrix in &feature_matrices[1..] {
309 combined =
310 scirs2_core::ndarray::concatenate(Axis(1), &[combined.view(), matrix.view()])
311 .map_err(|e| {
312 SklearsError::InvalidInput(format!("Concatenation failed: {}", e))
313 })?;
314 }
315
316 Ok(combined)
317 }
318
319 fn n_extractors(&self) -> usize {
320 self.extractors.len()
321 }
322
323 fn feature_splits(&self) -> Vec<usize> {
324 let mut splits = vec![0];
325 let mut cumsum = 0;
326
327 for extractor in &self.extractors {
328 if let Some(n_features) = extractor.n_features() {
329 cumsum += n_features;
330 splits.push(cumsum);
331 }
332 }
333
334 splits
335 }
336}
337
338#[derive(Debug, Clone)]
344pub struct IndexFeatureSelector {
345 selected_indices: Vec<usize>,
346}
347
348impl IndexFeatureSelector {
349 pub fn new(selected_indices: Vec<usize>) -> Self {
351 Self { selected_indices }
352 }
353
354 pub fn select_features(&self, features: &Array2<Float>) -> SklResult<Array2<Float>> {
356 if self.selected_indices.is_empty() {
357 return Err(SklearsError::InvalidInput(
358 "No features selected".to_string(),
359 ));
360 }
361
362 let n_features = features.ncols();
363 for &idx in &self.selected_indices {
364 if idx >= n_features {
365 return Err(SklearsError::InvalidInput(format!(
366 "Feature index {} out of range (max: {})",
367 idx,
368 n_features - 1
369 )));
370 }
371 }
372
373 let selected_cols: Vec<_> = self
375 .selected_indices
376 .iter()
377 .map(|&idx| features.column(idx).to_owned())
378 .collect();
379
380 let n_samples = features.nrows();
381 let n_selected = selected_cols.len();
382 let mut result = Array2::zeros((n_samples, n_selected));
383
384 for (i, col) in selected_cols.iter().enumerate() {
385 result.column_mut(i).assign(col);
386 }
387
388 Ok(result)
389 }
390
391 pub fn selected_indices(&self) -> &[usize] {
393 &self.selected_indices
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use crate::text::EmotionDetector;
401
402 #[test]
403 fn test_parallel_feature_union_basic() {
404 let detector1 = EmotionDetector::new();
406 let detector2 = EmotionDetector::new().min_confidence(0.2);
407
408 let union = ParallelFeatureUnion::new()
409 .add_extractor(detector1, "emotion1".to_string())
410 .add_extractor(detector2, "emotion2".to_string());
411
412 let documents = vec!["I am happy!".to_string(), "This is sad.".to_string()];
413
414 let features = union
415 .extract_union(&documents)
416 .expect("operation should succeed");
417
418 assert_eq!(features.nrows(), 2);
419 assert_eq!(features.ncols(), 14 + 14); }
421
422 #[test]
423 fn test_parallel_feature_union_feature_splits() {
424 let union = ParallelFeatureUnion::new()
425 .add_extractor(EmotionDetector::new(), "emotion1".to_string())
426 .add_extractor(EmotionDetector::new(), "emotion2".to_string());
427
428 let splits = union.feature_splits();
429 assert_eq!(splits, vec![0, 14, 28]); }
431
432 #[test]
433 fn test_weighted_feature_union() {
434 let union = WeightedFeatureUnion::new()
435 .add_weighted_extractor(EmotionDetector::new(), 2.0, "emotion1".to_string())
436 .add_weighted_extractor(EmotionDetector::new(), 0.5, "emotion2".to_string());
437
438 let documents = vec!["I am happy!".to_string()];
439
440 let features = union
441 .extract_union(&documents)
442 .expect("operation should succeed");
443
444 assert_eq!(features.nrows(), 1);
445 assert_eq!(features.ncols(), 14 + 14);
446 }
447
448 #[test]
449 fn test_feature_selector() {
450 let features = Array2::from_shape_vec(
451 (2, 5),
452 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0],
453 )
454 .expect("operation should succeed");
455
456 let selector = IndexFeatureSelector::new(vec![0, 2, 4]);
457 let selected = selector
458 .select_features(&features)
459 .expect("operation should succeed");
460
461 assert_eq!(selected.nrows(), 2);
462 assert_eq!(selected.ncols(), 3);
463 assert_eq!(selected[[0, 0]], 1.0);
464 assert_eq!(selected[[0, 1]], 3.0);
465 assert_eq!(selected[[0, 2]], 5.0);
466 }
467
468 #[test]
469 fn test_feature_selector_out_of_range() {
470 let features = Array2::zeros((2, 5));
471 let selector = IndexFeatureSelector::new(vec![10]);
472
473 assert!(selector.select_features(&features).is_err());
474 }
475}