Skip to main content

contree/data/
view.rs

1use crate::bitsets::{BitCollection, Bitset, BitsetInit};
2use crate::data::{Dataset, Feature};
3
4/// Per-feature Gini scores used to order features and split candidates when
5/// the heuristic ordering is enabled.
6#[derive(Clone, Debug)]
7pub struct HeuristicValues {
8    /// For each feature, indices into its possible splits, best Gini first.
9    gini_per_split: Vec<Vec<usize>>,
10    /// `(best Gini, feature index)` for each feature; sorted best first by
11    /// [`Self::sort_by_gini`].
12    best_gini_per_feature: Vec<(f64, usize)>,
13}
14
15impl HeuristicValues {
16    /// Neutral scores (Gini 1.0) for `num_features` features, in index order.
17    pub fn new(num_features: usize) -> Self {
18        Self {
19            gini_per_split: vec![Vec::new(); num_features],
20            best_gini_per_feature: (0..num_features).map(|i| (1.0, i)).collect(),
21        }
22    }
23
24    /// Stores the split order and best Gini of `feature`. `split_indices` must
25    /// already be sorted best first.
26    pub fn set_feature_ginis(&mut self, feature: usize, split_indices: Vec<usize>, best_gini: f64) {
27        if feature < self.gini_per_split.len() {
28            self.gini_per_split[feature] = split_indices;
29            self.best_gini_per_feature[feature] = (best_gini, feature);
30        }
31    }
32
33    /// Sorts features by their best Gini, lowest first.
34    pub fn sort_by_gini(&mut self) {
35        self.best_gini_per_feature
36            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
37    }
38}
39
40/// A subset of the dataset: the instances that reach one node of the tree.
41///
42/// Each feature column is kept sorted by value, so candidate thresholds are
43/// the positions where the value changes. Splitting a view partitions every
44/// column in one linear pass, preserving the order.
45pub struct DataView<'a> {
46    /// The full dataset the view refers into.
47    pub dataset: &'a Dataset,
48    /// Number of instances in the full dataset.
49    pub total_instances: usize,
50    /// For each feature, positions into `dataset[f]` of the instances in the
51    /// view, in increasing value order.
52    pub feature_columns: Vec<Vec<usize>>,
53    /// For each feature, positions in `feature_columns[f]` where the value
54    /// changes: the candidate thresholds.
55    pub possible_split_indices: Vec<Vec<usize>>,
56    /// Number of instances of each class in the view.
57    pub label_freq: Vec<usize>,
58    /// Whether features and splits are ordered by Gini.
59    pub sort_by_heuristic: bool,
60    /// Gini ordering of features and splits, when `sort_by_heuristic` is set.
61    pub heuristic_values: HeuristicValues,
62    /// The instances in the view; the cache key of the subproblem.
63    pub bitset: Bitset,
64}
65
66impl<'a> DataView<'a> {
67    /// The view of the whole dataset. The dataset's features must be sorted.
68    pub fn root(dataset: &'a Dataset, sort_by_heuristic: bool) -> Self {
69        let total_instances = dataset.count();
70        let mut label_freq = vec![0; dataset.num_labels()];
71        let mut feature_columns = Vec::new();
72        let mut possible_split_indices = Vec::new();
73
74        for (feature_idx, feature) in dataset.into_iter().enumerate() {
75            let feature_len = feature.len();
76
77            let mut splits = Vec::new();
78            let mut last_unique_index: Option<usize> = None;
79
80            for pos in 0..feature_len {
81                let el = &feature[pos];
82
83                if feature_idx == 0 {
84                    label_freq[el.label as usize] += 1;
85                }
86
87                if let Some(last) = last_unique_index {
88                    if el.unique_value_idx != last {
89                        splits.push(pos);
90                    }
91                }
92                last_unique_index = Some(el.unique_value_idx);
93            }
94            possible_split_indices.push(splits);
95            feature_columns.push((0..feature_len).collect::<Vec<usize>>());
96        }
97
98        let mut heuristic_values = HeuristicValues::new(dataset.num_features());
99
100        if sort_by_heuristic {
101            for feature_idx in 0..dataset.num_features() {
102                let feature = &dataset[feature_idx];
103                let idx = &feature_columns[feature_idx];
104                let (ordered_index, gini) = Self::compute_gini_for_all_splits(
105                    feature,
106                    idx,
107                    &possible_split_indices[feature_idx],
108                    &label_freq,
109                    dataset.num_labels(),
110                );
111
112                heuristic_values.set_feature_ginis(feature_idx, ordered_index, gini);
113            }
114            heuristic_values.sort_by_gini();
115        }
116
117        let mut bitset = Bitset::new(BitsetInit::Full(total_instances));
118        bitset.save_count();
119
120        Self {
121            dataset,
122            total_instances,
123            feature_columns,
124            possible_split_indices,
125            label_freq,
126            sort_by_heuristic,
127            heuristic_values,
128            bitset,
129        }
130    }
131
132    /// Number of instances in the view.
133    pub fn get_dataset_size(&self) -> usize {
134        self.feature_columns[0].len()
135    }
136
137    /// Number of features.
138    pub fn get_feature_number(&self) -> usize {
139        self.dataset.num_features()
140    }
141
142    /// The full, sorted column of feature `f`.
143    pub fn get_sorted_feature(&self, f: usize) -> &Feature {
144        &self.dataset[f]
145    }
146
147    /// Positions into [`Self::get_sorted_feature`] of the instances in the
148    /// view, in value order.
149    pub fn get_feature_indices(&self, f: usize) -> &[usize] {
150        &self.feature_columns[f]
151    }
152
153    /// Number of instances of each class in the view.
154    pub fn get_labels_freqs(&self) -> &[usize] {
155        &self.label_freq
156    }
157
158    /// Number of classes in the dataset.
159    pub fn get_num_labels(&self) -> usize {
160        self.dataset.num_labels()
161    }
162
163    /// Candidate thresholds of feature `f`, as positions in the sorted column.
164    pub fn get_possible_split_indices(&self, f: usize) -> &[usize] {
165        &self.possible_split_indices[f]
166    }
167
168    /// Largest number of candidate thresholds of any feature.
169    pub fn get_max_splits(&self) -> usize {
170        self.possible_split_indices
171            .iter()
172            .map(|x| x.len())
173            .max()
174            .unwrap_or(0)
175    }
176
177    /// Indices into the candidate thresholds of `feature`, best Gini first.
178    /// Empty unless the heuristic ordering is enabled.
179    pub fn ordered_possible_splits(&self, feature: usize) -> &[usize] {
180        &self.heuristic_values.gini_per_split[feature]
181    }
182
183    /// `(best Gini, feature)` pairs, best first when the heuristic ordering is
184    /// enabled and in feature order otherwise.
185    pub fn features_best_score(&self) -> &[(f64, usize)] {
186        &self.heuristic_values.best_gini_per_feature
187    }
188
189    #[inline]
190    fn compute_split_indices_for(col: &Feature, idxs: &[usize]) -> Vec<usize> {
191        if idxs.is_empty() {
192            return Vec::new();
193        }
194        let mut out = Vec::with_capacity(idxs.len() / 10);
195        let mut last = None::<usize>;
196
197        for (i, &pos) in idxs.iter().enumerate() {
198            let cur = col[pos].unique_value_idx;
199            if i > 0 && Some(cur) != last {
200                out.push(i);
201            }
202            last = Some(cur);
203        }
204
205        out.shrink_to_fit();
206        out
207    }
208
209    /// Fills the class histograms of both sides of a split of `feature_index`
210    /// at `split_point`. Both slices must start zeroed.
211    pub fn initialize_split_parameters(
212        &self,
213        feature_index: usize,
214        split_point: usize,
215        left_freq: &mut [usize],
216        right_freq: &mut [usize],
217    ) {
218        let num_labels = self.dataset.num_labels();
219
220        let total_size = self.get_dataset_size();
221        let feature_ids = self.get_feature_indices(feature_index);
222        let feature = self.get_sorted_feature(feature_index);
223
224        // Count the smaller side and derive the other by subtraction.
225        if 2 * split_point < total_size {
226            for i in 0..split_point {
227                let data_point = &feature[feature_ids[i]];
228                left_freq[data_point.label as usize] += 1;
229            }
230            for label in 0..num_labels {
231                right_freq[label] = self.label_freq[label] - left_freq[label];
232            }
233        } else {
234            for i in split_point..total_size {
235                let data_point = &feature[feature_ids[i]];
236                right_freq[data_point.label as usize] += 1;
237            }
238            for label in 0..num_labels {
239                left_freq[label] = self.label_freq[label] - right_freq[label];
240            }
241        }
242    }
243
244    /// Computes the weighted Gini of every candidate threshold of a feature.
245    ///
246    /// Returns the indices of the candidates sorted best first, and the best
247    /// Gini value.
248    fn compute_gini_for_all_splits(
249        feature: &Feature,
250        idx: &[usize],
251        possible_splits: &[usize],
252        label_freq: &[usize],
253        num_labels: usize,
254    ) -> (Vec<usize>, f64) {
255        if feature.is_empty() || possible_splits.is_empty() {
256            return (Vec::new(), 1.0);
257        }
258
259        let mut gini_values = Vec::with_capacity(possible_splits.len());
260        let mut left_label_freq = vec![0usize; num_labels];
261        let mut right_label_freq = label_freq.to_vec();
262        let mut best_gini = 1.0;
263
264        let mut last_pos = 0;
265
266        for (split_idx, &split_pos) in possible_splits.iter().enumerate() {
267            // Move the instances before this threshold to the left side.
268            for &data_idx in &idx[last_pos..split_pos] {
269                let label = feature[data_idx].label as usize;
270                right_label_freq[label] -= 1;
271                left_label_freq[label] += 1;
272            }
273
274            let left_count = split_pos;
275            let right_count = idx.len() - left_count;
276
277            let gini = Self::compute_gini_from_frequencies(
278                &left_label_freq,
279                &right_label_freq,
280                left_count,
281                right_count,
282            );
283
284            if gini < best_gini {
285                best_gini = gini;
286            }
287
288            gini_values.push((split_idx, gini));
289            last_pos = split_pos;
290        }
291
292        gini_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
293
294        let sorted_indices: Vec<usize> = gini_values.into_iter().map(|(idx, _)| idx).collect();
295
296        (sorted_indices, best_gini)
297    }
298
299    /// Weighted Gini impurity of a split, from the class histograms of its
300    /// two sides.
301    #[inline]
302    fn compute_gini_from_frequencies(
303        left_freq: &[usize],
304        right_freq: &[usize],
305        left_count: usize,
306        right_count: usize,
307    ) -> f64 {
308        let mut left_gini = 1.0;
309        let mut right_gini = 1.0;
310
311        if left_count > 0 {
312            for &freq in left_freq {
313                let prob = freq as f64 / left_count as f64;
314                left_gini -= prob * prob;
315            }
316        }
317
318        if right_count > 0 {
319            for &freq in right_freq {
320                let prob = freq as f64 / right_count as f64;
321                right_gini -= prob * prob;
322            }
323        }
324
325        let total = left_count + right_count;
326        if total > 0 {
327            (left_gini * left_count as f64 + right_gini * right_count as f64) / total as f64
328        } else {
329            1.0
330        }
331    }
332
333    /// Splits the view on feature `sf` at position `split_point` of its sorted
334    /// column. The left view holds the instances before the position.
335    pub fn split(&self, sf: usize, split_point: usize) -> (Self, Self) {
336        #[cfg(feature = "profiling")]
337        coz::scope!("Split view");
338        let col = &self.dataset[sf];
339        let sf_idxs = &self.feature_columns[sf];
340
341        let (sf_left_idxs, sf_right_idxs) = sf_idxs.split_at(split_point);
342
343        let mut left_bitset = Bitset::new(BitsetInit::Empty(self.total_instances));
344        let mut left_label_freq = vec![0; self.dataset.num_labels()];
345
346        for &pos in sf_left_idxs {
347            let el = &col[pos];
348            left_bitset.set(el.tid);
349            left_label_freq[el.label as usize] += 1;
350        }
351        left_bitset.save_count();
352
353        let mut right_label_freq = vec![0; self.dataset.num_labels()];
354        for label in 0..self.dataset.num_labels() {
355            right_label_freq[label] = self.label_freq[label] - left_label_freq[label];
356        }
357
358        let num_features = self.get_feature_number();
359        let left_size_estimate = split_point;
360        let right_size_estimate = self.get_dataset_size() - split_point;
361
362        let mut left_pfi: Vec<Vec<usize>> = (0..num_features)
363            .map(|_| Vec::with_capacity(left_size_estimate))
364            .collect();
365        let mut right_pfi: Vec<Vec<usize>> = (0..num_features)
366            .map(|_| Vec::with_capacity(right_size_estimate))
367            .collect();
368
369        let mut left_split_indices: Vec<Vec<usize>> = (0..num_features)
370            .map(|_| Vec::with_capacity(left_size_estimate / 10))
371            .collect();
372        let mut right_split_indices: Vec<Vec<usize>> = (0..num_features)
373            .map(|_| Vec::with_capacity(right_size_estimate / 10))
374            .collect();
375
376        let mut left_heuristics = HeuristicValues::new(num_features);
377        let mut right_heuristics = HeuristicValues::new(num_features);
378
379        // Scratch buffers for the Gini pass, reused across features. They are
380        // empty when the heuristic ordering is off.
381        let num_labels = self.dataset.num_labels();
382        let heuristic_width = if self.sort_by_heuristic {
383            num_labels
384        } else {
385            0
386        };
387        let mut left_label_running = vec![0usize; heuristic_width];
388        let mut right_label_running = vec![0usize; heuristic_width];
389        let mut left_label_remaining = vec![0usize; heuristic_width];
390        let mut right_label_remaining = vec![0usize; heuristic_width];
391        let mut left_gini_values: Vec<(usize, f64)> = Vec::new();
392        let mut right_gini_values: Vec<(usize, f64)> = Vec::new();
393
394        for f in 0..num_features {
395            if f == sf {
396                // The split feature is already partitioned by `split_at`.
397                left_pfi[f] = sf_left_idxs.to_vec();
398                right_pfi[f] = sf_right_idxs.to_vec();
399
400                left_split_indices[f] =
401                    Self::compute_split_indices_for(&self.dataset[f], sf_left_idxs);
402                right_split_indices[f] =
403                    Self::compute_split_indices_for(&self.dataset[f], sf_right_idxs);
404
405                if self.sort_by_heuristic {
406                    let (ordered_splits, best_gini) = Self::compute_gini_for_all_splits(
407                        &self.dataset[f],
408                        &left_pfi[f],
409                        &left_split_indices[f],
410                        &left_label_freq,
411                        self.dataset.num_labels(),
412                    );
413                    left_heuristics.set_feature_ginis(f, ordered_splits, best_gini);
414
415                    let (ordered_splits, best_gini) = Self::compute_gini_for_all_splits(
416                        &self.dataset[f],
417                        &right_pfi[f],
418                        &right_split_indices[f],
419                        &right_label_freq,
420                        self.dataset.num_labels(),
421                    );
422                    right_heuristics.set_feature_ginis(f, ordered_splits, best_gini);
423                }
424                continue;
425            }
426
427            let fcol = &self.dataset[f];
428            let parent_idxs = &self.feature_columns[f];
429
430            let mut left_last_unique: Option<usize> = None;
431            let mut right_last_unique: Option<usize> = None;
432            let mut left_counter = 0;
433            let mut right_counter = 0;
434
435            let mut left_best_gini = 1.0;
436            let mut right_best_gini = 1.0;
437
438            if self.sort_by_heuristic {
439                left_gini_values.clear();
440                right_gini_values.clear();
441                left_label_running.fill(0);
442                right_label_running.fill(0);
443                left_label_remaining.copy_from_slice(&left_label_freq);
444                right_label_remaining.copy_from_slice(&right_label_freq);
445            }
446
447            for &pos in parent_idxs {
448                let el = &fcol[pos];
449                let row = el.tid;
450                let label = el.label as usize;
451
452                if left_bitset.contains(row) {
453                    left_pfi[f].push(pos);
454
455                    if let Some(last) = left_last_unique {
456                        if el.unique_value_idx != last {
457                            left_split_indices[f].push(left_counter);
458
459                            if self.sort_by_heuristic {
460                                let gini = Self::compute_gini_from_frequencies(
461                                    &left_label_running,
462                                    &left_label_remaining,
463                                    left_counter,
464                                    left_size_estimate - left_counter,
465                                );
466                                if gini < left_best_gini {
467                                    left_best_gini = gini;
468                                }
469                                left_gini_values.push((left_split_indices[f].len() - 1, gini));
470                            }
471                        }
472                    }
473                    left_last_unique = Some(el.unique_value_idx);
474                    left_counter += 1;
475
476                    if self.sort_by_heuristic {
477                        left_label_remaining[label] -= 1;
478                        left_label_running[label] += 1;
479                    }
480                } else {
481                    right_pfi[f].push(pos);
482
483                    if let Some(last) = right_last_unique {
484                        if el.unique_value_idx != last {
485                            right_split_indices[f].push(right_counter);
486
487                            if self.sort_by_heuristic {
488                                let gini = Self::compute_gini_from_frequencies(
489                                    &right_label_running,
490                                    &right_label_remaining,
491                                    right_counter,
492                                    right_size_estimate - right_counter,
493                                );
494                                if gini < right_best_gini {
495                                    right_best_gini = gini;
496                                }
497                                right_gini_values.push((right_split_indices[f].len() - 1, gini));
498                            }
499                        }
500                    }
501                    right_last_unique = Some(el.unique_value_idx);
502                    right_counter += 1;
503
504                    if self.sort_by_heuristic {
505                        right_label_remaining[label] -= 1;
506                        right_label_running[label] += 1;
507                    }
508                }
509            }
510
511            if self.sort_by_heuristic {
512                left_gini_values
513                    .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
514                right_gini_values
515                    .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
516
517                let left_sorted_indices: Vec<usize> =
518                    left_gini_values.iter().map(|&(idx, _)| idx).collect();
519                let right_sorted_indices: Vec<usize> =
520                    right_gini_values.iter().map(|&(idx, _)| idx).collect();
521
522                left_heuristics.set_feature_ginis(f, left_sorted_indices, left_best_gini);
523                right_heuristics.set_feature_ginis(f, right_sorted_indices, right_best_gini);
524            }
525
526            // No `shrink_to_fit`: a view only lives while its node is expanded.
527        }
528
529        let mut right_bitset = self.bitset.intersect_with(&left_bitset, true);
530        right_bitset.save_count();
531
532        if self.sort_by_heuristic {
533            left_heuristics.sort_by_gini();
534            right_heuristics.sort_by_gini();
535        }
536
537        let left = Self {
538            dataset: self.dataset,
539            total_instances: self.total_instances,
540            feature_columns: left_pfi,
541            possible_split_indices: left_split_indices,
542            label_freq: left_label_freq,
543            sort_by_heuristic: self.sort_by_heuristic,
544            heuristic_values: left_heuristics,
545            bitset: left_bitset,
546        };
547
548        let right = Self {
549            dataset: self.dataset,
550            total_instances: self.total_instances,
551            feature_columns: right_pfi,
552            possible_split_indices: right_split_indices,
553            label_freq: right_label_freq,
554            sort_by_heuristic: self.sort_by_heuristic,
555            heuristic_values: right_heuristics,
556            bitset: right_bitset,
557        };
558
559        (left, right)
560    }
561
562    /// Number of instances in the view.
563    pub fn len(&self) -> usize {
564        debug_assert!(
565            self.bitset.count() == self.feature_columns[0].len(),
566            "Mismatched size in bitset and feature columns"
567        );
568        self.feature_columns[0].len()
569    }
570
571    /// Whether the view has no instances.
572    pub fn is_empty(&self) -> bool {
573        self.len() == 0
574    }
575}
576
577#[cfg(test)]
578mod data_view_tests {
579    use crate::bitsets::BitCollection;
580    use crate::data::view::DataView;
581    use crate::reader::data_reader::DataReader;
582    use crate::reader::DataReaderError;
583    use crate::tests::fixture;
584
585    #[test]
586    fn root_view_covers_the_whole_dataset() -> Result<(), DataReaderError> {
587        let reader = DataReader::default();
588        let mut dataset = reader.read_file(&fixture("avila_1k.txt"))?;
589        dataset.sort_features();
590        dataset.compute_unique_feature_values();
591
592        let view = DataView::root(&dataset, true);
593
594        assert_eq!(view.len(), dataset.count());
595        assert_eq!(view.bitset.count(), dataset.count());
596        assert_eq!(view.get_feature_number(), dataset.num_features());
597        assert_eq!(
598            view.get_labels_freqs().iter().sum::<usize>(),
599            dataset.count(),
600            "the root label histogram must account for every instance"
601        );
602
603        // Split candidates are positions strictly inside the column, and are
604        // reported in increasing order.
605        for f in 0..dataset.num_features() {
606            let splits = view.get_possible_split_indices(f);
607            assert!(splits.windows(2).all(|w| w[0] < w[1]));
608            assert!(splits.iter().all(|&i| i > 0 && i < dataset.count()));
609        }
610
611        Ok(())
612    }
613
614    #[test]
615    fn split_partitions_the_view_without_loss() -> Result<(), DataReaderError> {
616        let reader = DataReader::default();
617        let mut dataset = reader.read_file(&fixture("avila_1k.txt"))?;
618        dataset.sort_features();
619        dataset.compute_unique_feature_values();
620
621        let view = DataView::root(&dataset, false);
622        let feature = 4;
623        let split = view.get_possible_split_indices(feature)[3];
624        let (left, right) = view.split(feature, split);
625
626        assert_eq!(left.len() + right.len(), view.len());
627        assert_eq!(left.bitset.count(), left.len());
628        assert_eq!(right.bitset.count(), right.len());
629        for label in 0..dataset.num_labels() {
630            assert_eq!(
631                left.get_labels_freqs()[label] + right.get_labels_freqs()[label],
632                view.get_labels_freqs()[label]
633            );
634        }
635
636        Ok(())
637    }
638}