Source code for fortuna.conformal.regression.quantile

from typing import Optional

import jax.numpy as jnp

from fortuna.conformal.regression.base import ConformalRegressor
from fortuna.typing import Array


[docs]class QuantileConformalRegressor(ConformalRegressor):
[docs] def score( self, val_lower_bounds: Array, val_upper_bounds: Array, val_targets: Array, ) -> jnp.ndarray: """ Compute score function. Parameters ---------- val_lower_bounds: Array Interval lower bounds computed on a validation set. val_upper_bounds: Array Interval upper bounds computed on a validation set. val_targets: Array A two-dimensional array of validation target variables. Returns ------- jnp.ndarray The conformal scores. """ if val_lower_bounds.shape != val_upper_bounds.shape: raise ValueError( """The shapes of `val_lower_bounds` and `val_upper_bounds` must be the same, but shapes {} and {} were given, respectively.""".format( val_lower_bounds.shape, val_upper_bounds.shape ) ) if val_lower_bounds.ndim != 1: raise ValueError( "`val_lower_bounds` and `val_upper_bounds` must be one-dimensional arrays." ) if val_targets.shape[1] != 1: raise ValueError( """The second dimension of `val_targets` must have only one component.""" ) val_targets = val_targets.squeeze(1) return jnp.maximum( val_lower_bounds - val_targets, val_targets - val_upper_bounds )
[docs] def quantile( self, val_lower_bounds: Array, val_upper_bounds: Array, val_targets: Array, error: float, scores: Optional[Array] = None, ) -> Array: """ Compute a quantile of the scores. Parameters ---------- val_lower_bounds: Array Interval lower bounds computed on a validation set. val_upper_bounds: Array Interval upper bounds computed on a validation set. val_targets: Array A two-dimensional array of validation target variables. error: float Coverage error. This must be a scalar between 0 and 1, extremes included. This should correspond to the coverage error for which `val_lower_bounds`, `val_upper_bounds`, `test_lower_bounds` and `test_upper_bounds` were computed. scores: Optional[float] Conformal scores. This should be the output of :meth:`~fortuna.conformal.regression.quantile.QuantileConformalRegressor.score`. Returns ------- float The conformal quantile. """ if error < 0 or error > 1: raise ValueError("""`error` must be a scalar between 0 and 1.""") if scores is None: scores = self.score(val_lower_bounds, val_upper_bounds, val_targets) n = scores.shape[0] return jnp.quantile(scores, jnp.ceil((n + 1) * (1 - error)) / n)
[docs] def conformal_interval( self, val_lower_bounds: Array, val_upper_bounds: Array, test_lower_bounds: Array, test_upper_bounds: Array, val_targets: Array, error: float, quantile: Optional[float] = None, ) -> jnp.ndarray: """ Coverage interval of each of the test inputs, at the desired coverage error. This is supported only for scalar target variables. Parameters ---------- val_lower_bounds: Array Interval lower bounds computed on a validation set. val_upper_bounds: Array Interval upper bounds computed on a validation set. test_lower_bounds: Array Interval lower bounds computed on a test set. test_upper_bounds: Array Interval upper bounds computed on a test set. val_targets: Array A two-dimensional array of validation target variables. error: float Coverage error. This must be a scalar between 0 and 1, extremes included. This should correspond to the coverage error for which `val_lower_bounds`, `val_upper_bounds`, `test_lower_bounds` and `test_upper_bounds` were computed. quantile: Optional[float] Conformal quantiles. This should be the output of :meth:`~fortuna.conformal.regression.quantile.QuantileConformalRegressor.quantile`. Returns ------- jnp.ndarray The conformal intervals. The two components of the second axis correspond to the left and right interval bounds. """ if val_lower_bounds.shape != val_upper_bounds.shape: raise ValueError( f"""The shapes of `val_lower_bounds` and `val_upper_bounds` must be the same, but shapes {val_lower_bounds.shape} and {val_upper_bounds.shape} were found, respectively.""" ) if test_lower_bounds.shape != test_upper_bounds.shape: raise ValueError( f"""The shapes of `test_lower_bounds` and `test_upper_bounds` must be the same, but shapes {test_lower_bounds.shape} and {test_upper_bounds.shape} were found, respectively.""" ) if val_lower_bounds.ndim not in [1, 2]: raise ValueError( "`val_lower_bounds` and `val_upper_bounds` must be one- or two-dimensional arrays. If " "two-dimensional, the second dimension must be 1." ) if test_lower_bounds.ndim not in [1, 2]: raise ValueError( "`test_lower_bounds` and `test_upper_bounds` must be one- or two-dimensional arrays. If " "two-dimensional, the second dimension must be 1." ) if val_lower_bounds.ndim == 2: if val_lower_bounds.shape[1] != 1: raise ValueError( f"The second dimension of `val_lower_bounds` must have only one component, but" f"{val_lower_bounds.shape[1]} components were found." ) else: val_lower_bounds = val_lower_bounds.squeeze(1) val_upper_bounds = val_upper_bounds.squeeze(1) if test_lower_bounds.ndim == 2: if test_lower_bounds.shape[1] != 1: raise ValueError( f"The second dimension of `test_lower_bounds` must have only one component, but" f"{test_lower_bounds.shape[1]} components were found." ) else: test_lower_bounds = test_lower_bounds.squeeze(1) test_upper_bounds = test_upper_bounds.squeeze(1) if quantile is None: quantile = self.quantile( val_lower_bounds, val_upper_bounds, val_targets, error ) lows = test_lower_bounds - quantile highs = test_upper_bounds + quantile return jnp.array(list(zip(lows, highs)))