1use ndarray::{Array2, ArrayView1, ArrayView2};
8use rand::seq::SliceRandom;
9use rand::Rng;
10
11use crate::error::{Result, ShapError};
12
13#[derive(Debug, Clone, PartialEq, serde::Serialize)]
24pub struct Background {
25 data: Array2<f64>,
26}
27#[derive(serde::Deserialize)]
28struct BackgroundPayload {
29 data: Array2<f64>,
30}
31impl<'de> serde::Deserialize<'de> for Background {
32 fn deserialize<D: serde::Deserializer<'de>>(
33 deserializer: D,
34 ) -> std::result::Result<Self, D::Error> {
35 let payload = <BackgroundPayload as serde::Deserialize>::deserialize(deserializer)?;
36 Self::new(payload.data).map_err(serde::de::Error::custom)
37 }
38}
39
40impl Background {
41 pub fn validate(&self) -> Result<()> {
43 if self.data.nrows() == 0 {
44 return Err(ShapError::EmptyBackground);
45 }
46 if self.data.ncols() == 0 {
47 return Err(ShapError::InvalidConfiguration(
48 "background must contain at least one feature".into(),
49 ));
50 }
51 Ok(())
52 }
53 pub fn new(data: Array2<f64>) -> Result<Self> {
60 if data.nrows() == 0 {
61 return Err(ShapError::EmptyBackground);
62 }
63 if data.ncols() == 0 {
64 return Err(ShapError::InvalidConfiguration(
65 "background must contain at least one feature".to_string(),
66 ));
67 }
68
69 Ok(Self { data })
70 }
71
72 pub fn from_view(data: ArrayView2<'_, f64>) -> Result<Self> {
74 Self::new(data.to_owned())
75 }
76
77 pub fn data(&self) -> ArrayView2<'_, f64> {
79 self.data.view()
80 }
81
82 pub fn n_samples(&self) -> usize {
84 self.data.nrows()
85 }
86
87 pub fn n_features(&self) -> usize {
89 self.data.ncols()
90 }
91
92 pub fn row(&self, index: usize) -> Result<ArrayView1<'_, f64>> {
99 self.validate()?;
100 if index >= self.n_samples() {
101 return Err(ShapError::InvalidConfiguration(format!(
102 "background row index {index} is out of bounds for {} rows",
103 self.n_samples()
104 )));
105 }
106 self.data
107 .row(index)
108 .into_shape_with_order(self.n_features())
109 .map_err(|_| {
110 ShapError::InvalidConfiguration(format!("unable to access background row {index}"))
111 })
112 }
113
114 pub fn sample<R: Rng + ?Sized>(&self, n_samples: usize, rng: &mut R) -> Result<Self> {
122 self.validate()?;
123 if n_samples == 0 {
124 return Err(ShapError::InvalidConfiguration(
125 "background sample size must be greater than zero".to_string(),
126 ));
127 }
128
129 if n_samples >= self.n_samples() {
130 return Ok(self.clone());
131 }
132
133 let mut indices: Vec<usize> = (0..self.n_samples()).collect();
134 indices.shuffle(rng);
135 indices.truncate(n_samples);
136
137 let sampled = self.data.select(ndarray::Axis(0), &indices);
138
139 Self::new(sampled)
140 }
141
142 pub fn select(&self, indices: &[usize]) -> Result<Self> {
146 self.validate()?;
147 if indices.is_empty() {
148 return Err(ShapError::EmptyBackground);
149 }
150
151 for &index in indices {
152 if index >= self.n_samples() {
153 return Err(ShapError::InvalidConfiguration(format!(
154 "background row index {index} is out of bounds for {} rows",
155 self.n_samples()
156 )));
157 }
158 }
159
160 Self::new(self.data.select(ndarray::Axis(0), indices))
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
166pub enum BackgroundSampling {
167 #[default]
169 All,
170
171 Random(usize),
173}
174
175impl BackgroundSampling {
176 pub fn apply<R: Rng + ?Sized>(
178 self,
179 background: &Background,
180 rng: &mut R,
181 ) -> Result<Background> {
182 match self {
183 Self::All => Ok(background.clone()),
184 Self::Random(n_samples) => background.sample(n_samples, rng),
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use ndarray::array;
193 use rand::rngs::StdRng;
194 use rand::SeedableRng;
195
196 #[test]
197 fn creates_background() {
198 let data = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0],];
199
200 let background = Background::new(data).unwrap();
201
202 assert_eq!(background.n_samples(), 3);
203 assert_eq!(background.n_features(), 2);
204 }
205
206 #[test]
207 fn rejects_empty_background() {
208 let data = Array2::<f64>::zeros((0, 2));
209
210 let result = Background::new(data);
211
212 assert!(matches!(result, Err(ShapError::EmptyBackground)));
213 }
214
215 #[test]
216 fn exposes_data() {
217 let data = array![[1.0, 2.0], [3.0, 4.0],];
218
219 let background = Background::new(data.clone()).unwrap();
220
221 assert_eq!(background.data(), data.view());
222 }
223
224 #[test]
225 fn accesses_row() {
226 let data = array![[1.0, 2.0], [3.0, 4.0],];
227
228 let background = Background::new(data).unwrap();
229
230 let row = background.row(1).unwrap();
231
232 assert_eq!(row, array![3.0, 4.0].view());
233 }
234
235 #[test]
236 fn random_sampling_is_reproducible() {
237 let data = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [9.0, 10.0],];
238
239 let background = Background::new(data).unwrap();
240
241 let mut rng1 = StdRng::seed_from_u64(42);
242 let mut rng2 = StdRng::seed_from_u64(42);
243
244 let sample1 = background.sample(3, &mut rng1).unwrap();
245 let sample2 = background.sample(3, &mut rng2).unwrap();
246
247 assert_eq!(sample1.data(), sample2.data());
248 }
249
250 #[test]
251 fn sampling_all_rows_returns_clone() {
252 let data = array![[1.0, 2.0], [3.0, 4.0],];
253
254 let background = Background::new(data.clone()).unwrap();
255
256 let mut rng = StdRng::seed_from_u64(42);
257
258 let sampled = background.sample(10, &mut rng).unwrap();
259
260 assert_eq!(sampled.data(), data.view());
261 }
262
263 #[test]
264 fn selects_rows() {
265 let data = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0],];
266
267 let background = Background::new(data).unwrap();
268
269 let selected = background.select(&[2, 0]).unwrap();
270
271 assert_eq!(selected.data(), array![[5.0, 6.0], [1.0, 2.0],].view());
272 }
273
274 #[test]
275 fn rejects_empty_selection() {
276 let data = array![[1.0, 2.0], [3.0, 4.0],];
277
278 let background = Background::new(data).unwrap();
279
280 let result = background.select(&[]);
281
282 assert!(matches!(result, Err(ShapError::EmptyBackground)));
283 }
284
285 #[test]
286 fn rejects_invalid_selection_index() {
287 let data = array![[1.0, 2.0], [3.0, 4.0],];
288
289 let background = Background::new(data).unwrap();
290
291 let result = background.select(&[5]);
292
293 assert!(matches!(result, Err(ShapError::InvalidConfiguration(_))));
294 }
295
296 #[test]
297 fn sampling_strategy_all() {
298 let data = array![[1.0, 2.0], [3.0, 4.0],];
299
300 let background = Background::new(data.clone()).unwrap();
301
302 let mut rng = StdRng::seed_from_u64(42);
303
304 let result = BackgroundSampling::All
305 .apply(&background, &mut rng)
306 .unwrap();
307
308 assert_eq!(result.data(), data.view());
309 }
310
311 #[test]
312 fn sampling_strategy_random() {
313 let data = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0],];
314
315 let background = Background::new(data).unwrap();
316
317 let mut rng = StdRng::seed_from_u64(42);
318
319 let result = BackgroundSampling::Random(2)
320 .apply(&background, &mut rng)
321 .unwrap();
322
323 assert_eq!(result.n_samples(), 2);
324 assert_eq!(result.n_features(), 2);
325 }
326}