1use super::*;
2
3#[derive(Clone, Debug)]
4pub struct ParametricTermSummary {
5 pub name: String,
6 pub estimate: f64,
7 pub std_error: Option<f64>,
8 pub zvalue: Option<f64>,
9 pub pvalue: Option<f64>,
10}
11
12#[derive(Clone, Debug)]
13pub struct SmoothTermSummary {
14 pub name: String,
15 pub edf: f64,
16 pub ref_df: f64,
17 pub chi_sq: Option<f64>,
18 pub pvalue: Option<f64>,
19 pub continuous_order: Option<ContinuousSmoothnessOrder>,
20 pub basis_note: Option<String>,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum ContinuousSmoothnessOrderStatus {
29 Ok,
30 NonMaternRegime,
31 FirstOrderLimit,
32 IntrinsicLimit,
33 UndefinedZeroLambda,
34}
35
36#[derive(Clone, Debug)]
37pub struct ContinuousSmoothnessOrder {
38 pub lambda0: f64,
39 pub lambda1: f64,
40 pub lambda2: f64,
41 pub r_ratio: Option<f64>,
42 pub nu: Option<f64>,
43 pub kappa2: Option<f64>,
44 pub status: ContinuousSmoothnessOrderStatus,
45}
46
47#[derive(Clone, Debug)]
48pub struct ModelSummary {
49 pub family: String,
50 pub deviance_explained: Option<f64>,
51 pub reml_score: Option<f64>,
52 pub parametric_terms: Vec<ParametricTermSummary>,
53 pub smooth_terms: Vec<SmoothTermSummary>,
54 pub coefficient_se_source: Option<crate::model_types::CoefficientCovarianceDefinition>,
59}
60
61fn unscale_to_physical_lambdas(
79 lambda_tilde: [f64; 3],
80 normalization_scale: [f64; 3],
81) -> Option<[f64; 3]> {
82 let mut out = [f64::NAN; 3];
83 for k in 0..3 {
84 let c = normalization_scale[k];
85 if !(c.is_finite() && c > 0.0) {
86 return None;
87 }
88 out[k] = lambda_tilde[k] / c;
89 }
90 Some(out)
91}
92
93pub fn compute_continuous_smoothness_order(
167 lambda_tilde: [f64; 3],
168 normalization_scale: [f64; 3],
169 eps: f64,
170) -> ContinuousSmoothnessOrder {
171 let Some(lambda) = unscale_to_physical_lambdas(lambda_tilde, normalization_scale) else {
172 return ContinuousSmoothnessOrder {
173 lambda0: f64::NAN,
174 lambda1: f64::NAN,
175 lambda2: f64::NAN,
176 r_ratio: None,
177 nu: None,
178 kappa2: None,
179 status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
180 };
181 };
182 let [lambda0, lambda1, lambda2] = lambda;
183 if !lambda0.is_finite() || !lambda1.is_finite() || !lambda2.is_finite() {
184 return ContinuousSmoothnessOrder {
185 lambda0,
186 lambda1,
187 lambda2,
188 r_ratio: None,
189 nu: None,
190 kappa2: None,
191 status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
192 };
193 }
194 let lambda_scale = lambda0.abs().max(lambda1.abs()).max(lambda2.abs()).max(1.0);
199 let lambda_floor = eps * lambda_scale;
200
201 if lambda0 <= lambda_floor {
203 if lambda1 > lambda_floor && lambda2 > lambda_floor {
204 return ContinuousSmoothnessOrder {
205 lambda0,
206 lambda1,
207 lambda2,
208 r_ratio: None,
209 nu: Some(1.0),
210 kappa2: Some(0.0),
211 status: ContinuousSmoothnessOrderStatus::IntrinsicLimit,
212 };
213 }
214 return ContinuousSmoothnessOrder {
215 lambda0,
216 lambda1,
217 lambda2,
218 r_ratio: None,
219 nu: None,
220 kappa2: None,
221 status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
222 };
223 }
224 if lambda2 <= lambda_floor {
227 if lambda1 > lambda_floor && lambda1.is_finite() {
228 return ContinuousSmoothnessOrder {
229 lambda0,
230 lambda1,
231 lambda2,
232 r_ratio: None,
233 nu: Some(1.0),
234 kappa2: Some(lambda0 / lambda1),
235 status: ContinuousSmoothnessOrderStatus::FirstOrderLimit,
236 };
237 }
238 return ContinuousSmoothnessOrder {
239 lambda0,
240 lambda1,
241 lambda2,
242 r_ratio: None,
243 nu: None,
244 kappa2: None,
245 status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
246 };
247 }
248
249 let r_ratio = (lambda1 * lambda1) / (lambda0 * lambda2);
250 if !r_ratio.is_finite() {
251 return ContinuousSmoothnessOrder {
252 lambda0,
253 lambda1,
254 lambda2,
255 r_ratio: None,
256 nu: None,
257 kappa2: None,
258 status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
259 };
260 }
261
262 let discriminant = lambda1 * lambda1 - 4.0 * lambda0 * lambda2;
271 let disc_tol = eps * lambda_scale * lambda_scale;
272 let status = if discriminant < -disc_tol {
273 ContinuousSmoothnessOrderStatus::NonMaternRegime
274 } else {
275 ContinuousSmoothnessOrderStatus::Ok
278 };
279 if r_ratio <= 2.0 + eps {
280 return ContinuousSmoothnessOrder {
281 lambda0,
282 lambda1,
283 lambda2,
284 r_ratio: Some(r_ratio),
285 nu: None,
286 kappa2: None,
287 status,
288 };
289 }
290 let nu = r_ratio / (r_ratio - 2.0);
291 let kappa2 = lambda1 / ((r_ratio - 2.0) * lambda2);
302 if !nu.is_finite() || !kappa2.is_finite() {
303 return ContinuousSmoothnessOrder {
304 lambda0,
305 lambda1,
306 lambda2,
307 r_ratio: Some(r_ratio),
308 nu: None,
309 kappa2: None,
310 status: ContinuousSmoothnessOrderStatus::UndefinedZeroLambda,
311 };
312 }
313
314 ContinuousSmoothnessOrder {
315 lambda0,
316 lambda1,
317 lambda2,
318 r_ratio: Some(r_ratio),
319 nu: Some(nu),
320 kappa2: Some(kappa2),
321 status,
322 }
323}
324
325fn significance_stars(p: Option<f64>) -> &'static str {
326 match p {
327 Some(v) if v.is_finite() && v < 0.001 => "***",
328 Some(v) if v.is_finite() && v < 0.01 => "**",
329 Some(v) if v.is_finite() && v < 0.05 => "*",
330 Some(v) if v.is_finite() && v < 0.1 => ".",
331 _ => "",
332 }
333}
334
335fn format_pvalue(p: Option<f64>) -> String {
336 let Some(v) = p else {
337 return "NA".to_string();
338 };
339 if !v.is_finite() {
340 return "NA".to_string();
341 }
342 if v < 2e-16 {
343 "< 2e-16".to_string()
344 } else if v < 1e-4 {
345 format!("{v:.2e}")
346 } else {
347 format!("{v:.4}")
348 }
349}
350
351impl fmt::Display for ModelSummary {
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 let paramnamew = self
354 .parametric_terms
355 .iter()
356 .map(|t| t.name.len())
357 .max()
358 .unwrap_or(10)
359 .max("Term".len());
360 let smoothnamew = self
361 .smooth_terms
362 .iter()
363 .map(|t| t.name.len())
364 .max()
365 .unwrap_or(10)
366 .max("Term".len());
367
368 writeln!(f, "Family: {}", self.family)?;
369 let dev_txt = self
370 .deviance_explained
371 .map(|d| format!("{:.1}%", (100.0 * d).clamp(-9999.0, 9999.0)))
372 .unwrap_or_else(|| "NA".to_string());
373 let reml_txt = self
374 .reml_score
375 .map(|v| format!("{v:.4}"))
376 .unwrap_or_else(|| "NA".to_string());
377 writeln!(f, "Deviance Explained: {dev_txt} | REML Score: {reml_txt}")?;
378 if let Some(source) = self.coefficient_se_source {
379 writeln!(f, "Coefficient SE Covariance: {source}")?;
380 }
381 writeln!(f)?;
382
383 writeln!(f, "Parametric Terms:")?;
384 writeln!(f, "{:-<1$}", "", paramnamew + 59)?;
385 writeln!(
386 f,
387 "{:<namew$} {:>10} {:>12} {:>10} {:>19}",
388 "Term",
389 "Estimate",
390 "Standard Error",
391 "Z Statistic",
392 "Two-Sided P-Value",
393 namew = paramnamew
394 )?;
395 writeln!(f, "{:-<1$}", "", paramnamew + 59)?;
396 for term in &self.parametric_terms {
397 let estimate = format!("{:.4}", term.estimate);
398 let se = term
399 .std_error
400 .filter(|v| v.is_finite())
401 .map(|v| format!("{v:.4}"))
402 .unwrap_or_else(|| "NA".to_string());
403 let z = term
404 .zvalue
405 .filter(|v| v.is_finite())
406 .map(|v| format!("{v:.2}"))
407 .unwrap_or_else(|| "NA".to_string());
408 let p = format_pvalue(term.pvalue);
409 let stars = significance_stars(term.pvalue);
410 writeln!(
411 f,
412 "{:<namew$} {:>10} {:>12} {:>10} {:>19} {}",
413 term.name,
414 estimate,
415 se,
416 z,
417 p,
418 stars,
419 namew = paramnamew
420 )?;
421 }
422 writeln!(f)?;
423
424 writeln!(f, "Smooth Terms:")?;
425 writeln!(f, "{:-<1$}", "", smoothnamew + 86)?;
426 writeln!(
427 f,
428 "{:<namew$} {:>26} {:>30} {:>12} {:>10}",
429 "Term",
430 "Effective Degrees of Freedom",
431 "Reference Degrees of Freedom",
432 "Chi-Square",
433 "P-Value",
434 namew = smoothnamew
435 )?;
436 writeln!(f, "{:-<1$}", "", smoothnamew + 86)?;
437 for term in &self.smooth_terms {
438 let chisq = term
439 .chi_sq
440 .filter(|v| v.is_finite())
441 .map(|v| format!("{v:.3}"))
442 .unwrap_or_else(|| "NA".to_string());
443 let p = format_pvalue(term.pvalue);
444 let stars = significance_stars(term.pvalue);
445 writeln!(
446 f,
447 "{:<namew$} {:>26.2} {:>30.2} {:>12} {:>10} {}",
448 term.name,
449 term.edf,
450 term.ref_df,
451 chisq,
452 p,
453 stars,
454 namew = smoothnamew
455 )?;
456 }
457 writeln!(f)?;
458 let order_terms = self
459 .smooth_terms
460 .iter()
461 .filter_map(|t| t.continuous_order.as_ref().map(|o| (&t.name, o)))
462 .collect::<Vec<_>>();
463 if !order_terms.is_empty() {
464 writeln!(f, "Continuous Smoothness Order:")?;
465 writeln!(
466 f,
467 "{:<namew$} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>20}",
468 "Term",
469 "lambda0",
470 "lambda1",
471 "lambda2",
472 "R",
473 "nu",
474 "kappa^2",
475 "status",
476 namew = smoothnamew
477 )?;
478 for (name, o) in order_terms {
479 let r_txt = o
480 .r_ratio
481 .filter(|v| v.is_finite())
482 .map(|v| format!("{v:.4}"))
483 .unwrap_or_else(|| "NA".to_string());
484 let nu_txt =
485 o.nu.filter(|v| v.is_finite())
486 .map(|v| format!("{v:.4}"))
487 .unwrap_or_else(|| "NA".to_string());
488 let kappa_txt = o
489 .kappa2
490 .filter(|v| v.is_finite())
491 .map(|v| format!("{v:.4}"))
492 .unwrap_or_else(|| "NA".to_string());
493 let status_txt = match o.status {
494 ContinuousSmoothnessOrderStatus::Ok => "Ok",
495 ContinuousSmoothnessOrderStatus::NonMaternRegime => "NonMaternRegime",
496 ContinuousSmoothnessOrderStatus::FirstOrderLimit => "FirstOrderLimit",
497 ContinuousSmoothnessOrderStatus::IntrinsicLimit => "IntrinsicLimit",
498 ContinuousSmoothnessOrderStatus::UndefinedZeroLambda => "UndefinedZeroLambda",
499 };
500 writeln!(
501 f,
502 "{:<namew$} {:>10.3e} {:>10.3e} {:>10.3e} {:>10} {:>10} {:>10} {:>20}",
503 name,
504 o.lambda0,
505 o.lambda1,
506 o.lambda2,
507 r_txt,
508 nu_txt,
509 kappa_txt,
510 status_txt,
511 namew = smoothnamew
512 )?;
513 }
514 writeln!(f)?;
515 }
516 write!(
517 f,
518 "Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1"
519 )?;
520 Ok(())
521 }
522}