use chrono::{DateTime, Utc};
use crate::error::{ForecastError, Result};
#[derive(Debug, Clone, PartialEq)]
pub struct PointForecasts {
timestamps: Vec<DateTime<Utc>>,
values: Vec<f64>,
model_name: Option<String>,
}
impl PointForecasts {
pub fn new(timestamps: Vec<DateTime<Utc>>, values: Vec<f64>) -> Result<Self> {
if timestamps.len() != values.len() {
return Err(ForecastError::DimensionMismatch {
expected: timestamps.len(),
got: values.len(),
});
}
Ok(Self {
timestamps,
values,
model_name: None,
})
}
pub fn from_values(values: Vec<f64>) -> Self {
Self {
timestamps: Vec::new(),
values,
model_name: None,
}
}
pub fn empty() -> Self {
Self {
timestamps: Vec::new(),
values: Vec::new(),
model_name: None,
}
}
pub fn with_model_name(mut self, name: impl Into<String>) -> Self {
self.model_name = Some(name.into());
self
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn timestamps(&self) -> &[DateTime<Utc>] {
&self.timestamps
}
pub fn values(&self) -> &[f64] {
&self.values
}
pub fn values_mut(&mut self) -> &mut Vec<f64> {
&mut self.values
}
pub fn model_name(&self) -> Option<&str> {
self.model_name.as_deref()
}
pub fn has_timestamps(&self) -> bool {
!self.timestamps.is_empty()
}
pub fn get(&self, index: usize) -> Option<f64> {
self.values.get(index).copied()
}
pub fn get_timestamp(&self, index: usize) -> Option<&DateTime<Utc>> {
self.timestamps.get(index)
}
pub fn iter(&self) -> impl Iterator<Item = (&DateTime<Utc>, f64)> {
self.timestamps.iter().zip(self.values.iter().copied())
}
pub fn iter_values(&self) -> impl Iterator<Item = f64> + '_ {
self.values.iter().copied()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct QuantileForecasts {
timestamps: Vec<DateTime<Utc>>,
quantiles: Vec<f64>,
values: Vec<Vec<f64>>,
}
impl QuantileForecasts {
pub fn new(
timestamps: Vec<DateTime<Utc>>,
quantiles: Vec<f64>,
values: Vec<Vec<f64>>,
) -> Result<Self> {
if !timestamps.is_empty() && timestamps.len() != values.len() {
return Err(ForecastError::DimensionMismatch {
expected: timestamps.len(),
got: values.len(),
});
}
for &q in &quantiles {
if q <= 0.0 || q >= 1.0 {
return Err(ForecastError::InvalidParameter(format!(
"quantile must be in (0, 1), got {}",
q
)));
}
}
for window in quantiles.windows(2) {
if window[0] >= window[1] {
return Err(ForecastError::InvalidParameter(
"quantiles must be sorted in ascending order".to_string(),
));
}
}
for (i, row) in values.iter().enumerate() {
if row.len() != quantiles.len() {
return Err(ForecastError::DimensionMismatch {
expected: quantiles.len(),
got: row.len(),
});
}
for window in row.windows(2) {
if window[0] > window[1] {
return Err(ForecastError::InvalidParameter(format!(
"quantile values must be non-decreasing at time index {}",
i
)));
}
}
}
Ok(Self {
timestamps,
quantiles,
values,
})
}
pub fn from_values(quantiles: Vec<f64>, values: Vec<Vec<f64>>) -> Result<Self> {
Self::new(Vec::new(), quantiles, values)
}
pub fn empty(quantiles: Vec<f64>) -> Result<Self> {
for &q in &quantiles {
if q <= 0.0 || q >= 1.0 {
return Err(ForecastError::InvalidParameter(format!(
"quantile must be in (0, 1), got {}",
q
)));
}
}
for window in quantiles.windows(2) {
if window[0] >= window[1] {
return Err(ForecastError::InvalidParameter(
"quantiles must be sorted in ascending order".to_string(),
));
}
}
Ok(Self {
timestamps: Vec::new(),
quantiles,
values: Vec::new(),
})
}
pub fn n_times(&self) -> usize {
self.values.len()
}
pub fn n_quantiles(&self) -> usize {
self.quantiles.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn timestamps(&self) -> &[DateTime<Utc>] {
&self.timestamps
}
pub fn quantiles(&self) -> &[f64] {
&self.quantiles
}
pub fn values(&self) -> &[Vec<f64>] {
&self.values
}
pub fn has_timestamps(&self) -> bool {
!self.timestamps.is_empty()
}
pub fn at_time(&self, time_idx: usize) -> Option<&[f64]> {
self.values.get(time_idx).map(|v| v.as_slice())
}
pub fn at_quantile(&self, quantile_idx: usize) -> Option<Vec<f64>> {
if quantile_idx >= self.quantiles.len() {
return None;
}
Some(self.values.iter().map(|row| row[quantile_idx]).collect())
}
pub fn median(&self) -> Option<Vec<f64>> {
self.quantiles
.iter()
.position(|&q| (q - 0.5).abs() < 1e-10)
.and_then(|idx| self.at_quantile(idx))
}
pub fn get(&self, time_idx: usize, quantile_idx: usize) -> Option<f64> {
self.values
.get(time_idx)
.and_then(|row| row.get(quantile_idx))
.copied()
}
pub fn to_prediction_intervals(&self, coverage: f64) -> Option<PredictionIntervals> {
if coverage <= 0.0 || coverage >= 1.0 {
return None;
}
let alpha = 1.0 - coverage;
let lower_target = alpha / 2.0;
let upper_target = 1.0 - alpha / 2.0;
let lower_idx = self
.quantiles
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
((*a - lower_target).abs())
.partial_cmp(&((*b - lower_target).abs()))
.unwrap()
})?
.0;
let upper_idx = self
.quantiles
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
((*a - upper_target).abs())
.partial_cmp(&((*b - upper_target).abs()))
.unwrap()
})?
.0;
let lower = self.at_quantile(lower_idx)?;
let upper = self.at_quantile(upper_idx)?;
Some(PredictionIntervals {
timestamps: self.timestamps.clone(),
lower,
upper,
coverage,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PredictionIntervals {
timestamps: Vec<DateTime<Utc>>,
lower: Vec<f64>,
upper: Vec<f64>,
coverage: f64,
}
impl PredictionIntervals {
pub fn new(
timestamps: Vec<DateTime<Utc>>,
lower: Vec<f64>,
upper: Vec<f64>,
coverage: f64,
) -> Result<Self> {
if lower.len() != upper.len() {
return Err(ForecastError::DimensionMismatch {
expected: lower.len(),
got: upper.len(),
});
}
if !timestamps.is_empty() && timestamps.len() != lower.len() {
return Err(ForecastError::DimensionMismatch {
expected: timestamps.len(),
got: lower.len(),
});
}
if coverage <= 0.0 || coverage >= 1.0 {
return Err(ForecastError::InvalidParameter(format!(
"coverage must be in (0, 1), got {}",
coverage
)));
}
for (i, (&l, &u)) in lower.iter().zip(upper.iter()).enumerate() {
if l > u {
return Err(ForecastError::InvalidParameter(format!(
"lower bound {} exceeds upper bound {} at index {}",
l, u, i
)));
}
}
Ok(Self {
timestamps,
lower,
upper,
coverage,
})
}
pub fn from_bounds(lower: Vec<f64>, upper: Vec<f64>, coverage: f64) -> Result<Self> {
Self::new(Vec::new(), lower, upper, coverage)
}
pub fn len(&self) -> usize {
self.lower.len()
}
pub fn is_empty(&self) -> bool {
self.lower.is_empty()
}
pub fn timestamps(&self) -> &[DateTime<Utc>] {
&self.timestamps
}
pub fn lower(&self) -> &[f64] {
&self.lower
}
pub fn upper(&self) -> &[f64] {
&self.upper
}
pub fn coverage(&self) -> f64 {
self.coverage
}
pub fn has_timestamps(&self) -> bool {
!self.timestamps.is_empty()
}
pub fn get(&self, index: usize) -> Option<(f64, f64)> {
match (self.lower.get(index), self.upper.get(index)) {
(Some(&l), Some(&u)) => Some((l, u)),
_ => None,
}
}
pub fn widths(&self) -> Vec<f64> {
self.lower
.iter()
.zip(self.upper.iter())
.map(|(l, u)| u - l)
.collect()
}
pub fn midpoints(&self) -> Vec<f64> {
self.lower
.iter()
.zip(self.upper.iter())
.map(|(l, u)| (l + u) / 2.0)
.collect()
}
pub fn contains(&self, values: &[f64]) -> Vec<bool> {
self.lower
.iter()
.zip(self.upper.iter())
.zip(values.iter())
.map(|((&l, &u), &v)| v >= l && v <= u)
.collect()
}
pub fn empirical_coverage(&self, actuals: &[f64]) -> Option<f64> {
if actuals.len() != self.len() {
return None;
}
let contained = self.contains(actuals);
let count = contained.iter().filter(|&&x| x).count();
Some(count as f64 / actuals.len() as f64)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn make_timestamps(n: usize) -> Vec<DateTime<Utc>> {
(0..n)
.map(|i| {
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()
+ chrono::Duration::days(i as i64)
})
.collect()
}
mod point_forecasts {
use super::*;
#[test]
fn new_creates_valid_forecasts() {
let timestamps = make_timestamps(3);
let values = vec![1.0, 2.0, 3.0];
let forecasts = PointForecasts::new(timestamps.clone(), values.clone()).unwrap();
assert_eq!(forecasts.len(), 3);
assert!(!forecasts.is_empty());
assert!(forecasts.has_timestamps());
assert_eq!(forecasts.values(), &values);
assert_eq!(forecasts.timestamps(), ×tamps);
}
#[test]
fn new_fails_on_length_mismatch() {
let timestamps = make_timestamps(3);
let values = vec![1.0, 2.0];
let result = PointForecasts::new(timestamps, values);
assert!(result.is_err());
match result {
Err(ForecastError::DimensionMismatch { expected, got }) => {
assert_eq!(expected, 3);
assert_eq!(got, 2);
}
_ => panic!("Expected DimensionMismatch error"),
}
}
#[test]
fn from_values_creates_without_timestamps() {
let forecasts = PointForecasts::from_values(vec![1.0, 2.0, 3.0]);
assert_eq!(forecasts.len(), 3);
assert!(!forecasts.has_timestamps());
assert!(forecasts.timestamps().is_empty());
}
#[test]
fn empty_creates_empty_instance() {
let forecasts = PointForecasts::empty();
assert!(forecasts.is_empty());
assert_eq!(forecasts.len(), 0);
assert!(!forecasts.has_timestamps());
}
#[test]
fn with_model_name_sets_name() {
let forecasts = PointForecasts::from_values(vec![1.0]).with_model_name("ARIMA");
assert_eq!(forecasts.model_name(), Some("ARIMA"));
}
#[test]
fn model_name_is_none_by_default() {
let forecasts = PointForecasts::from_values(vec![1.0]);
assert!(forecasts.model_name().is_none());
}
#[test]
fn get_returns_value_at_index() {
let forecasts = PointForecasts::from_values(vec![10.0, 20.0, 30.0]);
assert_eq!(forecasts.get(0), Some(10.0));
assert_eq!(forecasts.get(1), Some(20.0));
assert_eq!(forecasts.get(2), Some(30.0));
assert_eq!(forecasts.get(3), None);
}
#[test]
fn get_timestamp_returns_timestamp_at_index() {
let timestamps = make_timestamps(3);
let forecasts = PointForecasts::new(timestamps.clone(), vec![1.0, 2.0, 3.0]).unwrap();
assert_eq!(forecasts.get_timestamp(0), Some(×tamps[0]));
assert_eq!(forecasts.get_timestamp(2), Some(×tamps[2]));
assert_eq!(forecasts.get_timestamp(3), None);
}
#[test]
fn iter_yields_timestamp_value_pairs() {
let timestamps = make_timestamps(3);
let values = vec![1.0, 2.0, 3.0];
let forecasts = PointForecasts::new(timestamps.clone(), values.clone()).unwrap();
let pairs: Vec<_> = forecasts.iter().collect();
assert_eq!(pairs.len(), 3);
assert_eq!(pairs[0], (×tamps[0], 1.0));
assert_eq!(pairs[1], (×tamps[1], 2.0));
assert_eq!(pairs[2], (×tamps[2], 3.0));
}
#[test]
fn iter_values_yields_only_values() {
let forecasts = PointForecasts::from_values(vec![1.0, 2.0, 3.0]);
let values: Vec<_> = forecasts.iter_values().collect();
assert_eq!(values, vec![1.0, 2.0, 3.0]);
}
#[test]
fn values_mut_allows_modification() {
let mut forecasts = PointForecasts::from_values(vec![1.0, 2.0, 3.0]);
forecasts.values_mut()[1] = 20.0;
assert_eq!(forecasts.values(), &[1.0, 20.0, 3.0]);
}
#[test]
fn clone_creates_independent_copy() {
let forecasts = PointForecasts::from_values(vec![1.0, 2.0]).with_model_name("Test");
let cloned = forecasts.clone();
assert_eq!(forecasts, cloned);
}
}
mod quantile_forecasts {
use super::*;
#[test]
fn new_creates_valid_forecasts() {
let timestamps = make_timestamps(2);
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![vec![8.0, 10.0, 12.0], vec![9.0, 12.0, 15.0]];
let forecasts =
QuantileForecasts::new(timestamps.clone(), quantiles.clone(), values.clone())
.unwrap();
assert_eq!(forecasts.n_times(), 2);
assert_eq!(forecasts.n_quantiles(), 3);
assert!(!forecasts.is_empty());
assert!(forecasts.has_timestamps());
}
#[test]
fn new_fails_on_timestamp_mismatch() {
let timestamps = make_timestamps(3); let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![
vec![8.0, 10.0, 12.0],
vec![9.0, 12.0, 15.0], ];
let result = QuantileForecasts::new(timestamps, quantiles, values);
assert!(result.is_err());
}
#[test]
fn new_fails_on_quantile_row_mismatch() {
let timestamps = make_timestamps(2);
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![
vec![8.0, 10.0, 12.0],
vec![9.0, 12.0], ];
let result = QuantileForecasts::new(timestamps, quantiles, values);
assert!(result.is_err());
}
#[test]
fn new_fails_on_invalid_quantile_below_zero() {
let quantiles = vec![-0.1, 0.5, 0.9];
let values = vec![vec![8.0, 10.0, 12.0]];
let result = QuantileForecasts::from_values(quantiles, values);
assert!(result.is_err());
}
#[test]
fn new_fails_on_invalid_quantile_above_one() {
let quantiles = vec![0.1, 0.5, 1.0];
let values = vec![vec![8.0, 10.0, 12.0]];
let result = QuantileForecasts::from_values(quantiles, values);
assert!(result.is_err());
}
#[test]
fn new_fails_on_unsorted_quantiles() {
let quantiles = vec![0.9, 0.5, 0.1]; let values = vec![vec![12.0, 10.0, 8.0]];
let result = QuantileForecasts::from_values(quantiles, values);
assert!(result.is_err());
}
#[test]
fn new_fails_on_non_monotonic_values() {
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![vec![10.0, 8.0, 12.0]];
let result = QuantileForecasts::from_values(quantiles, values);
assert!(result.is_err());
}
#[test]
fn from_values_creates_without_timestamps() {
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![vec![8.0, 10.0, 12.0]];
let forecasts = QuantileForecasts::from_values(quantiles, values).unwrap();
assert!(!forecasts.has_timestamps());
assert_eq!(forecasts.n_times(), 1);
}
#[test]
fn empty_creates_empty_instance() {
let forecasts = QuantileForecasts::empty(vec![0.1, 0.5, 0.9]).unwrap();
assert!(forecasts.is_empty());
assert_eq!(forecasts.n_times(), 0);
assert_eq!(forecasts.n_quantiles(), 3);
}
#[test]
fn at_time_returns_row() {
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![vec![8.0, 10.0, 12.0], vec![9.0, 12.0, 15.0]];
let forecasts = QuantileForecasts::from_values(quantiles, values).unwrap();
assert_eq!(forecasts.at_time(0), Some(&[8.0, 10.0, 12.0][..]));
assert_eq!(forecasts.at_time(1), Some(&[9.0, 12.0, 15.0][..]));
assert_eq!(forecasts.at_time(2), None);
}
#[test]
fn at_quantile_returns_column() {
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![vec![8.0, 10.0, 12.0], vec![9.0, 12.0, 15.0]];
let forecasts = QuantileForecasts::from_values(quantiles, values).unwrap();
assert_eq!(forecasts.at_quantile(0), Some(vec![8.0, 9.0]));
assert_eq!(forecasts.at_quantile(1), Some(vec![10.0, 12.0]));
assert_eq!(forecasts.at_quantile(2), Some(vec![12.0, 15.0]));
assert_eq!(forecasts.at_quantile(3), None);
}
#[test]
fn median_returns_50th_quantile() {
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![vec![8.0, 10.0, 12.0], vec![9.0, 12.0, 15.0]];
let forecasts = QuantileForecasts::from_values(quantiles, values).unwrap();
assert_eq!(forecasts.median(), Some(vec![10.0, 12.0]));
}
#[test]
fn median_returns_none_when_not_present() {
let quantiles = vec![0.1, 0.9];
let values = vec![vec![8.0, 12.0]];
let forecasts = QuantileForecasts::from_values(quantiles, values).unwrap();
assert_eq!(forecasts.median(), None);
}
#[test]
fn get_returns_specific_value() {
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![vec![8.0, 10.0, 12.0], vec![9.0, 12.0, 15.0]];
let forecasts = QuantileForecasts::from_values(quantiles, values).unwrap();
assert_eq!(forecasts.get(0, 0), Some(8.0));
assert_eq!(forecasts.get(1, 2), Some(15.0));
assert_eq!(forecasts.get(2, 0), None);
assert_eq!(forecasts.get(0, 3), None);
}
#[test]
fn to_prediction_intervals_extracts_bounds() {
let quantiles = vec![0.05, 0.1, 0.5, 0.9, 0.95];
let values = vec![
vec![5.0, 8.0, 10.0, 12.0, 15.0],
vec![6.0, 9.0, 12.0, 15.0, 18.0],
];
let forecasts = QuantileForecasts::from_values(quantiles, values).unwrap();
let intervals = forecasts.to_prediction_intervals(0.90).unwrap();
assert_eq!(intervals.coverage(), 0.90);
assert_eq!(intervals.len(), 2);
assert_eq!(intervals.lower(), &[5.0, 6.0]);
assert_eq!(intervals.upper(), &[15.0, 18.0]);
}
#[test]
fn to_prediction_intervals_returns_none_for_invalid_coverage() {
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![vec![8.0, 10.0, 12.0]];
let forecasts = QuantileForecasts::from_values(quantiles, values).unwrap();
assert!(forecasts.to_prediction_intervals(0.0).is_none());
assert!(forecasts.to_prediction_intervals(1.0).is_none());
assert!(forecasts.to_prediction_intervals(-0.1).is_none());
}
#[test]
fn allows_equal_quantile_values() {
let quantiles = vec![0.1, 0.5, 0.9];
let values = vec![vec![10.0, 10.0, 10.0]];
let result = QuantileForecasts::from_values(quantiles, values);
assert!(result.is_ok());
}
}
mod prediction_intervals {
use super::*;
#[test]
fn new_creates_valid_intervals() {
let timestamps = make_timestamps(3);
let lower = vec![8.0, 9.0, 10.0];
let upper = vec![12.0, 13.0, 14.0];
let intervals =
PredictionIntervals::new(timestamps.clone(), lower.clone(), upper.clone(), 0.90)
.unwrap();
assert_eq!(intervals.len(), 3);
assert!(!intervals.is_empty());
assert!(intervals.has_timestamps());
assert_eq!(intervals.coverage(), 0.90);
assert_eq!(intervals.lower(), &lower);
assert_eq!(intervals.upper(), &upper);
}
#[test]
fn new_fails_on_length_mismatch() {
let lower = vec![8.0, 9.0];
let upper = vec![12.0, 13.0, 14.0];
let result = PredictionIntervals::from_bounds(lower, upper, 0.90);
assert!(result.is_err());
}
#[test]
fn new_fails_on_timestamp_mismatch() {
let timestamps = make_timestamps(2);
let lower = vec![8.0, 9.0, 10.0];
let upper = vec![12.0, 13.0, 14.0];
let result = PredictionIntervals::new(timestamps, lower, upper, 0.90);
assert!(result.is_err());
}
#[test]
fn new_fails_on_invalid_coverage() {
let lower = vec![8.0];
let upper = vec![12.0];
assert!(PredictionIntervals::from_bounds(lower.clone(), upper.clone(), 0.0).is_err());
assert!(PredictionIntervals::from_bounds(lower.clone(), upper.clone(), 1.0).is_err());
assert!(PredictionIntervals::from_bounds(lower.clone(), upper.clone(), -0.5).is_err());
assert!(PredictionIntervals::from_bounds(lower.clone(), upper.clone(), 1.5).is_err());
}
#[test]
fn new_fails_when_lower_exceeds_upper() {
let lower = vec![8.0, 15.0, 10.0]; let upper = vec![12.0, 13.0, 14.0];
let result = PredictionIntervals::from_bounds(lower, upper, 0.90);
assert!(result.is_err());
}
#[test]
fn from_bounds_creates_without_timestamps() {
let intervals = PredictionIntervals::from_bounds(vec![8.0], vec![12.0], 0.90).unwrap();
assert!(!intervals.has_timestamps());
}
#[test]
fn get_returns_interval_at_index() {
let intervals = PredictionIntervals::from_bounds(
vec![8.0, 9.0, 10.0],
vec![12.0, 13.0, 14.0],
0.90,
)
.unwrap();
assert_eq!(intervals.get(0), Some((8.0, 12.0)));
assert_eq!(intervals.get(1), Some((9.0, 13.0)));
assert_eq!(intervals.get(2), Some((10.0, 14.0)));
assert_eq!(intervals.get(3), None);
}
#[test]
fn widths_calculates_interval_widths() {
let intervals = PredictionIntervals::from_bounds(
vec![8.0, 9.0, 10.0],
vec![12.0, 15.0, 11.0],
0.90,
)
.unwrap();
assert_eq!(intervals.widths(), vec![4.0, 6.0, 1.0]);
}
#[test]
fn midpoints_calculates_interval_centers() {
let intervals =
PredictionIntervals::from_bounds(vec![8.0, 10.0], vec![12.0, 20.0], 0.90).unwrap();
assert_eq!(intervals.midpoints(), vec![10.0, 15.0]);
}
#[test]
fn contains_checks_value_in_interval() {
let intervals = PredictionIntervals::from_bounds(
vec![8.0, 9.0, 10.0],
vec![12.0, 13.0, 14.0],
0.90,
)
.unwrap();
let values = vec![10.0, 15.0, 10.0]; assert_eq!(intervals.contains(&values), vec![true, false, true]);
}
#[test]
fn contains_includes_boundary_values() {
let intervals = PredictionIntervals::from_bounds(vec![8.0], vec![12.0], 0.90).unwrap();
assert_eq!(intervals.contains(&[8.0]), vec![true]); assert_eq!(intervals.contains(&[12.0]), vec![true]); }
#[test]
fn empirical_coverage_calculates_correctly() {
let intervals = PredictionIntervals::from_bounds(
vec![8.0, 9.0, 10.0, 11.0],
vec![12.0, 13.0, 14.0, 15.0],
0.90,
)
.unwrap();
let actuals = vec![10.0, 15.0, 12.0, 13.0];
assert_eq!(intervals.empirical_coverage(&actuals), Some(0.75));
}
#[test]
fn empirical_coverage_returns_none_on_length_mismatch() {
let intervals =
PredictionIntervals::from_bounds(vec![8.0, 9.0], vec![12.0, 13.0], 0.90).unwrap();
assert_eq!(intervals.empirical_coverage(&[10.0]), None);
}
#[test]
fn empirical_coverage_handles_all_contained() {
let intervals =
PredictionIntervals::from_bounds(vec![0.0, 0.0], vec![100.0, 100.0], 0.90).unwrap();
assert_eq!(intervals.empirical_coverage(&[50.0, 50.0]), Some(1.0));
}
#[test]
fn empirical_coverage_handles_none_contained() {
let intervals =
PredictionIntervals::from_bounds(vec![0.0, 0.0], vec![10.0, 10.0], 0.90).unwrap();
assert_eq!(intervals.empirical_coverage(&[50.0, 50.0]), Some(0.0));
}
#[test]
fn allows_equal_lower_and_upper() {
let intervals = PredictionIntervals::from_bounds(vec![10.0], vec![10.0], 0.90).unwrap();
assert_eq!(intervals.widths(), vec![0.0]);
assert_eq!(intervals.contains(&[10.0]), vec![true]);
}
}
}