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;
10use crate::sensitivity::SensitivityParts;
11
12pub(crate) mod did;
13mod iivm;
14pub(crate) mod irm;
15mod pliv;
16mod plr;
17
18pub use did::{Did, DidCs, DidScore};
19pub use iivm::Iivm;
20pub use irm::{Irm, IrmScore};
21pub use pliv::Pliv;
22pub use plr::{Plr, PlrScore};
23
24/// The data a model sees when estimating the effect of one treatment.
25///
26/// `#[non_exhaustive]`: fields may be added in future versions, so code outside
27/// this crate reads the fields but cannot construct the struct.
28#[derive(Debug, Clone, Copy)]
29#[non_exhaustive]
30pub struct ScoreInput<'a> {
31 pub y: ArrayView1<'a, f64>,
32 /// The treatment being estimated.
33 pub d: ArrayView1<'a, f64>,
34 /// Covariates, including any other treatments.
35 pub x: ArrayView2<'a, f64>,
36 pub z: Option<ArrayView1<'a, f64>>,
37 /// 0/1 time-period indicator (repeated cross-sections).
38 pub t: Option<ArrayView1<'a, f64>>,
39}
40
41/// Score elements plus the cross-fitted nuisance predictions behind them.
42///
43/// Build it with [`ScoreEstimate::new`] (the struct is `#[non_exhaustive]`, so
44/// fields can be added without breaking custom models).
45#[derive(Debug, Clone)]
46#[non_exhaustive]
47pub struct ScoreEstimate {
48 pub score: LinearScore,
49 /// `(name, out-of-fold predictions)` — e.g. `("ml_l", ℓ̂)`.
50 pub predictions: Vec<(&'static str, Array1<f64>)>,
51 /// Ingredients for [sensitivity analysis](crate::sensitivity), if the
52 /// model supports it.
53 pub sensitivity: Option<SensitivityParts>,
54}
55
56impl ScoreEstimate {
57 pub fn new(score: LinearScore, predictions: Vec<(&'static str, Array1<f64>)>) -> Self {
58 Self {
59 score,
60 predictions,
61 sensitivity: None,
62 }
63 }
64
65 /// Attach the model's sensitivity ingredients.
66 pub fn with_sensitivity(mut self, parts: SensitivityParts) -> Self {
67 self.sensitivity = Some(parts);
68 self
69 }
70}
71
72/// A DML model whose orthogonal score is linear in θ.
73///
74/// Implement this to plug a new score into the shared machinery (repeated
75/// cross-fitting, aggregation, inference, bootstrap) via [`fit_model`](crate::fit_model).
76pub trait LinearScoreModel {
77 /// Label for summaries, e.g. `"PLR (partialling out)"`.
78 fn name(&self) -> String;
79
80 /// Reject data the model cannot handle (called once before fitting).
81 fn validate(&self, _data: &DmlData) -> Result<()> {
82 Ok(())
83 }
84
85 /// Cross-fit the nuisances on `folds` and return the score elements.
86 fn estimate_score(&self, input: &ScoreInput<'_>, folds: &[Fold]) -> Result<ScoreEstimate>;
87}
88
89/// Builder methods shared by every model: sample-splitting settings and `fit`.
90///
91/// (`macro_rules!` generates code at compile time; here it pastes the same
92/// methods into each model's `impl` block instead of repeating them.)
93macro_rules! dml_model_api {
94 () => {
95 /// Number of cross-fitting folds (default 5).
96 pub fn n_folds(mut self, n_folds: usize) -> Self {
97 self.splitting.n_folds = n_folds;
98 self
99 }
100
101 /// Number of independent sample splits (default 1). Estimates are
102 /// aggregated across repetitions by the median, as in DoubleML.
103 pub fn n_rep(mut self, n_rep: usize) -> Self {
104 self.splitting.n_rep = n_rep;
105 self
106 }
107
108 /// Seed for the sample splits (default 0).
109 pub fn seed(mut self, seed: u64) -> Self {
110 self.splitting.seed = seed;
111 self
112 }
113
114 /// Draw seeded sample splits and fit.
115 pub fn fit(&self, data: &$crate::DmlData) -> $crate::Result<$crate::DmlFit> {
116 let splits = self.splitting.draw(data.n_obs())?;
117 $crate::fit_model(self, data, &splits)
118 }
119
120 /// Sensitivity benchmark for the covariate columns `benchmarking_set`:
121 /// refits this model without them (with the same sample splitting, so
122 /// `long` must come from [`fit`](Self::fit) with the current settings)
123 /// and compares it with `long`.
124 pub fn sensitivity_benchmark(
125 &self,
126 data: &$crate::DmlData,
127 long: &$crate::DmlFit,
128 benchmarking_set: &[usize],
129 ) -> $crate::Result<$crate::SensitivityBenchmark> {
130 let short = self.fit(&data.without_covariates(benchmarking_set)?)?;
131 long.sensitivity_benchmark(&short)
132 }
133
134 /// Fit with caller-supplied splits, one `Vec<Fold>` per repetition
135 /// (e.g. to reproduce another implementation exactly).
136 pub fn fit_with_folds(
137 &self,
138 data: &$crate::DmlData,
139 splits: &[Vec<$crate::Fold>],
140 ) -> $crate::Result<$crate::DmlFit> {
141 $crate::fit_model(self, data, splits)
142 }
143 };
144}
145pub(crate) use dml_model_api;