use ndarray::{Array1, Array2};
use solow_core::{Error, Result};
use solow_regression::LinearModel;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TwoFoldType {
Pooled,
Neumark,
Cotton,
Reimers,
SelfSubmitted(f64),
}
#[derive(Debug, Clone)]
pub struct TwoFold {
pub unexplained: f64,
pub explained: f64,
pub gap: f64,
}
#[derive(Debug, Clone)]
pub struct ThreeFold {
pub endowments: f64,
pub coefficients: f64,
pub interaction: f64,
pub gap: f64,
}
#[derive(Debug, Clone)]
pub struct OaxacaBlinder {
bifurcate: usize,
hasconst: bool,
neumark: Array2<f64>,
exog: Array2<f64>,
endog: Array1<f64>,
gap: f64,
len_f: usize,
len_s: usize,
exog_f_mean: Array1<f64>,
exog_s_mean: Array1<f64>,
f_params: Array1<f64>,
s_params: Array1<f64>,
}
fn add_constant_append(x: &Array2<f64>) -> Array2<f64> {
let (n, k) = x.dim();
let mut out = Array2::<f64>::zeros((n, k + 1));
out.slice_mut(ndarray::s![.., ..k]).assign(x);
for i in 0..n {
out[[i, k]] = 1.0;
}
out
}
fn col_means(x: &Array2<f64>) -> Array1<f64> {
let (n, k) = x.dim();
let mut m = Array1::<f64>::zeros(k);
for j in 0..k {
let mut s = 0.0;
for i in 0..n {
s += x[[i, j]];
}
m[j] = s / n as f64;
}
m
}
fn delete_col(x: &Array2<f64>, col: usize) -> Array2<f64> {
let (n, k) = x.dim();
let mut out = Array2::<f64>::zeros((n, k - 1));
let mut jj = 0;
for j in 0..k {
if j == col {
continue;
}
for i in 0..n {
out[[i, jj]] = x[[i, j]];
}
jj += 1;
}
out
}
fn select_rows(x: &Array2<f64>, rows: &[usize]) -> Array2<f64> {
let k = x.ncols();
let mut out = Array2::<f64>::zeros((rows.len(), k));
for (ii, &i) in rows.iter().enumerate() {
for j in 0..k {
out[[ii, j]] = x[[i, j]];
}
}
out
}
fn select_elems(v: &Array1<f64>, rows: &[usize]) -> Array1<f64> {
Array1::from_iter(rows.iter().map(|&i| v[i]))
}
fn mean(v: &Array1<f64>) -> f64 {
v.sum() / v.len() as f64
}
fn fit_params(endog: Array1<f64>, exog: Array2<f64>) -> Result<Array1<f64>> {
let res = LinearModel::ols(endog, exog)?.fit()?;
Ok(res.params)
}
impl OaxacaBlinder {
pub fn new(
endog: Array1<f64>,
exog: Array2<f64>,
bifurcate: usize,
hasconst: bool,
) -> Result<Self> {
let n = endog.len();
if exog.nrows() != n {
return Err(Error::Shape("endog length != exog rows".into()));
}
if bifurcate >= exog.ncols() {
return Err(Error::Value("bifurcate column out of range".into()));
}
let bi_col: Vec<f64> = (0..n).map(|i| exog[[i, bifurcate]]).collect();
let mut uniq: Vec<f64> = bi_col.clone();
uniq.sort_by(|a, b| a.total_cmp(b));
uniq.dedup();
if uniq.len() != 2 {
return Err(Error::Value(
"bifurcate column must take exactly two distinct values".into(),
));
}
let mut bi = [uniq[0], uniq[1]];
let mut rows_f: Vec<usize> = (0..n).filter(|&i| bi_col[i] == bi[0]).collect();
let mut rows_s: Vec<usize> = (0..n).filter(|&i| bi_col[i] == bi[1]).collect();
let endog_full = endog.clone();
let mut endog_f = select_elems(&endog_full, &rows_f);
let mut endog_s = select_elems(&endog_full, &rows_s);
let len_f = rows_f.len();
let len_s = rows_s.len();
let mut gap = mean(&endog_f) - mean(&endog_s);
if gap < 0.0 {
std::mem::swap(&mut rows_f, &mut rows_s);
std::mem::swap(&mut endog_f, &mut endog_s);
bi.swap(0, 1);
gap = mean(&endog_f) - mean(&endog_s);
}
let mut exog_f = delete_col(&select_rows(&exog, &rows_f), bifurcate);
let mut exog_s = delete_col(&select_rows(&exog, &rows_s), bifurcate);
let neumark = delete_col(&exog, bifurcate);
let (exog_full, neumark) = if hasconst {
(exog.clone(), neumark)
} else {
exog_f = add_constant_append(&exog_f);
exog_s = add_constant_append(&exog_s);
(add_constant_append(&exog), add_constant_append(&neumark))
};
let exog_f_mean = col_means(&exog_f);
let exog_s_mean = col_means(&exog_s);
let f_params = fit_params(endog_f, exog_f)?;
let s_params = fit_params(endog_s, exog_s)?;
Ok(OaxacaBlinder {
bifurcate,
hasconst,
neumark,
exog: exog_full,
endog,
gap,
len_f,
len_s,
exog_f_mean,
exog_s_mean,
f_params,
s_params,
})
}
pub fn gap(&self) -> f64 {
self.gap
}
pub fn f_params(&self) -> &Array1<f64> {
&self.f_params
}
pub fn s_params(&self) -> &Array1<f64> {
&self.s_params
}
pub fn exog_f_mean(&self) -> &Array1<f64> {
&self.exog_f_mean
}
pub fn exog_s_mean(&self) -> &Array1<f64> {
&self.exog_s_mean
}
fn t_params(&self, kind: TwoFoldType) -> Result<Array1<f64>> {
Ok(match kind {
TwoFoldType::Pooled => {
let full = fit_params(self.endog.clone(), self.exog.clone())?;
Array1::from_iter(
(0..full.len())
.filter(|&j| j != self.bifurcate)
.map(|j| full[j]),
)
}
TwoFoldType::Neumark => fit_params(self.endog.clone(), self.neumark.clone())?,
TwoFoldType::Cotton => {
let nf = self.len_f as f64;
let ns = self.len_s as f64;
&self.f_params * (nf / (nf + ns)) + &self.s_params * (ns / (nf + ns))
}
TwoFoldType::Reimers => (&self.f_params + &self.s_params) * 0.5,
TwoFoldType::SelfSubmitted(w) => &self.f_params * w + &self.s_params * (1.0 - w),
})
}
pub fn two_fold(&self, kind: TwoFoldType) -> Result<TwoFold> {
let tp = self.t_params(kind)?;
if tp.len() != self.f_params.len() {
return Err(Error::Shape("t_params dimension mismatch".into()));
}
let unexplained = self.exog_f_mean.dot(&(&self.f_params - &tp))
+ self.exog_s_mean.dot(&(&tp - &self.s_params));
let explained = (&self.exog_f_mean - &self.exog_s_mean).dot(&tp);
Ok(TwoFold {
unexplained,
explained,
gap: self.gap,
})
}
pub fn three_fold(&self) -> ThreeFold {
let dmean = &self.exog_f_mean - &self.exog_s_mean;
let dparams = &self.f_params - &self.s_params;
ThreeFold {
endowments: dmean.dot(&self.s_params),
coefficients: self.exog_s_mean.dot(&dparams),
interaction: dmean.dot(&dparams),
gap: self.gap,
}
}
pub fn hasconst(&self) -> bool {
self.hasconst
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
fn toy() -> (Array1<f64>, Array2<f64>) {
let exog = array![
[1.0, 0.0, 1.0],
[1.0, 0.0, 2.0],
[1.0, 0.0, 3.0],
[1.0, 1.0, 1.0],
[1.0, 1.0, 2.0],
[1.0, 1.0, 3.0],
];
let endog = array![1.0, 2.0, 3.0, 4.0, 5.5, 7.0];
(endog, exog)
}
#[test]
fn gap_is_nonnegative_and_decompositions_sum_to_gap() {
let (y, x) = toy();
let m = OaxacaBlinder::new(y, x, 1, true).unwrap();
assert!(m.gap() >= 0.0);
let tf = m.three_fold();
let s = tf.endowments + tf.coefficients + tf.interaction;
assert!((s - tf.gap).abs() < 1e-9, "three-fold sums to gap");
let two = m.two_fold(TwoFoldType::Pooled).unwrap();
assert!((two.unexplained + two.explained - two.gap).abs() < 1e-9);
}
#[test]
fn rejects_non_binary_group() {
let exog = array![[1.0, 0.0], [1.0, 1.0], [1.0, 2.0]];
let endog = array![1.0, 2.0, 3.0];
assert!(OaxacaBlinder::new(endog, exog, 1, true).is_err());
}
}