use std::collections::BTreeMap;
use super::structure_analysis::{
SmoothStructureAnalysis, analyze_smooth_ownership, smooth_term_feature_cols,
};
use super::{SmoothBasisSpec, SmoothTermSpec, TermCollectionSpec};
pub(crate) fn spatial_basis_warning_family_and_cols(
term: &SmoothTermSpec,
) -> Option<(&'static str, &[usize])> {
spatial_basis_warning_family_and_cols_basis(&term.basis)
}
pub(crate) fn spatial_basis_warning_family_and_cols_basis(
basis: &SmoothBasisSpec,
) -> Option<(&'static str, &[usize])> {
match basis {
SmoothBasisSpec::ByVariable { inner, .. }
| SmoothBasisSpec::FactorSumToZero { inner, .. } => {
spatial_basis_warning_family_and_cols_basis(inner)
}
SmoothBasisSpec::BySmooth { smooth, .. } => {
spatial_basis_warning_family_and_cols_basis(smooth)
}
SmoothBasisSpec::ThinPlate { feature_cols, .. } => Some(("thinplate/tps", feature_cols)),
SmoothBasisSpec::Sphere { feature_cols, .. } => Some(("sphere/sos", feature_cols)),
SmoothBasisSpec::ConstantCurvature { feature_cols, .. } => {
Some(("constant_curvature", feature_cols))
}
SmoothBasisSpec::Matern { feature_cols, .. } => Some(("matern", feature_cols)),
SmoothBasisSpec::MeasureJet { feature_cols, .. } => Some(("measurejet", feature_cols)),
SmoothBasisSpec::Duchon { feature_cols, .. } => Some(("duchon", feature_cols)),
SmoothBasisSpec::BSpline1D { .. }
| SmoothBasisSpec::Pca { .. }
| SmoothBasisSpec::TensorBSpline { .. }
| SmoothBasisSpec::FactorSmooth { .. } => None,
}
}
pub(crate) fn collect_spatial_smooth_usage_warnings(
spec: &TermCollectionSpec,
headers: &[String],
label: &str,
) -> Vec<String> {
let mut grouped: BTreeMap<&'static str, Vec<String>> = BTreeMap::new();
for term in &spec.smooth_terms {
let Some((family, feature_cols)) = spatial_basis_warning_family_and_cols(term) else {
continue;
};
if feature_cols.len() != 1 {
continue;
}
let col = feature_cols[0];
let feature_name = headers
.get(col)
.cloned()
.unwrap_or_else(|| format!("#{col}"));
grouped.entry(family).or_default().push(feature_name);
}
grouped
.into_iter()
.filter_map(|(family, cols)| {
if cols.len() < 2 {
return None;
}
let example = match family {
"thinplate/tps" => format!("thinplate({})", cols.join(", ")),
"matern" => format!("matern({})", cols.join(", ")),
"duchon" => format!("duchon({})", cols.join(", ")),
"sphere/sos" => format!("sphere({})", cols.join(", ")),
_ => return None,
};
let bad_example = match family {
"thinplate/tps" => cols
.iter()
.map(|col| format!("s({col}, type=tps)"))
.collect::<Vec<_>>()
.join(" + "),
"matern" => cols
.iter()
.map(|col| format!("s({col}, type=matern)"))
.collect::<Vec<_>>()
.join(" + "),
"duchon" => cols
.iter()
.map(|col| format!("s({col}, type=duchon)"))
.collect::<Vec<_>>()
.join(" + "),
"sphere/sos" => cols
.iter()
.map(|col| format!("s({col}, type=sphere)"))
.collect::<Vec<_>>()
.join(" + "),
_ => return None,
};
Some(format!(
"{label}: detected {} separate 1D {family} spatial smooths over [{}]. These build unrelated additive 1D smooths, not one shared spatial manifold. TIP: if you intended one spatial surface, replace `{bad_example}` with one multivariate term such as `{example}`.",
cols.len(),
cols.join(", "),
))
})
.collect()
}
pub(crate) fn collect_linear_smooth_overlap_warnings(
spec: &TermCollectionSpec,
headers: &[String],
label: &str,
) -> Vec<String> {
let linear_by_col = spec
.linear_terms
.iter()
.map(|term| (term.feature_col, term.name.as_str()))
.collect::<BTreeMap<_, _>>();
let mut warnings = Vec::new();
for smooth in &spec.smooth_terms {
let overlaps = smooth_term_feature_cols(smooth)
.into_iter()
.filter_map(|col| {
linear_by_col.get(&col).map(|linear_name| {
let feature_name = headers
.get(col)
.cloned()
.unwrap_or_else(|| format!("#{col}"));
(feature_name, (*linear_name).to_string())
})
})
.collect::<Vec<_>>();
if overlaps.is_empty() {
continue;
}
let overlap_features = overlaps
.iter()
.map(|(feature_name, _)| feature_name.as_str())
.collect::<Vec<_>>()
.join(", ");
let linear_terms = overlaps
.iter()
.map(|(_, linear_name)| format!("linear({linear_name})"))
.collect::<Vec<_>>()
.join(" + ");
warnings.push(format!(
"{label}: feature(s) [{overlap_features}] appear both in smooth term `{}` and explicit linear term(s) `{linear_terms}`. The fit now residualizes the smooth against the intercept and those overlapping linear columns, so the smooth contributes only the nonlinear remainder on those variables. This changes the term decomposition and interpretation.",
smooth.name
));
}
warnings
}
pub(crate) fn collect_hierarchical_smooth_overlap_warnings(
spec: &TermCollectionSpec,
headers: &[String],
label: &str,
) -> Vec<String> {
let feature_label = |col: usize| {
headers
.get(col)
.cloned()
.unwrap_or_else(|| format!("#{col}"))
};
let join_feature_labels = |cols: &[usize]| {
cols.iter()
.map(|&col| feature_label(col))
.collect::<Vec<_>>()
.join(", ")
};
let SmoothStructureAnalysis {
ownership_order,
term_feature_cols,
term_owners,
..
} = analyze_smooth_ownership(&spec.smooth_terms);
let mut warnings = Vec::new();
for &target_idx in &ownership_order {
let owners = &term_owners[target_idx];
if owners.is_empty() {
continue;
}
let target = &spec.smooth_terms[target_idx];
let target_features = join_feature_labels(&term_feature_cols[target_idx]);
let owner_descriptions = owners
.iter()
.map(|&owner_idx| {
format!(
"`{}` over [{}]",
spec.smooth_terms[owner_idx].name,
join_feature_labels(&term_feature_cols[owner_idx]),
)
})
.collect::<Vec<_>>()
.join(", ");
warnings.push(format!(
"{label}: smooth term `{}` over [{target_features}] overlaps nested or duplicate smooth term(s) {}. The fit uses automatic hierarchical ownership: those higher-priority smooth term(s) keep any shared realized subspace, and `{}` is residualized against that overlap before fitting.",
target.name, owner_descriptions, target.name,
));
}
warnings
}
pub fn collect_smooth_structure_warnings(
spec: &TermCollectionSpec,
headers: &[String],
label: &str,
) -> Vec<String> {
let mut warnings = collect_spatial_smooth_usage_warnings(spec, headers, label);
warnings.extend(collect_linear_smooth_overlap_warnings(spec, headers, label));
warnings.extend(collect_hierarchical_smooth_overlap_warnings(
spec, headers, label,
));
warnings
}