Source code for aurora.models.gam.result

# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Lucy Eduardo Arias

"""Result class for fitted GAM models."""

from __future__ import annotations

from typing import Any

import numpy as np


[docs] class GAMResult: """Result of fitting a Generalized Additive Model. Parameters ---------- coefficients : ndarray Spline coefficients. fitted_values : ndarray Fitted values at training data points. residuals : ndarray Residuals (y - fitted_values). lambda_ : float Smoothing parameter used. edf : float Effective degrees of freedom. basis : object Basis object (BSplineBasis or CubicSplineBasis). x : ndarray Training predictor values. y : ndarray Training response values. weights : ndarray, optional Observation weights. gcv_score : float, optional GCV score if lambda was selected automatically. Attributes ---------- All parameters are stored as attributes. n_obs_ : int Number of observations. lambda_opt : float Alias for ``lambda_`` (optimal smoothing parameter). r_squared : float Coefficient of determination (R-squared), computed from the residuals as ``1 - rss / tss``; can be negative. >>> # Predict at new points >>> y_pred = result.predict(np.linspace(0, 2 * np.pi, 2)) >>> print(result.summary()) >>> # Using dict with named variables >>> data = { ... 'response': y, ... 'temp': X[:, 0], ... 'humidity': X[:, 1] ... } >>> result = fit_gam_formula("response ~ s(temp) + humidity", data) >>> print(result.summary()) See Also -------- AdditiveGAMResult : Additive GAM with multiple smooth terms. fit_gam_formula : R-style formula interface. fit_glm : GLM fitting function. References ---------- Wood, S.N. (2017). Generalized Additive Models: An Introduction with R. CRC Press. """ def __init__( self, coefficients: np.ndarray, fitted_values: np.ndarray, residuals: np.ndarray, lambda_: float, edf: float, basis: Any, x: np.ndarray, y: np.ndarray, weights: np.ndarray | None = None, gcv_score: float | None = None, ): self.coefficients = coefficients self.fitted_values = fitted_values self.residuals = residuals self.lambda_ = lambda_ self.edf = edf self.basis = basis self.x = x self.y = y self.weights = weights self.gcv_score = gcv_score self.n_obs_ = len(x) @property def r_squared(self) -> float: """Coefficient of determination (R²). Returns ------- r_squared : float R² value, proportion of variance explained by the model. """ rss = np.sum(self.residuals**2) if self.weights is not None: rss = np.sum(self.weights * self.residuals**2) tss = np.sum((self.y - np.mean(self.y)) ** 2) return float(1 - rss / tss) @property def lambda_opt(self) -> float: """Alias for lambda_ (optimal smoothing parameter). Returns ------- lambda_opt : float The smoothing parameter used in the fit. """ return self.lambda_
[docs] def predict(self, x_new: np.ndarray) -> np.ndarray: """Predict at new x values. Parameters ---------- x_new : ndarray, shape (m,) New predictor values. Returns ------- y_pred : ndarray, shape (m,) Predicted values. """ x_new_arr = np.asarray(x_new, dtype=np.float64) if x_new_arr.ndim == 0: x_new_arr = x_new_arr.reshape(1) # Evaluate basis at new points X_new = self.basis.basis_matrix(x_new_arr) # Compute predictions y_pred = X_new @ self.coefficients return np.asarray(y_pred)
[docs] def summary(self) -> str: """Generate summary string of fit. Returns ------- summary : str Formatted summary of model fit. """ lines = [] lines.append("=" * 60) lines.append("Generalized Additive Model (GAM) - Fitted Summary") lines.append("=" * 60) lines.append("") # Model information lines.append("Model Information:") lines.append(f" Basis type: {type(self.basis).__name__}") lines.append(f" Number of basis: {len(self.coefficients)}") lines.append(f" Observations: {self.n_obs_}") if self.weights is not None: lines.append(" Weighted fit: Yes") lines.append("") # Smoothing information lines.append("Smoothing:") lines.append(f" Lambda: {self.lambda_:.6e}") lines.append(f" Effective DoF: {self.edf:.2f}") if self.gcv_score is not None: lines.append(f" GCV score: {self.gcv_score:.6e}") lines.append("") # Fit statistics rss = np.sum(self.residuals**2) if self.weights is not None: rss = np.sum(self.weights * self.residuals**2) tss = np.sum((self.y - np.mean(self.y)) ** 2) r_squared = 1 - rss / tss lines.append("Fit Statistics:") lines.append(f" Residual sum sq: {rss:.4f}") lines.append(f" R-squared: {r_squared:.4f}") lines.append(f" Residual std: {np.std(self.residuals):.4f}") lines.append("") # Residual diagnostics lines.append("Residuals:") lines.append(f" Min: {np.min(self.residuals):.4f}") lines.append(f" Q1: {np.quantile(self.residuals, 0.25):.4f}") lines.append(f" Median: {np.median(self.residuals):.4f}") lines.append(f" Q3: {np.quantile(self.residuals, 0.75):.4f}") lines.append(f" Max: {np.max(self.residuals):.4f}") lines.append("") lines.append("=" * 60) return "\n".join(lines)
[docs] def __repr__(self) -> str: """Return string representation.""" return ( f"GAMResult(n_obs={self.n_obs_}, n_basis={len(self.coefficients)}, " f"edf={self.edf:.2f}, lambda={self.lambda_:.4e})" )
__all__ = ["GAMResult"]