sklears_feature_extraction/
temporal_features.rs1use crate::{Float, SklResult, SklearsError};
12use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1};
13
14#[derive(Debug, Clone)]
42pub struct TemporalFeatureExtractor {
43 n_lags: usize,
44 window_sizes: Vec<usize>,
45 extract_trend: bool,
46 extract_seasonality: bool,
47 seasonal_periods: Vec<usize>,
48 fill_method: String, }
50
51impl TemporalFeatureExtractor {
52 pub fn new() -> Self {
54 Self {
55 n_lags: 5,
56 window_sizes: vec![3, 7, 14],
57 extract_trend: true,
58 extract_seasonality: true,
59 seasonal_periods: vec![7, 12, 24],
60 fill_method: "forward".to_string(),
61 }
62 }
63
64 pub fn n_lags(mut self, n_lags: usize) -> Self {
66 self.n_lags = n_lags;
67 self
68 }
69
70 pub fn window_sizes(mut self, window_sizes: Vec<usize>) -> Self {
72 self.window_sizes = window_sizes;
73 self
74 }
75
76 pub fn extract_trend(mut self, extract_trend: bool) -> Self {
78 self.extract_trend = extract_trend;
79 self
80 }
81
82 pub fn extract_seasonality(mut self, extract_seasonality: bool) -> Self {
84 self.extract_seasonality = extract_seasonality;
85 self
86 }
87
88 pub fn seasonal_periods(mut self, seasonal_periods: Vec<usize>) -> Self {
90 self.seasonal_periods = seasonal_periods;
91 self
92 }
93
94 pub fn fill_method(mut self, fill_method: String) -> Self {
96 self.fill_method = fill_method;
97 self
98 }
99
100 pub fn extract_features(&self, ts: &ArrayView1<Float>) -> SklResult<Array1<Float>> {
102 let n = ts.len();
103 if n == 0 {
104 return Err(SklearsError::InvalidInput(
105 "Time series cannot be empty".to_string(),
106 ));
107 }
108
109 let mut features = Vec::new();
110
111 for lag in 1..=self.n_lags {
113 if lag >= n {
114 features.push(0.0); } else {
116 features.push(ts[n - 1 - lag]);
117 }
118 }
119
120 for &window_size in &self.window_sizes {
122 if window_size <= n {
123 let start_idx = n.saturating_sub(window_size);
124 let window = ts.slice(s![start_idx..]);
125
126 let mean = window.sum() / window_size as Float;
128 features.push(mean);
129
130 let std = (window.mapv(|x| (x - mean).powi(2)).sum() / window_size as Float).sqrt();
132 features.push(std);
133
134 features.push(
136 *window
137 .iter()
138 .min_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
139 .expect("operation should succeed"),
140 );
141 features.push(
142 *window
143 .iter()
144 .max_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
145 .expect("operation should succeed"),
146 );
147 } else {
148 let mean = ts.sum() / n as Float;
150 let std = (ts.mapv(|x| (x - mean).powi(2)).sum() / n as Float).sqrt();
151 let min_val = *ts
152 .iter()
153 .min_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
154 .expect("operation should succeed");
155 let max_val = *ts
156 .iter()
157 .max_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
158 .expect("operation should succeed");
159
160 features.extend_from_slice(&[mean, std, min_val, max_val]);
161 }
162 }
163
164 if self.extract_trend {
166 let trend_features = self.extract_trend_features(ts)?;
167 features.extend_from_slice(&trend_features);
168 }
169
170 if self.extract_seasonality {
172 for &period in &self.seasonal_periods {
173 let seasonality_features = self.extract_seasonality_features(ts, period)?;
174 features.extend_from_slice(&seasonality_features);
175 }
176 }
177
178 Ok(Array1::from_vec(features))
179 }
180
181 fn extract_trend_features(&self, ts: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
182 let n = ts.len();
183 if n < 2 {
184 return Ok(vec![0.0, 0.0]); }
186
187 let x: Vec<Float> = (0..n).map(|i| i as Float).collect();
189 let y: Vec<Float> = ts.to_vec();
190
191 let x_mean = x.iter().sum::<Float>() / n as Float;
192 let y_mean = y.iter().sum::<Float>() / n as Float;
193
194 let numerator: Float = x
195 .iter()
196 .zip(y.iter())
197 .map(|(&xi, &yi)| (xi - x_mean) * (yi - y_mean))
198 .sum();
199 let denominator: Float = x.iter().map(|&xi| (xi - x_mean).powi(2)).sum();
200
201 let slope = if denominator.abs() < 1e-10 {
202 0.0
203 } else {
204 numerator / denominator
205 };
206 let intercept = y_mean - slope * x_mean;
207
208 Ok(vec![slope, intercept])
209 }
210
211 fn extract_seasonality_features(
212 &self,
213 ts: &ArrayView1<Float>,
214 period: usize,
215 ) -> SklResult<Vec<Float>> {
216 let n = ts.len();
217 if n < period {
218 return Ok(vec![0.0, 0.0]); }
220
221 let mut seasonal_means = vec![0.0; period];
223 let mut seasonal_counts = vec![0; period];
224
225 for (i, &val) in ts.iter().enumerate() {
226 let season_idx = i % period;
227 seasonal_means[season_idx] += val;
228 seasonal_counts[season_idx] += 1;
229 }
230
231 for i in 0..period {
233 if seasonal_counts[i] > 0 {
234 seasonal_means[i] /= seasonal_counts[i] as Float;
235 }
236 }
237
238 let overall_mean = seasonal_means.iter().sum::<Float>() / period as Float;
239 let amplitude = seasonal_means
240 .iter()
241 .map(|&x| (x - overall_mean).abs())
242 .fold(0.0_f64, |acc, x| acc.max(x));
243
244 Ok(vec![overall_mean, amplitude])
245 }
246}
247
248impl Default for TemporalFeatureExtractor {
249 fn default() -> Self {
250 Self::new()
251 }
252}
253
254#[derive(Debug, Clone)]
280pub struct SlidingWindowFeatures {
281 window_size: usize,
282 step_size: usize,
283 features: Vec<String>,
284}
285
286impl SlidingWindowFeatures {
287 pub fn new() -> Self {
289 Self {
290 window_size: 10,
291 step_size: 1,
292 features: vec![
293 "mean".to_string(),
294 "std".to_string(),
295 "min".to_string(),
296 "max".to_string(),
297 "volatility".to_string(),
298 ],
299 }
300 }
301
302 pub fn window_size(mut self, window_size: usize) -> Self {
304 self.window_size = window_size;
305 self
306 }
307
308 pub fn step_size(mut self, step_size: usize) -> Self {
310 self.step_size = step_size;
311 self
312 }
313
314 pub fn features(mut self, features: Vec<String>) -> Self {
316 self.features = features;
317 self
318 }
319
320 pub fn extract_features(&self, ts: &ArrayView1<Float>) -> SklResult<Array2<Float>> {
322 let n = ts.len();
323 if n < self.window_size {
324 return Err(SklearsError::InvalidInput(format!(
325 "Time series length ({}) must be >= window_size ({})",
326 n, self.window_size
327 )));
328 }
329
330 let n_windows = (n - self.window_size) / self.step_size + 1;
331 let n_features_per_window = self.features.len();
332 let mut result = Array2::zeros((n_windows, n_features_per_window));
333
334 for (window_idx, start_idx) in (0..=n - self.window_size)
335 .step_by(self.step_size)
336 .enumerate()
337 {
338 let window = ts.slice(s![start_idx..start_idx + self.window_size]);
339
340 for (feat_idx, feature_name) in self.features.iter().enumerate() {
341 let feature_value = match feature_name.as_str() {
342 "mean" => window.sum() / self.window_size as Float,
343 "std" => {
344 let mean = window.sum() / self.window_size as Float;
345 (window.mapv(|x| (x - mean).powi(2)).sum() / self.window_size as Float)
346 .sqrt()
347 }
348 "min" => *window
349 .iter()
350 .min_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
351 .expect("operation should succeed"),
352 "max" => *window
353 .iter()
354 .max_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
355 .expect("operation should succeed"),
356 "median" => {
357 let mut sorted_window = window.to_vec();
358 sorted_window
359 .sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
360 if self.window_size % 2 == 1 {
361 sorted_window[self.window_size / 2]
362 } else {
363 (sorted_window[self.window_size / 2 - 1]
364 + sorted_window[self.window_size / 2])
365 / 2.0
366 }
367 }
368 "volatility" if self.window_size > 1 => {
369 let returns: Vec<Float> = window
370 .windows(2)
371 .into_iter()
372 .map(|pair| (pair[1] - pair[0]) / pair[0].max(1e-10))
373 .collect();
374 let mean_return = returns.iter().sum::<Float>() / returns.len() as Float;
375 (returns
376 .iter()
377 .map(|&r| (r - mean_return).powi(2))
378 .sum::<Float>()
379 / returns.len() as Float)
380 .sqrt()
381 }
382 "volatility" => 0.0,
383 "skewness" => self.compute_skewness(&window),
384 "kurtosis" => self.compute_kurtosis(&window),
385 _ => 0.0, };
387
388 result[[window_idx, feat_idx]] = feature_value;
389 }
390 }
391
392 Ok(result)
393 }
394
395 fn compute_skewness(&self, data: &ArrayView1<Float>) -> Float {
396 let n = data.len() as Float;
397 if n < 3.0 {
398 return 0.0;
399 }
400
401 let mean = data.sum() / n;
402 let variance = data.mapv(|x| (x - mean).powi(2)).sum() / n;
403 let std_dev = variance.sqrt();
404
405 if std_dev < 1e-10 {
406 return 0.0;
407 }
408
409 data.mapv(|x| ((x - mean) / std_dev).powi(3)).sum() / n
410 }
411
412 fn compute_kurtosis(&self, data: &ArrayView1<Float>) -> Float {
413 let n = data.len() as Float;
414 if n < 4.0 {
415 return 0.0;
416 }
417
418 let mean = data.sum() / n;
419 let variance = data.mapv(|x| (x - mean).powi(2)).sum() / n;
420 let std_dev = variance.sqrt();
421
422 if std_dev < 1e-10 {
423 return 0.0;
424 }
425
426 let kurtosis = data.mapv(|x| ((x - mean) / std_dev).powi(4)).sum() / n;
427 kurtosis - 3.0 }
429}
430
431impl Default for SlidingWindowFeatures {
432 fn default() -> Self {
433 Self::new()
434 }
435}
436
437#[derive(Debug, Clone)]
464pub struct FourierTransformFeatures {
465 n_frequencies: usize,
466 include_power_spectrum: bool,
467 include_phase: bool,
468 normalize: bool,
469 window_function: String, }
471
472impl FourierTransformFeatures {
473 pub fn new() -> Self {
475 Self {
476 n_frequencies: 10,
477 include_power_spectrum: true,
478 include_phase: false,
479 normalize: true,
480 window_function: "hanning".to_string(),
481 }
482 }
483
484 pub fn n_frequencies(mut self, n_frequencies: usize) -> Self {
486 self.n_frequencies = n_frequencies;
487 self
488 }
489
490 pub fn include_power_spectrum(mut self, include_power_spectrum: bool) -> Self {
492 self.include_power_spectrum = include_power_spectrum;
493 self
494 }
495
496 pub fn include_phase(mut self, include_phase: bool) -> Self {
498 self.include_phase = include_phase;
499 self
500 }
501
502 pub fn normalize(mut self, normalize: bool) -> Self {
504 self.normalize = normalize;
505 self
506 }
507
508 pub fn window_function(mut self, window_function: String) -> Self {
510 self.window_function = window_function;
511 self
512 }
513
514 pub fn extract_features(&self, ts: &ArrayView1<Float>) -> SklResult<Array1<Float>> {
516 let n = ts.len();
517 if n == 0 {
518 return Err(SklearsError::InvalidInput(
519 "Time series cannot be empty".to_string(),
520 ));
521 }
522
523 let windowed_ts = self.apply_window(ts)?;
525
526 let fft_result = self.compute_dft(&windowed_ts)?;
528
529 let mut features = Vec::new();
530
531 let max_freq = (self.n_frequencies.min(n / 2)).max(1);
533
534 if self.include_power_spectrum {
535 for &(re, im) in fft_result.iter().take(max_freq) {
536 let power = re.powi(2) + im.powi(2); features.push(power);
538 }
539 }
540
541 if self.include_phase {
542 for &(re, im) in fft_result.iter().take(max_freq) {
543 let phase = im.atan2(re); features.push(phase);
545 }
546 }
547
548 let spectral_features = self.compute_spectral_features(&fft_result, max_freq)?;
550 features.extend_from_slice(&spectral_features);
551
552 if self.normalize && !features.is_empty() {
554 let max_val = features.iter().cloned().fold(0.0, Float::max);
555 if max_val > 1e-10 {
556 for feature in &mut features {
557 *feature /= max_val;
558 }
559 }
560 }
561
562 Ok(Array1::from_vec(features))
563 }
564
565 fn apply_window(&self, ts: &ArrayView1<Float>) -> SklResult<Array1<Float>> {
566 let n = ts.len();
567 let mut windowed = Array1::zeros(n);
568
569 match self.window_function.as_str() {
570 "hanning" => {
571 for i in 0..n {
572 let w = 0.5
573 * (1.0
574 - (2.0 * std::f64::consts::PI * i as Float / (n - 1) as Float).cos());
575 windowed[i] = ts[i] * w;
576 }
577 }
578 "hamming" => {
579 for i in 0..n {
580 let w = 0.54
581 - 0.46 * (2.0 * std::f64::consts::PI * i as Float / (n - 1) as Float).cos();
582 windowed[i] = ts[i] * w;
583 }
584 }
585 "blackman" => {
586 for i in 0..n {
587 let w = 0.42
588 - 0.5 * (2.0 * std::f64::consts::PI * i as Float / (n - 1) as Float).cos()
589 + 0.08 * (4.0 * std::f64::consts::PI * i as Float / (n - 1) as Float).cos();
590 windowed[i] = ts[i] * w;
591 }
592 }
593 _ => {
594 windowed.assign(ts);
596 }
597 }
598
599 Ok(windowed)
600 }
601
602 #[allow(clippy::needless_range_loop)] fn compute_dft(&self, ts: &Array1<Float>) -> SklResult<Vec<(Float, Float)>> {
604 let n = ts.len();
605 let mut result = vec![(0.0, 0.0); n];
606
607 for k in 0..n {
609 let mut real_sum = 0.0;
610 let mut imag_sum = 0.0;
611
612 for j in 0..n {
613 let angle = -2.0 * std::f64::consts::PI * (k * j) as Float / n as Float;
614 real_sum += ts[j] * angle.cos();
615 imag_sum += ts[j] * angle.sin();
616 }
617
618 result[k] = (real_sum, imag_sum);
619 }
620
621 Ok(result)
622 }
623
624 fn compute_spectral_features(
625 &self,
626 fft_result: &[(Float, Float)],
627 max_freq: usize,
628 ) -> SklResult<Vec<Float>> {
629 let mut features = Vec::new();
630
631 let mut weighted_sum = 0.0;
633 let mut total_power = 0.0;
634
635 #[allow(clippy::needless_range_loop)]
636 for i in 0..max_freq {
638 let power = fft_result[i].0.powi(2) + fft_result[i].1.powi(2);
639 weighted_sum += i as Float * power;
640 total_power += power;
641 }
642
643 let spectral_centroid = if total_power > 1e-10 {
644 weighted_sum / total_power
645 } else {
646 0.0
647 };
648 features.push(spectral_centroid);
649
650 let mut bandwidth_sum = 0.0;
652 #[allow(clippy::needless_range_loop)]
653 for i in 0..max_freq {
655 let power = fft_result[i].0.powi(2) + fft_result[i].1.powi(2);
656 bandwidth_sum += (i as Float - spectral_centroid).powi(2) * power;
657 }
658
659 let spectral_bandwidth = if total_power > 1e-10 {
660 (bandwidth_sum / total_power).sqrt()
661 } else {
662 0.0
663 };
664 features.push(spectral_bandwidth);
665
666 let target_energy = 0.85 * total_power;
668 let mut cumulative_energy = 0.0;
669 let mut rolloff_freq = 0.0;
670
671 #[allow(clippy::needless_range_loop)]
672 for i in 0..max_freq {
674 let power = fft_result[i].0.powi(2) + fft_result[i].1.powi(2);
675 cumulative_energy += power;
676 if cumulative_energy >= target_energy {
677 rolloff_freq = i as Float;
678 break;
679 }
680 }
681 features.push(rolloff_freq);
682
683 Ok(features)
684 }
685}
686
687impl Default for FourierTransformFeatures {
688 fn default() -> Self {
689 Self::new()
690 }
691}