use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use crate::crossfit::Fold;
use crate::data::DmlData;
use crate::error::{DmlError, Result};
use crate::score::LinearScore;
use crate::sensitivity::SensitivityParts;
mod apo;
mod autodml;
pub(crate) mod did;
mod iivm;
pub(crate) mod irm;
mod lplr;
mod nonlinear;
mod pliv;
mod plpr;
mod plr;
mod quantile;
mod rdflex;
mod ssm;
pub use apo::{Apo, Apos, AposFit};
pub use autodml::AutoDml;
pub use did::{Did, DidCs, DidScore};
pub use iivm::Iivm;
pub use irm::{Irm, IrmScore};
pub use lplr::{Lplr, LplrScore, TreatmentLearner};
pub use pliv::{Pliv, PlivScore, PlivVariant};
pub use plpr::{PanelApproach, Plpr, StaticPanel};
pub use plr::{Plr, PlrScore};
pub use quantile::{Cvar, Lpq, Pq, Qte, QteFit};
pub use rdflex::{FirstStage, RdFlex, RdFlexFit, RdKernel};
pub use ssm::{Ssm, SsmScore};
#[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 instruments: Option<ArrayView2<'a, f64>>,
pub t: Option<ArrayView1<'a, f64>>,
pub s: Option<ArrayView1<'a, f64>>,
pub rep: usize,
pub treatment: usize,
pub external: Option<&'a ExternalPredictions>,
}
impl ScoreInput<'_> {
pub fn predictions_or(
&self,
name: &str,
fit: impl FnOnce() -> Result<Array1<f64>>,
) -> Result<Array1<f64>> {
external_or(self.external_ref(), self.y.len(), name, fit)
}
pub(crate) fn external_ref(&self) -> Option<ExternalRef<'_>> {
self.external.map(|preds| ExternalRef {
preds,
treatment: self.treatment,
rep: self.rep,
})
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ExternalRef<'a> {
pub preds: &'a ExternalPredictions,
pub treatment: usize,
pub rep: usize,
}
pub(crate) fn external_or(
ext: Option<ExternalRef<'_>>,
n: usize,
name: &str,
fit: impl FnOnce() -> Result<Array1<f64>>,
) -> Result<Array1<f64>> {
match external_get(ext, n, name)? {
Some(p) => Ok(p),
None => fit(),
}
}
pub(crate) fn external_get(
ext: Option<ExternalRef<'_>>,
n: usize,
name: &str,
) -> Result<Option<Array1<f64>>> {
match ext.and_then(|e| e.preds.get(e.treatment, name).map(|p| (p, e.rep))) {
Some((p, rep)) => {
if p.nrows() != n || rep >= p.ncols() {
return Err(DmlError::DimensionMismatch(format!(
"external predictions for {name} have shape {:?}; expected ({n}, n_rep)",
p.dim()
)));
}
Ok(Some(p.column(rep).to_owned()))
}
None => Ok(None),
}
}
#[derive(Debug, Clone, Default)]
pub struct ExternalPredictions {
preds: Vec<(usize, String, Array2<f64>)>,
}
impl ExternalPredictions {
pub fn new() -> Self {
Self::default()
}
pub fn with(
mut self,
treatment: usize,
name: impl Into<String>,
predictions: Array2<f64>,
) -> Self {
let name = name.into();
self.preds
.retain(|(t, n, _)| !(*t == treatment && *n == name));
self.preds.push((treatment, name, predictions));
self
}
pub fn with_vector(
self,
treatment: usize,
name: impl Into<String>,
predictions: Array1<f64>,
) -> Self {
let n = predictions.len();
self.with(
treatment,
name,
predictions
.into_shape_with_order((n, 1))
.expect("1-D to (n, 1)"),
)
}
pub(crate) fn get(&self, treatment: usize, name: &str) -> Option<&Array2<f64>> {
self.preds
.iter()
.find(|(t, n, _)| *t == treatment && n == name)
.map(|(_, _, p)| p)
}
pub(crate) fn contains(&self, treatment: usize, name: &str) -> bool {
self.get(treatment, name).is_some()
}
}
#[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<(Name, Array1<f64>)>,
pub sensitivity: Option<SensitivityParts>,
pub targets: Vec<NuisanceTarget>,
pub cate: Option<CateSignal>,
}
pub type Name = std::borrow::Cow<'static, str>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LossKind {
Rmse,
LogLoss,
}
#[derive(Debug, Clone)]
pub struct NuisanceTarget {
pub name: Name,
pub target: Array1<f64>,
pub kind: LossKind,
}
impl NuisanceTarget {
pub fn regression(name: impl Into<Name>, target: Array1<f64>) -> Self {
Self {
name: name.into(),
target,
kind: LossKind::Rmse,
}
}
pub fn classification(name: impl Into<Name>, target: Array1<f64>) -> Self {
Self {
name: name.into(),
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<N: Into<Name>>(score: LinearScore, predictions: Vec<(N, Array1<f64>)>) -> Self {
Self {
score,
predictions: predictions
.into_iter()
.map(|(n, p)| (n.into(), p))
.collect(),
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;
#[derive(Default)]
pub struct NuisanceLearners {
regressors: Vec<(String, Box<dyn crate::learner::Regressor>)>,
classifiers: Vec<(String, Box<dyn crate::learner::Classifier>)>,
}
impl NuisanceLearners {
pub(crate) fn set_regressor(
&mut self,
name: &str,
learner: Box<dyn crate::learner::Regressor>,
) {
self.regressors.retain(|(n, _)| n != name);
self.regressors.push((name.to_string(), learner));
}
pub(crate) fn set_classifier(
&mut self,
name: &str,
learner: Box<dyn crate::learner::Classifier>,
) {
self.classifiers.retain(|(n, _)| n != name);
self.classifiers.push((name.to_string(), learner));
}
pub(crate) fn regressor<'a>(
&'a self,
name: &str,
default: &'a dyn crate::learner::Regressor,
) -> &'a dyn crate::learner::Regressor {
self.regressors
.iter()
.find(|(n, _)| n == name)
.map_or(default, |(_, l)| l.as_ref())
}
pub(crate) fn classifier<'a>(
&'a self,
name: &str,
default: &'a dyn crate::learner::Classifier,
) -> &'a dyn crate::learner::Classifier {
self.classifiers
.iter()
.find(|(n, _)| n == name)
.map_or(default, |(_, l)| l.as_ref())
}
pub(crate) fn check(
&self,
model: &str,
regressors: &[&str],
classifiers: &[&str],
) -> Result<()> {
for (name, valid) in self
.regressors
.iter()
.map(|(n, _)| (n, regressors))
.chain(self.classifiers.iter().map(|(n, _)| (n, classifiers)))
{
if !valid.contains(&name.as_str()) {
return Err(DmlError::InvalidArgument(format!(
"{model} has no nuisance {name:?} for this learner type (valid: {})",
valid.join(", ")
)));
}
}
Ok(())
}
}
macro_rules! nuisance_api {
() => {
pub fn nuisance_regressor(
self,
name: &str,
learner: impl $crate::learner::Regressor + 'static,
) -> Self {
self.nuisance_regressor_boxed(name, Box::new(learner))
}
pub fn nuisance_regressor_boxed(
mut self,
name: &str,
learner: Box<dyn $crate::learner::Regressor>,
) -> Self {
self.nuisance.set_regressor(name, learner);
self
}
pub fn nuisance_classifier(
self,
name: &str,
learner: impl $crate::learner::Classifier + 'static,
) -> Self {
self.nuisance_classifier_boxed(name, Box::new(learner))
}
pub fn nuisance_classifier_boxed(
mut self,
name: &str,
learner: Box<dyn $crate::learner::Classifier>,
) -> Self {
self.nuisance.set_classifier(name, learner);
self
}
};
}
pub(crate) use nuisance_api;