Skip to main content

causal_hub/datasets/
missing.rs

1use std::{
2    fmt::{Display, Formatter},
3    ops::Index,
4};
5
6use itertools::{Either, Itertools};
7use ndarray::prelude::*;
8use ndarray_stats::CorrelationExt;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    datasets::Dataset,
13    models::HasLabels,
14    types::{Error, Labels, Map, Result, Set},
15};
16
17/// A struct representing the missing data indicators.
18#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
19pub struct MissingMechanism {
20    labels: Labels,
21    pr: Map<usize, Set<usize>>,
22}
23
24impl MissingMechanism {
25    /// Create a new missing mechanism.
26    pub fn new(labels: Labels, mut pr: Map<usize, Set<usize>>) -> Result<Self> {
27        // Check if all indices are within bounds.
28        let n = labels.len();
29        for (&x, ys) in &pr {
30            if x >= n {
31                return Err(Error::IndexOutOfBounds(x));
32            }
33            for &y in ys {
34                if y >= n {
35                    return Err(Error::IndexOutOfBounds(y));
36                }
37            }
38        }
39
40        // Sort the missing mechanism.
41        pr.sort_keys();
42        pr.iter_mut().for_each(|(_, ys)| ys.sort());
43
44        Ok(Self { labels, pr })
45    }
46
47    /// Returns the number of missing variables.
48    pub fn len(&self) -> usize {
49        self.pr.len()
50    }
51
52    /// Checks if the missing mechanism is empty.
53    pub fn is_empty(&self) -> bool {
54        self.pr.is_empty()
55    }
56
57    /// Returns the missing variables.
58    pub fn keys(&self) -> impl Iterator<Item = &usize> {
59        self.pr.keys()
60    }
61
62    /// Returns the missingness parents.
63    pub fn values(&self) -> impl Iterator<Item = &Set<usize>> {
64        self.pr.values()
65    }
66
67    /// Checks if a variable is missing.
68    pub fn contains_key(&self, x: &usize) -> bool {
69        self.pr.contains_key(x)
70    }
71
72    /// Returns the missingness parents for a given variable.
73    pub fn get(&self, x: &usize) -> Option<&Set<usize>> {
74        self.pr.get(x)
75    }
76
77    /// Inserts a missing variable and its missingness parents.
78    pub fn insert(&mut self, x: usize, mut y: Set<usize>) {
79        // Sort the missingness parents.
80        y.sort();
81        // Insert in sorted order.
82        self.pr.insert_sorted(x, y);
83    }
84}
85
86impl HasLabels for MissingMechanism {
87    #[inline]
88    fn labels(&self) -> &Labels {
89        &self.labels
90    }
91}
92
93impl Index<usize> for MissingMechanism {
94    type Output = Set<usize>;
95
96    fn index(&self, index: usize) -> &Self::Output {
97        &self.pr[&index]
98    }
99}
100
101impl IntoIterator for MissingMechanism {
102    type Item = (usize, Set<usize>);
103    type IntoIter = indexmap::map::IntoIter<usize, Set<usize>>;
104
105    fn into_iter(self) -> Self::IntoIter {
106        self.pr.into_iter()
107    }
108}
109
110impl<'a> IntoIterator for &'a MissingMechanism {
111    type Item = (&'a usize, &'a Set<usize>);
112    type IntoIter = indexmap::map::Iter<'a, usize, Set<usize>>;
113
114    fn into_iter(self) -> Self::IntoIter {
115        self.pr.iter()
116    }
117}
118
119/// An enum representing different methods for handling missing data.
120#[non_exhaustive]
121#[derive(Clone, Copy, Debug)]
122pub enum MissingMethod {
123    /// List-wise deletion missing handling method.
124    LW,
125    /// Pair-wise deletion missing handling method.
126    PW,
127    /// Inverse probability weighting missing handling method.
128    IPW,
129    /// Augmented inverse probability weighting missing handling method.
130    AIPW,
131}
132
133/// Missing mechanism t types.
134#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
135pub enum MissingType {
136    /// Missing Completely At Random.
137    MCAR,
138    /// Missing At Random.
139    MAR,
140    /// Missing Not At Random.
141    MNAR,
142}
143
144impl Display for MissingType {
145    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146        match self {
147            Self::MCAR => write!(f, "MCAR"),
148            Self::MAR => write!(f, "MAR"),
149            Self::MNAR => write!(f, "MNAR"),
150        }
151    }
152}
153
154/// A struct for missing information in a tabular dataset.
155#[derive(Clone, Debug, Serialize, Deserialize)]
156pub struct MissingTable {
157    labels: Labels,
158    fully_observed: Set<usize>,
159    partially_observed: Set<usize>,
160    missing_mask: Array2<bool>,
161    missing_mask_by_cols: Array1<bool>,
162    missing_mask_by_rows: Array1<bool>,
163    missing_count: usize,
164    missing_count_by_cols: Array1<usize>,
165    missing_count_by_rows: Array1<usize>,
166    missing_rate: f64,
167    missing_rate_by_cols: Array1<f64>,
168    missing_rate_by_rows: Array1<f64>,
169    missing_correlation: Array2<f64>,
170    missing_covariance: Array2<f64>,
171    complete_cols_count: usize,
172    complete_rows_count: usize,
173}
174
175impl HasLabels for MissingTable {
176    #[inline]
177    fn labels(&self) -> &Labels {
178        &self.labels
179    }
180}
181
182impl MissingTable {
183    /// Create a new missing information table from the given labels and missing mask.
184    ///
185    /// # Arguments
186    ///
187    /// * `labels` - The labels of the dataset.
188    /// * `missing_mask` - A boolean matrix indicating missing values.
189    ///
190    /// # Returns
191    ///
192    /// A new missing information instance.
193    ///
194    pub fn new(mut labels: Labels, mut missing_mask: Array2<bool>) -> Result<Self> {
195        // Check if dimensions match.
196        if labels.len() != missing_mask.ncols() {
197            return Err(Error::IncompatibleShape(
198                &format!("|labels| = {}", labels.len()),
199                &format!("|cols| = {}", missing_mask.ncols()),
200            ));
201        }
202
203        // Check if labels are sorted.
204        if !labels.is_sorted() {
205            // Allocate indices to sort labels.
206            let mut indices: Vec<usize> = (0..labels.len()).collect();
207            // Sort the indices by labels.
208            indices.sort_by_key(|&i| &labels[i]);
209            // Sort the labels.
210            labels.sort();
211            // Allocate new missing mask.
212            let mut new_missing_mask = missing_mask.clone();
213            // Sort the new missing mask according to the sorted indices.
214            indices.into_iter().enumerate().for_each(|(i, j)| {
215                new_missing_mask
216                    .column_mut(i)
217                    .assign(&missing_mask.column(j));
218            });
219            // Update missing mask.
220            missing_mask = new_missing_mask;
221        }
222
223        // Compute missing counts.
224        let missing_count_by_cols = missing_mask.mapv(|x| x as usize).sum_axis(Axis(0));
225        let missing_count_by_rows = missing_mask.mapv(|x| x as usize).sum_axis(Axis(1));
226        let missing_count = missing_count_by_cols.sum();
227
228        // Compute missing mask by cols and rows.
229        let missing_mask_by_cols = missing_count_by_cols.mapv(|x| x > 0);
230        let missing_mask_by_rows = missing_count_by_rows.mapv(|x| x > 0);
231
232        // Compute fully and partially observed variable sets.
233        let (fully_observed, partially_observed) = missing_mask_by_cols
234            .iter()
235            .enumerate()
236            .partition_map(|(i, &x)| {
237                if !x {
238                    Either::Left(i)
239                } else {
240                    Either::Right(i)
241                }
242            });
243
244        // Compute complete counts.
245        let complete_cols_count = missing_mask_by_cols.mapv(|x| (!x) as usize).sum();
246        let complete_rows_count = missing_mask_by_rows.mapv(|x| (!x) as usize).sum();
247
248        // Compute missing rates.
249        let missing_rate_by_cols =
250            missing_count_by_cols.mapv(|x| x as f64) / missing_mask.nrows() as f64;
251        let missing_rate_by_rows =
252            missing_count_by_rows.mapv(|x| x as f64) / missing_mask.ncols() as f64;
253        let missing_rate = missing_count as f64 / missing_mask.len() as f64;
254
255        // TODO: Make this optional for large datasets.
256        // Map to numeric (float) mask.
257        let missing_mask_numeric = missing_mask.mapv(|x| x as u8 as f64);
258        // Transpose for correlation/covariance computation.
259        let missing_mask_numeric = missing_mask_numeric.t();
260        // Compute missing correlation.
261        let missing_correlation = missing_mask_numeric
262            .pearson_correlation()
263            .map_err(|evidence| Error::Stats(&evidence.to_string()))?;
264        // Compute missing covariance.
265        let missing_covariance = missing_mask_numeric
266            .cov(1.)
267            .map_err(|evidence| Error::Stats(&evidence.to_string()))?;
268
269        Ok(Self {
270            labels,
271            fully_observed,
272            partially_observed,
273            missing_mask,
274            missing_mask_by_cols,
275            missing_mask_by_rows,
276            missing_count,
277            missing_count_by_cols,
278            missing_count_by_rows,
279            missing_rate,
280            missing_rate_by_cols,
281            missing_rate_by_rows,
282            missing_correlation,
283            missing_covariance,
284            complete_cols_count,
285            complete_rows_count,
286        })
287    }
288
289    /// Get the set of fully observed variables.
290    ///
291    /// # Returns
292    ///
293    /// A reference to the set of fully observed variables.
294    ///
295    #[inline]
296    pub const fn fully_observed(&self) -> &Set<usize> {
297        &self.fully_observed
298    }
299
300    /// Get the set of partially observed variables.
301    ///
302    /// # Returns
303    ///
304    /// A reference to the set of partially observed variables.
305    ///
306    #[inline]
307    pub const fn partially_observed(&self) -> &Set<usize> {
308        &self.partially_observed
309    }
310
311    /// Get the missing mask indicating the presence of missing values in the table.
312    ///
313    /// # Returns
314    ///
315    /// A reference to the missing mask.
316    ///
317    #[inline]
318    pub const fn missing_mask(&self) -> &Array2<bool> {
319        &self.missing_mask
320    }
321
322    /// Get the missing mask indicating the presence of missing values in each column.
323    ///
324    /// # Returns
325    ///
326    /// A reference to the missing mask by columns.
327    ///
328    #[inline]
329    pub const fn missing_mask_by_cols(&self) -> &Array1<bool> {
330        &self.missing_mask_by_cols
331    }
332
333    /// Get the missing mask indicating the presence of missing values in each row.
334    ///
335    /// # Returns
336    ///
337    /// A reference to the missing mask by rows.
338    ///
339    #[inline]
340    pub const fn missing_mask_by_rows(&self) -> &Array1<bool> {
341        &self.missing_mask_by_rows
342    }
343
344    /// Get the total count of missing values in the table.
345    ///
346    /// # Returns
347    ///
348    /// The count of missing values.
349    ///
350    #[inline]
351    pub const fn missing_count(&self) -> usize {
352        self.missing_count
353    }
354
355    /// Get the count of missing values in each column.
356    ///
357    /// # Returns
358    ///
359    /// A reference to the missing count by columns.
360    ///
361    #[inline]
362    pub const fn missing_count_by_cols(&self) -> &Array1<usize> {
363        &self.missing_count_by_cols
364    }
365
366    /// Get the count of missing values in each row.
367    ///
368    /// # Returns
369    ///
370    /// A reference to the missing count by rows.
371    ///
372    #[inline]
373    pub const fn missing_count_by_rows(&self) -> &Array1<usize> {
374        &self.missing_count_by_rows
375    }
376
377    /// Get the overall missing rate in the table.
378    ///
379    /// # Returns
380    ///
381    /// The percentage of missing values.
382    ///
383    #[inline]
384    pub const fn missing_rate(&self) -> f64 {
385        self.missing_rate
386    }
387
388    /// Get the missing rate in each column.
389    ///
390    /// # Returns
391    ///
392    /// A reference to the missing percentage by columns.
393    ///
394    #[inline]
395    pub const fn missing_rate_by_cols(&self) -> &Array1<f64> {
396        &self.missing_rate_by_cols
397    }
398
399    /// Get the missing rate in each row.
400    ///
401    /// # Returns
402    ///
403    /// A reference to the missing percentage by rows.
404    ///
405    #[inline]
406    pub const fn missing_rate_by_rows(&self) -> &Array1<f64> {
407        &self.missing_rate_by_rows
408    }
409
410    /// Get the missing (Pearson) correlation matrix.
411    ///
412    /// # Returns
413    ///
414    /// A reference to the missing correlation matrix.
415    ///
416    #[inline]
417    pub const fn missing_correlation(&self) -> &Array2<f64> {
418        &self.missing_correlation
419    }
420
421    /// Get the missing (unbiased) covariance matrix.
422    ///
423    /// # Returns
424    ///
425    /// A reference to the missing covariance matrix.
426    ///
427    #[inline]
428    pub const fn missing_covariance(&self) -> &Array2<f64> {
429        &self.missing_covariance
430    }
431
432    /// Get the count of complete columns (without any missing values) in the table.
433    ///
434    /// # Returns
435    ///
436    /// The count of complete columns.
437    ///
438    #[inline]
439    pub const fn complete_cols_count(&self) -> usize {
440        self.complete_cols_count
441    }
442
443    /// Get the count of complete rows (without any missing values) in the table.
444    ///
445    /// # Returns
446    ///
447    /// The count of complete rows.
448    ///
449    #[inline]
450    pub const fn complete_rows_count(&self) -> usize {
451        self.complete_rows_count
452    }
453}
454
455/// A trait for incomplete datasets.
456pub trait IncDataset: Dataset + Sized {
457    /// The type of the missing data indicator.
458    type Missing;
459    /// The value of the missing data indicator.
460    const MISSING: Self::Missing;
461
462    /// The type of the complete dataset.
463    type Complete;
464    /// The type of the weighted dataset.
465    type Weighted;
466
467    /// Get the missing information.
468    ///
469    /// # Returns
470    ///
471    /// A reference to the missing information.
472    ///
473    fn missing(&self) -> &MissingTable;
474
475    /// Apply a missing data handling method to the dataset.
476    ///
477    /// # Arguments
478    ///
479    /// * `m` - The missing data handling method to apply.
480    /// * `x` - An optional set of variables to consider for missing data handling.
481    /// * `pr` - An optional missing mechanism specification.
482    ///
483    /// # Errors
484    ///
485    /// * If the set of variables to consider for missing data handling is empty.
486    /// * If any variable in the set is out of bounds.
487    ///
488    /// # Returns
489    ///
490    /// Either a complete or weighted dataset.
491    ///
492    fn apply_missing_method(
493        &self,
494        model: &MissingMethod,
495        x: Option<&Set<usize>>,
496        pr: Option<&MissingMechanism>,
497    ) -> Result<Either<Self::Complete, Self::Weighted>> {
498        // Get short alias for missing method.
499        use MissingMethod as MM;
500        // Apply the missing method with the provided arguments.
501        match (model, x, pr) {
502            (MM::LW, _, _) => self.lw_deletion().map(Either::Left),
503            (MM::PW, Some(x), _) => self.pw_deletion(x).map(Either::Left),
504            (MM::IPW, Some(x), Some(pr)) => self.ipw_deletion(x, pr).map(Either::Right),
505            (MM::AIPW, Some(x), Some(pr)) => self.aipw_deletion(x, pr).map(Either::Right),
506            _ => Err(Error::InvalidParameter(
507                "missing_method",
508                &format!(
509                    "Invalid arguments for applying missing method:\n\
510                    \t missing method:      '{model:?}' , \n\
511                    \t selected variables:  '{x:?}' , \n\
512                    \t missing mechanism:   '{pr:?}' .",
513                ),
514            )),
515        }
516    }
517
518    /// Compute the weights to perform IPW.
519    fn ipw_weights(
520        &self,
521        d_u: &Self::Complete,
522        u: &Set<usize>,
523        pr: &MissingMechanism,
524    ) -> Result<Array1<f64>>;
525
526    /// Perform list-wise (LW) deletion to handle missing data.
527    ///
528    /// # Errors
529    ///
530    /// * If the dataset is empty after LW deletion.
531    ///
532    /// # Returns
533    ///
534    /// A complete dataset obtained via LW deletion.
535    ///
536    fn lw_deletion(&self) -> Result<Self::Complete>;
537
538    /// Perform pair-wise (PW) deletion to handle missing data for the specified columns.
539    ///
540    /// # Arguments
541    ///
542    /// * `x` - A set of column indices for PW deletion.
543    ///
544    /// # Errors
545    ///
546    /// * If the set of variables to consider for missing data handling is empty.
547    /// * If any variable in the set is out of bounds.
548    ///
549    /// # Returns
550    ///
551    /// A complete dataset restricted to the specified columns via PW deletion.
552    ///
553    fn pw_deletion(&self, x: &Set<usize>) -> Result<Self::Complete>;
554
555    /// Perform inverse probability weighting (IPW) deletion to handle missing data for the specified columns.
556    ///
557    /// # Arguments
558    ///
559    /// * `x` - A set of column indices for IPW deletion.
560    /// * `pr` - The missing data indicators.
561    ///
562    /// # Errors
563    ///
564    /// * If the set of variables to consider for missing data handling is empty.
565    /// * If any variable in the set is out of bounds.
566    ///
567    /// # Returns
568    ///
569    /// A weighted dataset restricted to the specified columns via IPW deletion.
570    ///
571    fn ipw_deletion(&self, x: &Set<usize>, pr: &MissingMechanism) -> Result<Self::Weighted>;
572
573    /// Perform augmented inverse probability weighting (AIPW) deletion to handle missing data for the specified columns.
574    ///
575    /// # Arguments
576    ///
577    /// * `x` - A set of column indices for AIPW deletion.
578    /// * `pr` - The missing data indicators.
579    ///
580    /// # Errors
581    ///
582    /// * If the set of variables to consider for missing data handling is empty.
583    /// * If any variable in the set is out of bounds.
584    ///
585    /// # Returns
586    ///
587    /// A weighted dataset restricted to the specified columns via AIPW deletion.
588    ///
589    fn aipw_deletion(&self, x: &Set<usize>, pr: &MissingMechanism) -> Result<Self::Weighted>;
590}