use crate::preprocess::features;
#[derive(Debug, Clone)]
pub struct FeatureMatrix {
pub names: Vec<String>,
pub data: Vec<f64>,
pub n_rows: usize,
}
impl FeatureMatrix {
#[inline]
pub fn n_features(&self) -> usize {
self.names.len()
}
pub fn column(&self, idx: usize) -> Vec<f64> {
let nf = self.n_features();
(0..self.n_rows).map(|r| self.data[r * nf + idx]).collect()
}
pub fn column_by_name(&self, name: &str) -> Option<Vec<f64>> {
let idx = self.names.iter().position(|n| n == name)?;
Some(self.column(idx))
}
}
#[derive(Debug, Clone)]
pub struct AutoFeatureConfig {
pub period: Option<usize>,
pub rolling_windows: Vec<usize>,
pub variance_threshold: Option<f64>,
pub temporal_features: bool,
}
impl Default for AutoFeatureConfig {
fn default() -> Self {
Self {
period: None,
rolling_windows: vec![60, 360, 1440, 10080],
variance_threshold: None,
temporal_features: true,
}
}
}
impl AutoFeatureConfig {
pub fn with_period(mut self, period: usize) -> Self {
self.period = Some(period);
self
}
pub fn with_windows(mut self, windows: Vec<usize>) -> Self {
self.rolling_windows = windows;
self
}
pub fn with_variance_threshold(mut self, threshold: f64) -> Self {
self.variance_threshold = Some(threshold);
self
}
}
pub fn auto_features(
timestamps_ns: &[i64],
values: &[f64],
config: &AutoFeatureConfig,
) -> FeatureMatrix {
let n = timestamps_ns.len().min(values.len());
let mut names: Vec<String> = Vec::new();
let mut columns: Vec<Vec<f64>> = Vec::new();
if config.temporal_features {
let mut hour = Vec::with_capacity(n);
let mut dow = Vec::with_capacity(n);
let mut dom = Vec::with_capacity(n);
let mut month = Vec::with_capacity(n);
let mut weekend = Vec::with_capacity(n);
for &ts in timestamps_ns.iter().take(n) {
let secs = ts / 1_000_000_000;
let (h, d_of_w, d_of_m, m_of_y) = decompose_epoch_secs(secs);
hour.push(h as f64);
dow.push(d_of_w as f64);
dom.push(d_of_m as f64);
month.push(m_of_y as f64);
weekend.push(if d_of_w >= 5 { 1.0 } else { 0.0 });
}
names.extend([
"hour_of_day".into(),
"day_of_week".into(),
"day_of_month".into(),
"month_of_year".into(),
"is_weekend".into(),
]);
columns.extend([hour, dow, dom, month, weekend]);
}
let vals = &values[..n];
for &w in &config.rolling_windows {
if w == 0 || w > n {
continue;
}
let rmean = rolling_mean(vals, w);
names.push(format!("rolling_mean_{w}"));
columns.push(rmean);
let rstd = features::rolling_std(vals, w);
names.push(format!("rolling_std_{w}"));
columns.push(rstd);
let rmin = rolling_min(vals, w);
names.push(format!("rolling_min_{w}"));
columns.push(rmin);
let rmax = rolling_max(vals, w);
names.push(format!("rolling_max_{w}"));
columns.push(rmax);
}
let lag1 = features::lag(vals, 1);
names.push("lag_1".into());
columns.push(lag1);
let lag2 = features::lag(vals, 2);
names.push("lag_2".into());
columns.push(lag2);
if let Some(period) = config.period
&& period > 0
&& period < n
{
let lag_p = features::lag(vals, period);
names.push(format!("lag_{period}"));
columns.push(lag_p);
}
let n_features = names.len();
let mut data = vec![0.0; n * n_features];
for (col_idx, col) in columns.iter().enumerate() {
for row in 0..n {
data[row * n_features + col_idx] = col[row];
}
}
let mut matrix = FeatureMatrix {
names,
data,
n_rows: n,
};
if let Some(threshold) = config.variance_threshold {
prune_low_variance(&mut matrix, threshold);
}
matrix
}
fn prune_low_variance(matrix: &mut FeatureMatrix, threshold: f64) {
let nf = matrix.n_features();
let n = matrix.n_rows;
if nf == 0 || n < 2 {
return;
}
let mut keep: Vec<bool> = Vec::with_capacity(nf);
for col in 0..nf {
let mut sum = 0.0;
let mut sum_sq = 0.0;
let mut count = 0usize;
for row in 0..n {
let v = matrix.data[row * nf + col];
if !v.is_nan() {
sum += v;
sum_sq += v * v;
count += 1;
}
}
let var = if count > 1 {
(sum_sq - sum * sum / count as f64) / (count - 1) as f64
} else {
0.0
};
keep.push(var >= threshold);
}
let new_nf = keep.iter().filter(|&&k| k).count();
if new_nf == nf {
return; }
let new_names: Vec<String> = matrix
.names
.iter()
.zip(keep.iter())
.filter(|&(_, &k)| k)
.map(|(n, _)| n.clone())
.collect();
let mut new_data = Vec::with_capacity(n * new_nf);
for row in 0..n {
for (col, &k) in keep.iter().enumerate() {
if k {
new_data.push(matrix.data[row * nf + col]);
}
}
}
matrix.names = new_names;
matrix.data = new_data;
}
fn rolling_mean(values: &[f64], window: usize) -> Vec<f64> {
let n = values.len();
let mut out = vec![f64::NAN; n];
if window == 0 || window > n {
return out;
}
let mut sum: f64 = values[..window].iter().sum();
out[window - 1] = sum / window as f64;
const RECOMPUTE_INTERVAL: usize = 1024;
for i in window..n {
if (i - window).is_multiple_of(RECOMPUTE_INTERVAL) {
sum = values[i + 1 - window..=i].iter().sum();
} else {
sum += values[i] - values[i - window];
}
out[i] = sum / window as f64;
}
out
}
fn rolling_min(values: &[f64], window: usize) -> Vec<f64> {
let n = values.len();
let mut out = vec![f64::NAN; n];
if window == 0 || window > n {
return out;
}
let mut deque = std::collections::VecDeque::with_capacity(window);
for i in 0..n {
while let Some(&front) = deque.front() {
if front + window <= i {
deque.pop_front();
} else {
break;
}
}
while let Some(&back) = deque.back() {
if values[back] >= values[i] {
deque.pop_back();
} else {
break;
}
}
deque.push_back(i);
if i >= window - 1 {
out[i] = values[deque[0]];
}
}
out
}
fn rolling_max(values: &[f64], window: usize) -> Vec<f64> {
let n = values.len();
let mut out = vec![f64::NAN; n];
if window == 0 || window > n {
return out;
}
let mut deque = std::collections::VecDeque::with_capacity(window);
for i in 0..n {
while let Some(&front) = deque.front() {
if front + window <= i {
deque.pop_front();
} else {
break;
}
}
while let Some(&back) = deque.back() {
if values[back] <= values[i] {
deque.pop_back();
} else {
break;
}
}
deque.push_back(i);
if i >= window - 1 {
out[i] = values[deque[0]];
}
}
out
}
fn decompose_epoch_secs(epoch_secs: i64) -> (u32, u32, u32, u32) {
const SECS_PER_DAY: i64 = 86400;
let day_secs = epoch_secs.rem_euclid(SECS_PER_DAY);
let hour = (day_secs / 3600) as u32;
let days = epoch_secs.div_euclid(SECS_PER_DAY);
let dow = ((days + 3) % 7) as u32;
let (y, m, d) = civil_from_days(days);
let _ = y; let dom = d as u32;
let moy = m as u32;
(hour, dow, dom, moy)
}
fn civil_from_days(days: i64) -> (i64, i64, i64) {
let z = days + 719468;
let era = z.div_euclid(146097);
let doe = z.rem_euclid(146097);
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auto_features_basic() {
let n = 100;
let ts: Vec<i64> = (0..n).map(|i| i as i64 * 60_000_000_000).collect(); let vals: Vec<f64> = (0..n).map(|i| (i as f64 * 0.1).sin()).collect();
let config = AutoFeatureConfig::default().with_windows(vec![5, 10]);
let fm = auto_features(&ts, &vals, &config);
assert_eq!(fm.n_rows, n);
assert_eq!(fm.n_features(), 15);
assert!(fm.names.contains(&"hour_of_day".to_string()));
assert!(fm.names.contains(&"rolling_mean_5".to_string()));
assert!(fm.names.contains(&"lag_1".to_string()));
}
#[test]
fn test_auto_features_with_period() {
let n = 50;
let ts: Vec<i64> = (0..n).map(|i| i as i64 * 3_600_000_000_000).collect();
let vals: Vec<f64> = (0..n).map(|i| i as f64).collect();
let config = AutoFeatureConfig::default()
.with_windows(vec![5])
.with_period(24);
let fm = auto_features(&ts, &vals, &config);
assert_eq!(fm.n_features(), 12);
assert!(fm.names.contains(&"lag_24".to_string()));
}
#[test]
fn test_variance_threshold_pruning() {
let n = 50;
let ts: Vec<i64> = (0..n).map(|i| i as i64 * 60_000_000_000).collect();
let vals = vec![42.0; n];
let config = AutoFeatureConfig::default()
.with_windows(vec![5])
.with_variance_threshold(0.01);
let fm = auto_features(&ts, &vals, &config);
assert!(fm.n_features() < 12);
}
#[test]
fn test_feature_matrix_column_by_name() {
let n = 20;
let ts: Vec<i64> = (0..n).map(|i| i as i64 * 60_000_000_000).collect();
let vals: Vec<f64> = (0..n).map(|i| i as f64).collect();
let config = AutoFeatureConfig {
temporal_features: false,
rolling_windows: vec![],
period: None,
variance_threshold: None,
};
let fm = auto_features(&ts, &vals, &config);
assert_eq!(fm.n_features(), 2);
let lag1 = fm.column_by_name("lag_1").unwrap();
assert!(lag1[0].is_nan());
assert_eq!(lag1[1], 0.0);
assert_eq!(lag1[2], 1.0);
}
#[test]
fn test_rolling_mean() {
let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let rm = rolling_mean(&v, 3);
assert!(rm[0].is_nan());
assert!(rm[1].is_nan());
assert!((rm[2] - 2.0).abs() < 1e-9);
assert!((rm[3] - 3.0).abs() < 1e-9);
assert!((rm[4] - 4.0).abs() < 1e-9);
}
#[test]
fn test_rolling_min_max() {
let v = vec![3.0, 1.0, 4.0, 1.0, 5.0];
let rmin = rolling_min(&v, 3);
let rmax = rolling_max(&v, 3);
assert!(rmin[0].is_nan());
assert!(rmin[1].is_nan());
assert_eq!(rmin[2], 1.0);
assert_eq!(rmin[3], 1.0);
assert_eq!(rmin[4], 1.0);
assert_eq!(rmax[2], 4.0);
assert_eq!(rmax[3], 4.0);
assert_eq!(rmax[4], 5.0);
}
#[test]
fn test_decompose_epoch_known_date() {
let (hour, dow, dom, moy) = decompose_epoch_secs(1_705_314_600);
assert_eq!(hour, 10);
assert_eq!(dow, 0); assert_eq!(dom, 15);
assert_eq!(moy, 1);
}
#[test]
fn test_decompose_epoch_weekend() {
let (_, dow, _, _) = decompose_epoch_secs(1_705_158_000);
assert_eq!(dow, 5); }
#[test]
fn test_no_temporal_features() {
let n = 10;
let ts: Vec<i64> = (0..n).map(|i| i as i64 * 60_000_000_000).collect();
let vals: Vec<f64> = (0..n).map(|i| i as f64).collect();
let config = AutoFeatureConfig {
temporal_features: false,
rolling_windows: vec![3],
period: None,
variance_threshold: None,
};
let fm = auto_features(&ts, &vals, &config);
assert_eq!(fm.n_features(), 6);
assert!(!fm.names.contains(&"hour_of_day".to_string()));
}
}