Skip to main content

ternary_trees/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Decision trees and forests for ternary classification on {-1, 0, +1}.
4//!
5//! Provides TernaryDecisionTree with ternary splits, RandomForest with ternary voting,
6//! feature importance, and pruning with ternary entropy.
7
8/// A ternary value.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum Ternary {
11    Neg,
12    Zero,
13    Pos,
14}
15
16impl Ternary {
17    pub fn to_i8(self) -> i8 {
18        match self {
19            Ternary::Neg => -1,
20            Ternary::Zero => 0,
21            Ternary::Pos => 1,
22        }
23    }
24
25    pub fn from_i8(v: i8) -> Option<Self> {
26        match v {
27            -1 => Some(Ternary::Neg),
28            0 => Some(Ternary::Zero),
29            1 => Some(Ternary::Pos),
30            _ => None,
31        }
32    }
33
34    pub fn values() -> [Ternary; 3] {
35        [Ternary::Neg, Ternary::Zero, Ternary::Pos]
36    }
37}
38
39/// Feature value type for ternary features.
40pub type Feature = i8;
41
42/// A training sample: features + label.
43pub type Sample = (Vec<Feature>, Ternary);
44
45/// Compute ternary entropy.
46pub fn ternary_entropy(counts: [usize; 3]) -> f64 {
47    let total = counts[0] + counts[1] + counts[2] as usize;
48    if total == 0 {
49        return 0.0;
50    }
51    let mut h = 0.0;
52    for &c in &counts {
53        if c > 0 {
54            let p = c as f64 / total as f64;
55            h -= p * p.log2();
56        }
57    }
58    h
59}
60
61/// Count ternary labels in a slice.
62pub fn count_labels(samples: &[Sample]) -> [usize; 3] {
63    let mut counts = [0usize; 3];
64    for (_, label) in samples {
65        match label {
66            Ternary::Neg => counts[0] += 1,
67            Ternary::Zero => counts[1] += 1,
68            Ternary::Pos => counts[2] += 1,
69        }
70    }
71    counts
72}
73
74/// Majority vote label.
75pub fn majority(samples: &[Sample]) -> Ternary {
76    let counts = count_labels(samples);
77    if counts[0] >= counts[1] && counts[0] >= counts[2] {
78        Ternary::Neg
79    } else if counts[1] >= counts[2] {
80        Ternary::Zero
81    } else {
82        Ternary::Pos
83    }
84}
85
86/// A decision tree node.
87#[derive(Debug, Clone)]
88pub enum TreeNode {
89    Leaf {
90        label: Ternary,
91        confidence: f64,
92        count: usize,
93    },
94    Internal {
95        feature_idx: usize,
96        threshold: Feature,
97        left: Box<TreeNode>,   // value < threshold
98        middle: Box<TreeNode>, // value == threshold
99        right: Box<TreeNode>,  // value > threshold
100    },
101}
102
103/// Ternary decision tree.
104#[derive(Debug, Clone)]
105pub struct TernaryDecisionTree {
106    root: Option<TreeNode>,
107    max_depth: usize,
108    min_samples: usize,
109}
110
111impl TernaryDecisionTree {
112    pub fn new(max_depth: usize, min_samples: usize) -> Self {
113        TernaryDecisionTree {
114            root: None,
115            max_depth,
116            min_samples,
117        }
118    }
119
120    pub fn fit(&mut self, samples: &[Sample]) {
121        self.root = Some(self.build_tree(samples, 0));
122    }
123
124    fn build_tree(&self, samples: &[Sample], depth: usize) -> TreeNode {
125        let counts = count_labels(samples);
126        let total = counts[0] + counts[1] + counts[2];
127        let majority_label = majority(samples);
128        let confidence = if total > 0 {
129            let max_c = counts[0].max(counts[1]).max(counts[2]);
130            max_c as f64 / total as f64
131        } else {
132            0.0
133        };
134
135        if depth >= self.max_depth || total <= self.min_samples || confidence >= 0.95 {
136            return TreeNode::Leaf {
137                label: majority_label,
138                confidence,
139                count: total,
140            };
141        }
142
143        let n_features = samples[0].0.len();
144        let mut best_gain = 0.0f64;
145        let mut best_feat = 0;
146        let mut best_thresh = 0i8;
147
148        let parent_entropy = ternary_entropy(counts);
149
150        for fi in 0..n_features {
151            for &thresh in &[-1i8, 0i8] {
152                let mut left_counts = [0usize; 3];
153                let mut mid_counts = [0usize; 3];
154                let mut right_counts = [0usize; 3];
155                let mut left_total = 0;
156                let mut mid_total = 0;
157                let mut right_total = 0;
158
159                for (features, label) in samples {
160                    let val = features[fi];
161                    let idx = match label {
162                        Ternary::Neg => 0,
163                        Ternary::Zero => 1,
164                        Ternary::Pos => 2,
165                    };
166                    if val < thresh {
167                        left_counts[idx] += 1;
168                        left_total += 1;
169                    } else if val == thresh {
170                        mid_counts[idx] += 1;
171                        mid_total += 1;
172                    } else {
173                        right_counts[idx] += 1;
174                        right_total += 1;
175                    }
176                }
177
178                if left_total == 0 || mid_total == 0 || right_total == 0 {
179                    continue;
180                }
181
182                let child_entropy = (left_total as f64 * ternary_entropy(left_counts)
183                    + mid_total as f64 * ternary_entropy(mid_counts)
184                    + right_total as f64 * ternary_entropy(right_counts))
185                    / total as f64;
186
187                let gain = parent_entropy - child_entropy;
188                if gain > best_gain {
189                    best_gain = gain;
190                    best_feat = fi;
191                    best_thresh = thresh;
192                }
193            }
194        }
195
196        if best_gain < 1e-10 {
197            return TreeNode::Leaf {
198                label: majority_label,
199                confidence,
200                count: total,
201            };
202        }
203
204        let mut left_samples = Vec::new();
205        let mut mid_samples = Vec::new();
206        let mut right_samples = Vec::new();
207        for s in samples {
208            match s.0[best_feat].cmp(&best_thresh) {
209                std::cmp::Ordering::Less => left_samples.push(s.clone()),
210                std::cmp::Ordering::Equal => mid_samples.push(s.clone()),
211                std::cmp::Ordering::Greater => right_samples.push(s.clone()),
212            }
213        }
214
215        TreeNode::Internal {
216            feature_idx: best_feat,
217            threshold: best_thresh,
218            left: Box::new(self.build_tree(&left_samples, depth + 1)),
219            middle: Box::new(self.build_tree(&mid_samples, depth + 1)),
220            right: Box::new(self.build_tree(&right_samples, depth + 1)),
221        }
222    }
223
224    pub fn predict(&self, features: &[Feature]) -> Option<Ternary> {
225        self.root.as_ref().map(|node| Self::predict_node(node, features))
226    }
227
228    fn predict_node(node: &TreeNode, features: &[Feature]) -> Ternary {
229        match node {
230            TreeNode::Leaf { label, .. } => *label,
231            TreeNode::Internal {
232                feature_idx,
233                threshold,
234                left,
235                middle,
236                right,
237            } => {
238                let val = features[*feature_idx];
239                match val.cmp(threshold) {
240                    std::cmp::Ordering::Less => Self::predict_node(left, features),
241                    std::cmp::Ordering::Equal => Self::predict_node(middle, features),
242                    std::cmp::Ordering::Greater => Self::predict_node(right, features),
243                }
244            }
245        }
246    }
247
248    /// Count the number of leaves.
249    pub fn count_leaves(&self) -> usize {
250        self.root.as_ref().map_or(0, Self::count_leaves_node)
251    }
252
253    fn count_leaves_node(node: &TreeNode) -> usize {
254        match node {
255            TreeNode::Leaf { .. } => 1,
256            TreeNode::Internal {
257                left, middle, right, ..
258            } => {
259                Self::count_leaves_node(left)
260                    + Self::count_leaves_node(middle)
261                    + Self::count_leaves_node(right)
262            }
263        }
264    }
265
266    /// Prune: convert internal nodes to leaves if it doesn't hurt accuracy.
267    pub fn prune(&mut self, validation: &[Sample]) {
268        if let Some(root) = self.root.take() {
269            self.root = Some(self.prune_node(root, validation));
270        }
271    }
272
273    fn prune_node(&self, node: TreeNode, validation: &[Sample]) -> TreeNode {
274        match node {
275            TreeNode::Leaf { .. } => node,
276            TreeNode::Internal {
277                feature_idx,
278                threshold,
279                left,
280                middle,
281                right,
282            } => {
283                let left = Box::new(self.prune_node(*left, validation));
284                let middle = Box::new(self.prune_node(*middle, validation));
285                let right = Box::new(self.prune_node(*right, validation));
286
287                let internal_node = TreeNode::Internal {
288                    feature_idx,
289                    threshold,
290                    left: left.clone(),
291                    middle: middle.clone(),
292                    right: right.clone(),
293                };
294
295                // Compute accuracy with internal node
296                let internal_acc = validation
297                    .iter()
298                    .filter(|(f, l)| Self::predict_node(&internal_node, f) == *l)
299                    .count();
300
301                // Try converting to leaf
302                let filtered: Vec<Sample> = validation.to_vec();
303                let leaf_label = majority(&filtered);
304                let leaf_node = TreeNode::Leaf {
305                    label: leaf_label,
306                    confidence: 1.0,
307                    count: filtered.len(),
308                };
309                let leaf_acc = validation
310                    .iter()
311                    .filter(|(_, l)| leaf_label == *l)
312                    .count();
313
314                if leaf_acc >= internal_acc {
315                    leaf_node
316                } else {
317                    internal_node
318                }
319            }
320        }
321    }
322}
323
324/// Random forest for ternary classification.
325#[derive(Debug, Clone)]
326pub struct RandomForest {
327    pub trees: Vec<TernaryDecisionTree>,
328    pub n_features_per_split: usize,
329}
330
331impl RandomForest {
332    pub fn new(n_trees: usize, max_depth: usize, min_samples: usize, n_features_per_split: usize) -> Self {
333        let trees = (0..n_trees)
334            .map(|_| TernaryDecisionTree::new(max_depth, min_samples))
335            .collect();
336        RandomForest {
337            trees,
338            n_features_per_split,
339        }
340    }
341
342    pub fn fit(&mut self, samples: &[Sample]) {
343        let n = samples.len();
344        for tree in &mut self.trees {
345            // Bootstrap sample
346            let mut bootstrap = Vec::with_capacity(n);
347            for _ in 0..n {
348                let idx = simple_hash(n) % n;
349                bootstrap.push(samples[idx].clone());
350            }
351            tree.fit(&bootstrap);
352        }
353    }
354
355    pub fn predict(&self, features: &[Feature]) -> Ternary {
356        let mut counts = [0usize; 3];
357        for tree in &self.trees {
358            if let Some(label) = tree.predict(features) {
359                match label {
360                    Ternary::Neg => counts[0] += 1,
361                    Ternary::Zero => counts[1] += 1,
362                    Ternary::Pos => counts[2] += 1,
363                }
364            }
365        }
366        if counts[0] >= counts[1] && counts[0] >= counts[2] {
367            Ternary::Neg
368        } else if counts[1] >= counts[2] {
369            Ternary::Zero
370        } else {
371            Ternary::Pos
372        }
373    }
374
375    /// Compute feature importance via permutation.
376    pub fn feature_importance(&self, samples: &[Sample], n_features: usize) -> Vec<f64> {
377        let baseline_acc = self.accuracy(samples);
378        let mut importance = vec![0.0; n_features];
379
380        for fi in 0..n_features {
381            let mut permuted = samples.to_vec();
382            // Simple permutation: reverse order of feature fi
383            let n = permuted.len();
384            for i in 0..n / 2 {
385                let j = n - 1 - i;
386                let tmp = permuted[i].0[fi];
387                permuted[i].0[fi] = permuted[j].0[fi];
388                permuted[j].0[fi] = tmp;
389            }
390            let perm_acc = self.accuracy(&permuted);
391            importance[fi] = baseline_acc - perm_acc;
392        }
393        importance
394    }
395
396    pub fn accuracy(&self, samples: &[Sample]) -> f64 {
397        if samples.is_empty() {
398            return 0.0;
399        }
400        let correct = samples
401            .iter()
402            .filter(|(f, l)| self.predict(f) == *l)
403            .count();
404        correct as f64 / samples.len() as f64
405    }
406}
407
408/// Simple deterministic hash for bootstrap sampling.
409fn simple_hash(n: usize) -> usize {
410    n.wrapping_mul(1103515245).wrapping_add(12345)
411}
412
413/// Compute Gini impurity for ternary labels.
414pub fn gini_impurity(samples: &[Sample]) -> f64 {
415    let counts = count_labels(samples);
416    let total = counts[0] + counts[1] + counts[2];
417    if total == 0 {
418        return 0.0;
419    }
420    let mut gini = 0.0;
421    for &c in &counts {
422        let p = c as f64 / total as f64;
423        gini += p * p;
424    }
425    1.0 - gini
426}
427
428/// Compute information gain for a split.
429pub fn information_gain(parent: &[Sample], children: &[&[Sample]]) -> f64 {
430    let parent_h = ternary_entropy(count_labels(parent));
431    let parent_n = parent.len() as f64;
432    let mut child_h = 0.0;
433    for child in children {
434        let w = child.len() as f64 / parent_n;
435        child_h += w * ternary_entropy(count_labels(child));
436    }
437    parent_h - child_h
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_ternary_values() {
446        assert_eq!(Ternary::Neg.to_i8(), -1);
447        assert_eq!(Ternary::Zero.to_i8(), 0);
448        assert_eq!(Ternary::Pos.to_i8(), 1);
449    }
450
451    #[test]
452    fn test_ternary_entropy_uniform() {
453        let h = ternary_entropy([10, 10, 10]);
454        assert!((h - (3.0f64.log2())).abs() < 1e-10);
455    }
456
457    #[test]
458    fn test_ternary_entropy_pure() {
459        let h = ternary_entropy([100, 0, 0]);
460        assert!(h.abs() < 1e-10);
461    }
462
463    #[test]
464    fn test_count_labels() {
465        let samples = vec![
466            (vec![0], Ternary::Neg),
467            (vec![0], Ternary::Neg),
468            (vec![0], Ternary::Pos),
469        ];
470        let counts = count_labels(&samples);
471        assert_eq!(counts, [2, 0, 1]);
472    }
473
474    #[test]
475    fn test_majority() {
476        let samples = vec![
477            (vec![0], Ternary::Pos),
478            (vec![0], Ternary::Pos),
479            (vec![0], Ternary::Neg),
480        ];
481        assert_eq!(majority(&samples), Ternary::Pos);
482    }
483
484    #[test]
485    fn test_decision_tree_fit_predict() {
486        let samples = vec![
487            (vec![-1, -1], Ternary::Neg),
488            (vec![-1, 0], Ternary::Neg),
489            (vec![1, 1], Ternary::Pos),
490            (vec![1, 0], Ternary::Pos),
491            (vec![0, 0], Ternary::Zero),
492            (vec![0, 0], Ternary::Zero),
493        ];
494        let mut tree = TernaryDecisionTree::new(5, 1);
495        tree.fit(&samples);
496        assert_eq!(tree.predict(&[-1, -1]), Some(Ternary::Neg));
497        assert_eq!(tree.predict(&[1, 1]), Some(Ternary::Pos));
498    }
499
500    #[test]
501    fn test_decision_tree_single_class() {
502        let samples = vec![
503            (vec![1], Ternary::Pos),
504            (vec![1], Ternary::Pos),
505            (vec![1], Ternary::Pos),
506        ];
507        let mut tree = TernaryDecisionTree::new(5, 1);
508        tree.fit(&samples);
509        assert_eq!(tree.predict(&[1]), Some(Ternary::Pos));
510    }
511
512    #[test]
513    fn test_tree_leaves_count() {
514        let samples = vec![
515            (vec![-1], Ternary::Neg),
516            (vec![0], Ternary::Zero),
517            (vec![1], Ternary::Pos),
518        ];
519        let mut tree = TernaryDecisionTree::new(10, 1);
520        tree.fit(&samples);
521        assert!(tree.count_leaves() >= 1);
522    }
523
524    #[test]
525    fn test_gini_impurity() {
526        let pure = vec![(vec![0], Ternary::Pos)];
527        assert!(gini_impurity(&pure).abs() < 1e-10);
528
529        let mixed = vec![
530            (vec![0], Ternary::Neg),
531            (vec![0], Ternary::Zero),
532            (vec![0], Ternary::Pos),
533        ];
534        let g = gini_impurity(&mixed);
535        assert!((g - (1.0_f64 - 3.0_f64 * (1.0_f64 / 3.0_f64).powi(2))).abs() < 1e-10);
536    }
537
538    #[test]
539    fn test_information_gain() {
540        let parent = vec![
541            (vec![0], Ternary::Neg),
542            (vec![0], Ternary::Neg),
543            (vec![0], Ternary::Pos),
544            (vec![0], Ternary::Pos),
545        ];
546        let left = &parent[0..2];
547        let right = &parent[2..4];
548        let gain = information_gain(&parent, &[left, right]);
549        assert!(gain > 0.0);
550    }
551
552    #[test]
553    fn test_random_forest() {
554        let samples = vec![
555            (vec![-1, -1], Ternary::Neg),
556            (vec![-1, 0], Ternary::Neg),
557            (vec![1, 1], Ternary::Pos),
558            (vec![1, 0], Ternary::Pos),
559            (vec![0, 0], Ternary::Zero),
560            (vec![0, 0], Ternary::Zero),
561            (vec![-1, -1], Ternary::Neg),
562            (vec![1, 1], Ternary::Pos),
563        ];
564        let mut rf = RandomForest::new(5, 5, 1, 2);
565        rf.fit(&samples);
566        let pred = rf.predict(&[-1, -1]);
567        assert!(pred == Ternary::Neg);
568    }
569
570    #[test]
571    fn test_random_forest_accuracy() {
572        let samples = vec![
573            (vec![-1], Ternary::Neg),
574            (vec![0], Ternary::Zero),
575            (vec![1], Ternary::Pos),
576            (vec![-1], Ternary::Neg),
577            (vec![0], Ternary::Zero),
578            (vec![1], Ternary::Pos),
579        ];
580        let mut rf = RandomForest::new(5, 5, 1, 1);
581        rf.fit(&samples);
582        let acc = rf.accuracy(&samples);
583        assert!(acc > 0.0);
584    }
585
586    #[test]
587    fn test_feature_importance() {
588        let samples = vec![
589            (vec![-1, 0], Ternary::Neg),
590            (vec![1, 0], Ternary::Pos),
591            (vec![0, -1], Ternary::Zero),
592            (vec![-1, 0], Ternary::Neg),
593            (vec![1, 0], Ternary::Pos),
594            (vec![0, 1], Ternary::Zero),
595        ];
596        let mut rf = RandomForest::new(5, 5, 1, 2);
597        rf.fit(&samples);
598        let importance = rf.feature_importance(&samples, 2);
599        assert_eq!(importance.len(), 2);
600    }
601
602    #[test]
603    fn test_tree_prune() {
604        let samples = vec![
605            (vec![-1], Ternary::Neg),
606            (vec![0], Ternary::Zero),
607            (vec![1], Ternary::Pos),
608            (vec![-1], Ternary::Neg),
609            (vec![0], Ternary::Zero),
610            (vec![1], Ternary::Pos),
611        ];
612        let mut tree = TernaryDecisionTree::new(10, 1);
613        tree.fit(&samples);
614        let leaves_before = tree.count_leaves();
615        tree.prune(&samples);
616        // Pruning should not increase leaf count beyond reasonable bounds
617        assert!(tree.count_leaves() <= leaves_before || leaves_before <= 3);
618    }
619
620    #[test]
621    fn test_empty_samples_entropy() {
622        let h = ternary_entropy([0, 0, 0]);
623        assert!(h.abs() < 1e-10);
624    }
625
626    #[test]
627    fn test_predict_no_fit() {
628        let tree = TernaryDecisionTree::new(5, 1);
629        assert!(tree.predict(&[0]).is_none());
630    }
631
632    #[test]
633    fn test_from_i8() {
634        assert_eq!(Ternary::from_i8(-1), Some(Ternary::Neg));
635        assert_eq!(Ternary::from_i8(2), None);
636    }
637
638    #[test]
639    fn test_forest_empty_accuracy() {
640        let rf = RandomForest::new(3, 5, 1, 1);
641        assert_eq!(rf.accuracy(&[]), 0.0);
642    }
643
644    #[test]
645    fn test_ternary_values_array() {
646        let vals = Ternary::values();
647        assert_eq!(vals.len(), 3);
648        assert_eq!(vals[0], Ternary::Neg);
649        assert_eq!(vals[1], Ternary::Zero);
650        assert_eq!(vals[2], Ternary::Pos);
651    }
652
653    #[test]
654    fn test_gini_pure_vs_mixed() {
655        let pure = vec![(vec![0], Ternary::Pos), (vec![0], Ternary::Pos)];
656        let mixed = vec![(vec![0], Ternary::Neg), (vec![0], Ternary::Pos)];
657        assert!(gini_impurity(&pure) < gini_impurity(&mixed));
658    }
659
660    #[test]
661    fn test_decision_tree_zero_features() {
662        let samples = vec![
663            (vec![], Ternary::Neg),
664            (vec![], Ternary::Pos),
665        ];
666        let mut tree = TernaryDecisionTree::new(5, 1);
667        tree.fit(&samples);
668        // Should still produce a prediction
669        assert!(tree.predict(&[]).is_some());
670    }
671}