1#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
3#[serde(try_from = "FeaturePartitionPayload")]
4pub struct FeaturePartition {
5 groups: Vec<Vec<usize>>,
6 n_features: usize,
7}
8#[derive(serde::Deserialize)]
9struct FeaturePartitionPayload {
10 groups: Vec<Vec<usize>>,
11 n_features: usize,
12}
13impl TryFrom<FeaturePartitionPayload> for FeaturePartition {
14 type Error = ShapError;
15 fn try_from(payload: FeaturePartitionPayload) -> Result<Self> {
16 Self::new(payload.groups, payload.n_features)
17 }
18}
19
20use crate::{
21 coalition, evaluation::CoalitionEvaluator, Background, EvaluationConfig, Explainer,
22 Explanation, IndependentMasker, Link, Masker, Predict, Result, ShapError,
23};
24use ndarray::{Array2, Array3, ArrayView2};
25use rand::{rngs::StdRng, Rng, SeedableRng};
26
27pub struct PartitionExplainer<M, K = IndependentMasker> {
30 model: M,
31 masker: K,
32 partition: FeaturePartition,
33 max_features: usize,
34 evaluation: EvaluationConfig,
35 link: Link,
36}
37impl<M> PartitionExplainer<M, IndependentMasker> {
38 pub fn new(model: M, background: Background, partition: FeaturePartition) -> Self {
39 Self::from_masker(model, IndependentMasker::new(background), partition)
40 }
41}
42impl<M, K> PartitionExplainer<M, K> {
43 pub fn from_masker(model: M, masker: K, partition: FeaturePartition) -> Self {
44 Self {
45 model,
46 masker,
47 partition,
48 max_features: 20,
49 evaluation: EvaluationConfig {
50 coalition_batch_size: 64,
51 cache_capacity: 1 << 20,
52 max_model_rows: None,
53 },
54 link: Link::Identity,
55 }
56 }
57 pub fn with_max_features(mut self, n: usize) -> Self {
58 self.max_features = n;
59 self
60 }
61 pub fn with_evaluation_config(mut self, c: EvaluationConfig) -> Self {
62 self.evaluation = c;
63 self
64 }
65 pub fn with_link(mut self, link: Link) -> Self {
66 self.link = link;
67 self
68 }
69}
70impl<M: Predict, K: Masker> Explainer for PartitionExplainer<M, K> {
71 fn explain(&self, x: ArrayView2<'_, f64>) -> Result<Explanation> {
72 let m = self.masker.n_features();
73 self.partition.validate()?;
74 if self.partition.n_features() != m {
75 return Err(ShapError::DimensionMismatch {
76 expected: format!("partition for {m} features"),
77 found: format!("partition for {} features", self.partition.n_features()),
78 });
79 }
80 if x.nrows() == 0 {
81 return Err(ShapError::EmptyData);
82 }
83 if x.ncols() != self.masker.n_input_features() {
84 return Err(ShapError::DimensionMismatch {
85 expected: format!("{} input features", self.masker.n_input_features()),
86 found: format!("{}", x.ncols()),
87 });
88 }
89 if m > self.max_features || m >= 63 {
90 return Err(ShapError::InvalidConfiguration(format!(
91 "exact Owen values support at most {} features",
92 self.max_features
93 )));
94 }
95 let masks = coalition::all(m).collect::<Vec<_>>();
96 let mut first = CoalitionEvaluator::new(&self.model, &self.masker, self.evaluation)?;
97 let o = first.evaluate(x.row(0), &[0])?[0].len();
98 crate::error::checked_f64_shape(&[x.nrows(), m, o], "partition explanation")?;
99 let mut values = Array3::zeros((x.nrows(), m, o));
100 let mut bases = Array2::zeros((x.nrows(), o));
101 let groups = self.partition.groups();
102 let ng = groups.len();
103 let group_masks = groups
104 .iter()
105 .map(|g| g.iter().fold(0u64, |z, &j| z | (1u64 << j)))
106 .collect::<Vec<_>>();
107 let fg = factorials(ng.max(groups.iter().map(Vec::len).max().unwrap_or(0)));
108 for n in 0..x.nrows() {
109 let mut evaluator =
110 CoalitionEvaluator::new(&self.model, &self.masker, self.evaluation)?;
111 let cache = evaluator
112 .evaluate(x.row(n), &masks)?
113 .into_iter()
114 .map(|row| {
115 row.into_iter()
116 .map(|value| self.link.forward(value))
117 .collect::<Result<Vec<_>>>()
118 })
119 .collect::<Result<Vec<_>>>()?;
120 for k in 0..o {
121 bases[[n, k]] = cache[0][k]
122 }
123 for (g_index, group) in groups.iter().enumerate() {
124 let others = (0..ng).filter(|&g| g != g_index).collect::<Vec<_>>();
125 for &feature in group {
126 let peers = group
127 .iter()
128 .copied()
129 .filter(|&j| j != feature)
130 .collect::<Vec<_>>();
131 for outer in 0..(1u64 << others.len()) {
132 let selected_groups = outer.count_ones() as usize;
133 let outer_weight =
134 fg[selected_groups] * fg[ng - selected_groups - 1] / fg[ng];
135 let mut base_mask = 0u64;
136 for (pos, &g) in others.iter().enumerate() {
137 if outer & (1 << pos) != 0 {
138 base_mask |= group_masks[g]
139 }
140 }
141 for inner in 0..(1u64 << peers.len()) {
142 let selected_features = inner.count_ones() as usize;
143 let inner_weight = fg[selected_features]
144 * fg[group.len() - selected_features - 1]
145 / fg[group.len()];
146 let mut mask = base_mask;
147 for (pos, &j) in peers.iter().enumerate() {
148 if inner & (1 << pos) != 0 {
149 mask |= 1 << j
150 }
151 }
152 for k in 0..o {
153 values[[n, feature, k]] += outer_weight
154 * inner_weight
155 * (cache[(mask | (1 << feature)) as usize][k]
156 - cache[mask as usize][k]);
157 }
158 }
159 }
160 }
161 }
162 }
163 Explanation::new(values, bases, self.masker.attribution_data(x)?)
164 }
165}
166fn factorials(n: usize) -> Vec<f64> {
167 (0..=n)
168 .scan(1.0, |v, k| {
169 if k > 0 {
170 *v *= k as f64
171 }
172 Some(*v)
173 })
174 .collect()
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
178pub enum PartitionNode {
179 Feature(usize),
180 Group(Box<PartitionNode>, Box<PartitionNode>),
181}
182#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
183#[serde(try_from = "PartitionTreePayload")]
184pub struct PartitionTree {
185 root: PartitionNode,
186 n_features: usize,
187}
188#[derive(serde::Deserialize)]
189struct PartitionTreePayload {
190 root: PartitionNode,
191 n_features: usize,
192}
193impl TryFrom<PartitionTreePayload> for PartitionTree {
194 type Error = ShapError;
195 fn try_from(payload: PartitionTreePayload) -> Result<Self> {
196 Self::new(payload.root, payload.n_features)
197 }
198}
199impl PartitionTree {
200 pub fn new(root: PartitionNode, n_features: usize) -> Result<Self> {
201 if n_features == 0 {
202 return Err(ShapError::InvalidConfiguration(
203 "partition tree must contain features".into(),
204 ));
205 }
206 let mut seen = vec![false; n_features];
207 fn visit(n: &PartitionNode, seen: &mut [bool]) -> Result<()> {
208 match n {
209 PartitionNode::Feature(j) => {
210 if *j >= seen.len() || seen[*j] {
211 return Err(ShapError::InvalidConfiguration(
212 "partition tree must contain each feature exactly once".into(),
213 ));
214 }
215 seen[*j] = true
216 }
217 PartitionNode::Group(a, b) => {
218 visit(a, seen)?;
219 visit(b, seen)?
220 }
221 }
222 Ok(())
223 }
224 visit(&root, &mut seen)?;
225 if seen.iter().any(|x| !*x) {
226 return Err(ShapError::InvalidConfiguration(
227 "partition tree must contain each feature exactly once".into(),
228 ));
229 }
230 Ok(Self { root, n_features })
231 }
232 pub fn root(&self) -> &PartitionNode {
233 &self.root
234 }
235 pub fn n_features(&self) -> usize {
236 self.n_features
237 }
238 pub fn validate(&self) -> Result<()> {
240 Self::new(self.root.clone(), self.n_features).map(|_| ())
241 }
242 fn permutation_count(&self) -> Option<usize> {
243 fn rec(node: &PartitionNode) -> Option<usize> {
244 match node {
245 PartitionNode::Feature(_) => Some(1),
246 PartitionNode::Group(left, right) => {
247 rec(left)?.checked_mul(rec(right)?)?.checked_mul(2)
248 }
249 }
250 }
251 rec(&self.root)
252 }
253 fn permutations(&self) -> Vec<Vec<usize>> {
254 fn rec(n: &PartitionNode) -> Vec<Vec<usize>> {
255 match n {
256 PartitionNode::Feature(j) => vec![vec![*j]],
257 PartitionNode::Group(a, b) => {
258 let left = rec(a);
259 let right = rec(b);
260 let mut out = Vec::with_capacity(left.len() * right.len() * 2);
261 for l in &left {
262 for r in &right {
263 let mut lr = l.clone();
264 lr.extend(r);
265 out.push(lr);
266 let mut rl = r.clone();
267 rl.extend(l);
268 out.push(rl)
269 }
270 }
271 out
272 }
273 }
274 }
275 rec(&self.root)
276 }
277
278 fn sampled_permutations(&self, samples: usize, seed: u64) -> Vec<Vec<usize>> {
279 fn sample(node: &PartitionNode, rng: &mut StdRng) -> Vec<usize> {
280 match node {
281 PartitionNode::Feature(feature) => vec![*feature],
282 PartitionNode::Group(left, right) => {
283 let mut left = sample(left, rng);
284 let mut right = sample(right, rng);
285 if rng.gen_bool(0.5) {
286 left.append(&mut right);
287 left
288 } else {
289 right.append(&mut left);
290 right
291 }
292 }
293 }
294 }
295 let mut rng = StdRng::seed_from_u64(seed);
296 (0..samples).map(|_| sample(&self.root, &mut rng)).collect()
297 }
298}
299
300pub struct HierarchicalPartitionExplainer<M, K = IndependentMasker> {
302 model: M,
303 masker: K,
304 tree: PartitionTree,
305 max_permutations: usize,
306 approximate_samples: Option<(usize, u64)>,
307 evaluation: EvaluationConfig,
308 link: Link,
309}
310impl<M> HierarchicalPartitionExplainer<M, IndependentMasker> {
311 pub fn new(model: M, background: Background, tree: PartitionTree) -> Self {
312 Self::from_masker(model, IndependentMasker::new(background), tree)
313 }
314}
315impl<M, K> HierarchicalPartitionExplainer<M, K> {
316 pub fn from_masker(model: M, masker: K, tree: PartitionTree) -> Self {
317 Self {
318 model,
319 masker,
320 tree,
321 max_permutations: 65536,
322 approximate_samples: None,
323 evaluation: EvaluationConfig {
324 coalition_batch_size: 64,
325 cache_capacity: 1 << 20,
326 max_model_rows: None,
327 },
328 link: Link::Identity,
329 }
330 }
331 pub fn with_max_permutations(mut self, n: usize) -> Self {
332 self.max_permutations = n;
333 self
334 }
335 pub fn with_approximate_samples(mut self, samples: usize, seed: u64) -> Self {
338 self.approximate_samples = Some((samples, seed));
339 self
340 }
341 pub fn with_evaluation_config(mut self, c: EvaluationConfig) -> Self {
342 self.evaluation = c;
343 self
344 }
345 pub fn with_link(mut self, link: Link) -> Self {
346 self.link = link;
347 self
348 }
349}
350impl<M: Predict, K: Masker> Explainer for HierarchicalPartitionExplainer<M, K> {
351 fn explain(&self, x: ArrayView2<'_, f64>) -> Result<Explanation> {
352 let m = self.masker.n_features();
353 self.tree.validate()?;
354 if x.nrows() == 0 {
355 return Err(ShapError::EmptyData);
356 }
357 if x.ncols() != self.masker.n_input_features() || self.tree.n_features() != m {
358 return Err(ShapError::DimensionMismatch {
359 expected: format!(
360 "{} input features and {m} features in hierarchy",
361 self.masker.n_input_features()
362 ),
363 found: format!("data {}, hierarchy {}", x.ncols(), self.tree.n_features()),
364 });
365 }
366 if m >= 63 {
367 return Err(ShapError::InvalidConfiguration(
368 "hierarchical Owen values support at most 62 features".into(),
369 ));
370 }
371 let permutation_count = self.tree.permutation_count().ok_or_else(|| {
372 ShapError::InvalidConfiguration("hierarchy permutation count overflowed".into())
373 })?;
374 let permutations = if permutation_count > self.max_permutations {
375 let (samples, seed) = self.approximate_samples.ok_or_else(|| {
376 ShapError::InvalidConfiguration(format!(
377 "hierarchy generates {} permutations, exceeding limit {}",
378 permutation_count, self.max_permutations
379 ))
380 })?;
381 if samples == 0 {
382 return Err(ShapError::InvalidConfiguration(
383 "approximate hierarchy samples must be positive".into(),
384 ));
385 }
386 self.tree.sampled_permutations(samples, seed)
387 } else {
388 self.tree.permutations()
389 };
390 let step_count = permutations.len().checked_mul(m).ok_or_else(|| {
391 ShapError::InvalidConfiguration("hierarchy step count overflowed".into())
392 })?;
393 crate::error::checked_f64_shape(&[step_count], "hierarchy permutation steps")?;
394 let mut probe = CoalitionEvaluator::new(&self.model, &self.masker, self.evaluation)?;
395 let o = probe.evaluate(x.row(0), &[0])?[0].len();
396 crate::error::checked_f64_shape(&[x.nrows(), m, o], "hierarchical explanation")?;
397 let mut values = Array3::zeros((x.nrows(), m, o));
398 let mut bases = Array2::zeros((x.nrows(), o));
399 for n in 0..x.nrows() {
400 let mut requested = vec![0u64];
401 let mut steps = Vec::with_capacity(step_count);
402 for order in &permutations {
403 let mut mask = 0;
404 let mut before = 0;
405 for &j in order {
406 mask |= 1 << j;
407 requested.push(mask);
408 let after = requested.len() - 1;
409 steps.push((j, before, after));
410 before = after
411 }
412 }
413 let mut evaluator =
414 CoalitionEvaluator::new(&self.model, &self.masker, self.evaluation)?;
415 let evaluated = evaluator
416 .evaluate(x.row(n), &requested)?
417 .into_iter()
418 .map(|row| {
419 row.into_iter()
420 .map(|value| self.link.forward(value))
421 .collect::<Result<Vec<_>>>()
422 })
423 .collect::<Result<Vec<_>>>()?;
424 for k in 0..o {
425 bases[[n, k]] = evaluated[0][k]
426 }
427 for (j, before, after) in steps {
428 for k in 0..o {
429 values[[n, j, k]] +=
430 (evaluated[after][k] - evaluated[before][k]) / permutations.len() as f64
431 }
432 }
433 }
434 Explanation::new(values, bases, self.masker.attribution_data(x)?)
435 }
436}
437
438pub fn correlation_partition(background: &Background) -> Result<PartitionTree> {
441 let m = background.n_features();
442 let data = background.data();
443 let means = data.mean_axis(ndarray::Axis(0)).unwrap();
444 let mut clusters = (0..m)
445 .map(|j| (vec![j], PartitionNode::Feature(j)))
446 .collect::<Vec<_>>();
447 let corr = |a: usize, b: usize| {
448 let mut xy = 0.;
449 let mut xx = 0.;
450 let mut yy = 0.;
451 for i in 0..data.nrows() {
452 let x = data[[i, a]] - means[a];
453 let y = data[[i, b]] - means[b];
454 xy += x * y;
455 xx += x * x;
456 yy += y * y
457 }
458 if xx == 0. || yy == 0. {
459 0.
460 } else {
461 (xy / (xx * yy).sqrt()).abs()
462 }
463 };
464 while clusters.len() > 1 {
465 let mut best = (0, 1, f64::INFINITY);
466 for i in 0..clusters.len() {
467 for j in i + 1..clusters.len() {
468 let mut distance = 0.0;
469 for &a in &clusters[i].0 {
470 for &b in &clusters[j].0 {
471 distance += 1.0 - corr(a, b)
472 }
473 }
474 let d = distance / (clusters[i].0.len() * clusters[j].0.len()) as f64;
475 if d < best.2 {
476 best = (i, j, d)
477 }
478 }
479 }
480 let (i, j, _) = best;
481 let (right_features, right) = clusters.remove(j);
482 let (left_features, left) = clusters.remove(i);
483 let mut features = left_features;
484 features.extend(right_features);
485 clusters.push((
486 features,
487 PartitionNode::Group(Box::new(left), Box::new(right)),
488 ))
489 }
490 PartitionTree::new(clusters.pop().unwrap().1, m)
491}
492
493#[cfg(test)]
494#[allow(clippy::items_after_test_module)]
495mod tests {
496 use super::*;
497 use crate::{metrics::check_additivity, FixedMasker, FnModel, GroupedMasker};
498 use ndarray::{array, ArrayView2, Axis};
499 #[test]
500 fn owen_values_respect_groups_and_local_accuracy() {
501 let model = FnModel::new(|x: ArrayView2<'_, f64>| {
502 Ok(x.map_axis(Axis(1), |r| r[0] * r[1] + r[2])
503 .insert_axis(Axis(1)))
504 });
505 let bg = Background::new(array![[0., 0., 0.]]).unwrap();
506 let partition = FeaturePartition::new(vec![vec![0, 1], vec![2]], 3).unwrap();
507 let e = PartitionExplainer::new(model, bg, partition)
508 .explain(array![[1., 1., 1.]].view())
509 .unwrap();
510 assert!((e.values()[[0, 0, 0]] - 0.5).abs() < 1e-12);
511 assert!((e.values()[[0, 1, 0]] - 0.5).abs() < 1e-12);
512 assert!((e.values()[[0, 2, 0]] - 1.).abs() < 1e-12);
513 check_additivity(&e, array![[2.]].view(), 1e-12).unwrap();
514 }
515 #[test]
516 fn partition_explainer_preserves_structured_source_groups() {
517 let model = FnModel::new(|x: ArrayView2<'_, f64>| {
518 Ok(x.map_axis(Axis(1), |row| row[0] * row[1] + row[2])
519 .insert_axis(Axis(1)))
520 });
521 let masker = GroupedMasker::new(
522 FixedMasker::new(array![0., 0., 0.]).unwrap(),
523 vec![vec![0, 1], vec![2]],
524 )
525 .unwrap();
526 let explanation = PartitionExplainer::from_masker(
527 model,
528 masker,
529 FeaturePartition::new(vec![vec![0], vec![1]], 2).unwrap(),
530 )
531 .explain(array![[2., 3., 4.]].view())
532 .unwrap();
533 assert_eq!(explanation.values(), &array![[[6.], [4.]]]);
534 assert_eq!(explanation.data(), array![[2.5, 4.]].view());
535 }
536 #[test]
537 fn hierarchical_owen_values_are_locally_accurate() {
538 let tree = PartitionTree::new(
539 PartitionNode::Group(
540 Box::new(PartitionNode::Group(
541 Box::new(PartitionNode::Feature(0)),
542 Box::new(PartitionNode::Feature(1)),
543 )),
544 Box::new(PartitionNode::Feature(2)),
545 ),
546 3,
547 )
548 .unwrap();
549 assert_eq!(tree.permutations().len(), 4);
550 let model = FnModel::new(|x: ArrayView2<'_, f64>| {
551 Ok(x.map_axis(Axis(1), |r| r[0] * r[2]).insert_axis(Axis(1)))
552 });
553 let bg = Background::new(array![[0., 0., 0.]]).unwrap();
554 let e = HierarchicalPartitionExplainer::new(model, bg, tree)
555 .explain(array![[1., 8., 1.]].view())
556 .unwrap();
557 assert!((e.values()[[0, 0, 0]] - 0.5).abs() < 1e-12);
558 assert!(e.values()[[0, 1, 0]].abs() < 1e-12);
559 assert!((e.values()[[0, 2, 0]] - 0.5).abs() < 1e-12);
560 }
561 #[test]
562 fn correlation_clustering_contains_every_feature() {
563 let bg = Background::new(array![[0., 0., 2.], [1., 1., 1.], [2., 2., 0.]]).unwrap();
564 let tree = correlation_partition(&bg).unwrap();
565 assert_eq!(tree.n_features(), 3);
566 assert_eq!(tree.permutations().len(), 4);
567 }
568 #[test]
569 fn rejects_invalid_deserialized_style_partitions_before_evaluation() {
570 let invalid = FeaturePartition {
571 groups: vec![vec![0, 0]],
572 n_features: 2,
573 };
574 let model =
575 FnModel::new(|x: ArrayView2<'_, f64>| Ok(x.sum_axis(Axis(1)).insert_axis(Axis(1))));
576 let result =
577 PartitionExplainer::new(model, Background::new(array![[0., 0.]]).unwrap(), invalid)
578 .explain(array![[1., 1.]].view());
579 assert!(matches!(result, Err(ShapError::InvalidConfiguration(_))));
580 }
581 #[test]
582 fn checks_hierarchy_permutation_limit_before_generation() {
583 fn hierarchy(features: std::ops::Range<usize>) -> PartitionNode {
584 let mut nodes = features.map(PartitionNode::Feature).collect::<Vec<_>>();
585 while nodes.len() > 1 {
586 let right = nodes.pop().unwrap();
587 let left = nodes.pop().unwrap();
588 nodes.push(PartitionNode::Group(Box::new(left), Box::new(right)));
589 }
590 nodes.pop().unwrap()
591 }
592 let tree = PartitionTree::new(hierarchy(0..18), 18).unwrap();
593 assert_eq!(tree.permutation_count(), Some(1 << 17));
594 let model =
595 FnModel::new(|x: ArrayView2<'_, f64>| Ok(x.sum_axis(Axis(1)).insert_axis(Axis(1))));
596 let result = HierarchicalPartitionExplainer::new(
597 model,
598 Background::new(Array2::zeros((1, 18))).unwrap(),
599 tree.clone(),
600 )
601 .with_max_permutations(16)
602 .explain(Array2::ones((1, 18)).view());
603 assert!(matches!(result, Err(ShapError::InvalidConfiguration(_))));
604 let approximate = HierarchicalPartitionExplainer::new(
605 FnModel::new(|x: ArrayView2<'_, f64>| Ok(x.sum_axis(Axis(1)).insert_axis(Axis(1)))),
606 Background::new(Array2::zeros((1, 18))).unwrap(),
607 tree,
608 )
609 .with_max_permutations(16)
610 .with_approximate_samples(32, 7)
611 .explain(Array2::ones((1, 18)).view())
612 .unwrap();
613 assert!(approximate
614 .values()
615 .iter()
616 .all(|value| (*value - 1.0).abs() < 1e-12));
617 }
618 #[test]
619 fn binary_hierarchy_matches_flat_owen_values_for_two_groups() {
620 fn predict(x: ArrayView2<'_, f64>) -> Result<Array2<f64>> {
621 Ok(Array2::from_shape_fn((x.nrows(), 2), |(i, output)| {
622 let r = x.row(i);
623 if output == 0 {
624 r[0] * r[2] + r[1].sin() + r[3]
625 } else {
626 (r[0] + r[1]) * (r[2] - r[3])
627 }
628 }))
629 }
630 let background = Background::new(array![
631 [0., 0., 0., 0.],
632 [1., -1., 0.5, 2.],
633 [-0.5, 2., 1., -1.]
634 ])
635 .unwrap();
636 let sample = array![[2., 0.25, -1., 3.]];
637 let flat = PartitionExplainer::new(
638 FnModel::new(predict),
639 background.clone(),
640 FeaturePartition::new(vec![vec![0, 1], vec![2, 3]], 4).unwrap(),
641 )
642 .explain(sample.view())
643 .unwrap();
644 let hierarchy = PartitionTree::new(
645 PartitionNode::Group(
646 Box::new(PartitionNode::Group(
647 Box::new(PartitionNode::Feature(0)),
648 Box::new(PartitionNode::Feature(1)),
649 )),
650 Box::new(PartitionNode::Group(
651 Box::new(PartitionNode::Feature(2)),
652 Box::new(PartitionNode::Feature(3)),
653 )),
654 ),
655 4,
656 )
657 .unwrap();
658 let nested =
659 HierarchicalPartitionExplainer::new(FnModel::new(predict), background, hierarchy)
660 .explain(sample.view())
661 .unwrap();
662 for (actual, expected) in nested.values().iter().zip(flat.values()) {
663 assert!((actual - expected).abs() < 1e-12);
664 }
665 assert_eq!(nested.base_values(), flat.base_values());
666 }
667 #[test]
668 fn partition_logit_link_explains_log_odds() {
669 let model =
670 FnModel::new(|x: ArrayView2<'_, f64>| Ok(x.column(0).to_owned().insert_axis(Axis(1))));
671 let explanation = PartitionExplainer::new(
672 model,
673 Background::new(array![[0.5]]).unwrap(),
674 FeaturePartition::new(vec![vec![0]], 1).unwrap(),
675 )
676 .with_link(Link::Logit)
677 .explain(array![[0.8]].view())
678 .unwrap();
679 assert!((explanation.reconstructed()[[0, 0]] - 4f64.ln()).abs() < 1e-12);
680 }
681}
682impl FeaturePartition {
683 pub fn new(groups: Vec<Vec<usize>>, n_features: usize) -> crate::Result<Self> {
684 if n_features == 0 {
685 return Err(crate::ShapError::InvalidConfiguration(
686 "partition must contain at least one feature".into(),
687 ));
688 }
689 let mut seen = vec![false; n_features];
690 for g in &groups {
691 if g.is_empty() {
692 return Err(crate::ShapError::InvalidConfiguration(
693 "partition groups cannot be empty".into(),
694 ));
695 }
696 for &j in g {
697 if j >= n_features || seen[j] {
698 return Err(crate::ShapError::InvalidConfiguration(
699 "partition must contain every feature exactly once".into(),
700 ));
701 }
702 seen[j] = true
703 }
704 }
705 if seen.iter().any(|x| !*x) {
706 return Err(crate::ShapError::InvalidConfiguration(
707 "partition must contain every feature exactly once".into(),
708 ));
709 }
710 Ok(Self { groups, n_features })
711 }
712 pub fn validate(&self) -> crate::Result<()> {
714 Self::new(self.groups.clone(), self.n_features).map(|_| ())
715 }
716 pub fn groups(&self) -> &[Vec<usize>] {
717 &self.groups
718 }
719 pub fn n_features(&self) -> usize {
720 self.n_features
721 }
722}