use dml_core::{
Apo, Did, DidCs, DidScore, DmlData, DmlFit, ExternalPredictions, Iivm, Irm, IrmScore,
LogisticRegression, Lplr, Lpq, Ols, Pliv, PlivScore, Plr, PlrScore, Pq,
};
use ndarray::{Array1, Array2};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
type Arrays = (
Array1<f64>,
Array1<f64>,
Array2<f64>,
Array1<f64>,
Array1<f64>,
);
fn data(n: usize, seed: u64) -> Arrays {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let x = Array2::from_shape_fn((n, 3), |_| rng.gen::<f64>() * 2.0 - 1.0);
let (mut y, mut d, mut z, mut t) = (
Array1::zeros(n),
Array1::zeros(n),
Array1::zeros(n),
Array1::zeros(n),
);
for i in 0..n {
z[i] = f64::from(rng.gen::<f64>() < 0.5);
d[i] = f64::from(rng.gen::<f64>() < 0.3 + 0.3 * z[i] + 0.1 * x[[i, 0]]);
t[i] = f64::from(rng.gen::<f64>() < 0.5);
y[i] = d[i] + x[[i, 0]] - 0.5 * x[[i, 1]] + 0.3 * t[i] + rng.gen::<f64>();
}
(y, d, x, z, t)
}
fn all_external(fit: &DmlFit, names: &[&str]) -> ExternalPredictions {
names.iter().fold(ExternalPredictions::new(), |e, name| {
let p = fit
.predictions(name)
.unwrap_or_else(|| panic!("no predictions {name}"));
e.with(0, *name, p.slice(ndarray::s![.., .., 0]).to_owned())
})
}
fn assert_same(a: &DmlFit, b: &DmlFit) {
for (x, y) in a.all_coef().iter().zip(b.all_coef()) {
assert!((x - y).abs() < 1e-12, "coef {x} vs {y}");
}
for (x, y) in a.all_se().iter().zip(b.all_se()) {
assert!((x - y).abs() < 1e-12, "se {x} vs {y}");
}
}
#[test]
fn external_predictions_reproduce_every_linear_model() {
let (y, d, x, z, t) = data(400, 1);
let base = DmlData::new(y.clone(), d.clone(), x.clone()).unwrap();
let plr = Plr::new(Ols::new(), Ols::new()).n_rep(2);
let fit = plr.fit(&base).unwrap();
let ext = all_external(&fit, &["ml_l", "ml_m"]);
let refit = plr
.fit(&base.clone().with_external_predictions(ext))
.unwrap();
assert_same(&fit, &refit);
assert!(refit.nuisance_loss("ml_l").is_none() && refit.nuisance_loss("ml_m").is_none());
let plr_iv = Plr::new(Ols::new(), Ols::new()).score(PlrScore::IvType);
let fit = plr_iv.fit(&base).unwrap();
let ext = all_external(&fit, &["ml_m", "ml_g"]);
let refit = plr_iv
.fit(&base.clone().with_external_predictions(ext))
.unwrap();
assert_same(&fit, &refit);
assert!(refit.nuisance_loss("ml_l").is_some() && refit.nuisance_loss("ml_m").is_none());
for score in [IrmScore::Ate, IrmScore::Atte] {
let irm = Irm::new(Ols::new(), LogisticRegression::default()).score(score);
let fit = irm.fit(&base).unwrap();
let refit = irm
.fit(
&base
.clone()
.with_external_predictions(all_external(&fit, &["ml_g0", "ml_g1", "ml_m"])),
)
.unwrap();
assert_same(&fit, &refit);
}
let levels = &d + &z;
let multi = DmlData::new(y.clone(), levels, x.clone()).unwrap();
let apo = Apo::new(Ols::new(), LogisticRegression::default(), 1.0);
let fit = apo.fit(&multi).unwrap();
let refit =
apo.fit(&multi.clone().with_external_predictions(all_external(
&fit,
&["ml_g_d_lvl0", "ml_g_d_lvl1", "ml_m"],
)))
.unwrap();
assert_same(&fit, &refit);
let with_z = base.clone().with_instrument(z).unwrap();
let pliv = Pliv::new(Ols::new(), Ols::new(), Ols::new()).score(PlivScore::IvType);
let fit = pliv.fit(&with_z).unwrap();
let refit = pliv
.fit(
&with_z
.clone()
.with_external_predictions(all_external(&fit, &["ml_l", "ml_m", "ml_r", "ml_g"])),
)
.unwrap();
assert_same(&fit, &refit);
let iivm = Iivm::new(
Ols::new(),
LogisticRegression::default(),
LogisticRegression::default(),
);
let fit = iivm.fit(&with_z).unwrap();
let names = ["ml_g0", "ml_g1", "ml_m", "ml_r0", "ml_r1"];
let refit = iivm
.fit(
&with_z
.clone()
.with_external_predictions(all_external(&fit, &names)),
)
.unwrap();
assert_same(&fit, &refit);
let did = Did::new(Ols::new(), LogisticRegression::default()).score(DidScore::Observational);
let fit = did.fit(&base).unwrap();
let refit = did
.fit(
&base
.clone()
.with_external_predictions(all_external(&fit, &["ml_g0", "ml_g1", "ml_m"])),
)
.unwrap();
assert_same(&fit, &refit);
let cs = base.clone().with_time_indicator(t).unwrap();
let didcs = DidCs::new(Ols::new(), LogisticRegression::default());
let fit = didcs.fit(&cs).unwrap();
let names = [
"ml_g_d0_t0",
"ml_g_d0_t1",
"ml_g_d1_t0",
"ml_g_d1_t1",
"ml_m",
];
let refit = didcs
.fit(
&cs.clone()
.with_external_predictions(all_external(&fit, &names)),
)
.unwrap();
assert_same(&fit, &refit);
}
#[test]
fn external_predictions_for_quantile_models() {
let (y, d, x, z, _) = data(400, 3);
let base = DmlData::new(y, d, x).unwrap();
let pq = Pq::new(LogisticRegression::default(), LogisticRegression::default()).n_rep(2);
let fit = pq.fit(&base).unwrap();
let refit = pq
.fit(
&base
.clone()
.with_external_predictions(all_external(&fit, &["ml_g"])),
)
.unwrap();
assert_same(&fit, &refit);
assert!(refit.nuisance_loss("ml_g").is_none() && refit.nuisance_loss("ml_m").is_some());
let refit = pq
.fit(
&base
.clone()
.with_external_predictions(all_external(&fit, &["ml_g", "ml_m"])),
)
.unwrap();
assert!((refit.coef()[0] - fit.coef()[0]).abs() < 0.05);
assert_eq!(refit.nuisance_losses().count(), 0);
let iv = base.clone().with_instrument(z).unwrap();
let lpq = Lpq::new(LogisticRegression::default(), LogisticRegression::default());
let fit = lpq.fit(&iv).unwrap();
let names = [
"ml_m_z",
"ml_m_d_z0",
"ml_m_d_z1",
"ml_g_du_z0",
"ml_g_du_z1",
];
let refit = lpq
.fit(
&iv.clone()
.with_external_predictions(all_external(&fit, &names)),
)
.unwrap();
assert!((refit.coef()[0] - fit.coef()[0]).abs() < 0.05);
assert!(lpq
.fit(&iv.with_external_predictions(all_external(&fit, &names[..2])))
.is_err());
}
#[test]
fn external_predictions_for_lplr_need_inner_predictions() {
let mut rng = ChaCha8Rng::seed_from_u64(5);
let n = 400;
let x = Array2::from_shape_fn((n, 2), |_| rng.gen::<f64>() * 2.0 - 1.0);
let d: Array1<f64> = (0..n).map(|i| x[[i, 0]] + rng.gen::<f64>()).collect();
let y: Array1<f64> = (0..n)
.map(|i| {
let p = 1.0 / (1.0 + (-(0.5 * d[i] + x[[i, 1]])).exp());
f64::from(rng.gen::<f64>() < p)
})
.collect();
let base = DmlData::new(y, d, x).unwrap();
let lplr = Lplr::new(LogisticRegression::default(), Ols::new(), Ols::new()).n_rep(2);
let fit = lplr.fit(&base).unwrap();
let inner = |name: &'static str| (0..5).map(move |k| format!("{name}_inner_{k}"));
let mut names: Vec<String> = ["ml_M", "ml_m", "ml_a", "ml_t"].map(String::from).to_vec();
names.extend(inner("ml_M").chain(inner("ml_a")));
let names: Vec<&str> = names.iter().map(String::as_str).collect();
let refit = lplr
.fit(
&base
.clone()
.with_external_predictions(all_external(&fit, &names)),
)
.unwrap();
assert_same(&fit, &refit);
assert_eq!(refit.nuisance_losses().count(), 0);
let refit = lplr
.fit(
&base
.clone()
.with_external_predictions(all_external(&fit, &["ml_m"])),
)
.unwrap();
assert_same(&fit, &refit);
assert!(refit.nuisance_loss("ml_m").is_none() && refit.nuisance_loss("ml_M").is_some());
assert!(lplr
.fit(&base.with_external_predictions(all_external(&fit, &["ml_M"])))
.is_err());
}
#[test]
fn external_predictions_are_checked() {
let (y, d, x, _, _) = data(100, 2);
let base = DmlData::new(y, d, x).unwrap();
let wrong = ExternalPredictions::new().with(0, "ml_l", Array2::zeros((99, 1)));
let err = Plr::new(Ols::new(), Ols::new()).fit(&base.clone().with_external_predictions(wrong));
assert!(err.is_err());
let short = ExternalPredictions::new().with_vector(0, "ml_l", Array1::zeros(100));
assert!(Plr::new(Ols::new(), Ols::new())
.n_rep(2)
.fit(&base.with_external_predictions(short))
.is_err());
}