use crate::*;
use scirs2_core::ndarray::{s, Array1, ArrayView1};
use sklears_core::prelude::SklearsError;
#[derive(Debug, Clone)]
pub struct StatisticalMomentsExtractor {
max_order: usize,
normalize: bool,
include_cumulants: bool,
robust: bool,
}
impl StatisticalMomentsExtractor {
pub fn new() -> Self {
Self {
max_order: 4,
normalize: true,
include_cumulants: true,
robust: false,
}
}
pub fn max_order(mut self, max_order: usize) -> Self {
self.max_order = max_order;
self
}
pub fn normalize(mut self, normalize: bool) -> Self {
self.normalize = normalize;
self
}
pub fn include_cumulants(mut self, include_cumulants: bool) -> Self {
self.include_cumulants = include_cumulants;
self
}
pub fn robust(mut self, robust: bool) -> Self {
self.robust = robust;
self
}
pub fn extract_features(&self, data: &ArrayView1<Float>) -> SklResult<Array1<Float>> {
let n = data.len();
if n == 0 {
return Err(SklearsError::InvalidInput(
"Data cannot be empty".to_string(),
));
}
if n < 2 {
return Err(SklearsError::InvalidInput(
"Data must have at least 2 points".to_string(),
));
}
let mut features = Vec::new();
let mean = if self.robust {
self.robust_mean(data)
} else {
data.sum() / n as Float
};
features.push(mean);
let variance = if self.robust {
self.robust_variance(data, mean)
} else {
data.mapv(|x| (x - mean).powi(2)).sum() / (n - 1) as Float
};
features.push(variance);
let std_dev = variance.sqrt();
features.push(std_dev);
for order in 1..=self.max_order {
let raw_moment = data.mapv(|x| x.powi(order as i32)).sum() / n as Float;
features.push(raw_moment);
}
for order in 2..=self.max_order {
let central_moment = data.mapv(|x| (x - mean).powi(order as i32)).sum() / n as Float;
features.push(central_moment);
}
if self.normalize && std_dev > 1e-10 {
for order in 3..=self.max_order {
let standardized_moment = data
.mapv(|x| ((x - mean) / std_dev).powi(order as i32))
.sum()
/ n as Float;
features.push(standardized_moment);
}
}
if self.include_cumulants {
let cumulants = self.compute_cumulants(data, mean, variance)?;
features.extend_from_slice(&cumulants);
}
let skewness = if std_dev > 1e-10 {
data.mapv(|x| ((x - mean) / std_dev).powi(3)).sum() / n as Float
} else {
0.0
};
features.push(skewness);
let kurtosis = if std_dev > 1e-10 {
data.mapv(|x| ((x - mean) / std_dev).powi(4)).sum() / n as Float - 3.0
} else {
0.0
};
features.push(kurtosis);
let coefficient_of_variation = if mean.abs() > 1e-10 {
std_dev / mean.abs()
} else {
0.0
};
features.push(coefficient_of_variation);
Ok(Array1::from_vec(features))
}
fn robust_mean(&self, data: &ArrayView1<Float>) -> Float {
let mut sorted_data = data.to_vec();
sorted_data.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let n = sorted_data.len();
if n % 2 == 1 {
sorted_data[n / 2]
} else {
(sorted_data[n / 2 - 1] + sorted_data[n / 2]) / 2.0
}
}
fn robust_variance(&self, data: &ArrayView1<Float>, center: Float) -> Float {
let mad_values: Vec<Float> = data.iter().map(|&x| (x - center).abs()).collect();
let mut sorted_mad = mad_values;
sorted_mad.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let n = sorted_mad.len();
let mad = if n % 2 == 1 {
sorted_mad[n / 2]
} else {
(sorted_mad[n / 2 - 1] + sorted_mad[n / 2]) / 2.0
};
(1.4826 * mad).powi(2)
}
fn compute_cumulants(
&self,
data: &ArrayView1<Float>,
mean: Float,
variance: Float,
) -> SklResult<Vec<Float>> {
let n = data.len() as Float;
let mut cumulants = Vec::new();
cumulants.push(mean);
cumulants.push(variance);
let third_moment = data.mapv(|x| (x - mean).powi(3)).sum() / n;
cumulants.push(third_moment);
let fourth_moment = data.mapv(|x| (x - mean).powi(4)).sum() / n;
let fourth_cumulant = fourth_moment - 3.0 * variance.powi(2);
cumulants.push(fourth_cumulant);
Ok(cumulants)
}
}
impl Default for StatisticalMomentsExtractor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DistributionFeaturesExtractor {
n_quantiles: usize,
n_bins: usize,
include_shape_features: bool,
density_estimation: String,
}
impl DistributionFeaturesExtractor {
pub fn new() -> Self {
Self {
n_quantiles: 5,
n_bins: 10,
include_shape_features: true,
density_estimation: "histogram".to_string(),
}
}
pub fn n_quantiles(mut self, n_quantiles: usize) -> Self {
self.n_quantiles = n_quantiles;
self
}
pub fn n_bins(mut self, n_bins: usize) -> Self {
self.n_bins = n_bins;
self
}
pub fn include_shape_features(mut self, include_shape_features: bool) -> Self {
self.include_shape_features = include_shape_features;
self
}
pub fn density_estimation(mut self, method: String) -> Self {
self.density_estimation = method;
self
}
pub fn extract_features(&self, data: &ArrayView1<Float>) -> SklResult<Array1<Float>> {
let n = data.len();
if n == 0 {
return Err(SklearsError::InvalidInput(
"Data cannot be empty".to_string(),
));
}
let mut features = Vec::new();
let mut sorted_data = data.to_vec();
sorted_data.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
for i in 1..=self.n_quantiles {
let quantile = i as Float / (self.n_quantiles + 1) as Float;
let quantile_value = self.compute_quantile(&sorted_data, quantile);
features.push(quantile_value);
}
let percentiles = [0.05, 0.25, 0.5, 0.75, 0.95];
for &percentile in &percentiles {
let percentile_value = self.compute_quantile(&sorted_data, percentile);
features.push(percentile_value);
}
let q25 = self.compute_quantile(&sorted_data, 0.25);
let q75 = self.compute_quantile(&sorted_data, 0.75);
let iqr = q75 - q25;
features.push(iqr);
let range = sorted_data[n - 1] - sorted_data[0];
features.push(range);
let histogram = self.compute_histogram(&sorted_data)?;
features.extend_from_slice(&histogram);
if self.include_shape_features {
let shape_features = self.compute_shape_features(&sorted_data)?;
features.extend_from_slice(&shape_features);
}
Ok(Array1::from_vec(features))
}
fn compute_quantile(&self, sorted_data: &[Float], quantile: Float) -> Float {
let n = sorted_data.len();
let index = quantile * (n - 1) as Float;
let lower_index = index.floor() as usize;
let upper_index = index.ceil() as usize;
if lower_index == upper_index {
sorted_data[lower_index]
} else {
let weight = index - lower_index as Float;
sorted_data[lower_index] * (1.0 - weight) + sorted_data[upper_index] * weight
}
}
fn compute_histogram(&self, sorted_data: &[Float]) -> SklResult<Vec<Float>> {
if sorted_data.is_empty() {
return Ok(vec![0.0; self.n_bins]);
}
let min_val = sorted_data[0];
let max_val = sorted_data[sorted_data.len() - 1];
if (max_val - min_val).abs() < 1e-10 {
let mut hist = vec![0.0; self.n_bins];
hist[0] = sorted_data.len() as Float;
return Ok(hist);
}
let bin_width = (max_val - min_val) / self.n_bins as Float;
let mut histogram = vec![0.0; self.n_bins];
for &value in sorted_data {
let bin_index = ((value - min_val) / bin_width).floor() as usize;
let bin_index = bin_index.min(self.n_bins - 1); histogram[bin_index] += 1.0;
}
let total_count = sorted_data.len() as Float;
for count in &mut histogram {
*count /= total_count;
}
Ok(histogram)
}
fn compute_shape_features(&self, sorted_data: &[Float]) -> SklResult<Vec<Float>> {
let mut features = Vec::new();
let trim_amount = (sorted_data.len() as Float * 0.1).floor() as usize;
if trim_amount > 0 && trim_amount * 2 < sorted_data.len() {
let trimmed_slice = &sorted_data[trim_amount..sorted_data.len() - trim_amount];
let trimmed_mean = trimmed_slice.iter().sum::<Float>() / trimmed_slice.len() as Float;
features.push(trimmed_mean);
} else {
features.push(sorted_data.iter().sum::<Float>() / sorted_data.len() as Float);
}
let histogram = self.compute_histogram(sorted_data)?;
let mode_bin = histogram
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
.map(|(i, _)| i)
.unwrap_or(0);
let min_val = sorted_data[0];
let max_val = sorted_data[sorted_data.len() - 1];
let bin_width = (max_val - min_val) / self.n_bins as Float;
let mode_estimate = min_val + (mode_bin as Float + 0.5) * bin_width;
features.push(mode_estimate);
let mut peak_count = 0;
for i in 1..histogram.len() - 1 {
if histogram[i] > histogram[i - 1] && histogram[i] > histogram[i + 1] {
peak_count += 1;
}
}
features.push(peak_count as Float);
let tail_size = (self.n_bins as Float * 0.1).max(1.0) as usize;
let left_tail: Float = histogram[0..tail_size].iter().sum();
let right_tail: Float = histogram[histogram.len() - tail_size..].iter().sum();
features.push(left_tail + right_tail);
Ok(features)
}
}
impl Default for DistributionFeaturesExtractor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct EntropyFeaturesExtractor {
n_bins: usize,
include_conditional_entropy: bool,
window_size: usize,
tolerance: Float,
}
impl EntropyFeaturesExtractor {
pub fn new() -> Self {
Self {
n_bins: 10,
include_conditional_entropy: true,
window_size: 2,
tolerance: 0.2,
}
}
pub fn n_bins(mut self, n_bins: usize) -> Self {
self.n_bins = n_bins;
self
}
pub fn include_conditional_entropy(mut self, include_conditional_entropy: bool) -> Self {
self.include_conditional_entropy = include_conditional_entropy;
self
}
pub fn window_size(mut self, window_size: usize) -> Self {
self.window_size = window_size;
self
}
pub fn tolerance(mut self, tolerance: Float) -> Self {
self.tolerance = tolerance;
self
}
pub fn extract_features(&self, data: &ArrayView1<Float>) -> SklResult<Array1<Float>> {
let n = data.len();
if n == 0 {
return Err(SklearsError::InvalidInput(
"Data cannot be empty".to_string(),
));
}
let mut features = Vec::new();
let shannon_entropy = self.compute_shannon_entropy(data)?;
features.push(shannon_entropy);
let renyi_entropy = self.compute_renyi_entropy(data, 2.0)?;
features.push(renyi_entropy);
if n >= 2 * self.window_size {
let sample_entropy = self.compute_sample_entropy(data)?;
features.push(sample_entropy);
} else {
features.push(0.0); }
if n > self.window_size {
let approximate_entropy = self.compute_approximate_entropy(data)?;
features.push(approximate_entropy);
} else {
features.push(0.0); }
let differential_entropy = self.compute_differential_entropy(data)?;
features.push(differential_entropy);
if self.include_conditional_entropy && n >= 2 {
let conditional_entropy = self.compute_conditional_entropy(data)?;
features.push(conditional_entropy);
}
Ok(Array1::from_vec(features))
}
fn compute_shannon_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
let histogram = self.compute_normalized_histogram(data)?;
let mut entropy = 0.0;
for &prob in &histogram {
if prob > 1e-10 {
entropy -= prob * prob.ln();
}
}
Ok(entropy)
}
fn compute_renyi_entropy(&self, data: &ArrayView1<Float>, alpha: Float) -> SklResult<Float> {
if (alpha - 1.0).abs() < 1e-10 {
return self.compute_shannon_entropy(data); }
let histogram = self.compute_normalized_histogram(data)?;
let mut sum = 0.0;
for &prob in &histogram {
if prob > 1e-10 {
sum += prob.powf(alpha);
}
}
let entropy = if sum > 1e-10 {
sum.ln() / (1.0 - alpha)
} else {
0.0
};
Ok(entropy)
}
fn compute_sample_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
let n = data.len();
let m = self.window_size;
let r = self.tolerance;
if n < m + 1 {
return Ok(0.0);
}
let mut a = 0;
let mut b = 0;
for i in 0..(n - m) {
for j in 0..(n - m) {
if i != j {
let mut match_m = true;
let mut _match_m1 = true;
for k in 0..m {
if (data[i + k] - data[j + k]).abs() > r {
match_m = false;
_match_m1 = false;
break;
}
}
if match_m {
b += 1;
if i < n - m && j < n - m {
if (data[i + m] - data[j + m]).abs() <= r {
a += 1;
} else {
_match_m1 = false;
}
} else {
_match_m1 = false;
}
}
}
}
}
let sample_entropy = if a > 0 && b > 0 {
-(a as Float / b as Float).ln()
} else {
std::f64::consts::E };
Ok(sample_entropy)
}
fn compute_approximate_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
let n = data.len();
let m = self.window_size;
let r = self.tolerance;
if n < m + 1 {
return Ok(0.0);
}
let phi_m = self.compute_phi(data, m, r)?;
let phi_m1 = self.compute_phi(data, m + 1, r)?;
Ok(phi_m - phi_m1)
}
fn compute_phi(&self, data: &ArrayView1<Float>, m: usize, r: Float) -> SklResult<Float> {
let n = data.len();
let mut phi = 0.0;
for i in 0..(n - m + 1) {
let mut pattern_count = 0;
for j in 0..(n - m + 1) {
let mut matches = true;
for k in 0..m {
if (data[i + k] - data[j + k]).abs() > r {
matches = false;
break;
}
}
if matches {
pattern_count += 1;
}
}
if pattern_count > 0 {
phi += (pattern_count as Float / (n - m + 1) as Float).ln();
}
}
phi /= (n - m + 1) as Float;
Ok(phi)
}
fn compute_differential_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
let mean = data.sum() / data.len() as Float;
let variance = data.mapv(|x| (x - mean).powi(2)).sum() / (data.len() - 1) as Float;
let differential_entropy =
0.5 * (2.0 * std::f64::consts::PI * std::f64::consts::E * variance).ln();
Ok(differential_entropy)
}
fn compute_conditional_entropy(&self, data: &ArrayView1<Float>) -> SklResult<Float> {
let n = data.len();
if n < 2 {
return Ok(0.0);
}
let pairs: Vec<(Float, Float)> = (0..n - 1).map(|i| (data[i], data[i + 1])).collect();
let joint_entropy = self.compute_joint_entropy(&pairs)?;
let marginal_entropy = self.compute_shannon_entropy(&data.slice(s![0..n - 1]))?;
Ok(joint_entropy - marginal_entropy)
}
fn compute_joint_entropy(&self, pairs: &[(Float, Float)]) -> SklResult<Float> {
let n_bins = self.n_bins;
if pairs.is_empty() {
return Ok(0.0);
}
let x_min = pairs
.iter()
.map(|(x, _)| *x)
.fold(f64::INFINITY, |a, b| a.min(b));
let x_max = pairs
.iter()
.map(|(x, _)| *x)
.fold(f64::NEG_INFINITY, |a, b| a.max(b));
let y_min = pairs
.iter()
.map(|(_, y)| *y)
.fold(f64::INFINITY, |a, b| a.min(b));
let y_max = pairs
.iter()
.map(|(_, y)| *y)
.fold(f64::NEG_INFINITY, |a, b| a.max(b));
let x_range = if (x_max - x_min).abs() < 1e-10 {
1.0
} else {
x_max - x_min
};
let y_range = if (y_max - y_min).abs() < 1e-10 {
1.0
} else {
y_max - y_min
};
let mut joint_hist = vec![vec![0.0; n_bins]; n_bins];
for &(x, y) in pairs {
let x_bin = ((x - x_min) / x_range * n_bins as Float).floor() as usize;
let y_bin = ((y - y_min) / y_range * n_bins as Float).floor() as usize;
let x_bin = x_bin.min(n_bins - 1);
let y_bin = y_bin.min(n_bins - 1);
joint_hist[x_bin][y_bin] += 1.0;
}
let total = pairs.len() as Float;
let mut entropy = 0.0;
for row in joint_hist.iter().take(n_bins) {
for &count in row.iter().take(n_bins) {
let prob = count / total;
if prob > 1e-10 {
entropy -= prob * prob.ln();
}
}
}
Ok(entropy)
}
fn compute_normalized_histogram(&self, data: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
let n = data.len();
if n == 0 {
return Ok(vec![0.0; self.n_bins]);
}
let min_val = data.iter().cloned().fold(f64::INFINITY, |a, b| a.min(b));
let max_val = data
.iter()
.cloned()
.fold(f64::NEG_INFINITY, |a, b| a.max(b));
if (max_val - min_val).abs() < 1e-10 {
let mut hist = vec![0.0; self.n_bins];
hist[0] = 1.0;
return Ok(hist);
}
let bin_width = (max_val - min_val) / self.n_bins as Float;
let mut histogram = vec![0.0; self.n_bins];
for &value in data.iter() {
let bin_index = ((value - min_val) / bin_width).floor() as usize;
let bin_index = bin_index.min(self.n_bins - 1);
histogram[bin_index] += 1.0;
}
let total_count = n as Float;
for count in &mut histogram {
*count /= total_count;
}
Ok(histogram)
}
}
impl Default for EntropyFeaturesExtractor {
fn default() -> Self {
Self::new()
}
}