use std::collections::{BTreeSet, HashMap};
use super::error::Error;
use super::parse::{parse, ParsedFormula, RandomEffect};
use crate::{
ColumnId, Family, FitOptions, GroupIds, Grouping, GroupingRelation, ModelSpec, ReStructure,
Sizing,
};
pub enum Column {
Numeric(Vec<f64>),
Factor {
levels: Vec<String>,
codes: Vec<u32>,
},
}
impl Column {
pub fn factor_from_labels(labels: &[String]) -> Column {
let (levels, codes) = sorted_levels_and_codes(labels);
Column::Factor { levels, codes }
}
}
pub struct Table {
pub columns: Vec<(String, Column)>,
pub n: usize,
}
impl Table {
fn get(&self, name: &str) -> Option<&Column> {
self.columns.iter().find(|(n, _)| n == name).map(|(_, c)| c)
}
}
pub struct ReGroupInfo {
pub name: String,
pub terms: Vec<String>,
}
pub struct Lowered {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub n: usize,
pub p: usize,
pub col_names: Vec<String>,
pub model: ModelSpec,
pub ids: GroupIds,
pub re_groups: Vec<ReGroupInfo>,
pub opts: FitOptions,
}
pub fn lower(formula: &str, data: &Table, family: Family) -> Result<Lowered, Error> {
let ast = parse(formula)?;
materialize(&ast, data, family)
}
pub fn materialize(ast: &ParsedFormula, data: &Table, family: Family) -> Result<Lowered, Error> {
let n = data.n;
let y = match data.get(&ast.dependent) {
Some(Column::Numeric(v)) => v.clone(),
Some(Column::Factor { .. }) => {
return Err(Error::ResponseNotNumeric(ast.dependent.clone()))
}
None => return Err(Error::UnknownColumn(ast.dependent.clone())),
};
let mut col_names: Vec<String> = vec!["(Intercept)".to_string()];
let mut cols: Vec<Vec<f64>> = vec![vec![1.0; n]];
let mut numeric_main_col: HashMap<String, ColumnId> = HashMap::new();
let mut factor_main_cols: HashMap<String, Vec<(String, ColumnId)>> = HashMap::new();
for term in &ast.terms {
use super::parse::Term;
match term {
Term::Main { name } => match data.get(name) {
Some(Column::Numeric(v)) => {
numeric_main_col.insert(name.clone(), cols.len() as ColumnId);
col_names.push(name.clone());
cols.push(v.clone());
}
Some(Column::Factor { levels, codes }) => {
let mut dummies = Vec::new();
for (suffix, col) in factor_dummies(name, levels, codes) {
dummies.push((suffix.clone(), cols.len() as ColumnId));
col_names.push(suffix);
cols.push(col);
}
factor_main_cols.insert(name.clone(), dummies);
}
None => return Err(Error::UnknownColumn(name.clone())),
},
Term::Interaction { vars } => {
for (name, col) in interaction_columns(vars, data)? {
col_names.push(name);
cols.push(col);
}
}
}
}
let p = cols.len();
let mut x = vec![0.0; n * p];
for (j, col) in cols.iter().enumerate() {
for (i, &v) in col.iter().enumerate() {
x[i * p + j] = v;
}
}
let (model, ids, re_groups) =
lower_random_effects(ast, data, family, n, &numeric_main_col, &factor_main_cols)?;
let opts = FitOptions {
target_indices: (0..p as u32).collect(),
..FitOptions::default()
};
Ok(Lowered {
x,
y,
n,
p,
col_names,
model,
ids,
re_groups,
opts,
})
}
fn sorted_levels_and_codes(labels: &[String]) -> (Vec<String>, Vec<u32>) {
let levels: Vec<String> = labels
.iter()
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let index: HashMap<&str, u32> = levels
.iter()
.enumerate()
.map(|(i, l)| (l.as_str(), i as u32))
.collect();
let codes = labels.iter().map(|l| index[l.as_str()]).collect();
(levels, codes)
}
fn factor_dummies(var: &str, levels: &[String], codes: &[u32]) -> Vec<(String, Vec<f64>)> {
levels
.iter()
.enumerate()
.skip(1) .map(|(li, lvl)| {
let col: Vec<f64> = codes
.iter()
.map(|&c| if c as usize == li { 1.0 } else { 0.0 })
.collect();
(format!("{var}{lvl}"), col)
})
.collect()
}
fn expand_var(name: &str, data: &Table) -> Result<Vec<(String, Vec<f64>)>, Error> {
match data.get(name) {
Some(Column::Numeric(v)) => Ok(vec![(name.to_string(), v.clone())]),
Some(Column::Factor { levels, codes }) => Ok(factor_dummies(name, levels, codes)),
None => Err(Error::UnknownColumn(name.to_string())),
}
}
fn interaction_columns(vars: &[String], data: &Table) -> Result<Vec<(String, Vec<f64>)>, Error> {
let mut acc = expand_var(&vars[0], data)?;
for var in &vars[1..] {
let next = expand_var(var, data)?;
let mut out = Vec::with_capacity(acc.len() * next.len());
for (n2, c2) in &next {
for (n1, c1) in &acc {
let col: Vec<f64> = c1.iter().zip(c2).map(|(a, b)| a * b).collect();
out.push((format!("{n1}:{n2}"), col));
}
}
acc = out;
}
Ok(acc)
}
fn grouping_factor<'a>(name: &str, data: &'a Table) -> Result<(&'a [String], &'a [u32]), Error> {
match data.get(name) {
Some(Column::Factor { levels, codes }) => Ok((levels, codes)),
Some(Column::Numeric(_)) => Err(Error::WrongColumnKind {
name: name.to_string(),
expected: "a factor (grouping variable)",
}),
None => Err(Error::UnknownColumn(name.to_string())),
}
}
fn grouping_row_labels(name: &str, data: &Table) -> Result<Vec<String>, Error> {
let (levels, codes) = grouping_factor(name, data)?;
Ok(codes.iter().map(|&c| levels[c as usize].clone()).collect())
}
fn nested_padded_ids(parent_ids: &[u32], labels: &[String]) -> Vec<u32> {
let n_parents = parent_ids
.iter()
.copied()
.max()
.map(|m| m as usize + 1)
.unwrap_or(1);
let mut children_per_parent: Vec<BTreeSet<&str>> = vec![BTreeSet::new(); n_parents];
for (&p, c) in parent_ids.iter().zip(labels) {
children_per_parent[p as usize].insert(c.as_str());
}
let n_per_parent = children_per_parent
.iter()
.map(BTreeSet::len)
.max()
.unwrap_or(1)
.max(1);
let local_index: Vec<HashMap<&str, u32>> = children_per_parent
.iter()
.map(|set| {
set.iter()
.enumerate()
.map(|(i, &l)| (l, i as u32))
.collect()
})
.collect();
parent_ids
.iter()
.zip(labels)
.map(|(&p, c)| p * n_per_parent as u32 + local_index[p as usize][c.as_str()])
.collect()
}
const NESTING_INFLATION_BOUND: usize = 2;
fn detect_flat_nesting(primary_ids: &[u32], child_labels: &[String]) -> Option<Vec<u32>> {
let n_parents = primary_ids
.iter()
.copied()
.max()
.map(|m| m as usize + 1)
.unwrap_or(1);
let mut parent_of: HashMap<&str, u32> = HashMap::new();
let mut children_per_parent = vec![0usize; n_parents];
for (&p, c) in primary_ids.iter().zip(child_labels) {
match parent_of.insert(c.as_str(), p) {
Some(prev) if prev != p => return None, Some(_) => {} None => children_per_parent[p as usize] += 1, }
}
let w = children_per_parent.iter().copied().max().unwrap_or(0);
if n_parents * w > NESTING_INFLATION_BOUND * parent_of.len() {
return None; }
Some(nested_padded_ids(primary_ids, child_labels))
}
fn grouping_ids(re: &RandomEffect, data: &Table) -> Result<Vec<u32>, Error> {
match re {
RandomEffect::Intercept {
group,
parent: Some(parent),
} => {
let child = group.strip_prefix(&format!("{parent}:")).unwrap_or(group);
let (_, parent_ids) = grouping_factor(parent, data)?;
Ok(nested_padded_ids(
parent_ids,
&grouping_row_labels(child, data)?,
))
}
RandomEffect::Intercept {
group,
parent: None,
} if group.contains(':') => {
let (lhs, rhs) = group
.split_once(':')
.expect("group contains ':' per guard above");
let a = grouping_row_labels(lhs, data)?;
let b = grouping_row_labels(rhs, data)?;
let joined: Vec<String> = a.iter().zip(&b).map(|(x, y)| format!("{x}:{y}")).collect();
Ok(sorted_levels_and_codes(&joined).1)
}
RandomEffect::Intercept { group, .. } | RandomEffect::Slope { group, .. } => {
Ok(grouping_factor(group, data)?.1.to_vec())
}
}
}
fn slope_cols(
re: &RandomEffect,
numeric_main_col: &HashMap<String, ColumnId>,
factor_main_cols: &HashMap<String, Vec<(String, ColumnId)>>,
) -> Result<Vec<ColumnId>, Error> {
match re {
RandomEffect::Slope { vars, .. } => {
let mut out = Vec::new();
for v in vars {
if let Some(&cid) = numeric_main_col.get(v) {
out.push(cid);
} else if let Some(dummies) = factor_main_cols.get(v) {
out.extend(dummies.iter().map(|(_, cid)| *cid));
} else {
return Err(Error::SlopeVarNotInDesign(v.clone()));
}
}
Ok(out)
}
RandomEffect::Intercept { .. } => Ok(Vec::new()),
}
}
fn re_group_info(
re: &RandomEffect,
factor_main_cols: &HashMap<String, Vec<(String, ColumnId)>>,
) -> ReGroupInfo {
match re {
RandomEffect::Intercept { group, .. } => ReGroupInfo {
name: group.clone(),
terms: vec!["(Intercept)".to_string()],
},
RandomEffect::Slope { group, vars } => {
let mut terms = vec!["(Intercept)".to_string()];
for v in vars {
if let Some(dummies) = factor_main_cols.get(v) {
terms.extend(dummies.iter().map(|(name, _)| name.clone()));
} else {
terms.push(v.clone());
}
}
ReGroupInfo {
name: group.clone(),
terms,
}
}
}
}
fn lower_random_effects(
ast: &ParsedFormula,
data: &Table,
family: Family,
n: usize,
numeric_main_col: &HashMap<String, ColumnId>,
factor_main_cols: &HashMap<String, Vec<(String, ColumnId)>>,
) -> Result<(ModelSpec, GroupIds, Vec<ReGroupInfo>), Error> {
if ast.random_effects.is_empty() {
return Ok((
ModelSpec { family, re: None },
GroupIds::default(),
Vec::new(),
));
}
let re0 = &ast.random_effects[0];
let primary_slopes = slope_cols(re0, numeric_main_col, factor_main_cols)?;
let primary_ids = grouping_ids(re0, data)?;
debug_assert_eq!(primary_ids.len(), n);
let mut extra_groupings = Vec::new();
let mut extra_ids = Vec::new();
let mut re_groups = vec![re_group_info(re0, factor_main_cols)];
let mut have_nested = false;
for re in &ast.random_effects[1..] {
let slopes = slope_cols(re, numeric_main_col, factor_main_cols)?;
let (relation, ids) = match re {
RandomEffect::Intercept {
parent: Some(_), ..
} => (
GroupingRelation::NestedWithin { n_per_parent: 1 },
grouping_ids(re, data)?,
),
RandomEffect::Intercept {
group,
parent: None,
} if !group.contains(':') && !have_nested => {
match detect_flat_nesting(&primary_ids, &grouping_row_labels(group, data)?) {
Some(padded) => (GroupingRelation::NestedWithin { n_per_parent: 1 }, padded),
None => (
GroupingRelation::Crossed { n_clusters: 1 },
grouping_ids(re, data)?,
),
}
}
_ => (
GroupingRelation::Crossed { n_clusters: 1 },
grouping_ids(re, data)?,
),
};
have_nested |= matches!(relation, GroupingRelation::NestedWithin { .. });
extra_groupings.push(Grouping { relation, slopes });
extra_ids.push(ids);
re_groups.push(re_group_info(re, factor_main_cols));
}
let re = ReStructure {
sizing: Sizing::FixedClusters { n_clusters: 1 }, slopes: primary_slopes,
extra_groupings,
};
let ids = GroupIds {
primary: primary_ids,
extra: extra_ids,
};
Ok((
ModelSpec {
family,
re: Some(re),
},
ids,
re_groups,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formula::parse::RandomEffect;
#[test]
fn grouping_ids_nested_unbalanced_pads_to_max_per_parent() {
let table = Table {
columns: vec![
(
"g1".into(),
Column::factor_from_labels(&strs(&["A", "B", "B", "C", "C", "C"])),
),
(
"g2".into(),
Column::factor_from_labels(&strs(&["c1", "c1", "c2", "c1", "c2", "c3"])),
),
],
n: 6,
};
let re = RandomEffect::Intercept {
group: "g1:g2".to_string(),
parent: Some("g1".to_string()),
};
let ids = grouping_ids(&re, &table).unwrap();
assert_eq!(ids, vec![0, 3, 4, 6, 7, 8]);
}
fn strs(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn detect_flat_nesting_balanced_is_nested() {
let primary = vec![0, 0, 1, 1];
let child = strs(&["a", "b", "c", "d"]);
assert_eq!(
detect_flat_nesting(&primary, &child),
Some(vec![0, 1, 2, 3])
);
}
#[test]
fn detect_flat_nesting_shared_child_stays_crossed() {
let primary = vec![0, 1, 0, 1];
let child = strs(&["x", "x", "y", "y"]);
assert_eq!(detect_flat_nesting(&primary, &child), None);
}
#[test]
fn detect_flat_nesting_near_balanced_is_nested() {
let primary = vec![0, 0, 0, 1, 1, 2, 2, 2];
let child = strs(&["a", "b", "c", "d", "e", "f", "g", "h"]);
assert_eq!(
detect_flat_nesting(&primary, &child),
Some(vec![0, 1, 2, 3, 4, 6, 7, 8])
);
}
#[test]
fn detect_flat_nesting_high_inflation_stays_crossed() {
let primary = vec![0, 0, 0, 0, 0, 1, 2];
let child = strs(&["a", "b", "c", "d", "e", "f", "g"]);
assert_eq!(detect_flat_nesting(&primary, &child), None);
}
#[test]
fn second_flat_nesting_candidate_stays_crossed() {
let table = Table {
columns: vec![
(
"y".into(),
Column::Numeric(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
),
(
"g1".into(),
Column::factor_from_labels(&strs(&["A", "A", "A", "A", "B", "B", "B", "B"])),
),
(
"g2".into(),
Column::factor_from_labels(&strs(&[
"a1", "a1", "a2", "a2", "b1", "b1", "b2", "b2",
])),
),
(
"g3".into(),
Column::factor_from_labels(&strs(&[
"c1", "c1", "c2", "c2", "d1", "d1", "d2", "d2",
])),
),
],
n: 8,
};
let lo = super::lower("y ~ (1|g1) + (1|g2) + (1|g3)", &table, Family::Gaussian).unwrap();
let relations: Vec<_> = lo
.model
.re
.as_ref()
.unwrap()
.extra_groupings
.iter()
.map(|g| g.relation.clone())
.collect();
assert!(matches!(
relations[0],
GroupingRelation::NestedWithin { .. }
));
assert!(matches!(relations[1], GroupingRelation::Crossed { .. }));
}
}