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;
13mod autodml;
14pub(crate) mod did;
15mod iivm;
16pub(crate) mod irm;
17mod lplr;
18mod nonlinear;
19mod pliv;
20mod plpr;
21mod plr;
22mod quantile;
23mod rdflex;
24mod ssm;
25
26pub use apo::{Apo, Apos, AposFit};
27pub use autodml::AutoDml;
28pub use did::{Did, DidCs, DidScore};
29pub use iivm::Iivm;
30pub use irm::{Irm, IrmScore};
31pub use lplr::{Lplr, LplrScore, TreatmentLearner};
32pub use pliv::{Pliv, PlivScore, PlivVariant};
33pub use plpr::{PanelApproach, Plpr, StaticPanel};
34pub use plr::{Plr, PlrScore};
35pub use quantile::{Cvar, Lpq, Pq, Qte, QteFit};
36pub use rdflex::{FirstStage, RdFlex, RdFlexFit, RdKernel};
37pub use ssm::{Ssm, SsmScore};
38
39#[derive(Debug, Clone, Copy)]
44#[non_exhaustive]
45pub struct ScoreInput<'a> {
46 pub y: ArrayView1<'a, f64>,
47 pub d: ArrayView1<'a, f64>,
49 pub x: ArrayView2<'a, f64>,
51 pub z: Option<ArrayView1<'a, f64>>,
52 pub instruments: Option<ArrayView2<'a, f64>>,
54 pub t: Option<ArrayView1<'a, f64>>,
56 pub s: Option<ArrayView1<'a, f64>>,
58 pub rep: usize,
60 pub treatment: usize,
62 pub external: Option<&'a ExternalPredictions>,
64}
65
66impl ScoreInput<'_> {
67 pub fn predictions_or(
70 &self,
71 name: &str,
72 fit: impl FnOnce() -> Result<Array1<f64>>,
73 ) -> Result<Array1<f64>> {
74 external_or(self.external_ref(), self.y.len(), name, fit)
75 }
76
77 pub(crate) fn external_ref(&self) -> Option<ExternalRef<'_>> {
78 self.external.map(|preds| ExternalRef {
79 preds,
80 treatment: self.treatment,
81 rep: self.rep,
82 })
83 }
84}
85
86#[derive(Debug, Clone, Copy)]
88pub(crate) struct ExternalRef<'a> {
89 pub preds: &'a ExternalPredictions,
90 pub treatment: usize,
91 pub rep: usize,
92}
93
94pub(crate) fn external_or(
96 ext: Option<ExternalRef<'_>>,
97 n: usize,
98 name: &str,
99 fit: impl FnOnce() -> Result<Array1<f64>>,
100) -> Result<Array1<f64>> {
101 match external_get(ext, n, name)? {
102 Some(p) => Ok(p),
103 None => fit(),
104 }
105}
106
107pub(crate) fn external_get(
109 ext: Option<ExternalRef<'_>>,
110 n: usize,
111 name: &str,
112) -> Result<Option<Array1<f64>>> {
113 match ext.and_then(|e| e.preds.get(e.treatment, name).map(|p| (p, e.rep))) {
114 Some((p, rep)) => {
115 if p.nrows() != n || rep >= p.ncols() {
116 return Err(DmlError::DimensionMismatch(format!(
117 "external predictions for {name} have shape {:?}; expected ({n}, n_rep)",
118 p.dim()
119 )));
120 }
121 Ok(Some(p.column(rep).to_owned()))
122 }
123 None => Ok(None),
124 }
125}
126
127#[derive(Debug, Clone, Default)]
132pub struct ExternalPredictions {
133 preds: Vec<(usize, String, Array2<f64>)>,
134}
135
136impl ExternalPredictions {
137 pub fn new() -> Self {
138 Self::default()
139 }
140
141 pub fn with(
144 mut self,
145 treatment: usize,
146 name: impl Into<String>,
147 predictions: Array2<f64>,
148 ) -> Self {
149 let name = name.into();
150 self.preds
151 .retain(|(t, n, _)| !(*t == treatment && *n == name));
152 self.preds.push((treatment, name, predictions));
153 self
154 }
155
156 pub fn with_vector(
158 self,
159 treatment: usize,
160 name: impl Into<String>,
161 predictions: Array1<f64>,
162 ) -> Self {
163 let n = predictions.len();
164 self.with(
165 treatment,
166 name,
167 predictions
168 .into_shape_with_order((n, 1))
169 .expect("1-D to (n, 1)"),
170 )
171 }
172
173 pub(crate) fn get(&self, treatment: usize, name: &str) -> Option<&Array2<f64>> {
174 self.preds
175 .iter()
176 .find(|(t, n, _)| *t == treatment && n == name)
177 .map(|(_, _, p)| p)
178 }
179
180 pub(crate) fn contains(&self, treatment: usize, name: &str) -> bool {
181 self.get(treatment, name).is_some()
182 }
183}
184
185#[derive(Debug, Clone)]
192pub struct CateSignal {
193 pub signal: Array1<f64>,
194 pub theta_factor: Option<Array1<f64>>,
195 pub basis_scale: Option<Array1<f64>>,
196 pub is_effect: bool,
199}
200
201#[derive(Debug, Clone)]
206#[non_exhaustive]
207pub struct ScoreEstimate {
208 pub score: LinearScore,
209 pub predictions: Vec<(Name, Array1<f64>)>,
211 pub sensitivity: Option<SensitivityParts>,
214 pub targets: Vec<NuisanceTarget>,
216 pub cate: Option<CateSignal>,
218}
219
220pub type Name = std::borrow::Cow<'static, str>;
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum LossKind {
227 Rmse,
229 LogLoss,
231}
232
233#[derive(Debug, Clone)]
236pub struct NuisanceTarget {
237 pub name: Name,
238 pub target: Array1<f64>,
239 pub kind: LossKind,
240}
241
242impl NuisanceTarget {
243 pub fn regression(name: impl Into<Name>, target: Array1<f64>) -> Self {
244 Self {
245 name: name.into(),
246 target,
247 kind: LossKind::Rmse,
248 }
249 }
250
251 pub fn classification(name: impl Into<Name>, target: Array1<f64>) -> Self {
252 Self {
253 name: name.into(),
254 target,
255 kind: LossKind::LogLoss,
256 }
257 }
258
259 pub fn only_where(mut self, keep: impl Fn(usize) -> bool) -> Self {
261 for (i, v) in self.target.iter_mut().enumerate() {
262 if !keep(i) {
263 *v = f64::NAN;
264 }
265 }
266 self
267 }
268
269 pub fn loss(&self, prediction: &Array1<f64>) -> f64 {
271 let mut total = 0.0;
272 let mut count = 0usize;
273 for (&t, &p) in self.target.iter().zip(prediction) {
274 if t.is_nan() {
275 continue;
276 }
277 total += match self.kind {
278 LossKind::Rmse => (t - p).powi(2),
279 LossKind::LogLoss => {
280 let p = p.clamp(1e-15, 1.0 - 1e-15);
281 -(t * p.ln() + (1.0 - t) * (1.0 - p).ln())
282 }
283 };
284 count += 1;
285 }
286 if count == 0 {
287 return f64::NAN;
288 }
289 let mean = total / count as f64;
290 match self.kind {
291 LossKind::Rmse => mean.sqrt(),
292 LossKind::LogLoss => mean,
293 }
294 }
295}
296
297impl ScoreEstimate {
298 pub fn new<N: Into<Name>>(score: LinearScore, predictions: Vec<(N, Array1<f64>)>) -> Self {
301 Self {
302 score,
303 predictions: predictions
304 .into_iter()
305 .map(|(n, p)| (n.into(), p))
306 .collect(),
307 sensitivity: None,
308 targets: Vec::new(),
309 cate: None,
310 }
311 }
312
313 pub fn with_cate_signal(mut self, cate: CateSignal) -> Self {
315 self.cate = Some(cate);
316 self
317 }
318
319 pub fn with_targets(mut self, targets: Vec<NuisanceTarget>) -> Self {
321 self.targets = targets;
322 self
323 }
324
325 pub fn with_sensitivity(mut self, parts: SensitivityParts) -> Self {
327 self.sensitivity = Some(parts);
328 self
329 }
330}
331
332pub trait LinearScoreModel {
337 fn name(&self) -> String;
339
340 fn validate(&self, _data: &DmlData) -> Result<()> {
342 Ok(())
343 }
344
345 fn estimate_score(&self, input: &ScoreInput<'_>, folds: &[Fold]) -> Result<ScoreEstimate>;
347
348 fn strata(&self, _data: &DmlData) -> Option<Vec<usize>> {
352 None
353 }
354}
355
356macro_rules! dml_model_api {
361 () => {
362 pub fn n_folds(mut self, n_folds: usize) -> Self {
364 self.splitting.n_folds = n_folds;
365 self
366 }
367
368 pub fn n_rep(mut self, n_rep: usize) -> Self {
371 self.splitting.n_rep = n_rep;
372 self
373 }
374
375 pub fn seed(mut self, seed: u64) -> Self {
377 self.splitting.seed = seed;
378 self
379 }
380
381 pub fn fit(&self, data: &$crate::DmlData) -> $crate::Result<$crate::DmlFit> {
385 match data.clusters() {
386 Some(clusters) => {
387 let splits = self.splitting.draw_clustered(clusters)?;
388 $crate::fit_model_clustered(self, data, &splits)
389 }
390 None => {
391 let splits = match $crate::LinearScoreModel::strata(self, data) {
392 Some(strata) => self.splitting.draw_stratified(&strata)?,
393 None => self.splitting.draw(data.n_obs())?,
394 };
395 $crate::fit_model(self, data, &splits)
396 }
397 }
398 }
399
400 pub fn fit_with_cluster_folds(
403 &self,
404 data: &$crate::DmlData,
405 splits: &[Vec<$crate::ClusterFold>],
406 ) -> $crate::Result<$crate::DmlFit> {
407 $crate::fit_model_clustered(self, data, splits)
408 }
409
410 pub fn sensitivity_benchmark(
415 &self,
416 data: &$crate::DmlData,
417 long: &$crate::DmlFit,
418 benchmarking_set: &[usize],
419 ) -> $crate::Result<$crate::SensitivityBenchmark> {
420 let short = self.fit(&data.without_covariates(benchmarking_set)?)?;
421 long.sensitivity_benchmark(&short)
422 }
423
424 pub fn fit_with_folds(
427 &self,
428 data: &$crate::DmlData,
429 splits: &[Vec<$crate::Fold>],
430 ) -> $crate::Result<$crate::DmlFit> {
431 $crate::fit_model(self, data, splits)
432 }
433 };
434}
435pub(crate) use dml_model_api;
436
437#[derive(Default)]
442pub struct NuisanceLearners {
443 regressors: Vec<(String, Box<dyn crate::learner::Regressor>)>,
444 classifiers: Vec<(String, Box<dyn crate::learner::Classifier>)>,
445}
446
447impl NuisanceLearners {
448 pub(crate) fn set_regressor(
449 &mut self,
450 name: &str,
451 learner: Box<dyn crate::learner::Regressor>,
452 ) {
453 self.regressors.retain(|(n, _)| n != name);
454 self.regressors.push((name.to_string(), learner));
455 }
456
457 pub(crate) fn set_classifier(
458 &mut self,
459 name: &str,
460 learner: Box<dyn crate::learner::Classifier>,
461 ) {
462 self.classifiers.retain(|(n, _)| n != name);
463 self.classifiers.push((name.to_string(), learner));
464 }
465
466 pub(crate) fn regressor<'a>(
468 &'a self,
469 name: &str,
470 default: &'a dyn crate::learner::Regressor,
471 ) -> &'a dyn crate::learner::Regressor {
472 self.regressors
473 .iter()
474 .find(|(n, _)| n == name)
475 .map_or(default, |(_, l)| l.as_ref())
476 }
477
478 pub(crate) fn classifier<'a>(
480 &'a self,
481 name: &str,
482 default: &'a dyn crate::learner::Classifier,
483 ) -> &'a dyn crate::learner::Classifier {
484 self.classifiers
485 .iter()
486 .find(|(n, _)| n == name)
487 .map_or(default, |(_, l)| l.as_ref())
488 }
489
490 pub(crate) fn check(
493 &self,
494 model: &str,
495 regressors: &[&str],
496 classifiers: &[&str],
497 ) -> Result<()> {
498 for (name, valid) in self
499 .regressors
500 .iter()
501 .map(|(n, _)| (n, regressors))
502 .chain(self.classifiers.iter().map(|(n, _)| (n, classifiers)))
503 {
504 if !valid.contains(&name.as_str()) {
505 return Err(DmlError::InvalidArgument(format!(
506 "{model} has no nuisance {name:?} for this learner type (valid: {})",
507 valid.join(", ")
508 )));
509 }
510 }
511 Ok(())
512 }
513}
514
515macro_rules! nuisance_api {
518 () => {
519 pub fn nuisance_regressor(
522 self,
523 name: &str,
524 learner: impl $crate::learner::Regressor + 'static,
525 ) -> Self {
526 self.nuisance_regressor_boxed(name, Box::new(learner))
527 }
528
529 pub fn nuisance_regressor_boxed(
531 mut self,
532 name: &str,
533 learner: Box<dyn $crate::learner::Regressor>,
534 ) -> Self {
535 self.nuisance.set_regressor(name, learner);
536 self
537 }
538
539 pub fn nuisance_classifier(
542 self,
543 name: &str,
544 learner: impl $crate::learner::Classifier + 'static,
545 ) -> Self {
546 self.nuisance_classifier_boxed(name, Box::new(learner))
547 }
548
549 pub fn nuisance_classifier_boxed(
551 mut self,
552 name: &str,
553 learner: Box<dyn $crate::learner::Classifier>,
554 ) -> Self {
555 self.nuisance.set_classifier(name, learner);
556 self
557 }
558 };
559}
560pub(crate) use nuisance_api;