1use 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#[derive(Debug, Clone, Copy)]
29#[non_exhaustive]
30pub struct ScoreInput<'a> {
31 pub y: ArrayView1<'a, f64>,
32 pub d: ArrayView1<'a, f64>,
34 pub x: ArrayView2<'a, f64>,
36 pub z: Option<ArrayView1<'a, f64>>,
37 pub t: Option<ArrayView1<'a, f64>>,
39}
40
41#[derive(Debug, Clone)]
46#[non_exhaustive]
47pub struct ScoreEstimate {
48 pub score: LinearScore,
49 pub predictions: Vec<(&'static str, Array1<f64>)>,
51 pub sensitivity: Option<SensitivityParts>,
54 pub targets: Vec<NuisanceTarget>,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum LossKind {
61 Rmse,
63 LogLoss,
65}
66
67#[derive(Debug, Clone)]
70pub struct NuisanceTarget {
71 pub name: &'static str,
72 pub target: Array1<f64>,
73 pub kind: LossKind,
74}
75
76impl NuisanceTarget {
77 pub fn regression(name: &'static str, target: Array1<f64>) -> Self {
78 Self {
79 name,
80 target,
81 kind: LossKind::Rmse,
82 }
83 }
84
85 pub fn classification(name: &'static str, target: Array1<f64>) -> Self {
86 Self {
87 name,
88 target,
89 kind: LossKind::LogLoss,
90 }
91 }
92
93 pub fn only_where(mut self, keep: impl Fn(usize) -> bool) -> Self {
95 for (i, v) in self.target.iter_mut().enumerate() {
96 if !keep(i) {
97 *v = f64::NAN;
98 }
99 }
100 self
101 }
102
103 pub fn loss(&self, prediction: &Array1<f64>) -> f64 {
105 let mut total = 0.0;
106 let mut count = 0usize;
107 for (&t, &p) in self.target.iter().zip(prediction) {
108 if t.is_nan() {
109 continue;
110 }
111 total += match self.kind {
112 LossKind::Rmse => (t - p).powi(2),
113 LossKind::LogLoss => {
114 let p = p.clamp(1e-15, 1.0 - 1e-15);
115 -(t * p.ln() + (1.0 - t) * (1.0 - p).ln())
116 }
117 };
118 count += 1;
119 }
120 if count == 0 {
121 return f64::NAN;
122 }
123 let mean = total / count as f64;
124 match self.kind {
125 LossKind::Rmse => mean.sqrt(),
126 LossKind::LogLoss => mean,
127 }
128 }
129}
130
131impl ScoreEstimate {
132 pub fn new(score: LinearScore, predictions: Vec<(&'static str, Array1<f64>)>) -> Self {
133 Self {
134 score,
135 predictions,
136 sensitivity: None,
137 targets: Vec::new(),
138 }
139 }
140
141 pub fn with_targets(mut self, targets: Vec<NuisanceTarget>) -> Self {
143 self.targets = targets;
144 self
145 }
146
147 pub fn with_sensitivity(mut self, parts: SensitivityParts) -> Self {
149 self.sensitivity = Some(parts);
150 self
151 }
152}
153
154pub trait LinearScoreModel {
159 fn name(&self) -> String;
161
162 fn validate(&self, _data: &DmlData) -> Result<()> {
164 Ok(())
165 }
166
167 fn estimate_score(&self, input: &ScoreInput<'_>, folds: &[Fold]) -> Result<ScoreEstimate>;
169}
170
171macro_rules! dml_model_api {
176 () => {
177 pub fn n_folds(mut self, n_folds: usize) -> Self {
179 self.splitting.n_folds = n_folds;
180 self
181 }
182
183 pub fn n_rep(mut self, n_rep: usize) -> Self {
186 self.splitting.n_rep = n_rep;
187 self
188 }
189
190 pub fn seed(mut self, seed: u64) -> Self {
192 self.splitting.seed = seed;
193 self
194 }
195
196 pub fn fit(&self, data: &$crate::DmlData) -> $crate::Result<$crate::DmlFit> {
200 match data.clusters() {
201 Some(clusters) => {
202 let splits = self.splitting.draw_clustered(clusters)?;
203 $crate::fit_model_clustered(self, data, &splits)
204 }
205 None => {
206 let splits = self.splitting.draw(data.n_obs())?;
207 $crate::fit_model(self, data, &splits)
208 }
209 }
210 }
211
212 pub fn fit_with_cluster_folds(
215 &self,
216 data: &$crate::DmlData,
217 splits: &[Vec<$crate::ClusterFold>],
218 ) -> $crate::Result<$crate::DmlFit> {
219 $crate::fit_model_clustered(self, data, splits)
220 }
221
222 pub fn sensitivity_benchmark(
227 &self,
228 data: &$crate::DmlData,
229 long: &$crate::DmlFit,
230 benchmarking_set: &[usize],
231 ) -> $crate::Result<$crate::SensitivityBenchmark> {
232 let short = self.fit(&data.without_covariates(benchmarking_set)?)?;
233 long.sensitivity_benchmark(&short)
234 }
235
236 pub fn fit_with_folds(
239 &self,
240 data: &$crate::DmlData,
241 splits: &[Vec<$crate::Fold>],
242 ) -> $crate::Result<$crate::DmlFit> {
243 $crate::fit_model(self, data, splits)
244 }
245 };
246}
247pub(crate) use dml_model_api;