use super::*;
#[derive(Clone, Debug)]
pub struct ParametricTermSummary {
pub name: String,
pub estimate: f64,
pub std_error: Option<f64>,
pub zvalue: Option<f64>,
pub pvalue: Option<f64>,
}
#[derive(Clone, Debug)]
pub struct SmoothTermSummary {
pub name: String,
pub edf: f64,
pub ref_df: f64,
pub chi_sq: Option<f64>,
pub pvalue: Option<f64>,
pub continuous_order: Option<ContinuousSmoothnessOrder>,
pub basis_note: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ContinuousSmoothnessOrderStatus {
Ok,
NonMaternRegime,
FirstOrderLimit,
IntrinsicLimit,
UndefinedZeroLambda,
}
#[derive(Clone, Debug)]
pub struct ContinuousSmoothnessOrder {
pub lambda0: f64,
pub lambda1: f64,
pub lambda2: f64,
pub r_ratio: Option<f64>,
pub nu: Option<f64>,
pub kappa2: Option<f64>,
pub status: ContinuousSmoothnessOrderStatus,
}
#[derive(Clone, Debug)]
pub struct ModelSummary {
pub family: String,
pub deviance_explained: Option<f64>,
pub reml_score: Option<f64>,
pub parametric_terms: Vec<ParametricTermSummary>,
pub smooth_terms: Vec<SmoothTermSummary>,
}
fn unscale_to_physical_lambdas(
lambda_tilde: [f64; 3],
normalization_scale: [f64; 3],
) -> Option<[f64; 3]> {
let mut out = [f64::NAN; 3];
for k in 0..3 {
let c = normalization_scale[k];
if !(c.is_finite() && c > 0.0) {
return None;
}
out[k] = lambda_tilde[k] / c;
}
Some(out)
}
pub fn compute_continuous_smoothness_order(
lambda_tilde: [f64; 3],
normalization_scale: [f64; 3],
eps: f64,
) -> ContinuousSmoothnessOrder {
let Some(lambda) = unscale_to_physical_lambdas(lambda_tilde, normalization_scale) else {
return ContinuousSmoothnessOrder {
lambda0: f64::NAN,
lambda1: f64::NAN,
lambda2: f64::NAN,
r_ratio: None,
nu: None,
kappa2: None,
status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
};
};
let [lambda0, lambda1, lambda2] = lambda;
if !lambda0.is_finite() || !lambda1.is_finite() || !lambda2.is_finite() {
return ContinuousSmoothnessOrder {
lambda0,
lambda1,
lambda2,
r_ratio: None,
nu: None,
kappa2: None,
status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
};
}
let lambda_scale = lambda0.abs().max(lambda1.abs()).max(lambda2.abs()).max(1.0);
let lambda_floor = eps * lambda_scale;
if lambda0 <= lambda_floor {
if lambda1 > lambda_floor && lambda2 > lambda_floor {
return ContinuousSmoothnessOrder {
lambda0,
lambda1,
lambda2,
r_ratio: None,
nu: Some(1.0),
kappa2: Some(0.0),
status: ContinuousSmoothnessOrderStatus::IntrinsicLimit,
};
}
return ContinuousSmoothnessOrder {
lambda0,
lambda1,
lambda2,
r_ratio: None,
nu: None,
kappa2: None,
status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
};
}
if lambda2 <= lambda_floor {
if lambda1 > lambda_floor && lambda1.is_finite() {
return ContinuousSmoothnessOrder {
lambda0,
lambda1,
lambda2,
r_ratio: None,
nu: Some(1.0),
kappa2: Some(lambda0 / lambda1),
status: ContinuousSmoothnessOrderStatus::FirstOrderLimit,
};
}
return ContinuousSmoothnessOrder {
lambda0,
lambda1,
lambda2,
r_ratio: None,
nu: None,
kappa2: None,
status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
};
}
let r_ratio = (lambda1 * lambda1) / (lambda0 * lambda2);
if !r_ratio.is_finite() {
return ContinuousSmoothnessOrder {
lambda0,
lambda1,
lambda2,
r_ratio: None,
nu: None,
kappa2: None,
status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
};
}
let discriminant = lambda1 * lambda1 - 4.0 * lambda0 * lambda2;
let disc_tol = eps * lambda_scale * lambda_scale;
let status = if discriminant < -disc_tol {
ContinuousSmoothnessOrderStatus::NonMaternRegime
} else {
ContinuousSmoothnessOrderStatus::Ok
};
if r_ratio <= 2.0 + eps {
return ContinuousSmoothnessOrder {
lambda0,
lambda1,
lambda2,
r_ratio: Some(r_ratio),
nu: None,
kappa2: None,
status,
};
}
let nu = r_ratio / (r_ratio - 2.0);
let kappa2 = lambda1 / ((r_ratio - 2.0) * lambda2);
if !nu.is_finite() || !kappa2.is_finite() {
return ContinuousSmoothnessOrder {
lambda0,
lambda1,
lambda2,
r_ratio: Some(r_ratio),
nu: None,
kappa2: None,
status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
};
}
ContinuousSmoothnessOrder {
lambda0,
lambda1,
lambda2,
r_ratio: Some(r_ratio),
nu: Some(nu),
kappa2: Some(kappa2),
status,
}
}
fn significance_stars(p: Option<f64>) -> &'static str {
match p {
Some(v) if v.is_finite() && v < 0.001 => "***",
Some(v) if v.is_finite() && v < 0.01 => "**",
Some(v) if v.is_finite() && v < 0.05 => "*",
Some(v) if v.is_finite() && v < 0.1 => ".",
_ => "",
}
}
fn format_pvalue(p: Option<f64>) -> String {
let Some(v) = p else {
return "NA".to_string();
};
if !v.is_finite() {
return "NA".to_string();
}
if v < 2e-16 {
"< 2e-16".to_string()
} else if v < 1e-4 {
format!("{v:.2e}")
} else {
format!("{v:.4}")
}
}
impl fmt::Display for ModelSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let paramnamew = self
.parametric_terms
.iter()
.map(|t| t.name.len())
.max()
.unwrap_or(10)
.max("Term".len());
let smoothnamew = self
.smooth_terms
.iter()
.map(|t| t.name.len())
.max()
.unwrap_or(10)
.max("Term".len());
writeln!(f, "Family: {}", self.family)?;
let dev_txt = self
.deviance_explained
.map(|d| format!("{:.1}%", (100.0 * d).clamp(-9999.0, 9999.0)))
.unwrap_or_else(|| "NA".to_string());
let reml_txt = self
.reml_score
.map(|v| format!("{v:.4}"))
.unwrap_or_else(|| "NA".to_string());
writeln!(f, "Deviance Explained: {dev_txt} | REML Score: {reml_txt}")?;
writeln!(f)?;
writeln!(f, "Parametric Terms:")?;
writeln!(f, "{:-<1$}", "", paramnamew + 59)?;
writeln!(
f,
"{:<namew$} {:>10} {:>12} {:>10} {:>19}",
"Term",
"Estimate",
"Standard Error",
"Z Statistic",
"Two-Sided P-Value",
namew = paramnamew
)?;
writeln!(f, "{:-<1$}", "", paramnamew + 59)?;
for term in &self.parametric_terms {
let estimate = format!("{:.4}", term.estimate);
let se = term
.std_error
.filter(|v| v.is_finite())
.map(|v| format!("{v:.4}"))
.unwrap_or_else(|| "NA".to_string());
let z = term
.zvalue
.filter(|v| v.is_finite())
.map(|v| format!("{v:.2}"))
.unwrap_or_else(|| "NA".to_string());
let p = format_pvalue(term.pvalue);
let stars = significance_stars(term.pvalue);
writeln!(
f,
"{:<namew$} {:>10} {:>12} {:>10} {:>19} {}",
term.name,
estimate,
se,
z,
p,
stars,
namew = paramnamew
)?;
}
writeln!(f)?;
writeln!(f, "Smooth Terms:")?;
writeln!(f, "{:-<1$}", "", smoothnamew + 86)?;
writeln!(
f,
"{:<namew$} {:>26} {:>30} {:>12} {:>10}",
"Term",
"Effective Degrees of Freedom",
"Reference Degrees of Freedom",
"Chi-Square",
"P-Value",
namew = smoothnamew
)?;
writeln!(f, "{:-<1$}", "", smoothnamew + 86)?;
for term in &self.smooth_terms {
let chisq = term
.chi_sq
.filter(|v| v.is_finite())
.map(|v| format!("{v:.3}"))
.unwrap_or_else(|| "NA".to_string());
let p = format_pvalue(term.pvalue);
let stars = significance_stars(term.pvalue);
writeln!(
f,
"{:<namew$} {:>26.2} {:>30.2} {:>12} {:>10} {}",
term.name,
term.edf,
term.ref_df,
chisq,
p,
stars,
namew = smoothnamew
)?;
}
writeln!(f)?;
let order_terms = self
.smooth_terms
.iter()
.filter_map(|t| t.continuous_order.as_ref().map(|o| (&t.name, o)))
.collect::<Vec<_>>();
if !order_terms.is_empty() {
writeln!(f, "Continuous Smoothness Order:")?;
writeln!(
f,
"{:<namew$} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>20}",
"Term",
"lambda0",
"lambda1",
"lambda2",
"R",
"nu",
"kappa^2",
"status",
namew = smoothnamew
)?;
for (name, o) in order_terms {
let r_txt = o
.r_ratio
.filter(|v| v.is_finite())
.map(|v| format!("{v:.4}"))
.unwrap_or_else(|| "NA".to_string());
let nu_txt =
o.nu.filter(|v| v.is_finite())
.map(|v| format!("{v:.4}"))
.unwrap_or_else(|| "NA".to_string());
let kappa_txt = o
.kappa2
.filter(|v| v.is_finite())
.map(|v| format!("{v:.4}"))
.unwrap_or_else(|| "NA".to_string());
let status_txt = match o.status {
ContinuousSmoothnessOrderStatus::Ok => "Ok",
ContinuousSmoothnessOrderStatus::NonMaternRegime => "NonMaternRegime",
ContinuousSmoothnessOrderStatus::FirstOrderLimit => "FirstOrderLimit",
ContinuousSmoothnessOrderStatus::IntrinsicLimit => "IntrinsicLimit",
ContinuousSmoothnessOrderStatus::UndefinedZeroLambda => "UndefinedZeroLambda",
};
writeln!(
f,
"{:<namew$} {:>10.3e} {:>10.3e} {:>10.3e} {:>10} {:>10} {:>10} {:>20}",
name,
o.lambda0,
o.lambda1,
o.lambda2,
r_txt,
nu_txt,
kappa_txt,
status_txt,
namew = smoothnamew
)?;
}
writeln!(f)?;
}
write!(
f,
"Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1"
)?;
Ok(())
}
}