use super::distribution::quantile;
use crate::tensor::TensorStorage;
use crate::{Result, Tensor, TensorError};
use scirs2_core::numeric::{Float, ToPrimitive};
macro_rules! float_const {
($val:expr, $t:ty) => {
<$t as scirs2_core::num_traits::NumCast>::from($val)
.expect("float constant conversion should never fail for standard float types")
};
}
pub fn percentile<T>(x: &Tensor<T>, percentiles: &[T], axis: Option<i32>) -> Result<Tensor<T>>
where
T: Float + Default + Send + Sync + 'static + PartialOrd + bytemuck::Pod + bytemuck::Zeroable,
{
let quantiles: Vec<T> = percentiles
.iter()
.map(|&p| p / float_const!(100.0, T))
.collect();
quantile(x, &quantiles, axis)
}
pub fn range<T>(x: &Tensor<T>, axis: Option<i32>) -> Result<Tensor<T>>
where
T: Float + Default + Send + Sync + 'static + PartialOrd + bytemuck::Pod + bytemuck::Zeroable,
{
use crate::ops::reduction::{max, min};
let axes_slice = axis.map(|a| vec![a]);
let min_val = min(x, axes_slice.as_deref(), false)?;
let max_val = max(x, axes_slice.as_deref(), false)?;
use crate::ops::binary::sub;
sub(&max_val, &min_val)
}
pub fn skewness<T>(x: &Tensor<T>, axis: Option<i32>, bias: bool) -> Result<Tensor<T>>
where
T: Float
+ Default
+ Send
+ Sync
+ 'static
+ PartialOrd
+ ToPrimitive
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
match &x.storage {
TensorStorage::Cpu(arr) => {
if axis.is_some() {
return skewness(&x.flatten()?, None, bias);
}
let flat_data: Vec<T> = arr.iter().cloned().collect();
let n = flat_data.len();
if n < 3 {
return Err(TensorError::invalid_operation_simple(
"Skewness requires at least 3 data points".to_string(),
));
}
let mean =
flat_data.iter().cloned().fold(T::zero(), |acc, x| acc + x) / float_const!(n, T);
let mut m2 = T::zero();
let mut m3 = T::zero();
for &value in &flat_data {
let diff = value - mean;
let diff_sq = diff * diff;
let diff_cube = diff_sq * diff;
m2 = m2 + diff_sq;
m3 = m3 + diff_cube;
}
let divisor = if bias {
float_const!(n, T)
} else {
float_const!(n - 1, T)
};
m2 = m2 / divisor;
m3 = m3 / divisor;
let std_dev = m2.sqrt();
if std_dev == T::zero() {
return Ok(Tensor::from_scalar(T::zero()));
}
let skew = m3 / (std_dev * std_dev * std_dev);
Ok(Tensor::from_scalar(skew))
}
#[cfg(feature = "gpu")]
TensorStorage::Gpu(_) => {
let cpu_tensor = x.to_cpu()?;
skewness(&cpu_tensor, axis, bias)
}
}
}
pub fn kurtosis<T>(x: &Tensor<T>, axis: Option<i32>, bias: bool, fisher: bool) -> Result<Tensor<T>>
where
T: Float
+ Default
+ Send
+ Sync
+ 'static
+ PartialOrd
+ ToPrimitive
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
match &x.storage {
TensorStorage::Cpu(arr) => {
if axis.is_some() {
return kurtosis(&x.flatten()?, None, bias, fisher);
}
let flat_data: Vec<T> = arr.iter().cloned().collect();
let n = flat_data.len();
if n < 4 {
return Err(TensorError::invalid_operation_simple(
"Kurtosis requires at least 4 data points".to_string(),
));
}
let mean =
flat_data.iter().cloned().fold(T::zero(), |acc, x| acc + x) / float_const!(n, T);
let mut m2 = T::zero();
let mut m4 = T::zero();
for &value in &flat_data {
let diff = value - mean;
let diff_sq = diff * diff;
let diff_fourth = diff_sq * diff_sq;
m2 = m2 + diff_sq;
m4 = m4 + diff_fourth;
}
let divisor = if bias {
float_const!(n, T)
} else {
float_const!(n - 1, T)
};
m2 = m2 / divisor;
m4 = m4 / divisor;
if m2 == T::zero() {
return Ok(Tensor::from_scalar(T::zero()));
}
let kurt = m4 / (m2 * m2);
let result = if fisher {
kurt - float_const!(3.0, T)
} else {
kurt
};
Ok(Tensor::from_scalar(result))
}
#[cfg(feature = "gpu")]
TensorStorage::Gpu(_) => {
let cpu_tensor = x.to_cpu()?;
kurtosis(&cpu_tensor, axis, bias, fisher)
}
}
}
pub fn moment<T>(x: &Tensor<T>, order: usize, axis: Option<i32>, bias: bool) -> Result<Tensor<T>>
where
T: Float
+ Default
+ Send
+ Sync
+ 'static
+ PartialOrd
+ ToPrimitive
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
match &x.storage {
TensorStorage::Cpu(arr) => {
if axis.is_some() {
return moment(&x.flatten()?, order, None, bias);
}
let flat_data: Vec<T> = arr.iter().cloned().collect();
let n = flat_data.len();
if n == 0 {
return Err(TensorError::invalid_operation_simple(
"Cannot compute moment of empty tensor".to_string(),
));
}
let mean =
flat_data.iter().cloned().fold(T::zero(), |acc, x| acc + x) / float_const!(n, T);
let mut moment_sum = T::zero();
for &value in &flat_data {
let diff = value - mean;
let mut powered_diff = T::one();
for _ in 0..order {
powered_diff = powered_diff * diff;
}
moment_sum = moment_sum + powered_diff;
}
let divisor = if bias {
float_const!(n, T)
} else {
float_const!(n - 1, T)
};
let moment_val = moment_sum / divisor;
Ok(Tensor::from_scalar(moment_val))
}
#[cfg(feature = "gpu")]
TensorStorage::Gpu(_) => {
let cpu_tensor = x.to_cpu()?;
moment(&cpu_tensor, order, axis, bias)
}
}
}
#[cfg(test)]
mod tests {
use super::super::distribution::{correlation, covariance, median, quantile};
use super::super::histogram::histogram;
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_histogram_basic() {
let x = Tensor::<f64>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5])
.expect("test tensor creation should succeed");
let (counts, edges) =
histogram(&x, 5, Some((0.0, 6.0))).expect("test: operation should succeed");
assert_eq!(counts.shape().dims(), &[5]);
assert_eq!(edges.shape().dims(), &[6]);
let counts_vals = counts.as_slice().expect("tensor should be contiguous");
let edges_vals = edges.as_slice().expect("tensor should be contiguous");
assert_eq!(counts_vals, &[1, 1, 1, 1, 1]);
assert_relative_eq!(edges_vals[0], 0.0, epsilon = 1e-10);
assert_relative_eq!(edges_vals[5], 6.0, epsilon = 1e-10);
}
#[test]
fn test_quantile_basic() {
let x = Tensor::<f64>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5])
.expect("test tensor creation should succeed");
let q = vec![0.0, 0.25, 0.5, 0.75, 1.0];
let result = quantile(&x, &q, None).expect("test: quantile should succeed");
assert_eq!(result.shape().dims(), &[5]);
let vals = result.as_slice().expect("tensor should be contiguous");
assert_relative_eq!(vals[0], 1.0, epsilon = 1e-10); assert_relative_eq!(vals[2], 3.0, epsilon = 1e-10); assert_relative_eq!(vals[4], 5.0, epsilon = 1e-10); }
#[test]
fn test_median() {
let x = Tensor::<f64>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5])
.expect("test tensor creation should succeed");
let result = median(&x, None).expect("test: median should succeed");
assert_eq!(result.shape().dims(), &[1]);
assert_relative_eq!(
result.as_slice().expect("tensor should be contiguous")[0],
3.0,
epsilon = 1e-10
);
}
#[test]
fn test_covariance_basic() {
let x = Tensor::<f64>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2])
.expect("test tensor creation should succeed");
let result = covariance(&x, false).expect("test: covariance should succeed");
assert_eq!(result.shape().dims(), &[2, 2]);
let vals = result.as_slice().expect("tensor should be contiguous");
assert!(vals[0] > 0.0); assert!(vals[3] > 0.0); assert_relative_eq!(vals[1], vals[2], epsilon = 1e-10); }
#[test]
fn test_correlation_basic() {
let x = Tensor::<f64>::from_vec(vec![1.0, 2.0, 2.0, 4.0, 3.0, 6.0], &[3, 2])
.expect("test tensor creation should succeed");
let result = correlation(&x).expect("test: correlation should succeed");
assert_eq!(result.shape().dims(), &[2, 2]);
let vals = result.as_slice().expect("tensor should be contiguous");
assert_relative_eq!(vals[0], 1.0, epsilon = 1e-10);
assert_relative_eq!(vals[3], 1.0, epsilon = 1e-10);
assert_relative_eq!(vals[1], 1.0, epsilon = 1e-10);
assert_relative_eq!(vals[2], 1.0, epsilon = 1e-10);
}
#[test]
fn test_percentile() {
let x = Tensor::<f64>::from_vec(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0],
&[10],
)
.expect("test: operation should succeed");
let percentiles = vec![0.0, 25.0, 50.0, 75.0, 100.0];
let result = percentile(&x, &percentiles, None).expect("test: percentile should succeed");
assert_eq!(result.shape().dims(), &[5]);
let vals = result.as_slice().expect("tensor should be contiguous");
assert_relative_eq!(vals[0], 1.0, epsilon = 1e-6); assert_relative_eq!(vals[2], 5.5, epsilon = 1e-6); assert_relative_eq!(vals[4], 10.0, epsilon = 1e-6); }
#[test]
fn test_range() {
let x = Tensor::<f32>::from_vec(vec![1.0, 5.0, 2.0, 8.0, 3.0], &[5])
.expect("test tensor creation should succeed");
let result = range(&x, None).expect("test: range should succeed");
assert_eq!(result.shape().dims(), &[] as &[usize]);
let val = result.as_slice().expect("tensor should be contiguous")[0];
assert_relative_eq!(val, 7.0, epsilon = 1e-6); }
#[test]
fn test_skewness() {
let symmetric_data = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
let x = Tensor::<f64>::from_vec(symmetric_data, &[5])
.expect("test tensor creation should succeed");
let result = skewness(&x, None, false).expect("test: skewness should succeed");
let val = result.as_slice().expect("tensor should be contiguous")[0];
assert_relative_eq!(val, 0.0, epsilon = 1e-6);
let skewed_data = vec![1.0, 2.0, 3.0, 4.0, 10.0];
let x_skewed = Tensor::<f64>::from_vec(skewed_data, &[5])
.expect("test tensor creation should succeed");
let result_skewed =
skewness(&x_skewed, None, false).expect("test: skewness should succeed");
let val_skewed = result_skewed
.as_slice()
.expect("tensor should be contiguous")[0];
assert!(val_skewed > 0.0); }
#[test]
fn test_kurtosis() {
let normal_data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = Tensor::<f64>::from_vec(normal_data, &[5])
.expect("test tensor creation should succeed");
let result_fisher = kurtosis(&x, None, false, true).expect("test: kurtosis should succeed");
let val_fisher = result_fisher
.as_slice()
.expect("tensor should be contiguous")[0];
let result_pearson =
kurtosis(&x, None, false, false).expect("test: kurtosis should succeed");
let val_pearson = result_pearson
.as_slice()
.expect("tensor should be contiguous")[0];
assert_relative_eq!(val_fisher, val_pearson - 3.0, epsilon = 1e-10);
}
#[test]
fn test_moment() {
let x = Tensor::<f64>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5])
.expect("test tensor creation should succeed");
let m1 = moment(&x, 1, None, false).expect("test: moment should succeed");
let val1 = m1.as_slice().expect("tensor should be contiguous")[0];
assert_relative_eq!(val1, 0.0, epsilon = 1e-10);
let m2 = moment(&x, 2, None, false).expect("test: moment should succeed");
let val2 = m2.as_slice().expect("tensor should be contiguous")[0];
let mean = 3.0; let var_expected = ((1.0 - mean).powi(2)
+ (2.0 - mean).powi(2)
+ (3.0 - mean).powi(2)
+ (4.0 - mean).powi(2)
+ (5.0 - mean).powi(2))
/ 4.0; assert_relative_eq!(val2, var_expected, epsilon = 1e-10);
}
#[test]
fn test_edge_cases() {
let empty = Tensor::<f64>::zeros(&[0]);
assert!(moment(&empty, 2, None, false).is_err());
let too_few = Tensor::<f64>::from_vec(vec![1.0, 2.0], &[2])
.expect("test tensor creation should succeed");
assert!(skewness(&too_few, None, false).is_err());
let too_few_kurt = Tensor::<f64>::from_vec(vec![1.0, 2.0, 3.0], &[3])
.expect("test tensor creation should succeed");
assert!(kurtosis(&too_few_kurt, None, false, true).is_err());
}
}