use statrs::distribution::{ChiSquared, ContinuousCDF};
use crate::utils::{Matrix, Conclusion};
fn contains_negative<Number: Into<f64> + Clone>(matrix: &[Vec<Number>]) -> bool {
matrix.iter().any(|row| row.iter().any(|num| num.clone().into() < 0.0))
}
fn has_different_rows<Number: Into<f64> + Copy>(matrix: &[Vec<Number>]) -> bool {
if let Some(first_row_len) = matrix.first().map(|row| row.len()) {
matrix.iter().any(|row| row.len() != first_row_len)
} else {
panic!("\n[HYTE-Panic] You must not pass in an empty matrix!\n")
}
}
fn compute_totals<Number: Into<f64> + Copy>(matrix: &Vec<Vec<Number>>) -> Totals<f64> {
let mut totals = Totals {
column_totals: vec![0.0; matrix.first().map_or(0, Vec::len)],
row_totals: vec![0.0; matrix.len()],
grand_total: 0.0,
};
for (i, row) in matrix.into_iter().enumerate() {
let mut row_total = 0.0;
for (j, &num) in row.into_iter().enumerate() {
let num_f64: f64 = num.into();
row_total += num_f64;
totals.column_totals[j] += num_f64;
}
totals.row_totals[i] = row_total;
totals.grand_total += row_total;
}
totals
}
#[derive(Debug)]
struct Totals<Number: Into<f64> + Copy> {
column_totals: Vec<Number>,
row_totals: Vec<Number>,
grand_total: Number
}
#[derive(Debug)]
#[derive(PartialEq)]
pub struct ChiSquareResult {
pub test_type: &'static str,
pub statistic: f64,
pub df: usize,
pub p: f64,
}
pub fn test<Number: Into<f64> + Copy>(
test_type: &str,
observed_matrix: Matrix<Number>,
gof_probabilities: Option<Vec<f64>>,
print_output: bool
) -> Option<ChiSquareResult> {
match (test_type, observed_matrix) {
("toi", Matrix::TwoDimensional(matrix)) => {
if matrix.len() == 0 { panic!("\n[HYTE-Panic] You must not pass in an empty matrix!\n"); };
if contains_negative(&matrix) {
panic!("\n[HYTE-Panic] You must not pass in a matrix with a negative number!\n");
} else if has_different_rows(&matrix) {
panic!("\n[HYTE-Panic] You must not pass in a matrix with rows of different lengths!\n");
}
toi(matrix, print_output)
},
("gof", Matrix::OneDimensional(matrix)) => {
if matrix.len() == 0 { panic!("\n[HYTE-Panic] You must not pass in an empty matrix!\n"); };
if matrix.iter().any(|&num| num.into() < 0.0) {
panic!("\n[HYTE-Panic] You must not pass in a vector with a negative number!\n");
}
if let Some(probabilities) = gof_probabilities.as_ref() {
if probabilities.iter().any(|&prob| prob < 0.0 || prob > 1.0) {
panic!("\n[HYTE-Panic] You must not pass in a vector with a negative number!\n");
};
if probabilities.len() != matrix.len() {
panic!(
"[HYTE-Panic] The lengths of your observed vector ({}) and expected probabilities vector ({}) do not match!",
matrix.len(),
probabilities.len()
);
};
} else {
panic!("[HYTE-Panic] Expected probabilities must be provided for the goodness of fit test.");
}
gof(matrix, gof_probabilities, print_output)
},
_ => panic!("\n[HYTE-Panic] Test type for a ChiSquare test must be \"toi\" or \"gof\"!\n"),
}
}
fn toi<Number: Into<f64> + Copy>(matrix: Vec<Vec<Number>>, print_output: bool) -> Option<ChiSquareResult> {
let totals = compute_totals(&matrix);
let mut statistic: f64 = 0.0;
for (i, row) in matrix.iter().enumerate() {
for (j, &num) in row.iter().enumerate() {
let num_f64: f64 = num.into(); let expected_frequency = (totals.row_totals[i] * totals.column_totals[j]) / totals.grand_total;
statistic += ((num_f64 - expected_frequency).powi(2)) / expected_frequency;
}
};
let df = (&matrix.len() - 1) * (&matrix[0].len() - 1);
let chi = ChiSquared::new(df as f64).unwrap();
let p = 1.0 - chi.cdf(statistic);
let results = ChiSquareResult {
test_type: "Pearson's Chi-squared Test of Independence",
statistic,
df,
p,
};
if print_output {println!("\n------------------ HYTE ------------------\n\n{}\n\nX^2 test statistic = {:.2}\np-value = {:.3e}\nDegrees of freedom = {}\n\n------------------ HYTE ------------------\n", results.test_type, results.statistic, results.p, results.df);}
Some(results)
}
fn gof<Number: Into<f64> + Copy>(matrix: Vec<Number>, gof_probabilities: Option<Vec<f64>>, print_output: bool) -> Option<ChiSquareResult> {
let mut total: f64 = 0.0;
for &num in matrix.iter() {
total += num.into();
};
let mut expected_frequencies: Vec<f64> = Vec::new();
let mut statistic: f64 = 0.0;
for chance in gof_probabilities.unwrap() {
expected_frequencies.push(chance * total);
};
for (i, &num) in matrix.iter().enumerate() {
let num_f64: f64 = num.into();
statistic += ((num_f64 - expected_frequencies[i]).powi(2)) / expected_frequencies[i];
};
let df = &matrix.len() - 1;
let chi = ChiSquared::new(df as f64).unwrap();
let p = 1.0 - chi.cdf(statistic);
let results = ChiSquareResult {
test_type: "Pearson's Chi-squared Goodness Of Fit",
statistic,
df,
p,
};
if print_output {println!("\n---------------- HYTE ----------------\n\n{}\n\nX^2 test statistic = {:.2}\np-value = {:.3e}\nDegrees of freedom = {}\n\n---------------- HYTE ----------------\n", results.test_type, results.statistic, results.p, results.df);}
Some(results)
}
impl ChiSquareResult {
pub fn conclude(&self, significance_level: f64, print_output: bool) -> Conclusion {
if self.p < significance_level {
if print_output {println!("\n---------------------------- HYTE ----------------------------\n\nStatistical Conclusion\n\np-value = ({:.3e})\nsignificance level = ({:.3e})\n\np-value < s. l, therefore reject H_0\n\nThere is sufficient evidence to reject the null hypothesis.\n\n---------------------------- HYTE ----------------------------\n", self.p, significance_level);};
Conclusion::Reject
} else {
if print_output {println!("\n---------------------------- HYTE ----------------------------\n\nStatistical Conclusion\n\np-value = ({:.3e})\nsignificance level = ({:.3e})\n\np-value > s. l, therefore do not reject H_0\n\nThere is insufficient evidence to reject the null hypothesis.\n\n---------------------------- HYTE ----------------------------\n", self.p, significance_level);};
Conclusion::DoNotReject
}
}
pub fn conclude_by_convention(&self, print_output: bool) -> Conclusion {
if self.p < 0.05 {
if print_output {println!("\n---------------------------- HYTE ----------------------------\n\nStatistical Conclusion\n\np-value = ({:.3e})\nsignificance level = 0.5\n\np-value < s. l, therefore reject H_0\n\nThere is sufficient evidence to reject the null hypothesis.\n\n---------------------------- HYTE ----------------------------\n", self.p);};
Conclusion::Reject
} else {
if print_output {println!("\n---------------------------- HYTE ----------------------------\n\nStatistical Conclusion\n\np-value = ({:.3e})\nsignificance level = 0.5\n\np-value > s. l, therefore do not reject H_0\n\nThere is insufficient evidence to reject the null hypothesis.\n\n---------------------------- HYTE ----------------------------\n", self.p);};
Conclusion::DoNotReject
}
}
}