solow_stats/
inter_rater.rs1use ndarray::Array2;
11use solow_distributions::{norm_isf, norm_sf};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum FleissMethod {
16 Fleiss,
18 Randolph,
21}
22
23pub fn aggregate_raters(data: &Array2<f64>) -> (Array2<f64>, Vec<f64>) {
32 let mut cats: Vec<f64> = data.iter().copied().collect();
34 cats.sort_by(|a, b| a.total_cmp(b));
35 cats.dedup();
36 let n_cat = cats.len();
37 let n_rows = data.nrows();
38
39 let cat_index = |v: f64| -> usize {
40 cats.iter()
41 .position(|&c| c == v)
42 .expect("category present in label set")
43 };
44
45 let mut tt = Array2::<f64>::zeros((n_rows, n_cat));
46 for (i, row) in data.rows().into_iter().enumerate() {
47 for &v in row {
48 tt[[i, cat_index(v)]] += 1.0;
49 }
50 }
51 (tt, cats)
52}
53
54pub fn fleiss_kappa(table: &Array2<f64>, method: FleissMethod) -> f64 {
62 let n_cat = table.ncols() as f64;
63 let n_total: f64 = table.sum();
64 let n_rat = table
66 .rows()
67 .into_iter()
68 .map(|r| r.sum())
69 .fold(f64::NEG_INFINITY, f64::max);
70
71 let p_cat: Vec<f64> = (0..table.ncols())
73 .map(|j| table.column(j).sum() / n_total)
74 .collect();
75
76 let mut p_sum = 0.0;
78 for row in table.rows() {
79 let sq: f64 = row.iter().map(|&v| v * v).sum();
80 p_sum += (sq - n_rat) / (n_rat * (n_rat - 1.0));
81 }
82 let p_mean = p_sum / table.nrows() as f64;
83
84 let p_mean_exp = match method {
85 FleissMethod::Fleiss => p_cat.iter().map(|&p| p * p).sum::<f64>(),
86 FleissMethod::Randolph => 1.0 / n_cat,
87 };
88
89 (p_mean - p_mean_exp) / (1.0 - p_mean_exp)
90}
91
92#[derive(Debug, Clone)]
94pub struct KappaResults {
95 pub kappa: f64,
97 pub kappa_max: f64,
99 pub var_kappa: f64,
101 pub var_kappa0: f64,
103 pub std_kappa: f64,
105 pub std_kappa0: f64,
107 pub z_value: f64,
109 pub pvalue_one_sided: f64,
111 pub pvalue_two_sided: f64,
113 pub kappa_low: f64,
115 pub kappa_upp: f64,
117}
118
119pub fn cohens_kappa(table: &Array2<f64>, alpha: f64) -> KappaResults {
126 let n = table.nrows();
127 let nobs: f64 = table.sum();
128
129 let agree: f64 = (0..n).map(|i| table[[i, i]]).sum();
131
132 let freq_row: Vec<f64> = (0..n).map(|i| table.row(i).sum() / nobs).collect();
134 let freq_col: Vec<f64> = (0..n).map(|j| table.column(j).sum() / nobs).collect();
135 let agree_exp: f64 = (0..n).map(|i| freq_col[i] * freq_row[i]).sum();
137
138 let kappa = (agree / nobs - agree_exp) / (1.0 - agree_exp);
139
140 let probs_diag: Vec<f64> = (0..n).map(|i| table[[i, i]] / nobs).collect();
142 let mut term_a = 0.0;
143 for i in 0..n {
144 let inner = 1.0 - (freq_row[i] + freq_col[i]) * (1.0 - kappa);
145 term_a += probs_diag[i] * inner * inner;
146 }
147 let mut term_b = 0.0;
148 for i in 0..n {
149 for j in 0..n {
150 if i == j {
151 continue;
152 }
153 let inner = freq_col[i] + freq_row[j];
155 term_b += (table[[i, j]] / nobs) * inner * inner;
156 }
157 }
158 term_b *= (1.0 - kappa) * (1.0 - kappa);
159 let term_c = (kappa - agree_exp * (1.0 - kappa)).powi(2);
160 let var_kappa = (term_a + term_b - term_c) / ((1.0 - agree_exp).powi(2) * nobs);
161
162 let term_c0: f64 = (0..n)
164 .map(|i| freq_col[i] * freq_row[i] * (freq_col[i] + freq_row[i]))
165 .sum();
166 let var_kappa0 =
167 (agree_exp + agree_exp * agree_exp - term_c0) / ((1.0 - agree_exp).powi(2) * nobs);
168
169 let kappa_max =
170 ((0..n).map(|i| freq_row[i].min(freq_col[i])).sum::<f64>() - agree_exp) / (1.0 - agree_exp);
171
172 let std_kappa = var_kappa.sqrt();
173 let std_kappa0 = var_kappa0.sqrt();
174 let z_value = kappa / std_kappa0;
175 let pvalue_one_sided = norm_sf(z_value);
176 let pvalue_two_sided = norm_sf(z_value.abs()) * 2.0;
177 let delta = norm_isf(alpha) * std_kappa;
178
179 KappaResults {
180 kappa,
181 kappa_max,
182 var_kappa,
183 var_kappa0,
184 std_kappa,
185 std_kappa0,
186 z_value,
187 pvalue_one_sided,
188 pvalue_two_sided,
189 kappa_low: kappa - delta,
190 kappa_upp: kappa + delta,
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use ndarray::array;
198
199 #[test]
200 fn perfect_agreement_kappa_one() {
201 let t = array![[10.0, 0.0], [0.0, 15.0]];
202 let r = cohens_kappa(&t, 0.025);
203 assert!((r.kappa - 1.0).abs() < 1e-12);
204 }
205
206 #[test]
207 fn aggregate_then_fleiss_runs() {
208 let data = array![
210 [0.0, 0.0, 0.0, 1.0],
211 [1.0, 1.0, 2.0, 2.0],
212 [0.0, 1.0, 2.0, 0.0]
213 ];
214 let (tt, cats) = aggregate_raters(&data);
215 assert_eq!(cats, vec![0.0, 1.0, 2.0]);
216 assert_eq!(tt.dim(), (3, 3));
217 let k = fleiss_kappa(&tt, FleissMethod::Fleiss);
218 assert!(k.is_finite());
219 }
220}