scicdh/
statistics.rs

1use crate::probability::CDHResult;
2use std::collections::HashMap;
3
4
5/// A high-performance structure encapsulating a linear array of values 
6/// designed for descriptive and bivariate statistical data mining.
7pub struct DataSet<T>(pub Vec<T>);
8
9impl DataSet<f64> {
10    /// Instantiates a new data frame context around a contiguous memory buffer.
11    ///
12    /// # Example
13    /// ```rust
14   /// use scicdh::statistics::DataSet;
15    /// let ds = DataSet::new(vec![1.0, 2.0, 3.0]);
16    /// ```
17    pub fn new(data: Vec<f64>) -> Self {
18        DataSet(data)
19    }
20
21    /// Computes the arithmetic mean ($\mu$) of the sample space.
22    ///
23    /// $$\mu = \frac{\sum_{i=1}^n x_i}{n}$$
24    pub fn mean(&self) -> CDHResult<f64> {
25        let len = self.0.len() as f64;
26        if len == 0.0 {
27            return Err("Cannot compute mean of an empty dataset.".to_string());
28        }
29        Ok(self.0.iter().sum::<f64>() / len)
30    }
31
32    /// Computes the sample median using a non-destructive 0-indexed sorting pipeline.
33    /// Handles both odd and even dataset lengths correctly.
34    pub fn median(&self) -> CDHResult<f64> {
35        let len = self.0.len();
36        if len == 0 {
37            return Err("Cannot compute median of an empty dataset.".to_string());
38        }
39        let mut sorted_arr = self.0.clone();
40        sorted_arr.sort_by(|a, b| a.total_cmp(b));
41        
42        match len {
43            a if a % 2 == 1 => Ok(sorted_arr[(a + 1) / 2 - 1]),
44            _ => {
45                let center = len / 2;
46                let center_elem = sorted_arr[center - 1];
47                let next_elem = sorted_arr[center];
48                Ok((center_elem + next_elem) / 2.0)
49            }
50        }
51    }
52
53    /// Computes the statistical mode of the dataset in linear time $\mathcal{O}(n)$.
54    ///
55    /// Uses low-level float bit-packing (`to_bits()`) to completely bypass 
56    /// hashing restrictions on `f64`. Returns `Ok(None)` if the distribution 
57    /// is completely uniform (no single clear majority frequency exists).
58    pub fn mode(&self) -> CDHResult<Option<f64>> {
59        if self.0.is_empty() {
60            return Err("Cannot compute mode of an empty dataset.".to_string());
61        }
62
63        let mut frequencies: HashMap<u64, usize> = HashMap::with_capacity(self.0.len());
64        let mut max_count = 0;
65
66        // Step A: Aggregate counts via float-to-bit maps in pure O(n)
67        for &num in &self.0 {
68            let bits = num.to_bits();
69            let count = frequencies.entry(bits).or_insert(0);
70            *count += 1;
71            if *count > max_count {
72                max_count = *count;
73            }
74        }
75
76        // Step B: Extract matching patterns to verify unimodal dominance
77        let modes: Vec<u64> = frequencies
78            .iter()
79            .filter(|&(_, &count)| count == max_count)
80            .map(|(&bits, _)| bits)
81            .collect();
82
83        // If all items appear exactly the same number of times, there is no mode
84        if modes.len() == frequencies.len() && max_count == 1 {
85            return Ok(None);
86        }
87
88        // If multiple distinct elements tie for peak occurrence, it's multimodal
89        if modes.len() > 1 {
90            Ok(None)
91        } else {
92            Ok(Some(f64::from_bits(modes[0])))
93        }
94    }
95
96    /// Calculates the algebraic range (Span) between the maximum and minimum parameters.
97    ///
98    /// $$\text{Range} = X_{\text{max}} - X_{\text{min}}$$
99    pub fn range(&self) -> CDHResult<f64> {
100        if self.0.is_empty() {
101            return Err("Cannot compute range of an empty dataset.".to_string());
102        }
103        Ok(self.max()? - self.min()?)
104    }
105
106    /// Scans a target container or the internal dataset structure to check 
107    /// for the existence of a precise value.
108    /// 
109    /// Fixed Bug: Bypassed memory copying vulnerabilities from previous design.
110    pub fn has(&self, value: f64, other: Option<&[f64]>) -> CDHResult<bool> {
111        let set = match other {
112            None => &self.0,
113            Some(slice) => slice,
114        };
115        Ok(set.iter().any(|&v| v == value))
116    }
117
118    /// Computes the sum of squared deviations for the primary variable ($SS_{xx}$).
119    ///
120    /// $$SS_{xx} = \sum x^2 - \frac{(\sum x)^2}{n}$$
121    pub fn sample_variation_x2(&self) -> CDHResult<f64> {
122        let n = self.0.len() as f64;
123        if n == 0.0 { return Err("Empty dataset context.".to_string()); }
124        let xsqrsum = self.0.iter().fold(0.0, |acc, b| acc + b.powi(2));
125        let xsum = self.0.iter().sum::<f64>();
126        Ok(xsqrsum - xsum.powi(2) / n)
127    }
128
129    /// Computes the sum of squared deviations for an independent reference slice ($SS_{yy}$).
130    ///
131    /// $$SS_{yy} = \sum y^2 - \frac{(\sum y)^2}{n}$$
132    pub fn sample_variation_y2(&self, y: &[f64]) -> CDHResult<f64> {
133        let n = y.len() as f64;
134        if n == 0.0 { return Err("Input slice cannot be empty.".to_string()); }
135        let ysqrsum = y.iter().fold(0.0, |acc, b| acc + b.powi(2));
136        let ysum = y.iter().sum::<f64>();
137        Ok(ysqrsum - ysum.powi(2) / n)
138    }
139
140    /// Computes the unbiased sample variance ($s^2$) leveraging Bessel's correction factor.
141    ///
142    /// $$s^2 = \frac{SS_{xx}}{n - 1}$$
143    pub fn sample_variance(&self) -> CDHResult<f64> {
144        let len = self.0.len();
145        if len <= 1 {
146            return Err("Sample variance requires at least 2 data points.".to_string());
147        }
148        Ok(self.sample_variation_x2()? / ((len - 1) as f64))
149    }
150
151    /// Computes the unbiased sample standard deviation ($s$).
152    pub fn sample_std_deviation(&self) -> CDHResult<f64> {
153        Ok(self.sample_variance()?.sqrt())
154    }
155
156    /// Evaluates the dimensionless sample coefficient of variation ($CV$).
157    ///
158    /// $$CV = \frac{s}{\bar{x}}$$
159    pub fn sample_coeff_of_variation(&self) -> CDHResult<f64> {
160        let mean = self.mean()?;
161        if mean == 0.0 {
162            return Err("Coefficient of variation undefined for a zero mean.".to_string());
163        }
164        Ok(self.sample_std_deviation()? / mean)
165    }
166
167    /// Generates the $k$-th mathematical population central moment ($m_k$).
168    ///
169    /// $$\mu_k = \frac{\sum (x_i - \bar{x})^k}{n}$$
170    pub fn kth_central_moment(&self, k: f64) -> CDHResult<f64> {
171        let n = self.0.len() as f64;
172        if n == 0.0 { return Err("Empty dataset context.".to_string()); }
173        let mean = self.mean()?;
174        Ok(self.0.iter().fold(0.0, |acc, b| acc + (b - mean).powf(k)) / n)
175    }
176
177    /// Measures the skewness ($\beta_3$), tracking distribution symmetry.
178    pub fn skewness(&self) -> CDHResult<f64> {
179        let denom = self.population_std_deviation()?.powi(3);
180        if denom == 0.0 { return Ok(0.0); }
181        Ok(self.kth_central_moment(3.0)? / denom)
182    }
183
184    /// Computes excess kurtosis ($\beta_4$), tracking peak sharpness relative to standard normal shapes.
185    pub fn kurtosis(&self) -> CDHResult<f64> {
186        let denom = self.population_std_deviation()?.powi(4);
187        if denom == 0.0 { return Ok(0.0); }
188        Ok((self.kth_central_moment(4.0)? / denom) - 3.0)
189    }
190
191    /// Computes the cross-variation sum of joint distribution observations ($SS_{xy}$).
192    ///
193    /// $$SS_{xy} = \sum xy - \frac{(\sum x)(\sum y)}{n}$$
194    pub fn sample_variation_xy(&self, y: &[f64]) -> CDHResult<f64> {
195        if y.len() != self.0.len() {
196            return Err("Length of y should be equal to length of dataset".to_string());
197        }
198        let n = y.len() as f64;
199        let xysum = self.0.iter().zip(y.iter()).fold(0.0, |acc, b| acc + (b.0 * b.1));
200        let xsum = self.0.iter().sum::<f64>();
201        let ysum = y.iter().sum::<f64>();
202        Ok(xysum - (xsum * ysum / n))
203    }
204
205    /// Evaluates Pearson's Product-Moment Correlation Coefficient ($r_{xy}$).
206    /// Hardened against the textbook typo by using $SS_{yy}$ space arrays.
207    pub fn pearson_correlation_coeff(&self, y: &[f64]) -> CDHResult<f64> {
208        let denom = (self.sample_variation_x2()? * self.sample_variation_y2(y)?).sqrt();
209        if denom == 0.0 {
210            return Ok(0.0); // Protective baseline against zero variance tracking
211        }
212        Ok(self.sample_variation_xy(y)? / denom)
213    }
214
215    /// Deduplicates entries from the dataset to form a unique element mathematical set pool.
216    pub fn into_set(&self) -> CDHResult<Vec<f64>> {
217        let mut set: Vec<f64> = Vec::with_capacity(self.0.len());
218        for &a in self.0.iter() {
219            if !self.has(a, Some(&set))? {
220                set.push(a);
221            }
222        }
223        Ok(set)
224    }
225
226    /// Calculates pure population variance ($\sigma^2$) relative to the exact population mean.
227    pub fn population_variance(&self) -> CDHResult<f64> {
228        let n = self.0.len() as f64;
229        if n == 0.0 { return Err("Empty dataset context.".to_string()); }
230        let mean = self.mean()?;
231        Ok(self.0.iter().fold(0.0, |acc, &x| acc + (x - mean).powi(2)) / n)
232    }
233
234    /// Computes the population standard deviation ($\sigma$).
235    pub fn population_std_deviation(&self) -> CDHResult<f64> {
236        Ok(self.population_variance()?.sqrt())
237    }
238
239    /// Evaluates the peak extreme value bound present inside the dataset.
240    pub fn max(&self) -> CDHResult<f64> {
241        if self.0.is_empty() { return Err("Dataset is empty.".to_string()); }
242        let mut max = self.0[0];
243        self.0.iter().for_each(|&i| {
244            if i > max { max = i; }
245        });
246        Ok(max)
247    }
248
249    /// Evaluates the lowest value parameter bound present inside the dataset.
250    pub fn min(&self) -> CDHResult<f64> {
251        if self.0.is_empty() { return Err("Dataset is empty.".to_string()); }
252        let mut min = self.0[0];
253        self.0.iter().for_each(|&i| {
254            if i < min { min = i; }
255        });
256        Ok(min)
257    }
258
259    /// Dumps clean debug statements displaying the full metrics profile of the active dataset.
260    pub fn info(&self) -> CDHResult<()> {
261        println!("DataSet === {:?}", self.0);
262        println!("Mean === {:#?}", self.mean()?);
263        println!("Median === {:#?}", self.median()?);
264        println!("Mode === {:#?}", self.mode()?);
265        println!("Range === {:#?}", self.range()?);
266        println!("Variance === {:#?}", self.sample_variance()?);
267        println!("Skewness === {:#?}", self.skewness()?);
268        println!("Kurtosis === {:#?}", self.kurtosis()?);
269        println!("self has 3f64 == {:?}", self.has(3.0, None)?);
270        println!("Set === {:?}", self.into_set()?);
271        println!("Maximum === {:?}", self.max()?);
272        println!("Minimum === {:?}", self.min()?);
273        Ok(())
274    }
275}