1use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
5
6use crate::crossfit::Fold;
7use crate::data::DmlData;
8use crate::error::{DmlError, Result};
9use crate::score::LinearScore;
10use crate::sensitivity::SensitivityParts;
11
12mod apo;
13pub(crate) mod did;
14mod iivm;
15pub(crate) mod irm;
16mod lplr;
17mod nonlinear;
18mod pliv;
19mod plpr;
20mod plr;
21mod quantile;
22mod rdflex;
23mod ssm;
24
25pub use apo::{Apo, Apos, AposFit};
26pub use did::{Did, DidCs, DidScore};
27pub use iivm::Iivm;
28pub use irm::{Irm, IrmScore};
29pub use lplr::{Lplr, LplrScore, TreatmentLearner};
30pub use pliv::{Pliv, PlivScore, PlivVariant};
31pub use plpr::{PanelApproach, Plpr, StaticPanel};
32pub use plr::{Plr, PlrScore};
33pub use quantile::{Cvar, Lpq, Pq, Qte, QteFit};
34pub use rdflex::{FirstStage, RdFlex, RdFlexFit, RdKernel};
35pub use ssm::{Ssm, SsmScore};
36
37#[derive(Debug, Clone, Copy)]
42#[non_exhaustive]
43pub struct ScoreInput<'a> {
44 pub y: ArrayView1<'a, f64>,
45 pub d: ArrayView1<'a, f64>,
47 pub x: ArrayView2<'a, f64>,
49 pub z: Option<ArrayView1<'a, f64>>,
50 pub instruments: Option<ArrayView2<'a, f64>>,
52 pub t: Option<ArrayView1<'a, f64>>,
54 pub s: Option<ArrayView1<'a, f64>>,
56 pub rep: usize,
58 pub treatment: usize,
60 pub external: Option<&'a ExternalPredictions>,
62}
63
64impl ScoreInput<'_> {
65 pub fn predictions_or(
68 &self,
69 name: &str,
70 fit: impl FnOnce() -> Result<Array1<f64>>,
71 ) -> Result<Array1<f64>> {
72 external_or(self.external_ref(), self.y.len(), name, fit)
73 }
74
75 pub(crate) fn external_ref(&self) -> Option<ExternalRef<'_>> {
76 self.external.map(|preds| ExternalRef {
77 preds,
78 treatment: self.treatment,
79 rep: self.rep,
80 })
81 }
82}
83
84#[derive(Debug, Clone, Copy)]
86pub(crate) struct ExternalRef<'a> {
87 pub preds: &'a ExternalPredictions,
88 pub treatment: usize,
89 pub rep: usize,
90}
91
92pub(crate) fn external_or(
94 ext: Option<ExternalRef<'_>>,
95 n: usize,
96 name: &str,
97 fit: impl FnOnce() -> Result<Array1<f64>>,
98) -> Result<Array1<f64>> {
99 match ext.and_then(|e| e.preds.get(e.treatment, name).map(|p| (p, e.rep))) {
100 Some((p, rep)) => {
101 if p.nrows() != n || rep >= p.ncols() {
102 return Err(DmlError::DimensionMismatch(format!(
103 "external predictions for {name} have shape {:?}; expected ({n}, n_rep)",
104 p.dim()
105 )));
106 }
107 Ok(p.column(rep).to_owned())
108 }
109 None => fit(),
110 }
111}
112
113#[derive(Debug, Clone, Default)]
118pub struct ExternalPredictions {
119 preds: Vec<(usize, String, Array2<f64>)>,
120}
121
122impl ExternalPredictions {
123 pub fn new() -> Self {
124 Self::default()
125 }
126
127 pub fn with(
130 mut self,
131 treatment: usize,
132 name: impl Into<String>,
133 predictions: Array2<f64>,
134 ) -> Self {
135 let name = name.into();
136 self.preds
137 .retain(|(t, n, _)| !(*t == treatment && *n == name));
138 self.preds.push((treatment, name, predictions));
139 self
140 }
141
142 pub fn with_vector(
144 self,
145 treatment: usize,
146 name: impl Into<String>,
147 predictions: Array1<f64>,
148 ) -> Self {
149 let n = predictions.len();
150 self.with(
151 treatment,
152 name,
153 predictions
154 .into_shape_with_order((n, 1))
155 .expect("1-D to (n, 1)"),
156 )
157 }
158
159 pub(crate) fn get(&self, treatment: usize, name: &str) -> Option<&Array2<f64>> {
160 self.preds
161 .iter()
162 .find(|(t, n, _)| *t == treatment && n == name)
163 .map(|(_, _, p)| p)
164 }
165
166 pub(crate) fn contains(&self, treatment: usize, name: &str) -> bool {
167 self.get(treatment, name).is_some()
168 }
169}
170
171#[derive(Debug, Clone)]
178pub struct CateSignal {
179 pub signal: Array1<f64>,
180 pub theta_factor: Option<Array1<f64>>,
181 pub basis_scale: Option<Array1<f64>>,
182 pub is_effect: bool,
185}
186
187#[derive(Debug, Clone)]
192#[non_exhaustive]
193pub struct ScoreEstimate {
194 pub score: LinearScore,
195 pub predictions: Vec<(Name, Array1<f64>)>,
197 pub sensitivity: Option<SensitivityParts>,
200 pub targets: Vec<NuisanceTarget>,
202 pub cate: Option<CateSignal>,
204}
205
206pub type Name = std::borrow::Cow<'static, str>;
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum LossKind {
213 Rmse,
215 LogLoss,
217}
218
219#[derive(Debug, Clone)]
222pub struct NuisanceTarget {
223 pub name: Name,
224 pub target: Array1<f64>,
225 pub kind: LossKind,
226}
227
228impl NuisanceTarget {
229 pub fn regression(name: impl Into<Name>, target: Array1<f64>) -> Self {
230 Self {
231 name: name.into(),
232 target,
233 kind: LossKind::Rmse,
234 }
235 }
236
237 pub fn classification(name: impl Into<Name>, target: Array1<f64>) -> Self {
238 Self {
239 name: name.into(),
240 target,
241 kind: LossKind::LogLoss,
242 }
243 }
244
245 pub fn only_where(mut self, keep: impl Fn(usize) -> bool) -> Self {
247 for (i, v) in self.target.iter_mut().enumerate() {
248 if !keep(i) {
249 *v = f64::NAN;
250 }
251 }
252 self
253 }
254
255 pub fn loss(&self, prediction: &Array1<f64>) -> f64 {
257 let mut total = 0.0;
258 let mut count = 0usize;
259 for (&t, &p) in self.target.iter().zip(prediction) {
260 if t.is_nan() {
261 continue;
262 }
263 total += match self.kind {
264 LossKind::Rmse => (t - p).powi(2),
265 LossKind::LogLoss => {
266 let p = p.clamp(1e-15, 1.0 - 1e-15);
267 -(t * p.ln() + (1.0 - t) * (1.0 - p).ln())
268 }
269 };
270 count += 1;
271 }
272 if count == 0 {
273 return f64::NAN;
274 }
275 let mean = total / count as f64;
276 match self.kind {
277 LossKind::Rmse => mean.sqrt(),
278 LossKind::LogLoss => mean,
279 }
280 }
281}
282
283impl ScoreEstimate {
284 pub fn new<N: Into<Name>>(score: LinearScore, predictions: Vec<(N, Array1<f64>)>) -> Self {
287 Self {
288 score,
289 predictions: predictions
290 .into_iter()
291 .map(|(n, p)| (n.into(), p))
292 .collect(),
293 sensitivity: None,
294 targets: Vec::new(),
295 cate: None,
296 }
297 }
298
299 pub fn with_cate_signal(mut self, cate: CateSignal) -> Self {
301 self.cate = Some(cate);
302 self
303 }
304
305 pub fn with_targets(mut self, targets: Vec<NuisanceTarget>) -> Self {
307 self.targets = targets;
308 self
309 }
310
311 pub fn with_sensitivity(mut self, parts: SensitivityParts) -> Self {
313 self.sensitivity = Some(parts);
314 self
315 }
316}
317
318pub trait LinearScoreModel {
323 fn name(&self) -> String;
325
326 fn validate(&self, _data: &DmlData) -> Result<()> {
328 Ok(())
329 }
330
331 fn estimate_score(&self, input: &ScoreInput<'_>, folds: &[Fold]) -> Result<ScoreEstimate>;
333
334 fn strata(&self, _data: &DmlData) -> Option<Vec<usize>> {
338 None
339 }
340}
341
342macro_rules! dml_model_api {
347 () => {
348 pub fn n_folds(mut self, n_folds: usize) -> Self {
350 self.splitting.n_folds = n_folds;
351 self
352 }
353
354 pub fn n_rep(mut self, n_rep: usize) -> Self {
357 self.splitting.n_rep = n_rep;
358 self
359 }
360
361 pub fn seed(mut self, seed: u64) -> Self {
363 self.splitting.seed = seed;
364 self
365 }
366
367 pub fn fit(&self, data: &$crate::DmlData) -> $crate::Result<$crate::DmlFit> {
371 match data.clusters() {
372 Some(clusters) => {
373 let splits = self.splitting.draw_clustered(clusters)?;
374 $crate::fit_model_clustered(self, data, &splits)
375 }
376 None => {
377 let splits = match $crate::LinearScoreModel::strata(self, data) {
378 Some(strata) => self.splitting.draw_stratified(&strata)?,
379 None => self.splitting.draw(data.n_obs())?,
380 };
381 $crate::fit_model(self, data, &splits)
382 }
383 }
384 }
385
386 pub fn fit_with_cluster_folds(
389 &self,
390 data: &$crate::DmlData,
391 splits: &[Vec<$crate::ClusterFold>],
392 ) -> $crate::Result<$crate::DmlFit> {
393 $crate::fit_model_clustered(self, data, splits)
394 }
395
396 pub fn sensitivity_benchmark(
401 &self,
402 data: &$crate::DmlData,
403 long: &$crate::DmlFit,
404 benchmarking_set: &[usize],
405 ) -> $crate::Result<$crate::SensitivityBenchmark> {
406 let short = self.fit(&data.without_covariates(benchmarking_set)?)?;
407 long.sensitivity_benchmark(&short)
408 }
409
410 pub fn fit_with_folds(
413 &self,
414 data: &$crate::DmlData,
415 splits: &[Vec<$crate::Fold>],
416 ) -> $crate::Result<$crate::DmlFit> {
417 $crate::fit_model(self, data, splits)
418 }
419 };
420}
421pub(crate) use dml_model_api;