1use crate::{Predict, Result, ShapError};
2use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5pub enum MissingBranch {
6 Left,
7 Right,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11pub enum SplitComparison {
12 LessThan,
13 LessThanOrEqual,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub enum MissingValuePolicy {
18 NaN,
19 Zero,
20 None,
21}
22
23#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
26pub enum Node {
27 Leaf {
28 values: Vec<f64>,
29 cover: f64,
30 },
31 Split {
32 feature: usize,
33 threshold: f64,
34 left: usize,
35 right: usize,
36 missing: MissingBranch,
37 cover: f64,
38 },
39 NumericalSplit {
40 feature: usize,
41 threshold: f64,
42 comparison: SplitComparison,
43 left: usize,
44 right: usize,
45 missing: MissingBranch,
46 missing_value: MissingValuePolicy,
47 cover: f64,
48 },
49 CategoricalSplit {
50 feature: usize,
51 categories: Vec<i64>,
52 left: usize,
53 right: usize,
54 missing: MissingBranch,
55 missing_value: MissingValuePolicy,
56 cover: f64,
57 },
58}
59#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
61pub struct TreeArrays {
62 pub features: Vec<Option<usize>>,
63 pub thresholds: Vec<f64>,
64 pub left_children: Vec<Option<usize>>,
65 pub right_children: Vec<Option<usize>>,
66 pub missing: Vec<MissingBranch>,
67 pub leaf_values: Vec<Option<Vec<f64>>>,
68 pub covers: Vec<f64>,
69 pub root: usize,
70 pub n_features: usize,
71}
72impl Node {
73 pub fn cover(&self) -> f64 {
74 match self {
75 Self::Leaf { cover, .. }
76 | Self::Split { cover, .. }
77 | Self::NumericalSplit { cover, .. }
78 | Self::CategoricalSplit { cover, .. } => *cover,
79 }
80 }
81
82 pub fn split_feature(&self) -> Option<usize> {
83 match self {
84 Self::Leaf { .. } => None,
85 Self::Split { feature, .. }
86 | Self::NumericalSplit { feature, .. }
87 | Self::CategoricalSplit { feature, .. } => Some(*feature),
88 }
89 }
90
91 pub fn children(&self) -> Option<(usize, usize)> {
92 match self {
93 Self::Leaf { .. } => None,
94 Self::Split { left, right, .. }
95 | Self::NumericalSplit { left, right, .. }
96 | Self::CategoricalSplit { left, right, .. } => Some((*left, *right)),
97 }
98 }
99
100 pub(crate) fn hot_child(&self, value: f64) -> Option<usize> {
101 fn is_missing(value: f64, policy: MissingValuePolicy) -> bool {
102 match policy {
103 MissingValuePolicy::NaN => value.is_nan(),
104 MissingValuePolicy::Zero => value.is_nan() || value == 0.0,
105 MissingValuePolicy::None => false,
106 }
107 }
108 fn missing_child(branch: MissingBranch, left: usize, right: usize) -> usize {
109 match branch {
110 MissingBranch::Left => left,
111 MissingBranch::Right => right,
112 }
113 }
114 match self {
115 Self::Leaf { .. } => None,
116 Self::Split {
117 threshold,
118 left,
119 right,
120 missing,
121 ..
122 } => Some(if value.is_nan() {
123 missing_child(*missing, *left, *right)
124 } else if value <= *threshold {
125 *left
126 } else {
127 *right
128 }),
129 Self::NumericalSplit {
130 threshold,
131 comparison,
132 left,
133 right,
134 missing,
135 missing_value,
136 ..
137 } => Some(if is_missing(value, *missing_value) {
138 missing_child(*missing, *left, *right)
139 } else if match comparison {
140 SplitComparison::LessThan => value < *threshold,
141 SplitComparison::LessThanOrEqual => value <= *threshold,
142 } {
143 *left
144 } else {
145 *right
146 }),
147 Self::CategoricalSplit {
148 categories,
149 left,
150 right,
151 missing,
152 missing_value,
153 ..
154 } => Some(if is_missing(value, *missing_value) {
155 missing_child(*missing, *left, *right)
156 } else if value.is_finite()
157 && value.fract() == 0.0
158 && value >= i64::MIN as f64
159 && value <= i64::MAX as f64
160 && categories.contains(&(value as i64))
161 {
162 *left
163 } else {
164 *right
165 }),
166 }
167 }
168}
169
170#[derive(Debug, Clone, serde::Serialize)]
171pub struct Tree {
172 nodes: Vec<Node>,
173 root: usize,
174 n_features: usize,
175 n_outputs: usize,
176}
177#[derive(serde::Deserialize)]
178struct TreePayload {
179 nodes: Vec<Node>,
180 root: usize,
181 n_features: usize,
182 n_outputs: usize,
183}
184impl<'de> serde::Deserialize<'de> for Tree {
185 fn deserialize<D: serde::Deserializer<'de>>(
186 deserializer: D,
187 ) -> std::result::Result<Self, D::Error> {
188 let payload = <TreePayload as serde::Deserialize>::deserialize(deserializer)?;
189 let tree = Self::new(payload.nodes, payload.root, payload.n_features)
190 .map_err(serde::de::Error::custom)?;
191 if tree.n_outputs() != payload.n_outputs {
192 return Err(serde::de::Error::custom(
193 "serialized tree output count does not match its leaves",
194 ));
195 }
196 Ok(tree)
197 }
198}
199impl Tree {
200 pub fn validate(&self) -> Result<()> {
202 Self::new(self.nodes.clone(), self.root, self.n_features).map(|_| ())
203 }
204 pub fn from_arrays(a: TreeArrays) -> Result<Self> {
205 let n = a.features.len();
206 if [
207 a.thresholds.len(),
208 a.left_children.len(),
209 a.right_children.len(),
210 a.missing.len(),
211 a.leaf_values.len(),
212 a.covers.len(),
213 ]
214 .iter()
215 .any(|&x| x != n)
216 {
217 return Err(ShapError::DimensionMismatch {
218 expected: format!("{n} entries in every tree array"),
219 found: "inconsistent tree array lengths".into(),
220 });
221 }
222 let mut nodes = Vec::with_capacity(n);
223 for i in 0..n {
224 match (&a.features[i], &a.leaf_values[i]) {
225 (None, Some(values)) => nodes.push(Node::Leaf {
226 values: values.clone(),
227 cover: a.covers[i],
228 }),
229 (Some(feature), None) => nodes.push(Node::Split {
230 feature: *feature,
231 threshold: a.thresholds[i],
232 left: a.left_children[i].ok_or_else(|| {
233 ShapError::InvalidConfiguration(format!("split {i} has no left child"))
234 })?,
235 right: a.right_children[i].ok_or_else(|| {
236 ShapError::InvalidConfiguration(format!("split {i} has no right child"))
237 })?,
238 missing: a.missing[i],
239 cover: a.covers[i],
240 }),
241 _ => {
242 return Err(ShapError::InvalidConfiguration(format!(
243 "node {i} must be exactly one of leaf or split"
244 )))
245 }
246 }
247 }
248 Self::new(nodes, a.root, a.n_features)
249 }
250 pub fn new(nodes: Vec<Node>, root: usize, n_features: usize) -> Result<Self> {
251 if nodes.is_empty() || root >= nodes.len() {
252 return Err(ShapError::InvalidConfiguration(
253 "tree must have a valid root".into(),
254 ));
255 }
256 if n_features == 0 {
257 return Err(ShapError::InvalidConfiguration(
258 "tree must have at least one feature".into(),
259 ));
260 }
261 let mut outputs = None;
262 for (i, node) in nodes.iter().enumerate() {
263 if !node.cover().is_finite() || node.cover() < 0.0 {
264 return Err(ShapError::InvalidConfiguration(format!(
265 "node {i} has invalid cover"
266 )));
267 }
268 match node {
269 Node::Leaf { values, .. } => {
270 if values.is_empty() || values.iter().any(|v| !v.is_finite()) {
271 return Err(ShapError::InvalidConfiguration(format!(
272 "leaf {i} has invalid values"
273 )));
274 }
275 if outputs
276 .replace(values.len())
277 .is_some_and(|n| n != values.len())
278 {
279 return Err(ShapError::InvalidConfiguration(
280 "all leaves must have the same output count".into(),
281 ));
282 }
283 }
284 Node::Split {
285 feature,
286 left,
287 right,
288 threshold,
289 ..
290 } => {
291 if *feature >= n_features
292 || *left >= nodes.len()
293 || *right >= nodes.len()
294 || !threshold.is_finite()
295 {
296 return Err(ShapError::InvalidConfiguration(format!(
297 "split node {i} is invalid"
298 )));
299 }
300 }
301 Node::NumericalSplit {
302 feature,
303 left,
304 right,
305 threshold,
306 ..
307 } => {
308 if *feature >= n_features
309 || *left >= nodes.len()
310 || *right >= nodes.len()
311 || !threshold.is_finite()
312 {
313 return Err(ShapError::InvalidConfiguration(format!(
314 "numerical split node {i} is invalid"
315 )));
316 }
317 }
318 Node::CategoricalSplit {
319 feature,
320 categories,
321 left,
322 right,
323 ..
324 } => {
325 if *feature >= n_features
326 || *left >= nodes.len()
327 || *right >= nodes.len()
328 || categories.is_empty()
329 {
330 return Err(ShapError::InvalidConfiguration(format!(
331 "categorical split node {i} is invalid"
332 )));
333 }
334 }
335 }
336 }
337 let n_outputs =
338 outputs.ok_or_else(|| ShapError::InvalidConfiguration("tree has no leaves".into()))?;
339 let tree = Self {
340 nodes,
341 root,
342 n_features,
343 n_outputs,
344 };
345 tree.validate_graph()?;
346 Ok(tree)
347 }
348 fn validate_graph(&self) -> Result<()> {
349 fn visit(t: &Tree, i: usize, state: &mut [u8]) -> Result<()> {
350 if state[i] == 1 {
351 return Err(ShapError::InvalidConfiguration(
352 "tree contains a cycle".into(),
353 ));
354 }
355 if state[i] == 2 {
356 return Err(ShapError::InvalidConfiguration(
357 "tree node has multiple parents".into(),
358 ));
359 }
360 state[i] = 1;
361 if let Some((left, right)) = t.nodes[i].children() {
362 visit(t, left, state)?;
363 visit(t, right, state)?;
364 }
365 state[i] = 2;
366 Ok(())
367 }
368 let mut state = vec![0; self.nodes.len()];
369 visit(self, self.root, &mut state)?;
370 if state.contains(&0) {
371 return Err(ShapError::InvalidConfiguration(
372 "tree contains nodes unreachable from the root".into(),
373 ));
374 }
375 Ok(())
376 }
377 pub fn nodes(&self) -> &[Node] {
378 &self.nodes
379 }
380 pub fn root(&self) -> usize {
381 self.root
382 }
383 pub fn n_features(&self) -> usize {
384 self.n_features
385 }
386 pub fn n_outputs(&self) -> usize {
387 self.n_outputs
388 }
389 pub fn predict_row(&self, x: ArrayView1<'_, f64>) -> Result<&[f64]> {
390 if x.len() != self.n_features {
391 return Err(ShapError::DimensionMismatch {
392 expected: format!("{} features", self.n_features),
393 found: format!("{}", x.len()),
394 });
395 }
396 let mut i = self.root;
397 loop {
398 match &self.nodes[i] {
399 Node::Leaf { values, .. } => return Ok(values),
400 node => i = node.hot_child(x[node.split_feature().unwrap()]).unwrap(),
401 }
402 }
403 }
404 pub fn expected_value(&self) -> Vec<f64> {
405 fn rec(t: &Tree, i: usize) -> Vec<f64> {
406 match &t.nodes[i] {
407 Node::Leaf { values, .. } => values.clone(),
408 node => {
409 let (left, right) = node.children().unwrap();
410 let a = rec(t, left);
411 let b = rec(t, right);
412 let total = t.nodes[left].cover() + t.nodes[right].cover();
413 let p = if total > 0.0 {
414 t.nodes[left].cover() / total
415 } else {
416 0.5
417 };
418 a.into_iter()
419 .zip(b)
420 .map(|(x, y)| p * x + (1.0 - p) * y)
421 .collect()
422 }
423 }
424 }
425 rec(self, self.root)
426 }
427 #[cfg(test)]
428 pub(crate) fn conditional_value(&self, x: ArrayView1<'_, f64>, present: &[bool]) -> Vec<f64> {
429 fn rec(t: &Tree, i: usize, x: ArrayView1<'_, f64>, p: &[bool]) -> Vec<f64> {
430 match &t.nodes[i] {
431 Node::Leaf { values, .. } => values.clone(),
432 node => {
433 let feature = node.split_feature().unwrap();
434 let (left, right) = node.children().unwrap();
435 if p[feature] {
436 let c = node.hot_child(x[feature]).unwrap();
437 rec(t, c, x, p)
438 } else {
439 let a = rec(t, left, x, p);
440 let b = rec(t, right, x, p);
441 let total = t.nodes[left].cover() + t.nodes[right].cover();
442 let q = if total > 0.0 {
443 t.nodes[left].cover() / total
444 } else {
445 0.5
446 };
447 a.into_iter()
448 .zip(b)
449 .map(|(u, v)| q * u + (1.0 - q) * v)
450 .collect()
451 }
452 }
453 }
454 }
455 rec(self, self.root, x, present)
456 }
457}
458
459#[derive(Debug, Clone, serde::Serialize)]
460pub struct TreeEnsemble {
461 trees: Vec<(Tree, f64)>,
462 output_groups: Vec<Option<usize>>,
463 base_values: Array1<f64>,
464 n_features: usize,
465}
466#[derive(serde::Deserialize)]
467struct TreeEnsemblePayload {
468 trees: Vec<(Tree, f64)>,
469 #[serde(default)]
470 output_groups: Vec<Option<usize>>,
471 base_values: Array1<f64>,
472 n_features: usize,
473}
474impl<'de> serde::Deserialize<'de> for TreeEnsemble {
475 fn deserialize<D: serde::Deserializer<'de>>(
476 deserializer: D,
477 ) -> std::result::Result<Self, D::Error> {
478 let payload = <TreeEnsemblePayload as serde::Deserialize>::deserialize(deserializer)?;
479 let ensemble = if payload.output_groups.is_empty() {
480 Self::new(payload.trees, payload.base_values.to_vec())
481 } else {
482 Self::new_with_output_groups(
483 payload.trees,
484 payload.base_values.to_vec(),
485 payload.output_groups,
486 )
487 }
488 .map_err(serde::de::Error::custom)?;
489 if ensemble.n_features() != payload.n_features {
490 return Err(serde::de::Error::custom(
491 "serialized ensemble feature count does not match its trees",
492 ));
493 }
494 Ok(ensemble)
495 }
496}
497impl TreeEnsemble {
498 pub fn new(trees: Vec<(Tree, f64)>, base_values: Vec<f64>) -> Result<Self> {
499 let groups = vec![None; trees.len()];
500 Self::new_with_output_groups(trees, base_values, groups)
501 }
502
503 pub fn new_with_output_groups(
504 trees: Vec<(Tree, f64)>,
505 base_values: Vec<f64>,
506 output_groups: Vec<Option<usize>>,
507 ) -> Result<Self> {
508 if trees.is_empty() {
509 return Err(ShapError::InvalidConfiguration(
510 "ensemble must contain a tree".into(),
511 ));
512 }
513 let nf = trees[0].0.n_features();
514 let no = trees[0].0.n_outputs();
515 if base_values.len() != no
516 || output_groups.len() != trees.len()
517 || output_groups.iter().flatten().any(|&group| group >= no)
518 || base_values.iter().any(|v| !v.is_finite())
519 || trees
520 .iter()
521 .any(|(t, w)| t.n_features() != nf || t.n_outputs() != no || !w.is_finite())
522 {
523 return Err(ShapError::DimensionMismatch {
524 expected: format!("trees with {nf} features and {no} outputs"),
525 found: "inconsistent ensemble".into(),
526 });
527 }
528 Ok(Self {
529 trees,
530 output_groups,
531 base_values: Array1::from(base_values),
532 n_features: nf,
533 })
534 }
535 pub fn trees(&self) -> &[(Tree, f64)] {
536 &self.trees
537 }
538 pub fn output_groups(&self) -> &[Option<usize>] {
539 &self.output_groups
540 }
541 pub fn base_offset(&self) -> ndarray::ArrayView1<'_, f64> {
542 self.base_values.view()
543 }
544 pub fn n_features(&self) -> usize {
545 self.n_features
546 }
547 pub fn n_outputs(&self) -> usize {
548 self.base_values.len()
549 }
550 pub fn expected_value(&self) -> Array1<f64> {
551 let mut v = self.base_values.clone();
552 for (t, w) in &self.trees {
553 for (o, x) in t.expected_value().into_iter().enumerate() {
554 v[o] += w * x
555 }
556 }
557 v
558 }
559 pub fn predict_with_base_margin(
562 &self,
563 x: ArrayView2<'_, f64>,
564 base_margin: ArrayView2<'_, f64>,
565 ) -> Result<Array2<f64>> {
566 if base_margin.dim() != (x.nrows(), self.n_outputs()) {
567 return Err(ShapError::DimensionMismatch {
568 expected: format!("({}, {}) base margins", x.nrows(), self.n_outputs()),
569 found: format!("{:?}", base_margin.dim()),
570 });
571 }
572 if base_margin.iter().any(|value| !value.is_finite()) {
573 return Err(ShapError::InvalidConfiguration(
574 "base margins must be finite".into(),
575 ));
576 }
577 let mut prediction = self.predict(x)?;
578 for row in 0..prediction.nrows() {
579 for output in 0..prediction.ncols() {
580 prediction[[row, output]] += base_margin[[row, output]] - self.base_values[output];
581 }
582 }
583 Ok(prediction)
584 }
585 pub fn validate(&self) -> Result<()> {
587 for (t, w) in &self.trees {
588 t.validate()?;
589 if !w.is_finite() {
590 return Err(ShapError::InvalidConfiguration(
591 "tree weight must be finite".into(),
592 ));
593 }
594 }
595 Self::new_with_output_groups(
596 self.trees.clone(),
597 self.base_values.to_vec(),
598 self.output_groups.clone(),
599 )
600 .map(|_| ())
601 }
602}
603
604impl Predict for TreeEnsemble {
605 fn predict(&self, x: ArrayView2<'_, f64>) -> Result<Array2<f64>> {
606 self.validate()?;
607 if x.ncols() != self.n_features {
608 return Err(ShapError::DimensionMismatch {
609 expected: format!("{} features", self.n_features),
610 found: format!("{}", x.ncols()),
611 });
612 }
613 crate::error::checked_f64_shape(&[x.nrows(), self.base_values.len()], "tree prediction")?;
614 let mut out = Array2::from_shape_fn((x.nrows(), self.base_values.len()), |(_, o)| {
615 self.base_values[o]
616 });
617 for i in 0..x.nrows() {
618 for (t, w) in &self.trees {
619 for (o, v) in t.predict_row(x.row(i))?.iter().enumerate() {
620 out[[i, o]] += w * v
621 }
622 }
623 }
624 Ok(out)
625 }
626 fn n_features(&self) -> Option<usize> {
627 Some(self.n_features)
628 }
629 fn n_outputs(&self) -> Option<usize> {
630 Some(self.base_values.len())
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 #[test]
639 fn rejects_unreachable_and_shared_nodes() {
640 let leaf = || Node::Leaf {
641 values: vec![0.],
642 cover: 1.,
643 };
644 let unreachable = Tree::new(
645 vec![
646 Node::Split {
647 feature: 0,
648 threshold: 0.,
649 left: 1,
650 right: 2,
651 missing: MissingBranch::Left,
652 cover: 2.,
653 },
654 leaf(),
655 leaf(),
656 leaf(),
657 ],
658 0,
659 1,
660 );
661 assert!(unreachable.is_err());
662
663 let shared = Tree::new(
664 vec![
665 Node::Split {
666 feature: 0,
667 threshold: 0.,
668 left: 1,
669 right: 1,
670 missing: MissingBranch::Left,
671 cover: 2.,
672 },
673 leaf(),
674 ],
675 0,
676 1,
677 );
678 assert!(shared.is_err());
679 }
680
681 #[test]
682 fn rejects_non_finite_ensemble_base_values() {
683 let tree = Tree::new(
684 vec![Node::Leaf {
685 values: vec![1.],
686 cover: 1.,
687 }],
688 0,
689 1,
690 )
691 .unwrap();
692 assert!(TreeEnsemble::new(vec![(tree, 1.)], vec![f64::NAN]).is_err());
693 }
694}