1#![forbid(unsafe_code)]
2
3#[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
39pub type Feature = i8;
41
42pub type Sample = (Vec<Feature>, Ternary);
44
45pub 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
61pub 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
74pub 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#[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>, middle: Box<TreeNode>, right: Box<TreeNode>, },
101}
102
103#[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 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 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 let internal_acc = validation
297 .iter()
298 .filter(|(f, l)| Self::predict_node(&internal_node, f) == *l)
299 .count();
300
301 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#[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 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 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 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
408fn simple_hash(n: usize) -> usize {
410 n.wrapping_mul(1103515245).wrapping_add(12345)
411}
412
413pub 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
428pub 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 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 assert!(tree.predict(&[]).is_some());
670 }
671}