1use crate::algorithms::depth2::{ConTreeDepth2, Depth2Loss};
2use crate::algorithms::interval_pruner::{Bound, IntervalsPruner};
3use crate::algorithms::shared;
4use crate::algorithms::support_feasible_splits;
5use crate::caching::{Cache, Entry};
6use crate::common::{
7 Cost, FitOutcome, Loss, PointSelector, SearchConfig, SearchError, SearchStatus, Skipping,
8 Statistics,
9};
10use crate::data::view::DataView;
11use crate::data::Dataset;
12use crate::tree::Tree;
13use rand::rngs::StdRng;
14use rand::SeedableRng;
15use std::collections::VecDeque;
16use std::time::Instant;
17
18pub struct ConTree {
24 config: SearchConfig,
25 statistics: Statistics,
26 cache: Cache,
27 specialized: ConTreeDepth2,
28 runtime: Instant,
29 rng: StdRng,
30 status: SearchStatus,
31 trajectory: Vec<(f64, Cost)>,
33 loss: Loss,
34 invalid_cost: Option<Cost>,
36 pub tree: Tree,
37}
38
39impl ConTree {
40 pub fn with_config(config: SearchConfig) -> Self {
45 let mut solver = Self::new(
46 config.min_sup,
47 config.max_depth,
48 config.max_time,
49 config.max_error,
50 config.point_selector,
51 config.max_gap,
52 config.use_heuristic,
53 config.fast_d2,
54 );
55 solver.config = config;
56 solver
57 }
58
59 #[allow(clippy::too_many_arguments)]
61 pub fn new(
62 min_sup: usize,
63 max_depth: usize,
64 max_time: f64,
65 max_error: Cost,
66 split_selection_strategy: PointSelector,
67 max_gap: Cost,
68 use_heuristic: bool,
69 fast_d2: bool,
70 ) -> Self {
71 Self {
72 cache: Cache::default(),
73 config: SearchConfig::new(
74 min_sup,
75 max_depth,
76 max_time,
77 max_gap,
78 max_error,
79 use_heuristic,
80 fast_d2,
81 split_selection_strategy,
82 ),
83 statistics: Statistics::default(),
84 specialized: ConTreeDepth2,
85 runtime: Instant::now(),
86 rng: StdRng::from_os_rng(),
87 status: SearchStatus::Optimal,
88 trajectory: Vec::new(),
89 loss: Loss::default(),
90 invalid_cost: None,
91 tree: Tree::default(),
92 }
93 }
94
95 pub fn with_loss(mut self, loss: Loss) -> Self {
100 self.loss = loss;
101 self
102 }
103
104 pub fn with_skipping(mut self, skipping: Skipping) -> Self {
106 self.config.skipping = skipping;
107 self
108 }
109
110 pub fn with_random_state(mut self, seed: u64) -> Self {
113 self.rng = StdRng::seed_from_u64(seed);
114 self
115 }
116
117 pub fn fit(&mut self, dataset: &Dataset) -> Result<FitOutcome, SearchError> {
123 crate::algorithms::validate(&self.config, dataset)?;
124 self.config.cost_step = SearchConfig::cost_step_for(&self.loss);
125 self.config.fast_d2 &= !self.loss.uses_indices();
126 self.invalid_cost = None;
127 self.statistics.skipping_violations = 0;
128
129 let root_view = DataView::root(dataset, self.config.use_heuristic);
130
131 self.cache = Cache::new(self.config.max_depth, root_view.total_instances);
132
133 let root_index = self.cache.init();
134
135 self.statistics.num_features = root_view.get_feature_number();
136 self.statistics.num_samples = root_view.total_instances;
137
138 let (error, label) = self.leaf(&root_view);
139 let mut entry = Entry {
140 error,
141 label,
142 ..Entry::default()
143 };
144
145 if let Some(entry) = self.cache.get_mut(root_index) {
146 self.config.max_error = self.config.max_error.min(error);
147 entry.error = error;
148 entry.label = label;
149 }
150
151 let root_config = self.config;
152 self.runtime = Instant::now();
153 self.trajectory.clear();
154 self.expand_node_with_view(
155 &root_view,
156 &root_config,
157 &mut entry,
158 0,
159 true,
160 root_config.max_error,
161 );
162
163 self.statistics.error = entry.error;
164 self.statistics.cache_size = self.cache.len();
165 self.statistics.duration = self.elapsed_time();
166 self.get_solution_tree();
167
168 if let Some(cost) = self.invalid_cost {
169 return Err(SearchError::InvalidLeafCost { cost });
170 }
171 self.status = if !self.time_remains() {
172 SearchStatus::TimeLimit
173 } else if entry.error <= self.config.max_gap && self.config.max_gap > 0.0 {
174 SearchStatus::ErrorBoundReached
175 } else if self.statistics.skipping_violations > 0 {
176 SearchStatus::NotProven
177 } else {
178 SearchStatus::Optimal
179 };
180 self.tree.validate()?;
181
182 Ok(FitOutcome {
183 tree: self.tree.clone(),
184 statistics: self.statistics,
185 status: self.status,
186 })
187 }
188
189 pub fn trajectory(&self) -> &[(f64, Cost)] {
192 &self.trajectory
193 }
194
195 pub fn status(&self) -> SearchStatus {
197 self.status
198 }
199
200 fn leaf(&mut self, view: &DataView<'_>) -> (Cost, usize) {
202 shared::checked_leaf(
203 &self.loss,
204 view.get_labels_freqs(),
205 || view.instance_ids(),
206 &mut self.invalid_cost,
207 )
208 }
209
210 fn cache_child(&mut self, view: &DataView<'_>, child_depth: usize) -> (bool, usize, Entry) {
216 let (is_new, index) = self.cache.insert(&view.bitset, child_depth);
217 let depth = self.config.max_depth - child_depth;
218 let mut entry = Entry::default();
219
220 if let Some(slot) = self.cache.get_mut(index) {
221 if is_new {
222 let (error, label) = shared::checked_leaf(
223 &self.loss,
224 view.get_labels_freqs(),
225 || view.instance_ids(),
226 &mut self.invalid_cost,
227 );
228 slot.error = error;
229 slot.label = label;
230 slot.depth = depth;
231 slot.lower_bound = 0.0;
232 slot.is_valid = true;
233 }
234 entry = *slot;
235 }
236 (is_new, index, entry)
237 }
238
239 fn expand_node_with_view(
240 &mut self,
241 view: &DataView<'_>,
242 config: &SearchConfig,
243 current_best: &mut Entry,
244 parent_index: usize,
245 is_new: bool,
246 upper_bound: Cost,
247 ) {
248 #[cfg(feature = "profiling")]
249 coz::progress!();
250 if current_best.error == 0.0 || view.is_empty() {
251 if let Some(entry) = self.cache.get_mut(parent_index) {
255 entry.is_leaf = entry.feature == usize::MAX;
256 entry.mark_exact();
257 *current_best = *entry;
258 }
259 return;
260 }
261
262 if !is_new && current_best.is_optimal {
266 self.statistics.cache_hits += 1;
267 return;
268 }
269
270 if config.max_depth == 0 {
271 current_best.is_leaf = true;
272 current_best.mark_exact();
273
274 if let Some(entry) = self.cache.get_mut(parent_index) {
275 entry.is_leaf = true;
276 entry.mark_exact();
277 *current_best = *entry;
278 }
279
280 return;
281 }
282
283 if current_best.error <= config.max_gap || view.len() <= 1 {
284 current_best.mark_exact();
287 if let Some(entry) = self.cache.get_mut(parent_index) {
288 entry.mark_exact();
289 }
290 return;
291 }
292
293 if config.fast_d2 && config.max_depth == 2 {
296 let skipping = config.skipping.applies(&self.loss);
297 let tree = self.specialized.fit(
298 view,
299 config,
300 current_best,
301 upper_bound,
302 &mut self.statistics,
303 &mut Depth2Loss::new(&self.loss, skipping, &mut self.invalid_cost),
304 );
305 current_best.finalize_lower_bound(upper_bound);
306 current_best.is_optimal = current_best.is_valid;
307 let tree_index = self.cache.insert_tree(tree);
308 if let Some(entry) = self.cache.get_mut(parent_index) {
309 *entry = *current_best;
310 entry.tree_idx = Some(tree_index);
311 }
312 self.statistics.specialized_solver_call += 1;
313
314 return;
315 }
316
317 let num_features = view.get_feature_number();
318 let heuristics_data = view.features_best_score();
319 debug_assert!(
320 num_features == heuristics_data.len(),
321 "Missmatch with heuristics and features number"
322 );
323 for &(_, feat) in heuristics_data.iter().take(num_features) {
324 self.expand_on_feature(
325 view,
326 feat,
327 parent_index,
328 config,
329 current_best,
330 upper_bound.min(current_best.error),
331 );
332
333 if current_best.error == 0.0 {
334 current_best.mark_exact();
335 if let Some(entry) = self.cache.get_mut(parent_index) {
336 entry.mark_exact();
337 }
338
339 return;
340 }
341 if !self.time_remains() {
342 return;
343 }
344 }
345
346 current_best.finalize_lower_bound(upper_bound);
350 current_best.is_optimal = current_best.is_valid;
351 if let Some(entry) = self.cache.get_mut(parent_index) {
352 entry.lower_bound = current_best.lower_bound;
353 entry.is_valid = current_best.is_valid;
354 entry.is_optimal = current_best.is_valid;
355 }
356 }
357
358 fn expand_on_feature(
359 &mut self,
360 view: &DataView<'_>,
361 feature_index: usize,
362 cache_index: usize,
363 config: &SearchConfig,
364 current_best: &mut Entry,
365 upper_bound: Cost,
366 ) {
367 #[cfg(feature = "profiling")]
368 coz::scope!("expand_on_feature");
369 let feature_column = view.get_sorted_feature(feature_index);
370 let feature_column_ids = view.get_feature_indices(feature_index);
371
372 if config.point_selector == PointSelector::First {
373 self.expand_on_feature_gini_priority(
374 view,
375 feature_index,
376 cache_index,
377 config,
378 current_best,
379 upper_bound,
380 );
381 return;
382 }
383
384 let possible_index_split = view.get_possible_split_indices(feature_index);
385 if possible_index_split.is_empty() {
386 return;
387 }
388
389 let feasible =
393 support_feasible_splits(possible_index_split, view.len(), self.config.min_sup);
394 if feasible.is_empty() {
395 return;
396 }
397
398 let mut pruner = IntervalsPruner::new(
399 possible_index_split,
400 config.max_gap,
401 config.min_sup,
402 config.skipping.applies(&self.loss),
403 );
404 let mut queue = VecDeque::new();
405 let init_bound = Bound::new(feasible.start, feasible.end - 1, None, None);
406 queue.push_back(init_bound);
407
408 while let Some(mut current_bound) = queue.pop_front() {
409 if !self.time_remains() {
410 return;
411 }
412
413 if pruner.subinterval_pruning(¤t_bound, current_best.error.min(upper_bound)) {
417 continue;
418 }
419
420 pruner.interval_shrinking(&mut current_bound, current_best.error.min(upper_bound));
421 if !current_bound.is_valid() {
422 continue;
423 }
424
425 let selected_point = self.select_point(config, ¤t_bound);
426 let split_point = possible_index_split[selected_point];
427 let int_half_distance = split_point
428 .saturating_sub(possible_index_split[current_bound.left_bound])
429 .max(possible_index_split[current_bound.right_bound].saturating_sub(split_point))
430 as Cost;
431
432 let threshold_value = if selected_point > 0 {
433 let previous = feature_column_ids[possible_index_split[selected_point - 1]];
434 let point = feature_column_ids[split_point];
435 shared::threshold_between(
436 feature_column[previous].value(),
437 feature_column[point].value(),
438 )
439 } else {
440 let point = feature_column_ids[split_point];
441 shared::threshold_between(
442 feature_column[feature_column_ids[0]].value(),
443 feature_column[point].value(),
444 )
445 };
446
447 let (left_view, right_view) = view.split(feature_index, split_point);
448
449 if left_view.len() < self.config.min_sup || right_view.len() < self.config.min_sup {
450 continue;
451 }
452
453 let process_left_first = left_view.len() >= right_view.len();
456
457 let left_config = config.derive_left();
458 let mut left_entry = Entry::default();
459 let mut right_entry = Entry::default();
460
461 let (mut left_index, mut right_index) = (0, 0);
464 let (mut left_is_new, mut right_is_new);
465
466 let larger_upper_bound = current_best.error.min(upper_bound + config.cost_step);
467 self.statistics.general_solver_call += 1;
468
469 if process_left_first {
470 (left_is_new, left_index, left_entry) =
471 self.cache_child(&left_view, left_config.max_depth);
472
473 self.expand_node_with_view(
474 &left_view,
475 &left_config,
476 &mut left_entry,
477 left_index,
478 left_is_new,
479 larger_upper_bound,
480 );
481 left_entry.finalize_lower_bound(larger_upper_bound);
482 } else {
483 (right_is_new, right_index, right_entry) =
484 self.cache_child(&right_view, left_config.max_depth);
485
486 self.expand_node_with_view(
487 &right_view,
488 &left_config,
489 &mut right_entry,
490 right_index,
491 right_is_new,
492 larger_upper_bound,
493 );
494 right_entry.finalize_lower_bound(larger_upper_bound);
495 }
496
497 let larger_error = if process_left_first {
500 left_entry.lower_bound
501 } else {
502 right_entry.lower_bound
503 };
504 let budget = current_best.error.min(upper_bound + config.cost_step) - larger_error;
509 let widening = if config.skipping.applies(&self.loss) {
510 int_half_distance
511 } else {
512 0.0
513 };
514 let smaller_ub = budget + widening;
515 let smaller_upper_bound = smaller_ub.max(0.0);
516 let mut right_error: Option<Cost> = None;
519
520 if smaller_ub > 0.0 || budget == 0.0 {
524 self.statistics.general_solver_call += 1;
525 let right_config = config.derive_right(left_config.max_gap);
526
527 if process_left_first {
528 (right_is_new, right_index, right_entry) =
529 self.cache_child(&right_view, right_config.max_depth);
530
531 self.expand_node_with_view(
532 &right_view,
533 &right_config,
534 &mut right_entry,
535 right_index,
536 right_is_new,
537 smaller_upper_bound,
538 );
539 right_entry.finalize_lower_bound(smaller_upper_bound);
540 } else {
541 (left_is_new, left_index, left_entry) =
542 self.cache_child(&left_view, right_config.max_depth);
543
544 self.expand_node_with_view(
545 &left_view,
546 &right_config,
547 &mut left_entry,
548 left_index,
549 left_is_new,
550 smaller_upper_bound,
551 );
552 left_entry.finalize_lower_bound(smaller_upper_bound);
553 }
554
555 right_error = Some(right_entry.lower_bound);
558
559 let feature_best = left_entry.lower_bound + right_entry.lower_bound;
560 if left_entry.is_valid && right_entry.is_valid && feature_best < current_best.error
561 {
562 current_best.error = feature_best;
563 if config.is_root {
564 self.trajectory.push((self.elapsed_time(), feature_best));
565 }
566 current_best.feature = feature_index;
567 current_best.split = threshold_value;
568 current_best.left = left_index;
569 current_best.right = right_index;
570
571 let is_optimal = feature_best == 0.0;
572 current_best.is_optimal = is_optimal;
573
574 if let Some(entry) = self.cache.get_mut(cache_index) {
575 *entry = *current_best;
576 }
577
578 if feature_best == 0.0 {
579 return;
580 }
581 }
582 }
583
584 let left_score = left_entry.is_valid.then_some(left_entry.lower_bound);
588 pruner.add_result(selected_point, left_score, right_error);
589 if current_bound.left_bound == current_bound.right_bound {
590 continue;
591 }
592
593 let score_difference = (left_entry.lower_bound + right_error.unwrap_or(0.0)
594 - current_best.error.min(upper_bound))
595 .max(0.0);
596
597 let (left_bound, right_bound) = pruner.neighbourhood_pruning(
598 score_difference,
599 current_bound.left_bound,
600 current_bound.right_bound,
601 selected_point,
602 );
603
604 if left_bound <= current_bound.right_bound {
605 queue.push_back(Bound {
606 left_bound,
607 right_bound: current_bound.right_bound,
608 last_split_left_index: Some(selected_point),
609 last_split_right_index: current_bound.last_split_right_index,
610 });
611 }
612
613 if current_bound.left_bound <= right_bound {
614 queue.push_back(Bound {
615 left_bound: current_bound.left_bound,
616 right_bound,
617 last_split_left_index: current_bound.last_split_left_index,
618 last_split_right_index: Some(selected_point),
619 });
620 }
621 }
622
623 if let Some(entry) = self.cache.get_mut(cache_index) {
624 entry.is_optimal = true;
625 }
626 }
627
628 fn expand_on_feature_gini_priority(
629 &mut self,
630 view: &DataView<'_>,
631 feature_index: usize,
632 cache_index: usize,
633 config: &SearchConfig,
634 current_best: &mut Entry,
635 upper_bound: Cost,
636 ) {
637 let feature_column = view.get_sorted_feature(feature_index);
638 let feature_column_ids = view.get_feature_indices(feature_index);
639
640 let possible_splits = view.get_possible_split_indices(feature_index);
641
642 if possible_splits.is_empty() {
643 return;
644 }
645
646 let sorted_by_heuristic_indices = view.ordered_possible_splits(feature_index);
647
648 let mut pruner = IntervalsPruner::new(
649 possible_splits,
650 config.max_gap,
651 config.min_sup,
652 config.skipping.applies(&self.loss),
653 );
654
655 let mut queue = VecDeque::new();
656 let init_bound = Bound::new(0, possible_splits.len() - 1, None, None);
657 queue.push_back(init_bound);
658
659 let mut pruned = vec![false; possible_splits.len()];
660
661 for &split_idx in sorted_by_heuristic_indices {
662 if !self.time_remains() {
663 return;
664 }
665
666 if pruned[split_idx] {
667 continue;
668 }
669
670 let mut current_left = split_idx;
673 while current_left > 0 && pruned[current_left - 1] {
674 current_left -= 1;
675 }
676 current_left = current_left.saturating_sub(1);
677
678 let mut current_right = split_idx;
679 while current_right < possible_splits.len() - 1 && pruned[current_right + 1] {
680 current_right += 1;
681 }
682 if current_right < possible_splits.len() - 1 {
683 current_right += 1;
684 }
685
686 let split_point = possible_splits[split_idx];
687
688 let threshold_value = if split_idx > 0 {
689 let previous = feature_column_ids[possible_splits[split_idx - 1]];
690 let point = feature_column_ids[split_point];
691 shared::threshold_between(
692 feature_column[previous].value(),
693 feature_column[point].value(),
694 )
695 } else {
696 let point = feature_column_ids[split_point];
697 shared::threshold_between(
698 feature_column[feature_column_ids[0]].value(),
699 feature_column[point].value(),
700 )
701 };
702
703 let (left_view, right_view) = view.split(feature_index, split_point);
704
705 if left_view.len() < self.config.min_sup || right_view.len() < self.config.min_sup {
706 pruned[split_idx] = true;
707 continue;
708 }
709
710 let process_left_first = left_view.len() >= right_view.len();
713
714 let left_config = config.derive_left();
715 let mut left_entry = Entry::default();
716 let mut right_entry = Entry::default();
717
718 let (mut left_index, mut right_index) = (0, 0);
721 let (mut left_is_new, mut right_is_new);
722
723 let int_half_distance = split_point
724 .saturating_sub(possible_splits[0])
725 .max(possible_splits[possible_splits.len() - 1].saturating_sub(split_point))
726 as Cost;
727
728 let larger_upper_bound = current_best.error.min(upper_bound + config.cost_step);
729 self.statistics.general_solver_call += 1;
730
731 if process_left_first {
732 (left_is_new, left_index, left_entry) =
733 self.cache_child(&left_view, left_config.max_depth);
734
735 self.expand_node_with_view(
736 &left_view,
737 &left_config,
738 &mut left_entry,
739 left_index,
740 left_is_new,
741 larger_upper_bound,
742 );
743 left_entry.finalize_lower_bound(larger_upper_bound);
744 } else {
745 (right_is_new, right_index, right_entry) =
746 self.cache_child(&right_view, left_config.max_depth);
747
748 self.expand_node_with_view(
749 &right_view,
750 &left_config,
751 &mut right_entry,
752 right_index,
753 right_is_new,
754 larger_upper_bound,
755 );
756 right_entry.finalize_lower_bound(larger_upper_bound);
757 }
758
759 let larger_error = if process_left_first {
762 left_entry.lower_bound
763 } else {
764 right_entry.lower_bound
765 };
766 let budget = current_best.error.min(upper_bound + config.cost_step) - larger_error;
771 let widening = if config.skipping.applies(&self.loss) {
772 int_half_distance
773 } else {
774 0.0
775 };
776 let smaller_ub = budget + widening;
777 let smaller_upper_bound = smaller_ub.max(0.0);
778 let mut right_error: Option<Cost> = None;
779
780 if smaller_ub > 0.0 || budget == 0.0 {
784 self.statistics.general_solver_call += 1;
785 let right_config = config.derive_right(left_config.max_gap);
786
787 if process_left_first {
788 (right_is_new, right_index, right_entry) =
789 self.cache_child(&right_view, right_config.max_depth);
790
791 self.expand_node_with_view(
792 &right_view,
793 &right_config,
794 &mut right_entry,
795 right_index,
796 right_is_new,
797 smaller_upper_bound,
798 );
799 right_entry.finalize_lower_bound(smaller_upper_bound);
800 } else {
801 (left_is_new, left_index, left_entry) =
802 self.cache_child(&left_view, right_config.max_depth);
803
804 self.expand_node_with_view(
805 &left_view,
806 &right_config,
807 &mut left_entry,
808 left_index,
809 left_is_new,
810 smaller_upper_bound,
811 );
812 left_entry.finalize_lower_bound(smaller_upper_bound);
813 }
814
815 right_error = Some(right_entry.lower_bound);
818 let feature_best = left_entry.lower_bound + right_entry.lower_bound;
819 if left_entry.is_valid && right_entry.is_valid && feature_best < current_best.error
820 {
821 current_best.error = feature_best;
822 if config.is_root {
823 self.trajectory.push((self.elapsed_time(), feature_best));
824 }
825 current_best.feature = feature_index;
826 current_best.split = threshold_value;
827 current_best.left = left_index;
828 current_best.right = right_index;
829
830 let is_optimal = feature_best == 0.0;
831 current_best.is_optimal = is_optimal;
832
833 if let Some(entry) = self.cache.get_mut(cache_index) {
834 *entry = *current_best;
835 }
836 }
837 }
838
839 let left_score = left_entry.is_valid.then_some(left_entry.lower_bound);
843 pruner.add_result(split_idx, left_score, right_error);
844
845 let score_difference = (left_entry.lower_bound + right_error.unwrap_or(0.0)
846 - current_best.error.min(upper_bound))
847 .max(0.0);
848 let (new_left_bound, new_right_bound) = pruner.neighbourhood_pruning(
849 score_difference,
850 0,
851 possible_splits.len() - 1,
852 split_idx,
853 );
854
855 let skip_from = new_right_bound.saturating_add(1).max(current_left);
859 if skip_from < split_idx {
860 pruned[skip_from..split_idx].fill(true);
861 }
862 let skip_to = new_left_bound.min(current_right + 1);
863 if split_idx + 1 < skip_to {
864 pruned[split_idx + 1..skip_to].fill(true);
865 }
866
867 if current_best.error == 0.0 {
868 break;
869 }
870 }
871
872 if let Some(entry) = self.cache.get_mut(cache_index) {
873 entry.is_optimal = true;
874 }
875 }
876
877 pub fn statistics(&self) -> Statistics {
878 self.statistics
879 }
880
881 fn time_remains(&self) -> bool {
882 shared::time_remains(&self.runtime, self.config.max_time)
883 }
884
885 fn elapsed_time(&self) -> f64 {
886 shared::elapsed_time(&self.runtime)
887 }
888
889 pub fn select_point(&mut self, config: &SearchConfig, bound: &Bound) -> usize {
890 shared::select_point(config.point_selector, &mut self.rng, bound)
891 }
892
893 pub fn get_solution_tree(&mut self) {
895 self.tree = shared::build_solution_tree(&self.cache);
896 }
897}
898
899#[cfg(test)]
900mod contree_test {
901 use crate::algorithms::continuous_tree::ConTree;
902 use crate::common::{Cost, PointSelector};
903 use crate::reader::data_reader::DataReader;
904 use crate::reader::DataReaderError;
905 use crate::tests::fixture;
906
907 #[test]
908 fn deeper_search_never_scores_worse() -> Result<(), DataReaderError> {
909 let reader = DataReader::default();
910 let mut dataset = reader.read_file(&fixture("avila_1k.txt"))?;
911 dataset.sort_features();
912
913 let mut previous = Cost::INFINITY;
914 for depth in 1..=3 {
915 let mut contree: ConTree = ConTree::new(
916 1,
917 depth,
918 100.0,
919 Cost::INFINITY,
920 PointSelector::Mid,
921 0.0,
922 false,
923 true,
924 );
925 contree.fit(&dataset).expect("fit");
926
927 let error = contree.statistics().error;
928 assert!(
929 error <= previous,
930 "depth {depth} scored {error}, worse than depth {} at {previous}",
931 depth - 1
932 );
933 assert!(
934 error < dataset.count() as Cost,
935 "the search reported a nonsense error"
936 );
937 previous = error;
938 }
939
940 Ok(())
941 }
942
943 #[test]
944 fn the_depth_two_specialization_agrees_with_the_general_search() -> Result<(), DataReaderError>
945 {
946 let reader = DataReader::default();
947 let mut dataset = reader.read_file(&fixture("hepatitis.txt"))?;
948 dataset.sort_features();
949
950 let mut general: ConTree = ConTree::new(
953 1,
954 2,
955 100.0,
956 Cost::INFINITY,
957 PointSelector::Mid,
958 0.0,
959 false,
960 false,
961 );
962 general.fit(&dataset).expect("fit");
963
964 let mut specialized: ConTree = ConTree::new(
965 1,
966 2,
967 100.0,
968 Cost::INFINITY,
969 PointSelector::Mid,
970 0.0,
971 false,
972 true,
973 );
974 specialized.fit(&dataset).expect("fit");
975
976 assert_eq!(general.statistics().error, specialized.statistics().error);
977
978 Ok(())
979 }
980}