dml_core/model/mod.rs
1//! DML models with linear scores: [`Plr`], [`Irm`], [`Pliv`], and the
2//! [`LinearScoreModel`] trait for adding your own.
3
4use ndarray::{Array1, ArrayView1, ArrayView2};
5
6use crate::crossfit::Fold;
7use crate::data::DmlData;
8use crate::error::Result;
9use crate::score::LinearScore;
10
11pub(crate) mod irm;
12mod pliv;
13mod plr;
14
15pub use irm::{Irm, IrmScore};
16pub use pliv::Pliv;
17pub use plr::{Plr, PlrScore};
18
19/// The data a model sees when estimating the effect of one treatment.
20#[derive(Debug, Clone, Copy)]
21pub struct ScoreInput<'a> {
22 pub y: ArrayView1<'a, f64>,
23 /// The treatment being estimated.
24 pub d: ArrayView1<'a, f64>,
25 /// Covariates, including any other treatments.
26 pub x: ArrayView2<'a, f64>,
27 pub z: Option<ArrayView1<'a, f64>>,
28}
29
30/// Score elements plus the cross-fitted nuisance predictions behind them.
31#[derive(Debug, Clone)]
32pub struct ScoreEstimate {
33 pub score: LinearScore,
34 /// `(name, out-of-fold predictions)` — e.g. `("ml_l", ℓ̂)`.
35 pub predictions: Vec<(&'static str, Array1<f64>)>,
36}
37
38/// A DML model whose orthogonal score is linear in θ.
39///
40/// Implement this to plug a new score into the shared machinery (repeated
41/// cross-fitting, aggregation, inference, bootstrap) via [`fit_model`](crate::fit_model).
42pub trait LinearScoreModel {
43 /// Label for summaries, e.g. `"PLR (partialling out)"`.
44 fn name(&self) -> String;
45
46 /// Reject data the model cannot handle (called once before fitting).
47 fn validate(&self, _data: &DmlData) -> Result<()> {
48 Ok(())
49 }
50
51 /// Cross-fit the nuisances on `folds` and return the score elements.
52 fn estimate_score(&self, input: &ScoreInput<'_>, folds: &[Fold]) -> Result<ScoreEstimate>;
53}
54
55/// Builder methods shared by every model: sample-splitting settings and `fit`.
56///
57/// (`macro_rules!` generates code at compile time; here it pastes the same
58/// methods into each model's `impl` block instead of repeating them.)
59macro_rules! dml_model_api {
60 () => {
61 /// Number of cross-fitting folds (default 5).
62 pub fn n_folds(mut self, n_folds: usize) -> Self {
63 self.splitting.n_folds = n_folds;
64 self
65 }
66
67 /// Number of independent sample splits (default 1). Estimates are
68 /// aggregated across repetitions by the median, as in DoubleML.
69 pub fn n_rep(mut self, n_rep: usize) -> Self {
70 self.splitting.n_rep = n_rep;
71 self
72 }
73
74 /// Seed for the sample splits (default 0).
75 pub fn seed(mut self, seed: u64) -> Self {
76 self.splitting.seed = seed;
77 self
78 }
79
80 /// Draw seeded sample splits and fit.
81 pub fn fit(&self, data: &$crate::DmlData) -> $crate::Result<$crate::DmlFit> {
82 let splits = self.splitting.draw(data.n_obs())?;
83 $crate::fit_model(self, data, &splits)
84 }
85
86 /// Fit with caller-supplied splits, one `Vec<Fold>` per repetition
87 /// (e.g. to reproduce another implementation exactly).
88 pub fn fit_with_folds(
89 &self,
90 data: &$crate::DmlData,
91 splits: &[Vec<$crate::Fold>],
92 ) -> $crate::Result<$crate::DmlFit> {
93 $crate::fit_model(self, data, splits)
94 }
95 };
96}
97pub(crate) use dml_model_api;