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 external_get(ext, n, name)? {
100 Some(p) => Ok(p),
101 None => fit(),
102 }
103}
104
105pub(crate) fn external_get(
107 ext: Option<ExternalRef<'_>>,
108 n: usize,
109 name: &str,
110) -> Result<Option<Array1<f64>>> {
111 match ext.and_then(|e| e.preds.get(e.treatment, name).map(|p| (p, e.rep))) {
112 Some((p, rep)) => {
113 if p.nrows() != n || rep >= p.ncols() {
114 return Err(DmlError::DimensionMismatch(format!(
115 "external predictions for {name} have shape {:?}; expected ({n}, n_rep)",
116 p.dim()
117 )));
118 }
119 Ok(Some(p.column(rep).to_owned()))
120 }
121 None => Ok(None),
122 }
123}
124
125#[derive(Debug, Clone, Default)]
130pub struct ExternalPredictions {
131 preds: Vec<(usize, String, Array2<f64>)>,
132}
133
134impl ExternalPredictions {
135 pub fn new() -> Self {
136 Self::default()
137 }
138
139 pub fn with(
142 mut self,
143 treatment: usize,
144 name: impl Into<String>,
145 predictions: Array2<f64>,
146 ) -> Self {
147 let name = name.into();
148 self.preds
149 .retain(|(t, n, _)| !(*t == treatment && *n == name));
150 self.preds.push((treatment, name, predictions));
151 self
152 }
153
154 pub fn with_vector(
156 self,
157 treatment: usize,
158 name: impl Into<String>,
159 predictions: Array1<f64>,
160 ) -> Self {
161 let n = predictions.len();
162 self.with(
163 treatment,
164 name,
165 predictions
166 .into_shape_with_order((n, 1))
167 .expect("1-D to (n, 1)"),
168 )
169 }
170
171 pub(crate) fn get(&self, treatment: usize, name: &str) -> Option<&Array2<f64>> {
172 self.preds
173 .iter()
174 .find(|(t, n, _)| *t == treatment && n == name)
175 .map(|(_, _, p)| p)
176 }
177
178 pub(crate) fn contains(&self, treatment: usize, name: &str) -> bool {
179 self.get(treatment, name).is_some()
180 }
181}
182
183#[derive(Debug, Clone)]
190pub struct CateSignal {
191 pub signal: Array1<f64>,
192 pub theta_factor: Option<Array1<f64>>,
193 pub basis_scale: Option<Array1<f64>>,
194 pub is_effect: bool,
197}
198
199#[derive(Debug, Clone)]
204#[non_exhaustive]
205pub struct ScoreEstimate {
206 pub score: LinearScore,
207 pub predictions: Vec<(Name, Array1<f64>)>,
209 pub sensitivity: Option<SensitivityParts>,
212 pub targets: Vec<NuisanceTarget>,
214 pub cate: Option<CateSignal>,
216}
217
218pub type Name = std::borrow::Cow<'static, str>;
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum LossKind {
225 Rmse,
227 LogLoss,
229}
230
231#[derive(Debug, Clone)]
234pub struct NuisanceTarget {
235 pub name: Name,
236 pub target: Array1<f64>,
237 pub kind: LossKind,
238}
239
240impl NuisanceTarget {
241 pub fn regression(name: impl Into<Name>, target: Array1<f64>) -> Self {
242 Self {
243 name: name.into(),
244 target,
245 kind: LossKind::Rmse,
246 }
247 }
248
249 pub fn classification(name: impl Into<Name>, target: Array1<f64>) -> Self {
250 Self {
251 name: name.into(),
252 target,
253 kind: LossKind::LogLoss,
254 }
255 }
256
257 pub fn only_where(mut self, keep: impl Fn(usize) -> bool) -> Self {
259 for (i, v) in self.target.iter_mut().enumerate() {
260 if !keep(i) {
261 *v = f64::NAN;
262 }
263 }
264 self
265 }
266
267 pub fn loss(&self, prediction: &Array1<f64>) -> f64 {
269 let mut total = 0.0;
270 let mut count = 0usize;
271 for (&t, &p) in self.target.iter().zip(prediction) {
272 if t.is_nan() {
273 continue;
274 }
275 total += match self.kind {
276 LossKind::Rmse => (t - p).powi(2),
277 LossKind::LogLoss => {
278 let p = p.clamp(1e-15, 1.0 - 1e-15);
279 -(t * p.ln() + (1.0 - t) * (1.0 - p).ln())
280 }
281 };
282 count += 1;
283 }
284 if count == 0 {
285 return f64::NAN;
286 }
287 let mean = total / count as f64;
288 match self.kind {
289 LossKind::Rmse => mean.sqrt(),
290 LossKind::LogLoss => mean,
291 }
292 }
293}
294
295impl ScoreEstimate {
296 pub fn new<N: Into<Name>>(score: LinearScore, predictions: Vec<(N, Array1<f64>)>) -> Self {
299 Self {
300 score,
301 predictions: predictions
302 .into_iter()
303 .map(|(n, p)| (n.into(), p))
304 .collect(),
305 sensitivity: None,
306 targets: Vec::new(),
307 cate: None,
308 }
309 }
310
311 pub fn with_cate_signal(mut self, cate: CateSignal) -> Self {
313 self.cate = Some(cate);
314 self
315 }
316
317 pub fn with_targets(mut self, targets: Vec<NuisanceTarget>) -> Self {
319 self.targets = targets;
320 self
321 }
322
323 pub fn with_sensitivity(mut self, parts: SensitivityParts) -> Self {
325 self.sensitivity = Some(parts);
326 self
327 }
328}
329
330pub trait LinearScoreModel {
335 fn name(&self) -> String;
337
338 fn validate(&self, _data: &DmlData) -> Result<()> {
340 Ok(())
341 }
342
343 fn estimate_score(&self, input: &ScoreInput<'_>, folds: &[Fold]) -> Result<ScoreEstimate>;
345
346 fn strata(&self, _data: &DmlData) -> Option<Vec<usize>> {
350 None
351 }
352}
353
354macro_rules! dml_model_api {
359 () => {
360 pub fn n_folds(mut self, n_folds: usize) -> Self {
362 self.splitting.n_folds = n_folds;
363 self
364 }
365
366 pub fn n_rep(mut self, n_rep: usize) -> Self {
369 self.splitting.n_rep = n_rep;
370 self
371 }
372
373 pub fn seed(mut self, seed: u64) -> Self {
375 self.splitting.seed = seed;
376 self
377 }
378
379 pub fn fit(&self, data: &$crate::DmlData) -> $crate::Result<$crate::DmlFit> {
383 match data.clusters() {
384 Some(clusters) => {
385 let splits = self.splitting.draw_clustered(clusters)?;
386 $crate::fit_model_clustered(self, data, &splits)
387 }
388 None => {
389 let splits = match $crate::LinearScoreModel::strata(self, data) {
390 Some(strata) => self.splitting.draw_stratified(&strata)?,
391 None => self.splitting.draw(data.n_obs())?,
392 };
393 $crate::fit_model(self, data, &splits)
394 }
395 }
396 }
397
398 pub fn fit_with_cluster_folds(
401 &self,
402 data: &$crate::DmlData,
403 splits: &[Vec<$crate::ClusterFold>],
404 ) -> $crate::Result<$crate::DmlFit> {
405 $crate::fit_model_clustered(self, data, splits)
406 }
407
408 pub fn sensitivity_benchmark(
413 &self,
414 data: &$crate::DmlData,
415 long: &$crate::DmlFit,
416 benchmarking_set: &[usize],
417 ) -> $crate::Result<$crate::SensitivityBenchmark> {
418 let short = self.fit(&data.without_covariates(benchmarking_set)?)?;
419 long.sensitivity_benchmark(&short)
420 }
421
422 pub fn fit_with_folds(
425 &self,
426 data: &$crate::DmlData,
427 splits: &[Vec<$crate::Fold>],
428 ) -> $crate::Result<$crate::DmlFit> {
429 $crate::fit_model(self, data, splits)
430 }
431 };
432}
433pub(crate) use dml_model_api;
434
435#[derive(Default)]
440pub struct NuisanceLearners {
441 regressors: Vec<(String, Box<dyn crate::learner::Regressor>)>,
442 classifiers: Vec<(String, Box<dyn crate::learner::Classifier>)>,
443}
444
445impl NuisanceLearners {
446 pub(crate) fn set_regressor(
447 &mut self,
448 name: &str,
449 learner: Box<dyn crate::learner::Regressor>,
450 ) {
451 self.regressors.retain(|(n, _)| n != name);
452 self.regressors.push((name.to_string(), learner));
453 }
454
455 pub(crate) fn set_classifier(
456 &mut self,
457 name: &str,
458 learner: Box<dyn crate::learner::Classifier>,
459 ) {
460 self.classifiers.retain(|(n, _)| n != name);
461 self.classifiers.push((name.to_string(), learner));
462 }
463
464 pub(crate) fn regressor<'a>(
466 &'a self,
467 name: &str,
468 default: &'a dyn crate::learner::Regressor,
469 ) -> &'a dyn crate::learner::Regressor {
470 self.regressors
471 .iter()
472 .find(|(n, _)| n == name)
473 .map_or(default, |(_, l)| l.as_ref())
474 }
475
476 pub(crate) fn classifier<'a>(
478 &'a self,
479 name: &str,
480 default: &'a dyn crate::learner::Classifier,
481 ) -> &'a dyn crate::learner::Classifier {
482 self.classifiers
483 .iter()
484 .find(|(n, _)| n == name)
485 .map_or(default, |(_, l)| l.as_ref())
486 }
487
488 pub(crate) fn check(
491 &self,
492 model: &str,
493 regressors: &[&str],
494 classifiers: &[&str],
495 ) -> Result<()> {
496 for (name, valid) in self
497 .regressors
498 .iter()
499 .map(|(n, _)| (n, regressors))
500 .chain(self.classifiers.iter().map(|(n, _)| (n, classifiers)))
501 {
502 if !valid.contains(&name.as_str()) {
503 return Err(DmlError::InvalidArgument(format!(
504 "{model} has no nuisance {name:?} for this learner type (valid: {})",
505 valid.join(", ")
506 )));
507 }
508 }
509 Ok(())
510 }
511}
512
513macro_rules! nuisance_api {
516 () => {
517 pub fn nuisance_regressor(
520 self,
521 name: &str,
522 learner: impl $crate::learner::Regressor + 'static,
523 ) -> Self {
524 self.nuisance_regressor_boxed(name, Box::new(learner))
525 }
526
527 pub fn nuisance_regressor_boxed(
529 mut self,
530 name: &str,
531 learner: Box<dyn $crate::learner::Regressor>,
532 ) -> Self {
533 self.nuisance.set_regressor(name, learner);
534 self
535 }
536
537 pub fn nuisance_classifier(
540 self,
541 name: &str,
542 learner: impl $crate::learner::Classifier + 'static,
543 ) -> Self {
544 self.nuisance_classifier_boxed(name, Box::new(learner))
545 }
546
547 pub fn nuisance_classifier_boxed(
549 mut self,
550 name: &str,
551 learner: Box<dyn $crate::learner::Classifier>,
552 ) -> Self {
553 self.nuisance.set_classifier(name, learner);
554 self
555 }
556 };
557}
558pub(crate) use nuisance_api;