use crate::statistical_tests::distributions::chi2_cdf;
use scirs2_core::error::CoreError;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
type CoreResult<T> = std::result::Result<T, CoreError>;
pub fn chi2(X: &ArrayView2<f64>, y: &ArrayView1<f64>) -> CoreResult<(Array1<f64>, Array1<f64>)> {
let n_features = X.ncols();
let mut chi2_stats = Array1::zeros(n_features);
let mut p_values = Array1::zeros(n_features);
for i in 0..n_features {
let feature = X.column(i);
let (chi2_stat, p_val) = chi2_single_feature(&feature, y)?;
chi2_stats[i] = chi2_stat;
p_values[i] = p_val;
}
Ok((chi2_stats, p_values))
}
fn chi2_single_feature(feature: &ArrayView1<f64>, y: &ArrayView1<f64>) -> CoreResult<(f64, f64)> {
let contingency = create_contingency_table(feature, y)?;
chi2_contingency(&contingency.view())
}
pub fn chi2_contingency(contingency: &ArrayView2<f64>) -> CoreResult<(f64, f64)> {
let (n_rows, n_cols) = contingency.dim();
let row_totals = contingency.sum_axis(scirs2_core::ndarray::Axis(1));
let col_totals = contingency.sum_axis(scirs2_core::ndarray::Axis(0));
let grand_total = row_totals.sum();
if grand_total == 0.0 {
return Ok((0.0, 1.0));
}
let mut chi2_stat = 0.0;
for i in 0..n_rows {
for j in 0..n_cols {
let expected = row_totals[i] * col_totals[j] / grand_total;
if expected > 0.0 {
let observed = contingency[[i, j]];
chi2_stat += (observed - expected).powi(2) / expected;
}
}
}
let df = ((n_rows - 1) * (n_cols - 1)) as f64;
let p_value = 1.0 - chi2_cdf(chi2_stat, df);
Ok((chi2_stat, p_value))
}
fn create_contingency_table(
feature: &ArrayView1<f64>,
y: &ArrayView1<f64>,
) -> CoreResult<Array2<f64>> {
let mut feature_values: Vec<f64> = feature.to_vec();
feature_values.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
feature_values.dedup();
let mut target_values: Vec<f64> = y.to_vec();
target_values.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
target_values.dedup();
let n_feature_vals = feature_values.len();
let n_target_vals = target_values.len();
let mut contingency = Array2::zeros((n_feature_vals, n_target_vals));
for (feat_val, target_val) in feature.iter().zip(y.iter()) {
if let (Some(i), Some(j)) = (
feature_values
.iter()
.position(|&x| (x - feat_val).abs() < 1e-10),
target_values
.iter()
.position(|&x| (x - target_val).abs() < 1e-10),
) {
contingency[[i, j]] += 1.0;
}
}
Ok(contingency)
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_chi2_contingency() {
let contingency = array![[10.0, 5.0], [3.0, 12.0]];
let (chi2_stat, p_value) =
chi2_contingency(&contingency.view()).expect("operation should succeed");
assert!(chi2_stat > 0.0);
assert!((0.0..=1.0).contains(&p_value));
}
#[test]
#[allow(non_snake_case)]
fn test_chi2_feature_selection() {
let X = array![[1.0, 0.0], [1.0, 1.0], [0.0, 0.0], [0.0, 1.0]];
let y = array![1.0, 1.0, 0.0, 0.0];
let (chi2_stats, p_values) = chi2(&X.view(), &y.view()).expect("operation should succeed");
assert_eq!(chi2_stats.len(), 2);
assert_eq!(p_values.len(), 2);
assert!(chi2_stats.iter().all(|&x| x >= 0.0));
assert!(p_values.iter().all(|&x| (0.0..=1.0).contains(&x)));
}
}