1use faer::{ColRef, Mat, MatRef};
4use rayon::prelude::*;
5
6use crate::prelude::BixverseFloat;
7
8pub(crate) struct MiScratch {
14 joint: Vec<usize>,
15 marginal_i: Vec<usize>,
16 marginal_j: Vec<usize>,
17 n_bins: usize,
18}
19
20impl MiScratch {
21 pub(crate) fn new(n_bins: usize) -> Self {
31 Self {
32 joint: vec![0; n_bins * n_bins],
33 marginal_i: vec![0; n_bins],
34 marginal_j: vec![0; n_bins],
35 n_bins,
36 }
37 }
38
39 fn count(&mut self, col_i: ColRef<usize>, col_j: ColRef<usize>) {
41 self.joint.fill(0);
42 self.marginal_i.fill(0);
43 self.marginal_j.fill(0);
44 for (&bin_i, &bin_j) in col_i.iter().zip(col_j.iter()) {
45 self.joint[bin_i * self.n_bins + bin_j] += 1;
46 self.marginal_i[bin_i] += 1;
47 self.marginal_j[bin_j] += 1;
48 }
49 }
50
51 fn mutual_information<T: BixverseFloat>(&self, n_rows: usize) -> T {
53 let n = T::from_usize(n_rows).unwrap();
54 let prob_i: Vec<T> = self
55 .marginal_i
56 .iter()
57 .map(|&c| T::from_usize(c).unwrap() / n)
58 .collect();
59 let prob_j: Vec<T> = self
60 .marginal_j
61 .iter()
62 .map(|&c| T::from_usize(c).unwrap() / n)
63 .collect();
64 let mut mi = T::zero();
65 for i in 0..self.n_bins {
66 for j in 0..self.n_bins {
67 let joint_prob = T::from_usize(self.joint[i * self.n_bins + j]).unwrap() / n;
68 if joint_prob > T::zero() {
69 mi += joint_prob * (joint_prob / (prob_i[i] * prob_j[j])).ln();
70 }
71 }
72 }
73 mi
74 }
75
76 fn joint_entropy<T: BixverseFloat>(&self, n_rows: usize) -> T {
78 let n = T::from_usize(n_rows).unwrap();
79 let mut joint_entropy = T::zero();
80 for &count in &self.joint {
81 let joint_prob = T::from_usize(count).unwrap() / n;
82 if joint_prob > T::zero() {
83 joint_entropy -= joint_prob * joint_prob.ln();
84 }
85 }
86 joint_entropy
87 }
88
89 pub(crate) fn pair<T: BixverseFloat>(
102 &mut self,
103 col_i: ColRef<usize>,
104 col_j: ColRef<usize>,
105 with_joint_entropy: bool,
106 ) -> (T, T) {
107 self.count(col_i, col_j);
108 let n_rows = col_i.nrows();
109 let entropy = if with_joint_entropy {
110 self.joint_entropy(n_rows)
111 } else {
112 T::zero()
113 };
114 (self.mutual_information(n_rows), entropy)
115 }
116}
117
118pub(crate) fn resolve_n_bins<T: BixverseFloat>(n_bins: Option<usize>, n_rows: usize) -> usize {
133 n_bins.unwrap_or_else(|| T::from_usize(n_rows).unwrap().sqrt().to_usize().unwrap())
134}
135
136pub fn calculate_mi<T>(col_i: ColRef<usize>, col_j: ColRef<usize>, n_bins: Option<usize>) -> T
151where
152 T: BixverseFloat,
153{
154 let n_bins = resolve_n_bins::<T>(n_bins, col_i.nrows());
155 MiScratch::new(n_bins).pair(col_i, col_j, false).0
156}
157
158pub fn calculate_joint_entropy<T>(
171 col_i: ColRef<usize>,
172 col_j: ColRef<usize>,
173 n_bins: Option<usize>,
174) -> T
175where
176 T: BixverseFloat,
177{
178 let n_bins = resolve_n_bins::<T>(n_bins, col_i.nrows());
179 MiScratch::new(n_bins).pair(col_i, col_j, true).1
180}
181
182pub fn calculate_entropy<T>(col: ColRef<usize>, n_bins: Option<usize>) -> T
196where
197 T: BixverseFloat,
198{
199 let n_rows = col.nrows();
200 let n_bins =
201 n_bins.unwrap_or_else(|| T::from_usize(n_rows).unwrap().sqrt().to_usize().unwrap());
202 let mut counts = vec![0usize; n_bins];
203 for i in 0..n_rows {
204 counts[col[i]] += 1;
205 }
206 let n = T::from_usize(n_rows).unwrap();
207 let mut entropy = T::zero();
208 for &count in &counts {
209 if count > 0 {
210 let prob = T::from_usize(count).unwrap() / n;
211 entropy -= prob * prob.ln();
212 }
213 }
214 entropy
215}
216
217#[derive(Debug, Clone, Default, Copy)]
223pub enum BinningStrategy {
224 #[default]
226 EqualWidth,
227 EqualFrequency,
229}
230
231pub fn parse_bin_strategy_type(s: &str) -> Option<BinningStrategy> {
241 match s.to_lowercase().as_str() {
242 "equal_width" | "equalwidth" => Some(BinningStrategy::EqualWidth),
243 "equal_frequency" | "equal_freq" | "equalfreq" | "equalfrequency" => {
244 Some(BinningStrategy::EqualFrequency)
245 }
246 _ => None,
247 }
248}
249
250fn bin_equal_width<T>(col: &ColRef<T>, n_bins: usize) -> Vec<usize>
261where
262 T: BixverseFloat,
263{
264 let (min_val, max_val) = col.iter().fold(
265 (
266 T::from_f64(f64::INFINITY).unwrap(),
267 T::from_f64(f64::NEG_INFINITY).unwrap(),
268 ),
269 |(min, max), x| (min.min(*x), max.max(*x)),
270 );
271 let range = max_val - min_val;
272 if range == T::zero() {
273 return vec![0; col.nrows()];
274 }
275 let step = range / T::from_usize(n_bins).unwrap();
276 col.iter()
277 .map(|x| {
278 let bin = ((*x - min_val) / step).floor().to_usize().unwrap();
279 bin.min(n_bins - 1)
280 })
281 .collect()
282}
283
284fn bin_equal_frequency<T>(col: &ColRef<T>, n_bins: usize) -> Vec<usize>
295where
296 T: BixverseFloat,
297{
298 let n_rows = col.nrows();
299 let mut sorted_values: Vec<T> = col.iter().copied().collect();
300 sorted_values.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
301
302 let mut split_points = Vec::with_capacity(n_bins);
303 for k in 1..n_bins {
304 let idx = (k * n_rows) / n_bins;
305 split_points.push(sorted_values[idx.min(n_rows - 1)]);
306 }
307
308 let mut bin_assignments = vec![0; n_rows];
309 for (i, &value) in col.iter().enumerate() {
310 let mut bin = 0;
311 for &split in &split_points {
312 if value >= split {
313 bin += 1;
314 } else {
315 break;
316 }
317 }
318 bin_assignments[i] = bin.min(n_bins - 1);
319 }
320 bin_assignments
321}
322
323pub fn bin_matrix_cols<T>(
335 mat: &MatRef<T>,
336 n_bins: Option<usize>,
337 strategy: BinningStrategy,
338) -> Mat<usize>
339where
340 T: BixverseFloat,
341{
342 let (n_rows, n_cols) = mat.shape();
343 let n_bins =
344 n_bins.unwrap_or_else(|| T::from_usize(n_rows).unwrap().sqrt().to_usize().unwrap());
345 let binned_vals: Vec<Vec<usize>> = mat
346 .par_col_iter()
347 .map(|col| match strategy {
348 BinningStrategy::EqualWidth => bin_equal_width(&col, n_bins),
349 BinningStrategy::EqualFrequency => bin_equal_frequency(&col, n_bins),
350 })
351 .collect();
352 Mat::from_fn(n_rows, n_cols, |i, j| binned_vals[j][i])
353}
354
355#[cfg(test)]
360mod tests {
361 use super::*;
362 use faer::mat;
363
364 fn assert_approx_eq(a: f64, b: f64) {
365 assert!((a - b).abs() < 1e-10, "{} != {}", a, b);
366 }
367
368 #[test]
370 fn test_calculate_entropy_uniform() {
371 let data = mat![[0], [1], [0], [1]]; let col = data.col(0);
373
374 let ent: f64 = calculate_entropy(col, Some(2));
376 assert_approx_eq(ent, 2.0_f64.ln());
377 }
378
379 #[test]
381 fn test_calculate_entropy_zero() {
382 let data = mat![[0], [0], [0], [0]];
383 let col = data.col(0);
384
385 let ent: f64 = calculate_entropy(col, Some(1));
386 assert_approx_eq(ent, 0.0);
387 }
388
389 #[test]
391 fn test_calculate_joint_entropy() {
392 let data = mat![[0, 0], [1, 1], [0, 0], [1, 1]]; let je: f64 = calculate_joint_entropy(data.col(0), data.col(1), Some(2));
395 assert_approx_eq(je, 2.0_f64.ln());
396
397 let data_indep = mat![[0, 0], [0, 1], [1, 0], [1, 1]];
399 let je_indep: f64 = calculate_joint_entropy(data_indep.col(0), data_indep.col(1), Some(2));
400 assert_approx_eq(je_indep, 4.0_f64.ln());
401 }
402
403 #[test]
405 fn test_calculate_mi() {
406 let data = mat![[0, 0], [1, 1]];
407 let mi: f64 = calculate_mi(data.col(0), data.col(1), Some(2));
408 assert_approx_eq(mi, 2.0_f64.ln());
409
410 let data_indep = mat![[0, 0], [0, 1], [1, 0], [1, 1]];
412 let mi_indep: f64 = calculate_mi(data_indep.col(0), data_indep.col(1), Some(2));
413 assert_approx_eq(mi_indep, 0.0);
414 }
415
416 #[test]
418 fn test_bin_equal_width() {
419 let mat = mat![[0.0], [5.0], [10.0]];
420
421 let binned = bin_matrix_cols(&mat.as_ref(), Some(3), BinningStrategy::EqualWidth);
422
423 assert_eq!(binned.nrows(), 3);
424 assert_eq!(binned.get(0, 0), &0);
425 assert_eq!(binned.get(1, 0), &1);
426 assert_eq!(binned.get(2, 0), &2);
427 }
428
429 #[test]
431 fn test_bin_equal_width_constant_column() {
432 let mat = mat![[1.0], [1.0], [1.0]];
434 let binned = bin_matrix_cols(&mat.as_ref(), Some(3), BinningStrategy::EqualWidth);
435
436 assert_eq!(binned.get(0, 0), &0);
438 assert_eq!(binned.get(1, 0), &0);
439 assert_eq!(binned.get(2, 0), &0);
440 }
441
442 #[test]
444 fn test_bin_equal_frequency() {
445 let mat = mat![[1.0], [9.0], [2.0], [8.0]];
446
447 let binned = bin_matrix_cols(&mat.as_ref(), Some(2), BinningStrategy::EqualFrequency);
448
449 assert_eq!(binned.get(0, 0), &0); assert_eq!(binned.get(1, 0), &1); assert_eq!(binned.get(2, 0), &0); assert_eq!(binned.get(3, 0), &1); }
457
458 #[test]
460 fn test_parse_bin_strategy() {
461 assert!(matches!(
462 parse_bin_strategy_type("equal_width"),
463 Some(BinningStrategy::EqualWidth)
464 ));
465 assert!(matches!(
466 parse_bin_strategy_type("equal_freq"),
467 Some(BinningStrategy::EqualFrequency)
468 ));
469 assert!(parse_bin_strategy_type("random").is_none());
470 }
471}