use ndarray::{Array1, Array2};
use solow_regression::LinearModel;
#[derive(Debug, Clone, Copy)]
pub struct MediationResults {
pub acme_ctrl: f64,
pub acme_tx: f64,
pub ade_ctrl: f64,
pub ade_tx: f64,
pub total_effect: f64,
pub prop_med_ctrl: f64,
pub prop_med_tx: f64,
pub prop_med_avg: f64,
pub acme_avg: f64,
pub ade_avg: f64,
}
#[derive(Debug, Clone)]
pub struct Mediation {
pub outcome_endog: Array1<f64>,
pub outcome_exog: Array2<f64>,
pub mediator_endog: Array1<f64>,
pub mediator_exog: Array2<f64>,
pub exp_pos_outcome: usize,
pub exp_pos_mediator: usize,
pub med_pos_outcome: usize,
}
impl Mediation {
fn mediator_exog_at(&self, exposure: f64) -> Array2<f64> {
let mut m = self.mediator_exog.clone();
m.column_mut(self.exp_pos_mediator).fill(exposure);
m
}
fn outcome_exog_at(&self, exposure: f64, mediator: &Array1<f64>) -> Array2<f64> {
let mut o = self.outcome_exog.clone();
o.column_mut(self.exp_pos_outcome).fill(exposure);
o.column_mut(self.med_pos_outcome).assign(mediator);
o
}
pub fn fit(&self) -> MediationResults {
let beta_o = LinearModel::ols(self.outcome_endog.clone(), self.outcome_exog.clone())
.expect("outcome OLS")
.fit()
.expect("outcome fit")
.params;
let beta_m = LinearModel::ols(self.mediator_endog.clone(), self.mediator_exog.clone())
.expect("mediator OLS")
.fit()
.expect("mediator fit")
.params;
let pm: [Array1<f64>; 2] = [
self.mediator_exog_at(0.0).dot(&beta_m),
self.mediator_exog_at(1.0).dot(&beta_m),
];
let predict = |tm: usize, te: f64| self.outcome_exog_at(te, &pm[tm]).dot(&beta_o);
let po: [[Array1<f64>; 2]; 2] = [
[predict(0, 0.0), predict(0, 1.0)],
[predict(1, 0.0), predict(1, 1.0)],
];
let mean = |a: &Array1<f64>| a.sum() / a.len() as f64;
let acme_ctrl = mean(&(&po[1][0] - &po[0][0]));
let acme_tx = mean(&(&po[1][1] - &po[0][1]));
let ade_ctrl = mean(&(&po[0][1] - &po[0][0]));
let ade_tx = mean(&(&po[1][1] - &po[1][0]));
let total_effect = (acme_ctrl + acme_tx + ade_ctrl + ade_tx) / 2.0;
let prop_med_ctrl = acme_ctrl / total_effect;
let prop_med_tx = acme_tx / total_effect;
let prop_med_avg = (prop_med_ctrl + prop_med_tx) / 2.0;
let acme_avg = (acme_ctrl + acme_tx) / 2.0;
let ade_avg = (ade_ctrl + ade_tx) / 2.0;
MediationResults {
acme_ctrl,
acme_tx,
ade_ctrl,
ade_tx,
total_effect,
prop_med_ctrl,
prop_med_tx,
prop_med_avg,
acme_avg,
ade_avg,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn matches_baron_kenny() {
let t = array![0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0];
let z = array![10.0, 12.0, 14.0, 9.0, 11.0, 13.0, 8.0, 15.0, 7.0, 16.0];
let extra = array![0.3, -0.5, 0.1, 0.8, -0.2, 0.6, -0.7, 0.4, 0.2, -0.9];
let n = t.len();
let m: Array1<f64> =
Array1::from_iter((0..n).map(|i| 0.5 + 0.8 * t[i] + 0.05 * z[i] + extra[i]));
let y: Array1<f64> =
Array1::from_iter((0..n).map(|i| 1.0 + 1.5 * m[i] + 0.7 * t[i] + 0.02 * z[i]));
let ones = Array1::ones(n);
let mut oe = Array2::zeros((n, 4));
oe.column_mut(0).assign(&ones);
oe.column_mut(1).assign(&m);
oe.column_mut(2).assign(&t);
oe.column_mut(3).assign(&z);
let mut me = Array2::zeros((n, 3));
me.column_mut(0).assign(&ones);
me.column_mut(1).assign(&t);
me.column_mut(2).assign(&z);
let a_fit = LinearModel::ols(m.clone(), me.clone())
.unwrap()
.fit()
.unwrap()
.params[1];
let med = Mediation {
outcome_endog: y,
outcome_exog: oe,
mediator_endog: m,
mediator_exog: me,
exp_pos_outcome: 2,
exp_pos_mediator: 1,
med_pos_outcome: 1,
};
let r = med.fit();
assert!(
(r.acme_avg - a_fit * 1.5).abs() < 1e-8,
"acme {}",
r.acme_avg
);
assert!((r.ade_avg - 0.7).abs() < 1e-8, "ade {}", r.ade_avg);
assert!((r.acme_ctrl - r.acme_tx).abs() < 1e-10);
assert!((r.total_effect - (r.acme_avg + r.ade_avg)).abs() < 1e-8);
}
}