use ndarray::{Array1, ArrayView1, ArrayView2};
use crate::crossfit::Fold;
use crate::data::DmlData;
use crate::error::Result;
use crate::score::LinearScore;
use crate::sensitivity::SensitivityParts;
mod apo;
pub(crate) mod did;
mod iivm;
pub(crate) mod irm;
mod pliv;
mod plr;
pub use apo::{Apo, Apos, AposFit};
pub use did::{Did, DidCs, DidScore};
pub use iivm::Iivm;
pub use irm::{Irm, IrmScore};
pub use pliv::Pliv;
pub use plr::{Plr, PlrScore};
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ScoreInput<'a> {
pub y: ArrayView1<'a, f64>,
pub d: ArrayView1<'a, f64>,
pub x: ArrayView2<'a, f64>,
pub z: Option<ArrayView1<'a, f64>>,
pub t: Option<ArrayView1<'a, f64>>,
pub rep: usize,
}
#[derive(Debug, Clone)]
pub struct CateSignal {
pub signal: Array1<f64>,
pub theta_factor: Option<Array1<f64>>,
pub basis_scale: Option<Array1<f64>>,
pub is_effect: bool,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ScoreEstimate {
pub score: LinearScore,
pub predictions: Vec<(&'static str, Array1<f64>)>,
pub sensitivity: Option<SensitivityParts>,
pub targets: Vec<NuisanceTarget>,
pub cate: Option<CateSignal>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LossKind {
Rmse,
LogLoss,
}
#[derive(Debug, Clone)]
pub struct NuisanceTarget {
pub name: &'static str,
pub target: Array1<f64>,
pub kind: LossKind,
}
impl NuisanceTarget {
pub fn regression(name: &'static str, target: Array1<f64>) -> Self {
Self {
name,
target,
kind: LossKind::Rmse,
}
}
pub fn classification(name: &'static str, target: Array1<f64>) -> Self {
Self {
name,
target,
kind: LossKind::LogLoss,
}
}
pub fn only_where(mut self, keep: impl Fn(usize) -> bool) -> Self {
for (i, v) in self.target.iter_mut().enumerate() {
if !keep(i) {
*v = f64::NAN;
}
}
self
}
pub fn loss(&self, prediction: &Array1<f64>) -> f64 {
let mut total = 0.0;
let mut count = 0usize;
for (&t, &p) in self.target.iter().zip(prediction) {
if t.is_nan() {
continue;
}
total += match self.kind {
LossKind::Rmse => (t - p).powi(2),
LossKind::LogLoss => {
let p = p.clamp(1e-15, 1.0 - 1e-15);
-(t * p.ln() + (1.0 - t) * (1.0 - p).ln())
}
};
count += 1;
}
if count == 0 {
return f64::NAN;
}
let mean = total / count as f64;
match self.kind {
LossKind::Rmse => mean.sqrt(),
LossKind::LogLoss => mean,
}
}
}
impl ScoreEstimate {
pub fn new(score: LinearScore, predictions: Vec<(&'static str, Array1<f64>)>) -> Self {
Self {
score,
predictions,
sensitivity: None,
targets: Vec::new(),
cate: None,
}
}
pub fn with_cate_signal(mut self, cate: CateSignal) -> Self {
self.cate = Some(cate);
self
}
pub fn with_targets(mut self, targets: Vec<NuisanceTarget>) -> Self {
self.targets = targets;
self
}
pub fn with_sensitivity(mut self, parts: SensitivityParts) -> Self {
self.sensitivity = Some(parts);
self
}
}
pub trait LinearScoreModel {
fn name(&self) -> String;
fn validate(&self, _data: &DmlData) -> Result<()> {
Ok(())
}
fn estimate_score(&self, input: &ScoreInput<'_>, folds: &[Fold]) -> Result<ScoreEstimate>;
fn strata(&self, _data: &DmlData) -> Option<Vec<usize>> {
None
}
}
macro_rules! dml_model_api {
() => {
pub fn n_folds(mut self, n_folds: usize) -> Self {
self.splitting.n_folds = n_folds;
self
}
pub fn n_rep(mut self, n_rep: usize) -> Self {
self.splitting.n_rep = n_rep;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.splitting.seed = seed;
self
}
pub fn fit(&self, data: &$crate::DmlData) -> $crate::Result<$crate::DmlFit> {
match data.clusters() {
Some(clusters) => {
let splits = self.splitting.draw_clustered(clusters)?;
$crate::fit_model_clustered(self, data, &splits)
}
None => {
let splits = match $crate::LinearScoreModel::strata(self, data) {
Some(strata) => self.splitting.draw_stratified(&strata)?,
None => self.splitting.draw(data.n_obs())?,
};
$crate::fit_model(self, data, &splits)
}
}
}
pub fn fit_with_cluster_folds(
&self,
data: &$crate::DmlData,
splits: &[Vec<$crate::ClusterFold>],
) -> $crate::Result<$crate::DmlFit> {
$crate::fit_model_clustered(self, data, splits)
}
pub fn sensitivity_benchmark(
&self,
data: &$crate::DmlData,
long: &$crate::DmlFit,
benchmarking_set: &[usize],
) -> $crate::Result<$crate::SensitivityBenchmark> {
let short = self.fit(&data.without_covariates(benchmarking_set)?)?;
long.sensitivity_benchmark(&short)
}
pub fn fit_with_folds(
&self,
data: &$crate::DmlData,
splits: &[Vec<$crate::Fold>],
) -> $crate::Result<$crate::DmlFit> {
$crate::fit_model(self, data, splits)
}
};
}
pub(crate) use dml_model_api;