use super::chi_squared_test::chi_squared_stat;
use crate::error::{Error, Result};
use crate::resampling::{bootstrap_indices, percentile_ci};
use crate::rng::SplitMix64;
use crate::tests_stat::parametric::floor_to_i64;
pub fn cramers_v(table: &[Vec<f64>]) -> Result<f64> {
let (statistic, _, rows, cols) = chi_squared_stat(table)?;
let total: f64 = table.iter().flat_map(|row| row.iter()).sum();
let min_dim = min_dim_minus_one(rows, cols);
if total <= 0.0 || min_dim <= 0.0 {
return Ok(0.0);
}
Ok((statistic / (total * min_dim)).sqrt())
}
fn min_dim_minus_one(rows: usize, cols: usize) -> f64 {
let m = rows.min(cols).saturating_sub(1);
let lo = u32::try_from(m).unwrap_or(u32::MAX);
f64::from(lo)
}
pub fn cramers_v_bootstrap_ci(
table: &[Vec<f64>],
b: usize,
alpha: f64,
rng: &mut SplitMix64,
) -> Result<(f64, f64)> {
let (_, _, rows, cols) = chi_squared_stat(table)?;
let observations = flatten_to_cells(table, cols);
if observations.is_empty() {
return Err(Error::EmptyInput);
}
let draws = bootstrap_indices(observations.len(), b, rng);
let mut values = Vec::with_capacity(b);
for indices in &draws {
let resampled = rebuild_table(&observations, indices, rows, cols);
values.push(cramers_v(&resampled).unwrap_or(0.0));
}
percentile_ci(&values, alpha)
}
fn flatten_to_cells(table: &[Vec<f64>], cols: usize) -> Vec<usize> {
let mut obs = Vec::new();
for (i, row) in table.iter().enumerate() {
for (j, &count) in row.iter().enumerate() {
let reps = floor_to_i64(count.max(0.0).round());
for _ in 0..reps {
obs.push(i * cols + j);
}
}
}
obs
}
fn rebuild_table(
observations: &[usize],
indices: &[usize],
rows: usize,
cols: usize,
) -> Vec<Vec<f64>> {
let mut table = vec![vec![0.0; cols]; rows];
for &idx in indices {
if let Some(&cell) = observations.get(idx) {
let r = cell / cols;
let c = cell % cols;
if let Some(slot) = table.get_mut(r).and_then(|row| row.get_mut(c)) {
*slot += 1.0;
}
}
}
table
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn perfect_association_is_one() -> Result<()> {
let table = vec![vec![10.0, 0.0], vec![0.0, 10.0]];
let v = cramers_v(&table)?;
assert!((v - 1.0).abs() < 1e-12, "V was {v}");
Ok(())
}
}