1use crate::{EvaluationConfig, Explanation, Link, Result, ShapError};
4use ndarray::{Array2, Array3, ArrayView2, Axis};
5use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng};
6use serde::{Deserialize, Deserializer, Serialize};
7use std::collections::{HashMap, HashSet, VecDeque};
8
9#[derive(Debug, Clone, PartialEq, Serialize)]
11pub struct SparseMatrix {
12 rows: usize,
13 columns: usize,
14 indptr: Vec<usize>,
15 indices: Vec<usize>,
16 values: Vec<f64>,
17}
18
19#[derive(Deserialize)]
20struct SparseMatrixPayload {
21 rows: usize,
22 columns: usize,
23 indptr: Vec<usize>,
24 indices: Vec<usize>,
25 values: Vec<f64>,
26}
27
28impl<'de> Deserialize<'de> for SparseMatrix {
29 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
30 let payload = SparseMatrixPayload::deserialize(deserializer)?;
31 Self::new(
32 payload.rows,
33 payload.columns,
34 payload.indptr,
35 payload.indices,
36 payload.values,
37 )
38 .map_err(serde::de::Error::custom)
39 }
40}
41
42impl SparseMatrix {
43 pub fn new(
44 rows: usize,
45 columns: usize,
46 indptr: Vec<usize>,
47 indices: Vec<usize>,
48 values: Vec<f64>,
49 ) -> Result<Self> {
50 let matrix = Self {
51 rows,
52 columns,
53 indptr,
54 indices,
55 values,
56 };
57 matrix.validate()?;
58 Ok(matrix)
59 }
60
61 pub fn from_dense(dense: ArrayView2<'_, f64>) -> Result<Self> {
62 if dense.ncols() == 0 {
63 return Err(ShapError::InvalidConfiguration(
64 "sparse matrices must contain at least one column".into(),
65 ));
66 }
67 let mut indptr = Vec::with_capacity(dense.nrows().saturating_add(1));
68 let mut indices = Vec::new();
69 let mut values = Vec::new();
70 indptr.push(0);
71 for row in dense.rows() {
72 for (column, value) in row.iter().copied().enumerate() {
73 if value != 0.0 {
74 indices.push(column);
75 values.push(value);
76 }
77 }
78 indptr.push(indices.len());
79 }
80 Self::new(dense.nrows(), dense.ncols(), indptr, indices, values)
81 }
82
83 pub fn nrows(&self) -> usize {
84 self.rows
85 }
86
87 pub fn ncols(&self) -> usize {
88 self.columns
89 }
90
91 pub fn nnz(&self) -> usize {
92 self.values.len()
93 }
94
95 pub fn indptr(&self) -> &[usize] {
96 &self.indptr
97 }
98
99 pub fn indices(&self) -> &[usize] {
100 &self.indices
101 }
102
103 pub fn values(&self) -> &[f64] {
104 &self.values
105 }
106
107 pub fn validate(&self) -> Result<()> {
108 if self.columns == 0 {
109 return Err(ShapError::InvalidConfiguration(
110 "sparse matrices must contain at least one column".into(),
111 ));
112 }
113 if self.indptr.len() != self.rows.saturating_add(1)
114 || self.indptr.first().copied() != Some(0)
115 || self.indptr.last().copied() != Some(self.indices.len())
116 || self.indices.len() != self.values.len()
117 {
118 return Err(ShapError::InvalidConfiguration(
119 "invalid CSR pointer or value lengths".into(),
120 ));
121 }
122 if self.values.contains(&0.0) {
123 return Err(ShapError::InvalidConfiguration(
124 "canonical CSR values must not contain explicit zeros".into(),
125 ));
126 }
127 for row in 0..self.rows {
128 let start = self.indptr[row];
129 let end = self.indptr[row + 1];
130 if start > end || end > self.indices.len() {
131 return Err(ShapError::InvalidConfiguration(
132 "CSR row pointers must be monotonic and in bounds".into(),
133 ));
134 }
135 let row_indices = &self.indices[start..end];
136 if row_indices.iter().any(|index| *index >= self.columns)
137 || row_indices.windows(2).any(|pair| pair[0] >= pair[1])
138 {
139 return Err(ShapError::InvalidConfiguration(
140 "CSR column indices must be sorted, unique, and in bounds".into(),
141 ));
142 }
143 }
144 Ok(())
145 }
146
147 pub fn row(&self, row: usize) -> Result<SparseRowView<'_>> {
148 if row >= self.rows {
149 return Err(ShapError::InvalidSampleIndex {
150 index: row,
151 n_samples: self.rows,
152 });
153 }
154 let range = self.indptr[row]..self.indptr[row + 1];
155 Ok(SparseRowView {
156 columns: self.columns,
157 indices: &self.indices[range.clone()],
158 values: &self.values[range],
159 })
160 }
161
162 pub fn to_dense(&self) -> Result<Array2<f64>> {
163 crate::error::checked_f64_shape(&[self.rows, self.columns], "dense sparse-matrix view")?;
164 let mut dense = Array2::zeros((self.rows, self.columns));
165 for row in 0..self.rows {
166 let sparse = self.row(row)?;
167 for (&column, &value) in sparse.indices.iter().zip(sparse.values) {
168 dense[[row, column]] = value;
169 }
170 }
171 Ok(dense)
172 }
173
174 fn concatenate_rows(parts: &[Self]) -> Result<Self> {
175 let columns = parts.first().map_or(0, Self::ncols);
176 if columns == 0 || parts.iter().any(|part| part.ncols() != columns) {
177 return Err(ShapError::DimensionMismatch {
178 expected: format!("sparse batches with {columns} columns"),
179 found: "incompatible sparse batch columns".into(),
180 });
181 }
182 let rows = parts.iter().try_fold(0usize, |total, part| {
183 total.checked_add(part.nrows()).ok_or_else(|| {
184 ShapError::InvalidConfiguration("sparse batch row count overflow".into())
185 })
186 })?;
187 let nnz = parts.iter().try_fold(0usize, |total, part| {
188 total.checked_add(part.nnz()).ok_or_else(|| {
189 ShapError::InvalidConfiguration("sparse batch nonzero count overflow".into())
190 })
191 })?;
192 let pointers = rows.checked_add(1).ok_or_else(|| {
193 ShapError::InvalidConfiguration("sparse batch pointer count overflow".into())
194 })?;
195 let mut indptr = Vec::new();
196 let mut indices = Vec::new();
197 let mut values = Vec::new();
198 indptr.try_reserve_exact(pointers).map_err(|error| {
199 ShapError::InvalidConfiguration(format!(
200 "cannot allocate sparse batch row pointers: {error}"
201 ))
202 })?;
203 indices.try_reserve_exact(nnz).map_err(|error| {
204 ShapError::InvalidConfiguration(format!(
205 "cannot allocate sparse batch indices: {error}"
206 ))
207 })?;
208 values.try_reserve_exact(nnz).map_err(|error| {
209 ShapError::InvalidConfiguration(format!("cannot allocate sparse batch values: {error}"))
210 })?;
211 indptr.push(0);
212 for part in parts {
213 for row in 0..part.nrows() {
214 let range = part.indptr[row]..part.indptr[row + 1];
215 indices.extend_from_slice(&part.indices[range.clone()]);
216 values.extend_from_slice(&part.values[range]);
217 indptr.push(indices.len());
218 }
219 }
220 Self::new(rows, columns, indptr, indices, values)
221 }
222}
223
224#[derive(Debug, Clone, Copy)]
226pub struct SparseRowView<'a> {
227 columns: usize,
228 indices: &'a [usize],
229 values: &'a [f64],
230}
231
232impl SparseRowView<'_> {
233 pub fn len(&self) -> usize {
234 self.columns
235 }
236
237 pub fn is_empty(&self) -> bool {
238 self.columns == 0
239 }
240
241 pub fn indices(&self) -> &[usize] {
242 self.indices
243 }
244
245 pub fn values(&self) -> &[f64] {
246 self.values
247 }
248
249 pub fn get(&self, column: usize) -> Result<f64> {
250 if column >= self.columns {
251 return Err(ShapError::InvalidFeatureIndex {
252 index: column,
253 n_features: self.columns,
254 });
255 }
256 Ok(self
257 .indices
258 .binary_search(&column)
259 .map(|position| self.values[position])
260 .unwrap_or(0.0))
261 }
262}
263
264pub trait SparsePredict {
266 fn predict_sparse(&self, input: &SparseMatrix) -> Result<Array2<f64>>;
267 fn n_features(&self) -> Option<usize> {
268 None
269 }
270 fn n_outputs(&self) -> Option<usize> {
271 None
272 }
273}
274
275impl<T: SparsePredict + ?Sized> SparsePredict for &T {
276 fn predict_sparse(&self, input: &SparseMatrix) -> Result<Array2<f64>> {
277 (**self).predict_sparse(input)
278 }
279 fn n_features(&self) -> Option<usize> {
280 (**self).n_features()
281 }
282 fn n_outputs(&self) -> Option<usize> {
283 (**self).n_outputs()
284 }
285}
286
287pub struct FnSparseModel<F> {
288 predict_fn: F,
289 n_features: Option<usize>,
290 n_outputs: Option<usize>,
291}
292
293impl<F> FnSparseModel<F> {
294 pub fn new(predict_fn: F) -> Self {
295 Self {
296 predict_fn,
297 n_features: None,
298 n_outputs: None,
299 }
300 }
301 pub fn with_n_features(mut self, n_features: usize) -> Self {
302 self.n_features = Some(n_features);
303 self
304 }
305 pub fn with_n_outputs(mut self, n_outputs: usize) -> Self {
306 self.n_outputs = Some(n_outputs);
307 self
308 }
309}
310
311impl<F> SparsePredict for FnSparseModel<F>
312where
313 F: Fn(&SparseMatrix) -> Result<Array2<f64>>,
314{
315 fn predict_sparse(&self, input: &SparseMatrix) -> Result<Array2<f64>> {
316 (self.predict_fn)(input)
317 }
318 fn n_features(&self) -> Option<usize> {
319 self.n_features
320 }
321 fn n_outputs(&self) -> Option<usize> {
322 self.n_outputs
323 }
324}
325
326#[derive(Debug, Clone)]
328pub struct SparseIndependentMasker {
329 background: SparseMatrix,
330}
331
332impl SparseIndependentMasker {
333 pub fn new(background: SparseMatrix) -> Result<Self> {
334 if background.nrows() == 0 {
335 return Err(ShapError::EmptyBackground);
336 }
337 Ok(Self { background })
338 }
339
340 pub fn background(&self) -> &SparseMatrix {
341 &self.background
342 }
343
344 pub fn mask(&self, sample: SparseRowView<'_>, present: &[bool]) -> Result<SparseMatrix> {
345 if sample.len() != self.background.ncols() || present.len() != self.background.ncols() {
346 return Err(ShapError::DimensionMismatch {
347 expected: format!("{} sparse features", self.background.ncols()),
348 found: format!("sample {}, mask {}", sample.len(), present.len()),
349 });
350 }
351 let pointer_capacity = self.background.nrows().checked_add(1).ok_or_else(|| {
352 ShapError::InvalidConfiguration("sparse mask row pointer count overflow".into())
353 })?;
354 let additional = self
355 .background
356 .nrows()
357 .checked_mul(sample.indices.len())
358 .and_then(|count| count.checked_add(self.background.nnz()))
359 .ok_or_else(|| {
360 ShapError::InvalidConfiguration("sparse masked nonzero bound overflow".into())
361 })?;
362 let mut indptr = Vec::new();
363 let mut indices = Vec::new();
364 let mut values = Vec::new();
365 indptr
366 .try_reserve_exact(pointer_capacity)
367 .map_err(|error| {
368 ShapError::InvalidConfiguration(format!(
369 "cannot allocate sparse mask row pointers: {error}"
370 ))
371 })?;
372 indices.try_reserve(additional).map_err(|error| {
373 ShapError::InvalidConfiguration(format!(
374 "cannot allocate sparse masked indices: {error}"
375 ))
376 })?;
377 values.try_reserve(additional).map_err(|error| {
378 ShapError::InvalidConfiguration(format!(
379 "cannot allocate sparse masked values: {error}"
380 ))
381 })?;
382 indptr.push(0);
383 for row in 0..self.background.nrows() {
384 let background = self.background.row(row)?;
385 let mut sample_position = 0;
386 let mut background_position = 0;
387 while sample_position < sample.indices.len()
388 || background_position < background.indices.len()
389 {
390 let sample_column = sample.indices.get(sample_position).copied();
391 let background_column = background.indices.get(background_position).copied();
392 let column = match (sample_column, background_column) {
393 (Some(left), Some(right)) => left.min(right),
394 (Some(left), None) => left,
395 (None, Some(right)) => right,
396 (None, None) => break,
397 };
398 let sample_value = if sample_column == Some(column) {
399 let value = sample.values[sample_position];
400 sample_position += 1;
401 value
402 } else {
403 0.0
404 };
405 let background_value = if background_column == Some(column) {
406 let value = background.values[background_position];
407 background_position += 1;
408 value
409 } else {
410 0.0
411 };
412 let value = if present[column] {
413 sample_value
414 } else {
415 background_value
416 };
417 if value != 0.0 {
418 indices.push(column);
419 values.push(value);
420 }
421 }
422 indptr.push(indices.len());
423 }
424 SparseMatrix::new(
425 self.background.nrows(),
426 self.background.ncols(),
427 indptr,
428 indices,
429 values,
430 )
431 }
432}
433
434struct SparseCoalitionEvaluator<'a, M> {
435 model: &'a M,
436 masker: &'a SparseIndependentMasker,
437 config: EvaluationConfig,
438 cache: HashMap<u64, Vec<f64>>,
439 order: VecDeque<u64>,
440 rows_evaluated: usize,
441 outputs: Option<usize>,
442}
443
444impl<'a, M: SparsePredict> SparseCoalitionEvaluator<'a, M> {
445 fn new(
446 model: &'a M,
447 masker: &'a SparseIndependentMasker,
448 config: EvaluationConfig,
449 ) -> Result<Self> {
450 Ok(Self {
451 model,
452 masker,
453 config: config.validate()?,
454 cache: HashMap::new(),
455 order: VecDeque::new(),
456 rows_evaluated: 0,
457 outputs: None,
458 })
459 }
460
461 fn evaluate(&mut self, sample: SparseRowView<'_>, masks: &[u64]) -> Result<Vec<Vec<f64>>> {
462 let mut result = HashMap::new();
463 let mut missing = Vec::new();
464 let mut seen = HashSet::new();
465 for &mask in masks {
466 if let Some(value) = self.cache.get(&mask).cloned() {
467 self.touch(mask);
468 result.insert(mask, value);
469 } else if seen.insert(mask) {
470 missing.push(mask);
471 }
472 }
473 for chunk in missing.chunks(self.config.coalition_batch_size) {
474 let parts = chunk
475 .iter()
476 .map(|mask| {
477 self.masker.mask(
478 sample,
479 &crate::coalition::members(*mask, self.masker.background.ncols()),
480 )
481 })
482 .collect::<Result<Vec<_>>>()?;
483 let batch = SparseMatrix::concatenate_rows(&parts)?;
484 if self
485 .config
486 .max_model_rows
487 .is_some_and(|limit| self.rows_evaluated.saturating_add(batch.nrows()) > limit)
488 {
489 return Err(ShapError::InvalidConfiguration(
490 "model row evaluation limit exceeded".into(),
491 ));
492 }
493 if let Some(features) = self.model.n_features() {
494 if features != batch.ncols() {
495 return Err(ShapError::DimensionMismatch {
496 expected: format!("{features} sparse model features"),
497 found: format!("{}", batch.ncols()),
498 });
499 }
500 }
501 let predictions = self.model.predict_sparse(&batch)?;
502 if predictions.nrows() != batch.nrows() || predictions.ncols() == 0 {
503 return Err(ShapError::DimensionMismatch {
504 expected: format!("({}, outputs>0)", batch.nrows()),
505 found: format!("{:?}", predictions.dim()),
506 });
507 }
508 if predictions.iter().any(|value| !value.is_finite()) {
509 return Err(ShapError::ModelError(
510 "sparse prediction contains a non-finite value".into(),
511 ));
512 }
513 if let Some(outputs) = self.outputs {
514 if outputs != predictions.ncols() {
515 return Err(ShapError::OutputDimensionMismatch {
516 expected: outputs,
517 found: predictions.ncols(),
518 });
519 }
520 } else {
521 self.outputs = Some(predictions.ncols());
522 }
523 self.rows_evaluated =
524 self.rows_evaluated
525 .checked_add(batch.nrows())
526 .ok_or_else(|| {
527 ShapError::InvalidConfiguration("sparse row count overflow".into())
528 })?;
529 let mut offset = 0;
530 for (&mask, part) in chunk.iter().zip(&parts) {
531 let end = offset + part.nrows();
532 let value = predictions
533 .slice_axis(Axis(0), ndarray::Slice::from(offset..end))
534 .mean_axis(Axis(0))
535 .unwrap()
536 .to_vec();
537 offset = end;
538 while self.cache.len() >= self.config.cache_capacity {
539 let Some(evicted) = self.order.pop_front() else {
540 break;
541 };
542 self.cache.remove(&evicted);
543 }
544 self.cache.insert(mask, value.clone());
545 self.order.push_back(mask);
546 result.insert(mask, value);
547 }
548 }
549 masks
550 .iter()
551 .map(|mask| {
552 result.get(mask).cloned().ok_or_else(|| {
553 ShapError::Other("sparse coalition cache invariant failed".into())
554 })
555 })
556 .collect()
557 }
558
559 fn touch(&mut self, mask: u64) {
560 if let Some(position) = self.order.iter().position(|value| *value == mask) {
561 self.order.remove(position);
562 }
563 self.order.push_back(mask);
564 }
565}
566
567pub struct SparsePermutationExplainer<M> {
570 model: M,
571 masker: SparseIndependentMasker,
572 n_permutations: usize,
573 seed: u64,
574 antithetic: bool,
575 link: Link,
576 evaluation: EvaluationConfig,
577}
578
579impl<M> SparsePermutationExplainer<M> {
580 pub fn new(model: M, background: SparseMatrix) -> Result<Self> {
581 Ok(Self {
582 model,
583 masker: SparseIndependentMasker::new(background)?,
584 n_permutations: 128,
585 seed: 0,
586 antithetic: true,
587 link: Link::Identity,
588 evaluation: EvaluationConfig {
589 coalition_batch_size: 64,
590 cache_capacity: 65536,
591 max_model_rows: None,
592 },
593 })
594 }
595 pub fn with_n_permutations(mut self, count: usize) -> Self {
596 self.n_permutations = count;
597 self
598 }
599 pub fn with_seed(mut self, seed: u64) -> Self {
600 self.seed = seed;
601 self
602 }
603 pub fn with_antithetic(mut self, enabled: bool) -> Self {
604 self.antithetic = enabled;
605 self
606 }
607 pub fn with_link(mut self, link: Link) -> Self {
608 self.link = link;
609 self
610 }
611 pub fn with_evaluation_config(mut self, config: EvaluationConfig) -> Self {
612 self.evaluation = config;
613 self
614 }
615}
616
617impl<M: SparsePredict> SparsePermutationExplainer<M> {
618 pub fn explain(&self, input: &SparseMatrix) -> Result<Explanation> {
619 input.validate()?;
620 let features = self.masker.background.ncols();
621 if input.nrows() == 0 {
622 return Err(ShapError::EmptyData);
623 }
624 if input.ncols() != features {
625 return Err(ShapError::DimensionMismatch {
626 expected: format!("{features} sparse features"),
627 found: format!("{}", input.ncols()),
628 });
629 }
630 if features >= 63 {
631 return Err(ShapError::InvalidConfiguration(
632 "sparse permutation SHAP currently supports at most 62 features".into(),
633 ));
634 }
635 if self.n_permutations == 0 {
636 return Err(ShapError::InvalidConfiguration(
637 "n_permutations must be positive".into(),
638 ));
639 }
640 self.n_permutations.checked_mul(features).ok_or_else(|| {
641 ShapError::InvalidConfiguration("sparse permutation step count overflow".into())
642 })?;
643 let mut probe = SparseCoalitionEvaluator::new(&self.model, &self.masker, self.evaluation)?;
644 let outputs = probe.evaluate(input.row(0)?, &[0])?[0].len();
645 crate::error::checked_f64_shape(
646 &[input.nrows(), features, outputs],
647 "sparse permutation explanation",
648 )?;
649 let mut values = Array3::zeros((input.nrows(), features, outputs));
650 let mut bases = Array2::zeros((input.nrows(), outputs));
651 for sample_index in 0..input.nrows() {
652 let sample = input.row(sample_index)?;
653 let mut rng = StdRng::seed_from_u64(sparse_sample_seed(self.seed, sample));
654 let mut requested = vec![0u64];
655 let mut steps = Vec::with_capacity(self.n_permutations * features);
656 let mut generated = 0;
657 while generated < self.n_permutations {
658 let mut order = (0..features).collect::<Vec<_>>();
659 order.shuffle(&mut rng);
660 append_order(&order, &mut requested, &mut steps);
661 generated += 1;
662 if self.antithetic && generated < self.n_permutations {
663 order.reverse();
664 append_order(&order, &mut requested, &mut steps);
665 generated += 1;
666 }
667 }
668 let mut evaluator =
669 SparseCoalitionEvaluator::new(&self.model, &self.masker, self.evaluation)?;
670 let evaluated = evaluator
671 .evaluate(sample, &requested)?
672 .into_iter()
673 .map(|row| {
674 row.into_iter()
675 .map(|value| self.link.forward(value))
676 .collect::<Result<Vec<_>>>()
677 })
678 .collect::<Result<Vec<_>>>()?;
679 for output in 0..outputs {
680 bases[[sample_index, output]] = evaluated[0][output];
681 }
682 for (feature, before, after) in steps {
683 for output in 0..outputs {
684 values[[sample_index, feature, output]] += (evaluated[after][output]
685 - evaluated[before][output])
686 / self.n_permutations as f64;
687 }
688 }
689 }
690 Explanation::new(values, bases, input.to_dense()?)
691 }
692}
693
694fn append_order(order: &[usize], requested: &mut Vec<u64>, steps: &mut Vec<(usize, usize, usize)>) {
695 let mut mask = 0u64;
696 let mut before = 0usize;
697 for &feature in order {
698 mask |= 1u64 << feature;
699 requested.push(mask);
700 let after = requested.len() - 1;
701 steps.push((feature, before, after));
702 before = after;
703 }
704}
705
706fn sparse_sample_seed(seed: u64, sample: SparseRowView<'_>) -> u64 {
707 fn mix(mut value: u64) -> u64 {
708 value ^= value >> 30;
709 value = value.wrapping_mul(0xBF58_476D_1CE4_E5B9);
710 value ^= value >> 27;
711 value = value.wrapping_mul(0x94D0_49BB_1331_11EB);
712 value ^ (value >> 31)
713 }
714 sample.indices.iter().zip(sample.values).fold(
715 mix(seed ^ sample.columns as u64),
716 |state, (&index, &value)| mix(state ^ mix(index as u64) ^ mix(value.to_bits())),
717 )
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723 use ndarray::array;
724 use std::cell::Cell;
725
726 #[test]
727 fn csr_validation_and_dense_round_trip() {
728 let dense = array![[0., 2., 0.], [3., 0., 4.]];
729 let sparse = SparseMatrix::from_dense(dense.view()).unwrap();
730 assert_eq!(sparse.nnz(), 3);
731 assert_eq!(sparse.to_dense().unwrap(), dense);
732 assert!(SparseMatrix::new(1, 2, vec![0, 2], vec![1, 1], vec![2., 3.]).is_err());
733 }
734
735 #[test]
736 fn sparse_masker_merges_rows_without_dense_coalitions() {
737 let background =
738 SparseMatrix::from_dense(array![[0., 2., 0.], [3., 0., 4.]].view()).unwrap();
739 let sample_matrix = SparseMatrix::from_dense(array![[5., 0., 6.]].view()).unwrap();
740 let masker = SparseIndependentMasker::new(background).unwrap();
741 let masked = masker
742 .mask(sample_matrix.row(0).unwrap(), &[true, false, true])
743 .unwrap();
744 assert_eq!(
745 masked.to_dense().unwrap(),
746 array![[5., 2., 6.], [5., 0., 6.]]
747 );
748 assert_eq!(masked.nnz(), 5);
749 }
750
751 #[test]
752 fn sparse_permutation_matches_additive_model_without_dense_model_input() {
753 let sparse_calls = Cell::new(0usize);
754 let model = FnSparseModel::new(|input: &SparseMatrix| {
755 sparse_calls.set(sparse_calls.get() + 1);
756 Ok(Array2::from_shape_fn((input.nrows(), 1), |(row, _)| {
757 let sparse = input.row(row).unwrap();
758 sparse
759 .indices()
760 .iter()
761 .zip(sparse.values())
762 .map(|(&column, &value)| (column as f64 + 1.0) * value)
763 .sum()
764 }))
765 });
766 let background = SparseMatrix::from_dense(array![[0., 0., 0.]].view()).unwrap();
767 let input = SparseMatrix::from_dense(array![[2., 0., 4.]].view()).unwrap();
768 let explanation = SparsePermutationExplainer::new(model, background)
769 .unwrap()
770 .with_n_permutations(2)
771 .explain(&input)
772 .unwrap();
773 assert_eq!(explanation.values(), array![[[2.], [0.], [12.]]].view());
774 assert_eq!(explanation.reconstructed(), array![[14.]]);
775 assert!(sparse_calls.get() > 0);
776 }
777}