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