Skip to main content

causal_hub/estimators/structures/
scoring_criterion.rs

1use std::marker::PhantomData;
2
3use dry::macro_for;
4
5use crate::{
6    estimators::CPDEstimator,
7    models::{CIM, CPD, CatCIM, CatCPD, GaussCPD, HasLabels},
8    types::{Error, Labels, Result, Set},
9};
10
11/// A trait for types wrapping an underlying parameter estimator,
12/// such as scoring criteria and conditional independence tests.
13pub trait HasEstimator {
14    /// The wrapped estimator type.
15    type Estimator;
16
17    /// Returns a reference to the wrapped estimator.
18    ///
19    /// # Returns
20    ///
21    /// A reference to the wrapped estimator.
22    ///
23    fn estimator(&self) -> &Self::Estimator;
24}
25
26/// A trait for scoring criteria used in score-based structure learning.
27pub trait ScoringCriterion {
28    /// Computes the score for a given variable and its conditioning set.
29    ///
30    /// # Arguments
31    ///
32    /// * `x` - The variable to score.
33    /// * `z` - The conditioning set.
34    ///
35    /// # Returns
36    ///
37    /// The computed score.
38    ///
39    fn call(&self, x: &Set<usize>, z: &Set<usize>) -> Result<f64>;
40}
41
42macro_rules! impl_scoring_struct {
43    ($name:ident, $doc:expr) => {
44        #[doc = $doc]
45        pub struct $name<'a, E, P> {
46            _p_marker: PhantomData<P>,
47            estimator: &'a E,
48        }
49
50        impl<'a, E, P> $name<'a, E, P> {
51            #[doc = concat!("Creates a new `", stringify!($name), "` instance.")]
52            ///
53            /// # Arguments
54            ///
55            /// * `estimator` - A reference to the estimator.
56            ///
57            /// # Returns
58            ///
59            #[doc = concat!("A new `", stringify!($name), "` instance.")]
60            #[inline]
61            pub const fn new(estimator: &'a E) -> Self {
62                Self {
63                    _p_marker: PhantomData,
64                    estimator,
65                }
66            }
67        }
68
69        impl<E, P> HasEstimator for $name<'_, E, P> {
70            type Estimator = E;
71
72            #[inline]
73            fn estimator(&self) -> &Self::Estimator {
74                self.estimator
75            }
76        }
77
78        impl<'a, E, P> HasLabels for $name<'a, E, P>
79        where
80            E: HasLabels,
81        {
82            #[inline]
83            fn labels(&self) -> &Labels {
84                self.estimator.labels()
85            }
86        }
87    };
88}
89
90impl_scoring_struct!(LL, "The Log Likelihood (`LL`).");
91impl_scoring_struct!(AIC, "The Akaike Information Criterion (`AIC`).");
92impl_scoring_struct!(AICC, "The Akaike Information Criterion Corrected (`AICc`).");
93impl_scoring_struct!(BIC, "The Bayesian Information Criterion (`BIC`).");
94impl_scoring_struct!(
95    BICC,
96    "The Bayesian Information Criterion Corrected (`BICc`)."
97);
98impl_scoring_struct!(HQC, "The Hannan-Quinn Criterion (`HQC`).");
99
100macro_for!($type in [CatCPD, GaussCPD, CatCIM] {
101
102    impl<E> ScoringCriterion for LL<'_, E, $type>
103    where
104        E: CPDEstimator<$type>,
105    {
106        #[inline]
107        fn call(&self, x: &Set<usize>, z: &Set<usize>) -> Result<f64> {
108            // Compute the intensity matrices for the sets.
109            let p_xz = self.estimator.fit(x, z)?;
110            // Get the log-likelihood.
111            let log_likelihood = p_xz
112                .fitted_log_likelihood()
113                .ok_or_else(|| Error::MissingLogLikelihood())?;
114
115            // Compute the score.
116            Ok(log_likelihood)
117        }
118    }
119
120    impl<E> ScoringCriterion for AIC<'_, E, $type>
121    where
122        E: CPDEstimator<$type>,
123    {
124        #[inline]
125        fn call(&self, x: &Set<usize>, z: &Set<usize>) -> Result<f64> {
126            // Compute the intensity matrices for the sets.
127            let p_xz = self.estimator.fit(x, z)?;
128            // Get the log-likelihood.
129            let log_likelihood = p_xz
130                .fitted_log_likelihood()
131                .ok_or_else(|| Error::MissingLogLikelihood())?;
132            // Get the number of parameters.
133            let k = p_xz.parameters_size() as f64;
134
135            // Compute the score.
136            Ok(log_likelihood - k)
137        }
138    }
139
140    impl<E> ScoringCriterion for AICC<'_, E, $type>
141    where
142        E: CPDEstimator<$type>,
143    {
144        #[inline]
145        fn call(&self, x: &Set<usize>, z: &Set<usize>) -> Result<f64> {
146            // Compute the intensity matrices for the sets.
147            let p_xz = self.estimator.fit(x, z)?;
148            // Get the sample size.
149            let n = p_xz
150                .fitted_statistics()
151                .ok_or_else(|| Error::MissingSufficientStatistics())?
152                .fitted_size();
153            // Get the log-likelihood.
154            let log_likelihood = p_xz
155                .fitted_log_likelihood()
156                .ok_or_else(|| Error::MissingLogLikelihood())?;
157            // Get the number of parameters.
158            let k = p_xz.parameters_size() as f64;
159
160            // Apply sample size correction.
161            let c = n / (f64::max(n - k - 2., 1.));
162
163            // Compute the score.
164            Ok(log_likelihood - k * c)
165        }
166    }
167
168    impl<E> ScoringCriterion for BIC<'_, E, $type>
169    where
170        E: CPDEstimator<$type>,
171    {
172        #[inline]
173        fn call(&self, x: &Set<usize>, z: &Set<usize>) -> Result<f64> {
174            // Compute the intensity matrices for the sets.
175            let p_xz = self.estimator.fit(x, z)?;
176            // Get the sample size.
177            let n = p_xz
178                .fitted_statistics()
179                .ok_or_else(|| Error::MissingSufficientStatistics())?
180                .fitted_size();
181            // Get the log-likelihood.
182            let log_likelihood = p_xz
183                .fitted_log_likelihood()
184                .ok_or_else(|| Error::MissingLogLikelihood())?;
185            // Get the number of parameters.
186            let k = p_xz.parameters_size() as f64;
187
188            // Compute the score.
189            Ok(log_likelihood - 0.5 * k * f64::ln(n))
190        }
191    }
192
193    impl<E> ScoringCriterion for BICC<'_, E, $type>
194    where
195        E: CPDEstimator<$type>,
196    {
197        #[inline]
198        fn call(&self, x: &Set<usize>, z: &Set<usize>) -> Result<f64> {
199            // Compute the intensity matrices for the sets.
200            let p_xz = self.estimator.fit(x, z)?;
201            // Get the sample size.
202            let n = p_xz
203                .fitted_statistics()
204                .ok_or_else(|| Error::MissingSufficientStatistics())?
205                .fitted_size();
206            // Get the log-likelihood.
207            let log_likelihood = p_xz
208                .fitted_log_likelihood()
209                .ok_or_else(|| Error::MissingLogLikelihood())?;
210            // Get the number of parameters.
211            let k = p_xz.parameters_size() as f64;
212
213            // Apply sample size correction.
214            let c = n / (f64::max(n - k - 2., 1.));
215
216            // Compute the score.
217            Ok(log_likelihood - 0.5 * k * c * f64::ln(n))
218        }
219    }
220
221    impl<E> ScoringCriterion for HQC<'_, E, $type>
222    where
223        E: CPDEstimator<$type>,
224    {
225        #[inline]
226        fn call(&self, x: &Set<usize>, z: &Set<usize>) -> Result<f64> {
227            // Compute the intensity matrices for the sets.
228            let p_xz = self.estimator.fit(x, z)?;
229            // Get the sample size.
230            let n = p_xz
231                .fitted_statistics()
232                .ok_or_else(|| Error::MissingSufficientStatistics())?
233                .fitted_size();
234            // Get the log-likelihood.
235            let log_likelihood = p_xz
236                .fitted_log_likelihood()
237                .ok_or_else(|| Error::MissingLogLikelihood())?;
238            // Get the number of parameters.
239            let k = p_xz.parameters_size() as f64;
240
241            // Compute the score.
242            Ok(log_likelihood - 0.5 * k * f64::ln(f64::ln(n)))
243        }
244    }
245
246});