Skip to main content

causal_hub/random/datasets/table/categorical/
incomplete.rs

1use ndarray::prelude::*;
2use ndarray_stats::QuantileExt;
3use rand::{Rng, RngExt};
4use rand_distr::Uniform;
5
6use crate::{
7    datasets::{CatIncTable, CatTable, CatType, Dataset, IncDataset, MissingMechanism},
8    models::HasLabels,
9    random::Random,
10    types::{Error, Result},
11};
12
13/// A struct representing a random incomplete categorical table dataset generator.
14pub struct RngCatIncTable<'a, R> {
15    rng: &'a mut R,
16    dataset: &'a CatTable,
17    missing_mechanism: &'a MissingMechanism,
18    p_min: f64,
19    p_max: f64,
20}
21
22impl<'a, R: Rng> RngCatIncTable<'a, R> {
23    /// Creates a new `RngCatIncTable` instance.
24    ///
25    /// # Arguments
26    ///
27    /// * `rng` - A mutable reference to a random number generator.
28    /// * `dataset` - A reference to the complete categorical table dataset.
29    /// * `missing_mechanism` - A reference to the missingness mechanism.
30    /// * `p_min` - The minimum probability of missingness.
31    /// * `p_max` - The maximum probability of missingness.
32    ///
33    /// # Returns
34    ///
35    /// A new `RngCatIncTable` instance.
36    pub fn new(
37        rng: &'a mut R,
38        dataset: &'a CatTable,
39        missing_mechanism: &'a MissingMechanism,
40        p_min: f64,
41        p_max: f64,
42    ) -> Result<Self> {
43        // Check that dataset labels are equals to missing mechanism labels.
44        if dataset.labels() != missing_mechanism.labels() {
45            return Err(Error::InvalidParameter(
46                "missing_mechanism",
47                "labels do not match dataset labels",
48            ));
49        }
50        // Check that p_min and p_max are in [0, 1].
51        if !(0.0..=1.0).contains(&p_min) {
52            return Err(Error::InvalidParameter("p_min", "must be in [0, 1]"));
53        }
54        if !(0.0..=1.0).contains(&p_max) {
55            return Err(Error::InvalidParameter("p_max", "must be in [0, 1]"));
56        }
57        // Check that p_min is less than or equal to p_max.
58        if p_min > p_max {
59            return Err(Error::InvalidParameter(
60                "p_min",
61                "must be less than or equal to p_max",
62            ));
63        }
64
65        Ok(Self {
66            rng,
67            dataset,
68            missing_mechanism,
69            p_min,
70            p_max,
71        })
72    }
73}
74
75impl<R: Rng> Random for RngCatIncTable<'_, R> {
76    type Output = Result<CatIncTable>;
77
78    fn random(&mut self) -> Self::Output {
79        // Get the missing indicator.
80        const M: CatType = CatIncTable::MISSING;
81        // Get dataset support.
82        let support = self.dataset.support().clone();
83        // Get dataset values.
84        let mut values = self.dataset.values().clone();
85
86        // Define the uniform distributions for sampling.
87        let p_s = Uniform::new_inclusive(0., 1.)?;
88        let p_u = Uniform::new_inclusive(self.p_min, self.p_max)?;
89
90        // Iterate over the missing mechanism.
91        for (&x, pa_x) in self.missing_mechanism {
92            // Get mutable reference to the column X.
93            let mut c_x = values.column_mut(x);
94
95            // Check if the variable has no parents.
96            if pa_x.is_empty() {
97                // Sample a missingness probability.
98                let p_x = self.rng.sample(p_u);
99                // Modify the corresponding column.
100                c_x.iter_mut().for_each(|x| {
101                    if self.rng.sample(p_s) < p_x {
102                        *x = M;
103                    }
104                });
105                // Continue to the next variable.
106                continue;
107            }
108
109            // For each parent ...
110            for &z in pa_x {
111                // Get reference to the column Z.
112                let c_z = self.dataset.values().column(z);
113                // Get the mode of the parent via direct counting.
114                let mut m_z = Array::from_elem(CatType::MAX as usize, 0);
115                c_z.iter().for_each(|&z| m_z[z as usize] += 1);
116                let s_z = match m_z.argmax() {
117                    Ok(s_z) => s_z as CatType,
118                    // If the mode cannot be found, skip this parent.
119                    _ => continue,
120                };
121                // Modify the corresponding column.
122                azip!((x in &mut c_x, z in &c_z) {
123                    // Sample a missingness probability for X given parent Z.
124                    if
125                        (self.rng.sample(p_s) < self.p_max && *z == s_z) ||
126                        (self.rng.sample(p_s) < self.p_min && *z != s_z)
127                    {
128                        *x = M;
129                    }
130                });
131            }
132        }
133
134        // Return the incomplete dataset.
135        CatIncTable::new(support, values)
136    }
137}