Skip to main content

torsh_tensor/
stats.rs

1//! Comprehensive tensor statistical operations
2//!
3//! This module provides a wide range of statistical functions for tensors,
4//! including descriptive statistics, percentiles, histograms, correlations,
5//! and probability distributions.
6
7use crate::{FloatElement, Tensor, TensorElement};
8use torsh_core::error::{Result, TorshError};
9
10/// Statistical computation modes
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum StatMode {
13    /// Population statistics (divide by N)
14    Population,
15    /// Sample statistics (divide by N-1)
16    Sample,
17}
18
19/// Histogram configuration
20#[derive(Debug, Clone)]
21pub struct HistogramConfig {
22    /// Number of bins
23    pub bins: usize,
24    /// Minimum value (auto-computed if None)
25    pub min_val: Option<f64>,
26    /// Maximum value (auto-computed if None)
27    pub max_val: Option<f64>,
28    /// Include values outside range in first/last bins
29    pub include_outliers: bool,
30}
31
32impl Default for HistogramConfig {
33    fn default() -> Self {
34        Self {
35            bins: 50,
36            min_val: None,
37            max_val: None,
38            include_outliers: true,
39        }
40    }
41}
42
43/// Histogram result
44#[derive(Debug, Clone)]
45pub struct Histogram {
46    /// Bin counts
47    pub counts: Vec<usize>,
48    /// Bin edges (length = counts.len() + 1)
49    pub edges: Vec<f64>,
50    /// Total number of values
51    pub total_count: usize,
52}
53
54/// Correlation methods
55#[derive(Debug, Clone, Copy, PartialEq)]
56pub enum CorrelationMethod {
57    /// Pearson correlation coefficient
58    Pearson,
59    /// Spearman rank correlation
60    Spearman,
61    /// Kendall tau correlation
62    Kendall,
63}
64
65/// Statistical summary
66#[derive(Debug, Clone)]
67pub struct StatSummary {
68    pub count: usize,
69    pub mean: f64,
70    pub std: f64,
71    pub min: f64,
72    pub max: f64,
73    pub q25: f64, // 25th percentile
74    pub q50: f64, // 50th percentile (median)
75    pub q75: f64, // 75th percentile
76}
77
78/// Statistical operations for tensors
79impl<
80        T: TensorElement
81            + FloatElement
82            + Copy
83            + Default
84            + std::ops::Add<Output = T>
85            + std::ops::AddAssign
86            + std::ops::Sub<Output = T>
87            + std::ops::Mul<Output = T>
88            + std::ops::MulAssign
89            + std::ops::Div<Output = T>
90            + PartialOrd
91            + num_traits::FromPrimitive
92            + std::iter::Sum,
93    > Tensor<T>
94{
95    /// Compute mean along specified dimensions (legacy stats implementation)
96    pub fn mean_stats(&self, dims: Option<&[usize]>, keepdim: bool) -> Result<Self> {
97        let sum = if let Some(dims) = dims {
98            self.sum_dim(&dims.iter().map(|&d| d as i32).collect::<Vec<_>>(), keepdim)?
99        } else {
100            self.sum()?
101        };
102        let count = if let Some(dims) = dims {
103            dims.iter()
104                .map(|&d| self.shape().dims()[d])
105                .product::<usize>() as f64
106        } else {
107            self.numel() as f64
108        };
109
110        sum.div_scalar(
111            <T as num_traits::FromPrimitive>::from_f64(count)
112                .unwrap_or_else(|| <T as num_traits::One>::one()),
113        )
114    }
115
116    /// Compute variance along specified dimensions
117    pub fn var(&self, dims: Option<&[usize]>, keepdim: bool, mode: StatMode) -> Result<Self> {
118        let mean = self.mean(dims, false)?; // Always get scalar mean for broadcasting
119        let mean_value = mean.item()?; // Extract scalar value
120        let diff = self.sub_scalar(mean_value)?;
121        let squared_diff = diff.mul_op(&diff)?;
122        let sum_sq = if let Some(dims) = dims {
123            squared_diff.sum_dim(&dims.iter().map(|&d| d as i32).collect::<Vec<_>>(), keepdim)?
124        } else {
125            squared_diff.sum()?
126        };
127
128        let count = if let Some(dims) = dims {
129            dims.iter()
130                .map(|&d| self.shape().dims()[d])
131                .product::<usize>()
132        } else {
133            self.numel()
134        };
135
136        let divisor = match mode {
137            StatMode::Population => count,
138            StatMode::Sample => count - 1,
139        };
140
141        if divisor == 0 {
142            return Err(TorshError::InvalidArgument(
143                "Cannot compute variance with zero degrees of freedom".to_string(),
144            ));
145        }
146
147        sum_sq.div_scalar(
148            <T as num_traits::FromPrimitive>::from_usize(divisor)
149                .unwrap_or_else(|| <T as num_traits::One>::one()),
150        )
151    }
152
153    /// Compute standard deviation along specified dimensions
154    pub fn std(&self, dims: Option<&[usize]>, keepdim: bool, mode: StatMode) -> Result<Self> {
155        let variance = self.var(dims, keepdim, mode)?;
156        variance.sqrt()
157    }
158
159    /// Compute percentile along the last dimension
160    pub fn percentile(&self, q: f64, dim: Option<usize>, _keepdim: bool) -> Result<Self> {
161        if !(0.0..=100.0).contains(&q) {
162            return Err(TorshError::InvalidArgument(format!(
163                "Percentile must be between 0 and 100, got {q}"
164            )));
165        }
166
167        let dim = dim.unwrap_or(self.shape().ndim() - 1);
168        if dim >= self.shape().ndim() {
169            return Err(TorshError::dimension_error(
170                &format!(
171                    "Dimension {} out of bounds for tensor with {} dimensions",
172                    dim,
173                    self.shape().ndim()
174                ),
175                "tensor statistics operation",
176            ));
177        }
178
179        // For now, implement a simple linear interpolation method
180        // In a full implementation, this would be optimized
181        let (sorted, _indices) = self.sort(Some(dim as i32), false)?; // Sort in ascending order
182        let size = self.shape().dims()[dim];
183
184        // Calculate the position in the sorted array
185        let pos = q / 100.0 * (size - 1) as f64;
186        let lower_idx = pos.floor() as usize;
187        let upper_idx = (pos.ceil() as usize).min(size - 1);
188        let weight = pos - pos.floor();
189
190        if lower_idx == upper_idx {
191            // Exact index
192            sorted.select(dim as i32, lower_idx as i64)
193        } else {
194            // Interpolate between two values
195            let lower = sorted.select(dim as i32, lower_idx as i64)?;
196            let upper = sorted.select(dim as i32, upper_idx as i64)?;
197            let diff = upper.sub(&lower)?;
198            let weight_scalar = <T as TensorElement>::from_f64(weight)
199                .unwrap_or_else(|| <T as TensorElement>::from_f64(0.0).unwrap_or_default());
200            let weighted_diff = diff.mul_scalar(weight_scalar)?;
201            lower.add_op(&weighted_diff)
202        }
203    }
204
205    /// Compute median (50th percentile)
206    pub fn median(&self, dim: Option<usize>, keepdim: bool) -> Result<Self> {
207        self.percentile(50.0, dim, keepdim)
208    }
209
210    /// Compute quantiles at specified levels
211    pub fn quantile(&self, q: &[f64], dim: Option<usize>, keepdim: bool) -> Result<Vec<Self>> {
212        let mut results = Vec::new();
213        for &quantile in q {
214            results.push(self.percentile(quantile * 100.0, dim, keepdim)?);
215        }
216        Ok(results)
217    }
218
219    /// Create histogram of tensor values
220    pub fn histogram(&self, config: &HistogramConfig) -> Result<Histogram> {
221        let data = self.to_vec()?;
222
223        if data.is_empty() {
224            return Ok(Histogram {
225                counts: vec![0; config.bins],
226                edges: (0..=config.bins).map(|i| i as f64).collect(),
227                total_count: 0,
228            });
229        }
230
231        // Compute min and max if not provided
232        let min_val = config.min_val.unwrap_or_else(|| {
233            data.iter()
234                .map(|&x| TensorElement::to_f64(&x).expect("f64 conversion should succeed"))
235                .fold(f64::INFINITY, f64::min)
236        });
237        let max_val = config.max_val.unwrap_or_else(|| {
238            data.iter()
239                .map(|&x| TensorElement::to_f64(&x).expect("f64 conversion should succeed"))
240                .fold(f64::NEG_INFINITY, f64::max)
241        });
242
243        if min_val >= max_val {
244            return Err(TorshError::InvalidArgument(
245                "Minimum value must be less than maximum value".to_string(),
246            ));
247        }
248
249        // Create bin edges
250        let bin_width = (max_val - min_val) / config.bins as f64;
251        let edges: Vec<f64> = (0..=config.bins)
252            .map(|i| min_val + i as f64 * bin_width)
253            .collect();
254
255        // Count values in each bin
256        let mut counts = vec![0; config.bins];
257        for &value in data.iter() {
258            let val = TensorElement::to_f64(&value).expect("f64 conversion should succeed");
259
260            let bin_idx = if val <= min_val {
261                if config.include_outliers {
262                    0
263                } else {
264                    continue;
265                }
266            } else if val >= max_val {
267                if config.include_outliers {
268                    config.bins - 1
269                } else {
270                    continue;
271                }
272            } else {
273                ((val - min_val) / bin_width).floor() as usize
274            };
275
276            let bin_idx = bin_idx.min(config.bins - 1);
277            counts[bin_idx] += 1;
278        }
279
280        Ok(Histogram {
281            counts,
282            edges,
283            total_count: data.len(),
284        })
285    }
286
287    /// Compute correlation coefficient with another tensor
288    pub fn correlation(&self, other: &Self, method: CorrelationMethod) -> Result<T> {
289        if self.shape() != other.shape() {
290            return Err(TorshError::ShapeMismatch {
291                expected: self.shape().dims().to_vec(),
292                got: other.shape().dims().to_vec(),
293            });
294        }
295
296        match method {
297            CorrelationMethod::Pearson => self.pearson_correlation(other),
298            CorrelationMethod::Spearman => self.spearman_correlation(other),
299            CorrelationMethod::Kendall => self.kendall_correlation(other),
300        }
301    }
302
303    /// Pearson correlation coefficient
304    fn pearson_correlation(&self, other: &Self) -> Result<T> {
305        let n = self.numel() as f64;
306        if n < 2.0 {
307            return Err(TorshError::InvalidArgument(
308                "Need at least 2 values for correlation".to_string(),
309            ));
310        }
311
312        // Compute means
313        let mean_x = self.mean(None, false)?;
314        let mean_y = other.mean(None, false)?;
315
316        let mean_x_data = mean_x.to_vec()?;
317        let mean_y_data = mean_y.to_vec()?;
318        let mean_x_val = mean_x_data[0];
319        let mean_y_val = mean_y_data[0];
320
321        // Compute deviations and products
322        let self_data = self.to_vec()?;
323        let other_data = other.to_vec()?;
324
325        let mut sum_xy = 0.0;
326        let mut sum_xx = 0.0;
327        let mut sum_yy = 0.0;
328
329        for (&x, &y) in self_data.iter().zip(other_data.iter()) {
330            let dx = TensorElement::to_f64(&x).expect("f64 conversion should succeed")
331                - TensorElement::to_f64(&mean_x_val).expect("f64 conversion should succeed");
332            let dy = TensorElement::to_f64(&y).expect("f64 conversion should succeed")
333                - TensorElement::to_f64(&mean_y_val).expect("f64 conversion should succeed");
334
335            sum_xy += dx * dy;
336            sum_xx += dx * dx;
337            sum_yy += dy * dy;
338        }
339
340        let denominator = (sum_xx * sum_yy).sqrt();
341        if denominator.abs() < f64::EPSILON {
342            return Err(TorshError::InvalidArgument(
343                "Cannot compute correlation: one variable has zero variance".to_string(),
344            ));
345        }
346
347        let correlation = sum_xy / denominator;
348        Ok(<T as TensorElement>::from_f64(correlation).expect("f64 conversion should succeed"))
349    }
350
351    /// Spearman rank correlation coefficient
352    fn spearman_correlation(&self, other: &Self) -> Result<T> {
353        // Convert to ranks and compute Pearson correlation of ranks
354        let self_ranks = self.rank()?;
355        let other_ranks = other.rank()?;
356        self_ranks.pearson_correlation(&other_ranks)
357    }
358
359    /// Kendall tau correlation coefficient
360    fn kendall_correlation(&self, other: &Self) -> Result<T> {
361        let n = self.numel();
362        if n < 2 {
363            return Err(TorshError::InvalidArgument(
364                "Need at least 2 values for Kendall correlation".to_string(),
365            ));
366        }
367
368        let self_data = self.to_vec()?;
369        let other_data = other.to_vec()?;
370
371        let mut concordant = 0;
372        let mut discordant = 0;
373        let mut tied_x = 0;
374        let mut tied_y = 0;
375        let mut tied_xy = 0;
376
377        for i in 0..n {
378            for j in i + 1..n {
379                let x1 =
380                    TensorElement::to_f64(&self_data[i]).expect("f64 conversion should succeed");
381                let x2 =
382                    TensorElement::to_f64(&self_data[j]).expect("f64 conversion should succeed");
383                let y1 =
384                    TensorElement::to_f64(&other_data[i]).expect("f64 conversion should succeed");
385                let y2 =
386                    TensorElement::to_f64(&other_data[j]).expect("f64 conversion should succeed");
387
388                let dx = x2 - x1;
389                let dy = y2 - y1;
390
391                if dx.abs() < f64::EPSILON && dy.abs() < f64::EPSILON {
392                    tied_xy += 1;
393                } else if dx.abs() < f64::EPSILON {
394                    tied_x += 1;
395                } else if dy.abs() < f64::EPSILON {
396                    tied_y += 1;
397                } else if dx * dy > 0.0 {
398                    concordant += 1;
399                } else {
400                    discordant += 1;
401                }
402            }
403        }
404
405        let total_pairs = n * (n - 1) / 2;
406        let effective_pairs = total_pairs - tied_x - tied_y - tied_xy;
407
408        if effective_pairs == 0 {
409            return Ok(<T as TensorElement>::from_f64(0.0).expect("f64 conversion should succeed"));
410        }
411
412        let tau = (concordant as f64 - discordant as f64) / effective_pairs as f64;
413        Ok(<T as TensorElement>::from_f64(tau).expect("f64 conversion should succeed"))
414    }
415
416    /// Compute ranks of tensor elements
417    fn rank(&self) -> Result<Self> {
418        let data = self.to_vec()?;
419        let n = data.len();
420
421        // Create index-value pairs and sort by value
422        let mut indexed_values: Vec<(usize, T)> =
423            data.iter().enumerate().map(|(i, &val)| (i, val)).collect();
424
425        indexed_values.sort_by(|a, b| {
426            TensorElement::to_f64(&a.1)
427                .expect("f64 conversion should succeed")
428                .partial_cmp(&TensorElement::to_f64(&b.1).expect("f64 conversion should succeed"))
429                .unwrap_or(std::cmp::Ordering::Equal)
430        });
431
432        // Assign ranks (1-based, handle ties with average rank)
433        let mut ranks = vec![T::default(); n];
434        let mut i = 0;
435
436        while i < n {
437            let mut j = i;
438            while j < n
439                && TensorElement::to_f64(&indexed_values[j].1)
440                    .expect("f64 conversion should succeed")
441                    == TensorElement::to_f64(&indexed_values[i].1)
442                        .expect("f64 conversion should succeed")
443            {
444                j += 1;
445            }
446
447            // Average rank for tied values
448            let avg_rank = (i + j + 1) as f64 / 2.0;
449            for k in i..j {
450                ranks[indexed_values[k].0] = <T as TensorElement>::from_f64(avg_rank)
451                    .expect("f64 conversion should succeed");
452            }
453            i = j;
454        }
455
456        Self::from_data(ranks, self.shape().dims().to_vec(), self.device)
457    }
458
459    /// Generate comprehensive statistical summary
460    pub fn describe(&self) -> Result<StatSummary> {
461        let data = self.to_vec()?;
462        if data.is_empty() {
463            return Err(TorshError::InvalidArgument(
464                "Cannot compute statistics on empty tensor".to_string(),
465            ));
466        }
467
468        let count = data.len();
469        let values: Vec<f64> = data
470            .iter()
471            .map(|&x| TensorElement::to_f64(&x).expect("f64 conversion should succeed"))
472            .collect();
473
474        // Basic statistics
475        let sum: f64 = values.iter().sum();
476        let mean = sum / count as f64;
477
478        let variance =
479            values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (count - 1).max(1) as f64;
480        let std = variance.sqrt();
481
482        let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
483        let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
484
485        // Percentiles
486        let mut sorted_values = values.clone();
487        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
488
489        let q25 = percentile_sorted(&sorted_values, 25.0);
490        let q50 = percentile_sorted(&sorted_values, 50.0);
491        let q75 = percentile_sorted(&sorted_values, 75.0);
492
493        Ok(StatSummary {
494            count,
495            mean,
496            std,
497            min,
498            max,
499            q25,
500            q50,
501            q75,
502        })
503    }
504
505    /// Compute covariance matrix for 2D tensor (each column is a variable)
506    pub fn cov(&self, mode: StatMode) -> Result<Self> {
507        let shape = self.shape();
508        if shape.ndim() != 2 {
509            return Err(TorshError::dimension_error(
510                "Covariance matrix requires 2D tensor",
511                "covariance computation",
512            ));
513        }
514
515        let (n_samples, n_features) = (shape.dims()[0], shape.dims()[1]);
516        if n_samples < 2 {
517            return Err(TorshError::InvalidArgument(
518                "Need at least 2 samples for covariance".to_string(),
519            ));
520        }
521
522        // Center the data by subtracting per-column means
523        // Compute mean of each column (feature) independently
524        let data = self.to_vec()?;
525        let mut centered_data = data.clone();
526        for feat in 0..n_features {
527            let col_sum = (0..n_samples).fold(<T as num_traits::Zero>::zero(), |acc, row| {
528                acc + data[row * n_features + feat]
529            });
530            let col_mean = col_sum
531                / <T as num_traits::FromPrimitive>::from_usize(n_samples)
532                    .unwrap_or_else(|| <T as num_traits::One>::one());
533            for row in 0..n_samples {
534                centered_data[row * n_features + feat] = data[row * n_features + feat] - col_mean;
535            }
536        }
537        let centered = Self::from_data(centered_data, vec![n_samples, n_features], self.device())?;
538
539        // Compute covariance matrix: (X^T * X) / (n - 1)
540        let centered_t = centered.transpose(1, 0)?;
541        let cov_unnormalized = centered_t.matmul(&centered)?;
542
543        let divisor = match mode {
544            StatMode::Population => n_samples,
545            StatMode::Sample => n_samples - 1,
546        };
547
548        cov_unnormalized.div_scalar(
549            <T as num_traits::FromPrimitive>::from_usize(divisor)
550                .unwrap_or_else(|| <T as num_traits::One>::one()),
551        )
552    }
553
554    /// Compute correlation matrix for 2D tensor
555    pub fn corrcoef(&self) -> Result<Self> {
556        let cov_matrix = self.cov(StatMode::Sample)?;
557        let cov_data = cov_matrix.to_vec()?;
558        let n_features = cov_matrix.shape().dims()[0];
559
560        // Extract diagonal elements (variances)
561        let mut std_devs = Vec::with_capacity(n_features);
562        for i in 0..n_features {
563            let variance = TensorElement::to_f64(&cov_data[i * n_features + i])
564                .expect("f64 conversion should succeed");
565            std_devs.push(variance.sqrt());
566        }
567
568        // Normalize covariance matrix to get correlation matrix
569        let mut corr_data = Vec::with_capacity(cov_data.len());
570        for i in 0..n_features {
571            for j in 0..n_features {
572                let cov_val = TensorElement::to_f64(&cov_data[i * n_features + j])
573                    .expect("f64 conversion should succeed");
574                let corr_val = if std_devs[i] > f64::EPSILON && std_devs[j] > f64::EPSILON {
575                    cov_val / (std_devs[i] * std_devs[j])
576                } else {
577                    0.0
578                };
579                corr_data.push(
580                    <T as TensorElement>::from_f64(corr_val)
581                        .expect("f64 conversion should succeed"),
582                );
583            }
584        }
585
586        Self::from_data(corr_data, vec![n_features, n_features], self.device)
587    }
588}
589
590/// Helper function to compute percentile from sorted array
591fn percentile_sorted(sorted_values: &[f64], q: f64) -> f64 {
592    if sorted_values.is_empty() {
593        return 0.0;
594    }
595
596    let pos = q / 100.0 * (sorted_values.len() - 1) as f64;
597    let lower_idx = pos.floor() as usize;
598    let upper_idx = (pos.ceil() as usize).min(sorted_values.len() - 1);
599
600    if lower_idx == upper_idx {
601        sorted_values[lower_idx]
602    } else {
603        let weight = pos - pos.floor();
604        sorted_values[lower_idx] * (1.0 - weight) + sorted_values[upper_idx] * weight
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611    use torsh_core::device::DeviceType;
612
613    #[test]
614    fn test_basic_statistics() {
615        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
616        let tensor = Tensor::from_data(data, vec![5], DeviceType::Cpu)
617            .expect("tensor creation should succeed");
618
619        let mean = tensor.mean(None, false).expect("mean should succeed");
620        assert!(
621            (mean.to_vec().expect("to_vec conversion should succeed")[0] - 3.0_f32).abs()
622                < 1e-6_f32
623        );
624
625        let var_sample = tensor
626            .var(None, false, StatMode::Sample)
627            .expect("variance should succeed");
628        assert!(
629            (var_sample
630                .to_vec()
631                .expect("to_vec conversion should succeed")[0]
632                - 2.5_f32)
633                .abs()
634                < 1e-6_f32
635        );
636
637        let std_sample = tensor
638            .std(None, false, StatMode::Sample)
639            .expect("std should succeed");
640        assert!(
641            (std_sample
642                .to_vec()
643                .expect("to_vec conversion should succeed")[0]
644                - 2.5_f32.sqrt())
645            .abs()
646                < 1e-6
647        );
648    }
649
650    #[test]
651    fn test_percentiles() {
652        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
653        let tensor = Tensor::from_data(data, vec![10], DeviceType::Cpu)
654            .expect("tensor creation should succeed");
655
656        let median = tensor.median(None, false).expect("median should succeed");
657        assert!(
658            (median.to_vec().expect("to_vec conversion should succeed")[0] - 5.5_f32).abs()
659                < 1e-6_f32
660        );
661
662        let q25 = tensor
663            .percentile(25.0, None, false)
664            .expect("percentile should succeed");
665        assert!(
666            (q25.to_vec().expect("to_vec conversion should succeed")[0] - 3.25_f32).abs()
667                < 1e-6_f32
668        );
669
670        let q75 = tensor
671            .percentile(75.0, None, false)
672            .expect("percentile should succeed");
673        assert!(
674            (q75.to_vec().expect("to_vec conversion should succeed")[0] - 7.75_f32).abs()
675                < 1e-6_f32
676        );
677    }
678
679    #[test]
680    fn test_histogram() {
681        let data = vec![1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 4.0, 5.0];
682        let tensor = Tensor::from_data(data, vec![9], DeviceType::Cpu)
683            .expect("tensor creation should succeed");
684
685        let config = HistogramConfig {
686            bins: 5,
687            min_val: Some(1.0),
688            max_val: Some(5.0),
689            include_outliers: true,
690        };
691
692        let hist = tensor.histogram(&config).expect("histogram should succeed");
693        assert_eq!(hist.total_count, 9);
694        assert_eq!(hist.counts.len(), 5);
695        assert_eq!(hist.edges.len(), 6);
696    }
697
698    #[test]
699    fn test_correlation() {
700        let x_data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
701        let y_data = vec![2.0, 4.0, 6.0, 8.0, 10.0]; // Perfect positive correlation
702
703        let x = Tensor::from_data(x_data, vec![5], DeviceType::Cpu)
704            .expect("tensor creation should succeed");
705        let y = Tensor::from_data(y_data, vec![5], DeviceType::Cpu)
706            .expect("tensor creation should succeed");
707
708        let corr = x
709            .correlation(&y, CorrelationMethod::Pearson)
710            .expect("correlation should succeed");
711        assert!((corr - 1.0_f32).abs() < 1e-6_f32); // Should be 1.0 for perfect positive correlation
712    }
713
714    #[test]
715    fn test_statistical_summary() {
716        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
717        let tensor = Tensor::from_data(data, vec![10], DeviceType::Cpu)
718            .expect("tensor creation should succeed");
719
720        let summary = tensor.describe().expect("describe should succeed");
721        assert_eq!(summary.count, 10);
722        assert!((summary.mean - 5.5).abs() < 1e-6);
723        assert!((summary.q50 - 5.5).abs() < 1e-6); // Median
724        assert_eq!(summary.min, 1.0);
725        assert_eq!(summary.max, 10.0);
726    }
727
728    #[test]
729    fn test_covariance_matrix() {
730        // Create a 2D tensor (samples x features)
731        let data = vec![1.0, 2.0, 2.0, 4.0, 3.0, 6.0, 4.0, 8.0];
732        let tensor = Tensor::from_data(data, vec![4, 2], DeviceType::Cpu)
733            .expect("tensor creation should succeed");
734
735        let cov_matrix = tensor
736            .cov(StatMode::Sample)
737            .expect("covariance should succeed");
738        assert_eq!(cov_matrix.shape().dims(), &[2, 2]);
739
740        // Check that it's symmetric (off-diagonal elements should be equal)
741        let cov_data = cov_matrix
742            .to_vec()
743            .expect("to_vec conversion should succeed");
744        // For a 2x2 matrix [[a, b], [c, d]] stored as [a, b, c, d]:
745        // Symmetry means b == c (cov_data[1] == cov_data[2])
746        assert!((cov_data[1] as f64 - cov_data[2] as f64).abs() < 1e-6);
747    }
748
749    #[test]
750    fn test_covariance_column_wise() {
751        // Two features with different column means: col0 has mean 2.0, col1 has mean 20.0
752        // Data: [[1, 10], [2, 20], [3, 30]]  =>  col means: [2, 20]
753        // Centered: [[-1,-10],[0,0],[1,10]]
754        // Cov sample (n-1=2):
755        //   cov(0,0) = ((-1)^2 + 0^2 + 1^2) / 2 = 1.0
756        //   cov(0,1) = cov(1,0) = ((-1)(-10)+0+1*10) / 2 = 20/2 = 10.0
757        //   cov(1,1) = (100+0+100) / 2 = 100.0
758        let data: Vec<f32> = vec![1.0, 10.0, 2.0, 20.0, 3.0, 30.0];
759        let tensor = Tensor::from_data(data, vec![3, 2], DeviceType::Cpu)
760            .expect("tensor creation should succeed");
761
762        let cov = tensor.cov(StatMode::Sample).expect("cov should succeed");
763        let cov_data = cov.to_vec().expect("to_vec should succeed");
764
765        // cov_data = [cov00, cov01, cov10, cov11]
766        assert!(
767            (cov_data[0] - 1.0_f32).abs() < 1e-4,
768            "cov00 = {}",
769            cov_data[0]
770        );
771        assert!(
772            (cov_data[1] - 10.0_f32).abs() < 1e-4,
773            "cov01 = {}",
774            cov_data[1]
775        );
776        assert!(
777            (cov_data[2] - 10.0_f32).abs() < 1e-4,
778            "cov10 = {}",
779            cov_data[2]
780        );
781        assert!(
782            (cov_data[3] - 100.0_f32).abs() < 1e-4,
783            "cov11 = {}",
784            cov_data[3]
785        );
786    }
787
788    #[test]
789    fn test_ranks() {
790        let data = vec![3.0, 1.0, 4.0, 2.0, 2.0]; // Contains ties
791        let tensor = Tensor::from_data(data, vec![5], DeviceType::Cpu)
792            .expect("tensor creation should succeed");
793
794        let ranks = tensor.rank().expect("rank should be available");
795        let rank_data = ranks.to_vec().expect("to_vec conversion should succeed");
796
797        // Check that ranks are in expected range
798        for &rank in rank_data.iter() {
799            assert!((1.0_f32..=5.0_f32).contains(&rank));
800        }
801    }
802}