use ndarray::Array2;
use solow_distributions::{norm_isf, norm_sf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FleissMethod {
Fleiss,
Randolph,
}
pub fn aggregate_raters(data: &Array2<f64>) -> (Array2<f64>, Vec<f64>) {
let mut cats: Vec<f64> = data.iter().copied().collect();
cats.sort_by(|a, b| a.total_cmp(b));
cats.dedup();
let n_cat = cats.len();
let n_rows = data.nrows();
let cat_index = |v: f64| -> usize {
cats.iter()
.position(|&c| c == v)
.expect("category present in label set")
};
let mut tt = Array2::<f64>::zeros((n_rows, n_cat));
for (i, row) in data.rows().into_iter().enumerate() {
for &v in row {
tt[[i, cat_index(v)]] += 1.0;
}
}
(tt, cats)
}
pub fn fleiss_kappa(table: &Array2<f64>, method: FleissMethod) -> f64 {
let n_cat = table.ncols() as f64;
let n_total: f64 = table.sum();
let n_rat = table
.rows()
.into_iter()
.map(|r| r.sum())
.fold(f64::NEG_INFINITY, f64::max);
let p_cat: Vec<f64> = (0..table.ncols())
.map(|j| table.column(j).sum() / n_total)
.collect();
let mut p_sum = 0.0;
for row in table.rows() {
let sq: f64 = row.iter().map(|&v| v * v).sum();
p_sum += (sq - n_rat) / (n_rat * (n_rat - 1.0));
}
let p_mean = p_sum / table.nrows() as f64;
let p_mean_exp = match method {
FleissMethod::Fleiss => p_cat.iter().map(|&p| p * p).sum::<f64>(),
FleissMethod::Randolph => 1.0 / n_cat,
};
(p_mean - p_mean_exp) / (1.0 - p_mean_exp)
}
#[derive(Debug, Clone)]
pub struct KappaResults {
pub kappa: f64,
pub kappa_max: f64,
pub var_kappa: f64,
pub var_kappa0: f64,
pub std_kappa: f64,
pub std_kappa0: f64,
pub z_value: f64,
pub pvalue_one_sided: f64,
pub pvalue_two_sided: f64,
pub kappa_low: f64,
pub kappa_upp: f64,
}
pub fn cohens_kappa(table: &Array2<f64>, alpha: f64) -> KappaResults {
let n = table.nrows();
let nobs: f64 = table.sum();
let agree: f64 = (0..n).map(|i| table[[i, i]]).sum();
let freq_row: Vec<f64> = (0..n).map(|i| table.row(i).sum() / nobs).collect();
let freq_col: Vec<f64> = (0..n).map(|j| table.column(j).sum() / nobs).collect();
let agree_exp: f64 = (0..n).map(|i| freq_col[i] * freq_row[i]).sum();
let kappa = (agree / nobs - agree_exp) / (1.0 - agree_exp);
let probs_diag: Vec<f64> = (0..n).map(|i| table[[i, i]] / nobs).collect();
let mut term_a = 0.0;
for i in 0..n {
let inner = 1.0 - (freq_row[i] + freq_col[i]) * (1.0 - kappa);
term_a += probs_diag[i] * inner * inner;
}
let mut term_b = 0.0;
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
let inner = freq_col[i] + freq_row[j];
term_b += (table[[i, j]] / nobs) * inner * inner;
}
}
term_b *= (1.0 - kappa) * (1.0 - kappa);
let term_c = (kappa - agree_exp * (1.0 - kappa)).powi(2);
let var_kappa = (term_a + term_b - term_c) / ((1.0 - agree_exp).powi(2) * nobs);
let term_c0: f64 = (0..n)
.map(|i| freq_col[i] * freq_row[i] * (freq_col[i] + freq_row[i]))
.sum();
let var_kappa0 =
(agree_exp + agree_exp * agree_exp - term_c0) / ((1.0 - agree_exp).powi(2) * nobs);
let kappa_max =
((0..n).map(|i| freq_row[i].min(freq_col[i])).sum::<f64>() - agree_exp) / (1.0 - agree_exp);
let std_kappa = var_kappa.sqrt();
let std_kappa0 = var_kappa0.sqrt();
let z_value = kappa / std_kappa0;
let pvalue_one_sided = norm_sf(z_value);
let pvalue_two_sided = norm_sf(z_value.abs()) * 2.0;
let delta = norm_isf(alpha) * std_kappa;
KappaResults {
kappa,
kappa_max,
var_kappa,
var_kappa0,
std_kappa,
std_kappa0,
z_value,
pvalue_one_sided,
pvalue_two_sided,
kappa_low: kappa - delta,
kappa_upp: kappa + delta,
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn perfect_agreement_kappa_one() {
let t = array![[10.0, 0.0], [0.0, 15.0]];
let r = cohens_kappa(&t, 0.025);
assert!((r.kappa - 1.0).abs() < 1e-12);
}
#[test]
fn aggregate_then_fleiss_runs() {
let data = array![
[0.0, 0.0, 0.0, 1.0],
[1.0, 1.0, 2.0, 2.0],
[0.0, 1.0, 2.0, 0.0]
];
let (tt, cats) = aggregate_raters(&data);
assert_eq!(cats, vec![0.0, 1.0, 2.0]);
assert_eq!(tt.dim(), (3, 3));
let k = fleiss_kappa(&tt, FleissMethod::Fleiss);
assert!(k.is_finite());
}
}