1use std::fmt::Write as _;
16
17use crate::config::Params;
18
19pub(crate) const ZERO_THRESHOLD: f64 = 1e-35_f32 as f64;
21
22const CATEGORY_COVERAGE: f32 = 0.99;
24
25#[derive(Debug, Clone, PartialEq)]
26pub enum BinMapper {
27 Numerical {
28 upper_bounds: Vec<f64>,
30 nan_bin: bool,
32 min: f64,
34 max: f64,
35 },
36 Categorical {
37 categories: Vec<i32>,
39 },
40 Trivial,
41}
42
43struct Distinct {
45 values: Vec<f64>,
47 counts: Vec<i64>,
48 nan_count: i64,
49 total: i64,
50}
51
52impl Distinct {
53 fn new(sample: &[f64], total_rows: usize) -> Distinct {
56 let nan_count = sample.iter().filter(|v| v.is_nan()).count();
57 let mut sorted: Vec<f64> = sample.iter().copied().filter(|v| v.abs() > ZERO_THRESHOLD).collect();
58 sorted.sort_unstable_by(f64::total_cmp);
59 let total = total_rows.max(sample.len()) as i64;
60 let zero_count = total - sorted.len() as i64 - nan_count as i64;
61
62 let mut values = Vec::new();
63 let mut counts = Vec::new();
64 if sorted.first().is_none_or(|&first| first > 0.0 && zero_count > 0) {
65 values.push(0.0);
66 counts.push(zero_count);
67 }
68 if let Some(&first) = sorted.first() {
69 values.push(first);
70 counts.push(1);
71 }
72 for pair in sorted.windows(2) {
73 let (prev, value) = (pair[0], pair[1]);
74 if equal_ordered(prev, value) {
75 *values.last_mut().expect("non-empty") = value;
76 *counts.last_mut().expect("non-empty") += 1;
77 continue;
78 }
79 if prev < 0.0 && value > 0.0 {
80 values.push(0.0);
81 counts.push(zero_count);
82 }
83 values.push(value);
84 counts.push(1);
85 }
86 if sorted.last().is_some_and(|&last| last < 0.0 && zero_count > 0) {
87 values.push(0.0);
88 counts.push(zero_count);
89 }
90 Distinct {
91 values,
92 counts,
93 nan_count: nan_count as i64,
94 total,
95 }
96 }
97
98 fn present_values(&self) -> usize {
99 self.counts.iter().filter(|&&c| c > 0).count()
100 }
101}
102
103fn equal_ordered(a: f64, b: f64) -> bool {
105 b <= a.next_up()
106}
107
108fn push_bound(bounds: &mut Vec<f64>, value: f64) -> bool {
110 if bounds.last().is_some_and(|&last| equal_ordered(last, value)) {
111 return false;
112 }
113 bounds.push(value);
114 true
115}
116
117fn greedy_find_bin(values: &[f64], counts: &[i64], max_bin: i64, total_cnt: i64, min_data_in_bin: i64) -> Vec<f64> {
119 let mut bounds = Vec::new();
120 if values.len() as i64 <= max_bin {
121 let mut cur_cnt_inbin = 0;
122 for i in 0..values.len().saturating_sub(1) {
123 cur_cnt_inbin += counts[i];
124 if cur_cnt_inbin >= min_data_in_bin
125 && push_bound(&mut bounds, ((values[i] + values[i + 1]) / 2.0).next_up())
126 {
127 cur_cnt_inbin = 0;
128 }
129 }
130 } else {
131 let (upper, lower) = greedy_bin_edges(values, counts, max_bin, total_cnt, min_data_in_bin);
132 for (up, low) in upper.iter().zip(&lower[1..]) {
133 push_bound(&mut bounds, ((up + low) / 2.0).next_up());
134 }
135 }
136 bounds.push(f64::INFINITY);
137 bounds
138}
139
140fn greedy_bin_edges(
144 values: &[f64],
145 counts: &[i64],
146 max_bin: i64,
147 total_cnt: i64,
148 min_data_in_bin: i64,
149) -> (Vec<f64>, Vec<f64>) {
150 let mut max_bin = max_bin;
151 if min_data_in_bin > 0 {
152 max_bin = max_bin.min(total_cnt / min_data_in_bin).max(1);
153 }
154 let mut mean_bin_size = total_cnt as f64 / max_bin as f64;
155 let mut rest_bin_cnt = max_bin;
156 let mut rest_sample_cnt = total_cnt;
157 let is_big: Vec<bool> = counts.iter().map(|&c| c as f64 >= mean_bin_size).collect();
158 for (&count, _) in counts.iter().zip(&is_big).filter(|(_, &big)| big) {
159 rest_bin_cnt -= 1;
160 rest_sample_cnt -= count;
161 }
162 mean_bin_size = rest_sample_cnt as f64 / rest_bin_cnt as f64;
163
164 let slots = max_bin as usize + 1;
166 let mut upper = vec![f64::INFINITY; slots];
167 let mut lower = vec![f64::INFINITY; slots];
168 let mut bin_cnt = 0usize;
169 lower[0] = values[0];
170 let mut cur_cnt_inbin = 0;
171 for i in 0..values.len() - 1 {
172 if !is_big[i] {
173 rest_sample_cnt -= counts[i];
174 }
175 cur_cnt_inbin += counts[i];
176 let cur = cur_cnt_inbin as f64;
177 if is_big[i] || cur >= mean_bin_size || (is_big[i + 1] && cur >= f64::max(1.0, mean_bin_size * 0.5)) {
178 upper[bin_cnt] = values[i];
179 bin_cnt += 1;
180 lower[bin_cnt] = values[i + 1];
181 if bin_cnt as i64 >= max_bin - 1 {
182 break;
183 }
184 cur_cnt_inbin = 0;
185 if !is_big[i] {
186 rest_bin_cnt -= 1;
187 mean_bin_size = rest_sample_cnt as f64 / rest_bin_cnt as f64;
188 }
189 }
190 }
191 bin_cnt += 1;
192 upper.truncate(bin_cnt - 1);
193 lower.truncate(bin_cnt);
194 (upper, lower)
195}
196
197fn find_bin_with_zero_as_one_bin(
200 distinct: &Distinct,
201 max_bin: i64,
202 total_sample_cnt: i64,
203 min_data_in_bin: i64,
204) -> Vec<f64> {
205 let (values, counts) = (&distinct.values, &distinct.counts);
206 let (mut left_cnt_data, mut cnt_zero, mut right_cnt_data) = (0i64, 0i64, 0i64);
207 for (&value, &count) in values.iter().zip(counts) {
208 if value <= -ZERO_THRESHOLD {
209 left_cnt_data += count;
210 } else if value > ZERO_THRESHOLD {
211 right_cnt_data += count;
212 } else {
213 cnt_zero += count;
214 }
215 }
216
217 let left_cnt = values.iter().position(|&v| v > -ZERO_THRESHOLD).unwrap_or(values.len());
218 let mut bounds = Vec::new();
219 if left_cnt > 0 && max_bin > 1 {
220 let share = left_cnt_data as f64 / (total_sample_cnt - cnt_zero) as f64 * (max_bin - 1) as f64;
221 let left_max_bin = (share as i64).max(1);
222 bounds = greedy_find_bin(
223 &values[..left_cnt],
224 &counts[..left_cnt],
225 left_max_bin,
226 left_cnt_data,
227 min_data_in_bin,
228 );
229 if let Some(last) = bounds.last_mut() {
230 *last = -ZERO_THRESHOLD;
231 }
232 }
233
234 let right_start = (left_cnt..values.len()).find(|&i| values[i] > ZERO_THRESHOLD);
235 let right_max_bin = max_bin - 1 - bounds.len() as i64;
236 match right_start {
237 Some(start) if right_max_bin > 0 => {
238 let right = greedy_find_bin(
239 &values[start..],
240 &counts[start..],
241 right_max_bin,
242 right_cnt_data,
243 min_data_in_bin,
244 );
245 bounds.push(ZERO_THRESHOLD);
246 bounds.extend(right);
247 }
248 _ => bounds.push(f64::INFINITY),
249 }
250 bounds
251}
252
253impl BinMapper {
254 pub fn numerical(sample: &[f64], total_rows: usize, params: &Params) -> BinMapper {
257 let distinct = Distinct::new(sample, total_rows);
258 let min_data_in_bin = params.min_data_in_bin as i64;
259 let max_bin = params.max_bin as i64;
260 let nan_bin = distinct.nan_count > 0;
261 let upper_bounds = if nan_bin {
262 find_bin_with_zero_as_one_bin(
263 &distinct,
264 max_bin - 1,
265 distinct.total - distinct.nan_count,
266 min_data_in_bin,
267 )
268 } else {
269 find_bin_with_zero_as_one_bin(&distinct, max_bin, distinct.total, min_data_in_bin)
270 };
271 let num_bins = upper_bounds.len() + usize::from(nan_bin);
272 debug_assert!(num_bins <= params.max_bin, "{num_bins} bins over max_bin");
273 if num_bins <= 1 || (!nan_bin && distinct.present_values() == 1) {
274 return BinMapper::Trivial;
275 }
276 BinMapper::Numerical {
277 upper_bounds,
278 nan_bin,
279 min: distinct.values[0],
280 max: *distinct.values.last().expect("non-empty"),
281 }
282 }
283
284 pub fn categorical(sample: &[f64], total_rows: usize, params: &Params) -> BinMapper {
289 let distinct = Distinct::new(sample, total_rows);
290 let mut other_count = distinct.nan_count;
291 let mut by_value: Vec<(i32, i64)> = Vec::new();
292 for (&value, &count) in distinct.values.iter().zip(&distinct.counts) {
293 let category = value as i32;
294 if category < 0 {
295 other_count += count;
296 } else if let Some(last) = by_value.last_mut().filter(|last| last.0 == category) {
297 last.1 += count;
298 } else {
299 by_value.push((category, count));
300 }
301 }
302 let rest_count = distinct.total - other_count;
303 if rest_count <= 0 {
304 return BinMapper::Trivial;
305 }
306 by_value.sort_by_key(|&(_, count)| std::cmp::Reverse(count));
308 let cut_cnt = (f64::from(rest_count as f32 * CATEGORY_COVERAGE) + 0.5) as i64;
309 let distinct_cnt = by_value.len() + usize::from(other_count > 0);
310 let max_bin = distinct_cnt.min(params.max_bin);
311 let min_data_in_bin = params.min_data_in_bin as i64;
312
313 let mut categories = Vec::new();
314 let mut used_cnt = 0;
315 for (index, &(category, count)) in by_value.iter().enumerate() {
316 let num_bin = categories.len() + 1;
317 if !(used_cnt < cut_cnt || num_bin < max_bin) || num_bin >= params.max_bin {
318 break;
319 }
320 if count < min_data_in_bin && index > 1 {
321 break;
322 }
323 categories.push(category);
324 used_cnt += count;
325 }
326 if categories.is_empty() {
327 return BinMapper::Trivial;
328 }
329 BinMapper::Categorical { categories }
330 }
331
332 pub fn num_bins(&self) -> u32 {
334 match self {
335 BinMapper::Numerical {
336 upper_bounds, nan_bin, ..
337 } => upper_bounds.len() as u32 + u32::from(*nan_bin),
338 BinMapper::Categorical { categories } => categories.len() as u32 + 1,
339 BinMapper::Trivial => 0,
340 }
341 }
342
343 pub fn value_to_bin(&self, value: f64) -> u8 {
345 match self {
346 BinMapper::Numerical {
347 upper_bounds, nan_bin, ..
348 } => {
349 if value.is_nan() {
350 if *nan_bin {
351 return upper_bounds.len() as u8;
352 }
353 return upper_bounds.partition_point(|&bound| bound < 0.0) as u8;
354 }
355 upper_bounds.partition_point(|&bound| bound < value) as u8
356 }
357 BinMapper::Categorical { categories } => categorical_bin(categories, value),
358 BinMapper::Trivial => 0,
359 }
360 }
361
362 pub fn info_string(&self) -> String {
365 match self {
366 BinMapper::Numerical { min, max, .. } => format!("[{}:{}]", format_g17(*min), format_g17(*max)),
367 BinMapper::Categorical { categories } => {
368 let mut out = String::from("-1");
369 for category in categories {
370 write!(out, ":{category}").expect("write to String");
371 }
372 out
373 }
374 BinMapper::Trivial => "none".to_string(),
375 }
376 }
377}
378
379pub(crate) fn categorical_bin(categories: &[i32], value: f64) -> u8 {
381 if value.is_nan() {
382 return 0;
383 }
384 let category = value as i32;
385 if category < 0 {
386 return 0;
387 }
388 categories
389 .iter()
390 .position(|&c| c == category)
391 .map_or(0, |index| index as u8 + 1)
392}
393
394pub(crate) fn format_g17(value: f64) -> String {
397 if value.is_nan() {
398 return "nan".to_string();
399 }
400 if value.is_infinite() {
401 return if value > 0.0 { "inf" } else { "-inf" }.to_string();
402 }
403 if value == 0.0 {
404 return if value.is_sign_negative() { "-0" } else { "0" }.to_string();
405 }
406 let scientific = format!("{value:.16e}");
407 let (mantissa, exponent) = scientific.split_once('e').expect("scientific notation");
408 let exponent: i32 = exponent.parse().expect("exponent");
409 if (-4..17).contains(&exponent) {
410 let decimals = (16 - exponent) as usize;
411 let fixed = format!("{value:.decimals$}");
412 return trim_fraction(&fixed).to_string();
413 }
414 let sign = if exponent < 0 { '-' } else { '+' };
415 format!("{}e{sign}{:02}", trim_fraction(mantissa), exponent.abs())
416}
417
418fn trim_fraction(number: &str) -> &str {
419 if number.contains('.') {
420 number.trim_end_matches('0').trim_end_matches('.')
421 } else {
422 number
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 fn params(max_bin: usize, min_data_in_bin: usize) -> Params {
431 Params {
432 max_bin,
433 min_data_in_bin,
434 ..Params::default()
435 }
436 }
437
438 fn values(seed: u32, count: usize) -> Vec<f64> {
440 (0..count as u32)
441 .map(|i| {
442 let h = crate::quantize::mix32(crate::quantize::mix32(i) ^ seed);
443 (f64::from(h >> 8) / 16_777_216.0 - 0.5) * 8.0
444 })
445 .collect()
446 }
447
448 fn bounds(mapper: &BinMapper) -> &[f64] {
449 match mapper {
450 BinMapper::Numerical { upper_bounds, .. } => upper_bounds,
451 other => panic!("not numerical: {other:?}"),
452 }
453 }
454
455 #[test]
456 fn value_to_bin_agrees_with_thresholds() {
457 let mut sample = values(1, 5000);
458 sample.extend([0.0; 300]);
459 sample.extend([1.25; 400]);
460 for (case, mapper) in [
461 (
462 "max_bin 255",
463 BinMapper::numerical(&sample, sample.len(), ¶ms(255, 3)),
464 ),
465 ("max_bin 7", BinMapper::numerical(&sample, sample.len(), ¶ms(7, 3))),
466 ("with nan", {
467 let mut with_nan = sample.clone();
468 with_nan.push(f64::NAN);
469 BinMapper::numerical(&with_nan, with_nan.len(), ¶ms(63, 3))
470 }),
471 ] {
472 let upper_bounds = bounds(&mapper).to_vec();
473 assert_eq!(
474 *upper_bounds.last().expect("bounds"),
475 f64::INFINITY,
476 "{case}: last bound"
477 );
478 assert!(upper_bounds.windows(2).all(|w| w[0] < w[1]), "{case}: ascending");
479 let mut probes = values(2, 2000);
480 for &bound in &upper_bounds {
481 probes.extend([bound, bound.next_up(), bound.next_down()]);
482 }
483 probes.extend([
484 0.0,
485 -0.0,
486 f64::INFINITY,
487 f64::NEG_INFINITY,
488 ZERO_THRESHOLD,
489 -ZERO_THRESHOLD,
490 1e-300,
491 ]);
492 for value in probes {
493 let bin = usize::from(mapper.value_to_bin(value));
494 assert!(bin < upper_bounds.len(), "{case}: {value} bin {bin}");
495 for (t, &bound) in upper_bounds.iter().enumerate() {
496 assert_eq!(
497 bin <= t,
498 value <= bound,
499 "{case}: value {value} bin {bin} threshold {t}"
500 );
501 }
502 }
503 }
504 }
505
506 #[test]
507 fn nan_bin_present_or_absent() {
508 let mut sample = values(3, 1000);
509 let without = BinMapper::numerical(&sample, sample.len(), ¶ms(255, 3));
510 let zero_bin = without.value_to_bin(0.0);
511 assert_eq!(without.value_to_bin(f64::NAN), zero_bin, "no nan bin: NaN bins as zero");
512 assert_eq!(without.num_bins() as usize, bounds(&without).len());
513
514 sample.push(f64::NAN);
515 let with = BinMapper::numerical(&sample, sample.len(), ¶ms(255, 3));
516 let BinMapper::Numerical {
517 upper_bounds, nan_bin, ..
518 } = &with
519 else {
520 panic!("numerical")
521 };
522 assert!(*nan_bin);
523 assert_eq!(with.num_bins() as usize, upper_bounds.len() + 1);
524 assert_eq!(usize::from(with.value_to_bin(f64::NAN)), upper_bounds.len());
525 assert!(with.num_bins() <= 255);
526 }
527
528 #[test]
529 fn trivial_and_all_nan() {
530 let p = params(255, 3);
531 assert_eq!(BinMapper::numerical(&[2.5; 10], 10, &p), BinMapper::Trivial);
532 assert_eq!(BinMapper::numerical(&[], 10, &p), BinMapper::Trivial, "all zero");
533 assert_eq!(
534 BinMapper::numerical(&[1e-36; 4], 4, &p),
535 BinMapper::Trivial,
536 "below kZeroThreshold is zero"
537 );
538 let one_and_nan = BinMapper::numerical(&[2.5, f64::NAN], 2, &p);
539 assert_eq!(one_and_nan.num_bins(), 3, "zero bin, value bin, nan bin");
540 let zeros_and_value = BinMapper::numerical(&[2.5; 10], 20, &p);
541 assert_eq!(bounds(&zeros_and_value), &[ZERO_THRESHOLD, f64::INFINITY]);
542 assert_eq!(zeros_and_value.info_string(), "[0:2.5]");
543 assert_eq!(BinMapper::Trivial.info_string(), "none");
544 }
545
546 #[test]
547 fn few_distinct_values_split_at_midpoints() {
548 let sample: Vec<f64> = [-2.0, -1.0, 1.0, 2.0, 3.0].iter().flat_map(|&v| [v; 5]).collect();
549 let mapper = BinMapper::numerical(&sample, sample.len() + 5, ¶ms(255, 3));
550 assert_eq!(
551 bounds(&mapper),
552 &[
553 (-1.5f64).next_up(),
554 -ZERO_THRESHOLD,
555 ZERO_THRESHOLD,
556 1.5f64.next_up(),
557 2.5f64.next_up(),
558 f64::INFINITY
559 ]
560 );
561 assert_eq!(mapper.info_string(), "[-2:3]");
562 }
563
564 #[test]
565 fn categorical_orders_by_count_then_value() {
566 let mut sample = Vec::new();
567 for (value, count) in [
568 (3.0, 10),
569 (7.0, 30),
570 (1.0, 10),
571 (2.7, 20),
572 (-4.0, 50),
573 (f64::NAN, 5),
574 (9.0, 1),
575 ] {
576 sample.extend(std::iter::repeat_n(value, count));
577 }
578 let total = sample.len() + 15;
580 let mapper = BinMapper::categorical(&sample, total, ¶ms(255, 3));
581 assert_eq!(
583 mapper,
584 BinMapper::Categorical {
585 categories: vec![7, 2, 0, 1, 3]
586 }
587 );
588 assert_eq!(mapper.num_bins(), 6);
589 assert_eq!(mapper.info_string(), "-1:7:2:0:1:3");
590 for (value, bin) in [
591 (7.0, 1),
592 (2.9, 2),
593 (0.0, 3),
594 (-0.5, 3),
595 (3.0, 5),
596 (-4.0, 0),
597 (f64::NAN, 0),
598 (9.0, 0),
599 ] {
600 assert_eq!(mapper.value_to_bin(value), bin, "category {value}");
601 }
602 }
603
604 #[test]
605 fn categorical_caps_at_max_bin() {
606 let sample: Vec<f64> = (1..=40).flat_map(|v| std::iter::repeat_n(f64::from(v), 10)).collect();
607 let mapper = BinMapper::categorical(&sample, sample.len(), ¶ms(16, 3));
608 let BinMapper::Categorical { categories } = &mapper else {
609 panic!("categorical")
610 };
611 assert_eq!(categories, &(1..=15).collect::<Vec<_>>());
612 assert_eq!(mapper.num_bins(), 16);
613 }
614
615 #[test]
616 fn categorical_trivial_without_categories() {
617 let p = params(255, 3);
618 assert_eq!(BinMapper::categorical(&[-1.0, f64::NAN], 2, &p), BinMapper::Trivial);
619 assert_eq!(
620 BinMapper::categorical(&[4.0; 3], 3, &p),
621 BinMapper::Categorical { categories: vec![4] }
622 );
623 }
624
625 #[test]
626 fn g17_matches_printf() {
627 for (value, expected) in [
628 (0.1, "0.10000000000000001"),
629 (1.0, "1"),
630 (-5.0, "-5"),
631 (123_456.0, "123456"),
632 (1e16, "10000000000000000"),
633 (1e17, "1e+17"),
634 (0.0001, "0.0001"),
635 (0.000_012_5, "1.2500000000000001e-05"),
636 (ZERO_THRESHOLD, "1.0000000180025095e-35"),
637 ] {
638 assert_eq!(format_g17(value), expected, "{value:e}");
639 }
640 for printed in [
641 "-3.7722751561227339",
642 "3.0660367390488967",
643 "933.34701911841285",
644 "1.2345678901234568e+17",
645 ] {
646 let value: f64 = printed.parse().expect("number");
647 assert_eq!(format_g17(value), printed);
648 }
649 }
650}