#[derive(Debug, Clone)]
pub struct LjungBoxResult {
pub statistic: f64,
pub p_value: f64,
pub lags: usize,
pub df: usize,
}
impl LjungBoxResult {
pub fn is_white_noise(&self, alpha: f64) -> bool {
self.p_value > alpha
}
}
pub fn ljung_box(residuals: &[f64], lags: Option<usize>, fitted_params: usize) -> LjungBoxResult {
let n = residuals.len();
if n < 3 {
return LjungBoxResult {
statistic: f64::NAN,
p_value: f64::NAN,
lags: 0,
df: 0,
};
}
let lags = lags.unwrap_or_else(|| 10.min(n / 5).max(1));
let lags = lags.min(n - 1);
let mean: f64 = residuals.iter().sum::<f64>() / n as f64;
let centered: Vec<f64> = residuals.iter().map(|&x| x - mean).collect();
let var: f64 = centered.iter().map(|&x| x * x).sum::<f64>();
if var == 0.0 {
return LjungBoxResult {
statistic: 0.0,
p_value: 1.0,
lags,
df: lags.saturating_sub(fitted_params),
};
}
let mut q = 0.0;
for k in 1..=lags {
let acf_k: f64 = centered
.iter()
.skip(k)
.zip(centered.iter())
.map(|(&a, &b)| a * b)
.sum::<f64>()
/ var;
q += (acf_k * acf_k) / (n - k) as f64;
}
q *= n as f64 * (n + 2) as f64;
let df = lags.saturating_sub(fitted_params);
let df = df.max(1);
let p_value = chi_squared_sf(q, df);
LjungBoxResult {
statistic: q,
p_value,
lags,
df,
}
}
#[derive(Debug, Clone)]
pub struct DurbinWatsonResult {
pub statistic: f64,
pub interpretation: AutocorrelationType,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AutocorrelationType {
PositiveStrong,
PositiveWeak,
None,
NegativeWeak,
NegativeStrong,
}
pub fn durbin_watson(residuals: &[f64]) -> DurbinWatsonResult {
let n = residuals.len();
if n < 2 {
return DurbinWatsonResult {
statistic: f64::NAN,
interpretation: AutocorrelationType::None,
};
}
let sum_diff_sq: f64 = residuals.windows(2).map(|w| (w[1] - w[0]).powi(2)).sum();
let sum_sq: f64 = residuals.iter().map(|&r| r * r).sum();
if sum_sq == 0.0 {
return DurbinWatsonResult {
statistic: 2.0,
interpretation: AutocorrelationType::None,
};
}
let dw = sum_diff_sq / sum_sq;
let interpretation = if dw < 0.5 {
AutocorrelationType::PositiveStrong
} else if dw < 1.5 {
AutocorrelationType::PositiveWeak
} else if dw <= 2.5 {
AutocorrelationType::None
} else if dw < 3.5 {
AutocorrelationType::NegativeWeak
} else {
AutocorrelationType::NegativeStrong
};
DurbinWatsonResult {
statistic: dw,
interpretation,
}
}
pub fn box_pierce(residuals: &[f64], lags: Option<usize>) -> LjungBoxResult {
let n = residuals.len();
if n < 3 {
return LjungBoxResult {
statistic: f64::NAN,
p_value: f64::NAN,
lags: 0,
df: 0,
};
}
let lags = lags.unwrap_or_else(|| 10.min(n / 5).max(1));
let lags = lags.min(n - 1);
let mean: f64 = residuals.iter().sum::<f64>() / n as f64;
let centered: Vec<f64> = residuals.iter().map(|&x| x - mean).collect();
let var: f64 = centered.iter().map(|&x| x * x).sum::<f64>();
if var == 0.0 {
return LjungBoxResult {
statistic: 0.0,
p_value: 1.0,
lags,
df: lags,
};
}
let mut q = 0.0;
for k in 1..=lags {
let acf_k: f64 = centered
.iter()
.skip(k)
.zip(centered.iter())
.map(|(&a, &b)| a * b)
.sum::<f64>()
/ var;
q += acf_k * acf_k;
}
q *= n as f64;
let p_value = chi_squared_sf(q, lags);
LjungBoxResult {
statistic: q,
p_value,
lags,
df: lags,
}
}
fn chi_squared_sf(x: f64, df: usize) -> f64 {
if x <= 0.0 || df == 0 {
return 1.0;
}
let k = df as f64;
if df > 30 {
let z = ((x / k).powf(1.0 / 3.0) - (1.0 - 2.0 / (9.0 * k))) / (2.0 / (9.0 * k)).sqrt();
return normal_sf(z);
}
incomplete_gamma_q(k / 2.0, x / 2.0)
}
fn incomplete_gamma_q(a: f64, x: f64) -> f64 {
if x < 0.0 || a <= 0.0 {
return 1.0;
}
if x == 0.0 {
return 1.0;
}
if x < a + 1.0 {
1.0 - gamma_series_p(a, x)
} else {
gamma_cf_q(a, x)
}
}
fn gamma_series_p(a: f64, x: f64) -> f64 {
if x == 0.0 {
return 0.0;
}
let mut sum = 1.0 / a;
let mut term = sum;
for n in 1..200 {
term *= x / (a + n as f64);
sum += term;
if term.abs() < sum.abs() * 1e-15 {
break;
}
}
sum * (-x + a * x.ln() - ln_gamma(a)).exp()
}
fn gamma_cf_q(a: f64, x: f64) -> f64 {
let mut b = x + 1.0 - a;
let mut c = 1.0 / 1e-30;
let mut d = 1.0 / b;
let mut h = d;
for i in 1..200 {
let an = -(i as f64) * (i as f64 - a);
b += 2.0;
d = an * d + b;
if d.abs() < 1e-30 {
d = 1e-30;
}
c = b + an / c;
if c.abs() < 1e-30 {
c = 1e-30;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < 1e-15 {
break;
}
}
(-x + a * x.ln() - ln_gamma(a)).exp() * h
}
fn ln_gamma(x: f64) -> f64 {
if x <= 0.0 {
return f64::INFINITY;
}
let coefficients = [
76.18009172947146,
-86.50532032941677,
24.01409824083091,
-1.231739572450155,
0.1208650973866179e-2,
-0.5395239384953e-5,
];
let y = x;
let mut tmp = x + 5.5;
tmp -= (x + 0.5) * tmp.ln();
let mut ser = 1.000000000190015;
for (j, &coef) in coefficients.iter().enumerate() {
ser += coef / (y + 1.0 + j as f64);
}
-tmp + (2.5066282746310005 * ser / x).ln()
}
fn normal_sf(x: f64) -> f64 {
0.5 * erfc(x / std::f64::consts::SQRT_2)
}
fn erfc(x: f64) -> f64 {
let t = 1.0 / (1.0 + 0.5 * x.abs());
let tau = t
* (-x * x - 1.26551223
+ t * (1.00002368
+ t * (0.37409196
+ t * (0.09678418
+ t * (-0.18628806
+ t * (0.27886807
+ t * (-1.13520398
+ t * (1.48851587 + t * (-0.82215223 + t * 0.17087277)))))))))
.exp();
if x >= 0.0 {
tau
} else {
2.0 - tau
}
}
#[derive(Debug, Clone)]
pub struct JarqueBeraResult {
pub statistic: f64,
pub p_value: f64,
pub skewness: f64,
pub excess_kurtosis: f64,
}
impl JarqueBeraResult {
pub fn is_normal(&self, alpha: f64) -> bool {
self.p_value > alpha
}
}
pub fn jarque_bera(residuals: &[f64]) -> JarqueBeraResult {
let n = residuals.len() as f64;
if residuals.len() < 3 {
return JarqueBeraResult {
statistic: f64::NAN,
p_value: f64::NAN,
skewness: f64::NAN,
excess_kurtosis: f64::NAN,
};
}
let mean = residuals.iter().sum::<f64>() / n;
let m2: f64 = residuals.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / n;
let m3: f64 = residuals.iter().map(|r| (r - mean).powi(3)).sum::<f64>() / n;
let m4: f64 = residuals.iter().map(|r| (r - mean).powi(4)).sum::<f64>() / n;
if m2 == 0.0 {
return JarqueBeraResult {
statistic: 0.0,
p_value: 1.0,
skewness: 0.0,
excess_kurtosis: 0.0,
};
}
let skewness = m3 / m2.powf(1.5);
let kurtosis = m4 / m2.powi(2);
let excess_kurtosis = kurtosis - 3.0;
let jb = n / 6.0 * (skewness.powi(2) + excess_kurtosis.powi(2) / 4.0);
let p_value = chi_squared_sf(jb, 2);
JarqueBeraResult {
statistic: jb,
p_value,
skewness,
excess_kurtosis,
}
}
#[derive(Debug, Clone)]
pub struct ResidualDiagnostics {
pub ljung_box: LjungBoxResult,
pub durbin_watson: DurbinWatsonResult,
pub jarque_bera: JarqueBeraResult,
pub mean: f64,
pub variance: f64,
pub n: usize,
}
impl ResidualDiagnostics {
pub fn is_adequate(&self, alpha: f64) -> bool {
self.ljung_box.is_white_noise(alpha)
}
}
impl std::fmt::Display for ResidualDiagnostics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Residual Diagnostics (n={})", self.n)?;
writeln!(
f,
" Mean: {:.6}, Variance: {:.6}",
self.mean, self.variance
)?;
writeln!(
f,
" Ljung-Box: Q={:.4}, p={:.4}",
self.ljung_box.statistic, self.ljung_box.p_value
)?;
writeln!(
f,
" Durbin-Watson: d={:.4} ({:?})",
self.durbin_watson.statistic, self.durbin_watson.interpretation
)?;
write!(
f,
" Jarque-Bera: JB={:.4}, p={:.4} (skew={:.4}, kurt={:.4})",
self.jarque_bera.statistic,
self.jarque_bera.p_value,
self.jarque_bera.skewness,
self.jarque_bera.excess_kurtosis
)
}
}
pub fn diagnose_residuals(residuals: &[f64], fitted_params: usize) -> ResidualDiagnostics {
let n = residuals.len();
let mean = if n > 0 {
residuals.iter().sum::<f64>() / n as f64
} else {
0.0
};
let variance = if n > 1 {
residuals.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (n - 1) as f64
} else {
0.0
};
ResidualDiagnostics {
ljung_box: ljung_box(residuals, None, fitted_params),
durbin_watson: durbin_watson(residuals),
jarque_bera: jarque_bera(residuals),
mean,
variance,
n,
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn ljung_box_white_noise() {
let residuals: Vec<f64> = (0..100)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let result = ljung_box(&residuals, Some(10), 0);
assert!(result.statistic >= 0.0);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
assert_eq!(result.lags, 10);
}
#[test]
fn ljung_box_autocorrelated() {
let mut residuals = vec![0.0; 100];
residuals[0] = 1.0;
for i in 1..100 {
residuals[i] = 0.9 * residuals[i - 1] + 0.1 * ((i * 17) % 23) as f64 / 23.0;
}
let result = ljung_box(&residuals, Some(10), 0);
assert!(result.statistic > 0.0);
assert!(result.p_value < 0.5);
}
#[test]
fn ljung_box_constant() {
let residuals = vec![1.0; 50];
let result = ljung_box(&residuals, Some(5), 0);
assert_eq!(result.statistic, 0.0);
assert_eq!(result.p_value, 1.0);
}
#[test]
fn ljung_box_short() {
let residuals = vec![1.0, 2.0];
let result = ljung_box(&residuals, Some(5), 0);
assert!(result.statistic.is_nan());
}
#[test]
fn ljung_box_empty() {
let result = ljung_box(&[], Some(5), 0);
assert!(result.statistic.is_nan());
}
#[test]
fn ljung_box_is_white_noise() {
let result = LjungBoxResult {
statistic: 5.0,
p_value: 0.3,
lags: 10,
df: 10,
};
assert!(result.is_white_noise(0.05));
assert!(!result.is_white_noise(0.5));
}
#[test]
fn ljung_box_with_fitted_params() {
let residuals: Vec<f64> = (0..100)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let result_0 = ljung_box(&residuals, Some(10), 0);
let result_2 = ljung_box(&residuals, Some(10), 2);
assert_eq!(result_0.df, 10);
assert_eq!(result_2.df, 8);
}
#[test]
fn durbin_watson_no_autocorrelation() {
let residuals: Vec<f64> = (0..100)
.map(|i| if i % 2 == 0 { 0.5 } else { -0.5 })
.collect();
let result = durbin_watson(&residuals);
assert!(result.statistic >= 0.0 && result.statistic <= 4.0);
}
#[test]
fn durbin_watson_positive_autocorrelation() {
let mut residuals = vec![0.0; 100];
residuals[0] = 1.0;
for i in 1..100 {
residuals[i] = 0.95 * residuals[i - 1];
}
let result = durbin_watson(&residuals);
assert!(result.statistic < 1.0);
assert!(
result.interpretation == AutocorrelationType::PositiveStrong
|| result.interpretation == AutocorrelationType::PositiveWeak
);
}
#[test]
fn durbin_watson_negative_autocorrelation() {
let residuals: Vec<f64> = (0..100)
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
.collect();
let result = durbin_watson(&residuals);
assert!(result.statistic > 3.0);
assert!(
result.interpretation == AutocorrelationType::NegativeStrong
|| result.interpretation == AutocorrelationType::NegativeWeak
);
}
#[test]
fn durbin_watson_constant() {
let residuals = vec![1.0; 50];
let result = durbin_watson(&residuals);
assert_eq!(result.statistic, 0.0);
}
#[test]
fn durbin_watson_short() {
let result = durbin_watson(&[1.0]);
assert!(result.statistic.is_nan());
}
#[test]
fn durbin_watson_zero_residuals() {
let residuals = vec![0.0; 50];
let result = durbin_watson(&residuals);
assert_eq!(result.statistic, 2.0);
assert_eq!(result.interpretation, AutocorrelationType::None);
}
#[test]
fn box_pierce_white_noise() {
let residuals: Vec<f64> = (0..100)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let result = box_pierce(&residuals, Some(10));
assert!(result.statistic >= 0.0);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
}
#[test]
fn box_pierce_vs_ljung_box() {
let residuals: Vec<f64> = (0..100)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let bp = box_pierce(&residuals, Some(10));
let lb = ljung_box(&residuals, Some(10), 0);
assert!(bp.statistic >= 0.0);
assert!(lb.statistic >= 0.0);
assert!(lb.statistic >= bp.statistic * 0.9);
}
#[test]
fn box_pierce_constant() {
let residuals = vec![1.0; 50];
let result = box_pierce(&residuals, Some(5));
assert_eq!(result.statistic, 0.0);
assert_eq!(result.p_value, 1.0);
}
#[test]
fn box_pierce_short() {
let residuals = vec![1.0, 2.0];
let result = box_pierce(&residuals, Some(5));
assert!(result.statistic.is_nan());
}
#[test]
fn chi_squared_sf_zero() {
let p = chi_squared_sf(0.0, 5);
assert_relative_eq!(p, 1.0, epsilon = 0.01);
}
#[test]
fn chi_squared_sf_known_values() {
let p = chi_squared_sf(2.0, 2);
assert!(p > 0.3 && p < 0.4);
let p = chi_squared_sf(18.31, 10);
assert!(p > 0.03 && p < 0.07);
}
#[test]
fn chi_squared_sf_large_df() {
let p = chi_squared_sf(50.0, 50);
assert!(p > 0.3 && p < 0.7);
}
#[test]
fn jarque_bera_normal_data() {
let residuals: Vec<f64> = (0..200)
.map(|i| {
let x = ((i * 17 + 13) % 97) as f64 / 97.0;
let y = ((i * 31 + 7) % 89) as f64 / 89.0;
(x - 0.5) + (y - 0.5)
})
.collect();
let result = jarque_bera(&residuals);
assert!(result.statistic >= 0.0);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
assert!(result.skewness.abs() < 1.0);
}
#[test]
fn jarque_bera_skewed_data() {
let mut residuals = vec![0.1; 100];
residuals.extend(vec![10.0; 5]);
let result = jarque_bera(&residuals);
assert!(result.statistic > 0.0);
assert!(result.skewness.abs() > 0.5, "Data should be skewed");
}
#[test]
fn jarque_bera_constant_data() {
let residuals = vec![3.0; 50];
let result = jarque_bera(&residuals);
assert_relative_eq!(result.statistic, 0.0, epsilon = 1e-10);
assert_relative_eq!(result.p_value, 1.0, epsilon = 1e-10);
assert_relative_eq!(result.skewness, 0.0, epsilon = 1e-10);
assert_relative_eq!(result.excess_kurtosis, 0.0, epsilon = 1e-10);
}
#[test]
fn jarque_bera_too_short() {
let residuals = vec![1.0, 2.0];
let result = jarque_bera(&residuals);
assert!(result.statistic.is_nan());
assert!(result.p_value.is_nan());
assert!(result.skewness.is_nan());
assert!(result.excess_kurtosis.is_nan());
}
#[test]
fn jarque_bera_is_normal() {
let result = JarqueBeraResult {
statistic: 1.0,
p_value: 0.60,
skewness: 0.1,
excess_kurtosis: 0.05,
};
assert!(result.is_normal(0.05));
assert!(!result.is_normal(0.99));
}
#[test]
fn jarque_bera_single_value() {
let residuals = vec![42.0];
let result = jarque_bera(&residuals);
assert!(result.statistic.is_nan());
}
#[test]
fn jarque_bera_empty() {
let result = jarque_bera(&[]);
assert!(result.statistic.is_nan());
}
#[test]
fn diagnose_residuals_basic() {
let residuals: Vec<f64> = (0..100)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let diag = diagnose_residuals(&residuals, 0);
assert_eq!(diag.n, 100);
assert!(!diag.mean.is_nan());
assert!(diag.variance >= 0.0);
assert!(!diag.ljung_box.statistic.is_nan());
assert!(!diag.durbin_watson.statistic.is_nan());
assert!(!diag.jarque_bera.statistic.is_nan());
}
#[test]
fn diagnose_residuals_empty() {
let diag = diagnose_residuals(&[], 0);
assert_eq!(diag.n, 0);
assert_relative_eq!(diag.mean, 0.0, epsilon = 1e-10);
assert_relative_eq!(diag.variance, 0.0, epsilon = 1e-10);
}
#[test]
fn diagnose_residuals_single_value() {
let diag = diagnose_residuals(&[5.0], 0);
assert_eq!(diag.n, 1);
assert_relative_eq!(diag.mean, 5.0, epsilon = 1e-10);
assert_relative_eq!(diag.variance, 0.0, epsilon = 1e-10);
}
#[test]
fn diagnose_residuals_constant() {
let residuals = vec![2.0; 50];
let diag = diagnose_residuals(&residuals, 0);
assert_eq!(diag.n, 50);
assert_relative_eq!(diag.mean, 2.0, epsilon = 1e-10);
assert_relative_eq!(diag.variance, 0.0, epsilon = 1e-10);
assert!(diag.ljung_box.is_white_noise(0.05));
}
#[test]
fn diagnose_residuals_is_adequate() {
let residuals: Vec<f64> = (0..100)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let diag = diagnose_residuals(&residuals, 0);
assert_eq!(diag.is_adequate(0.05), diag.ljung_box.is_white_noise(0.05));
}
#[test]
fn diagnose_residuals_display() {
let residuals: Vec<f64> = (0..50)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let diag = diagnose_residuals(&residuals, 0);
let display = format!("{}", diag);
assert!(display.contains("Residual Diagnostics"));
assert!(display.contains("Ljung-Box"));
assert!(display.contains("Durbin-Watson"));
assert!(display.contains("Jarque-Bera"));
}
#[test]
fn diagnose_residuals_with_fitted_params() {
let residuals: Vec<f64> = (0..100)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let diag0 = diagnose_residuals(&residuals, 0);
let diag3 = diagnose_residuals(&residuals, 3);
assert_relative_eq!(
diag0.ljung_box.statistic,
diag3.ljung_box.statistic,
epsilon = 1e-10
);
assert!(diag0.ljung_box.df > diag3.ljung_box.df);
}
#[test]
fn ljung_box_default_lags() {
let residuals: Vec<f64> = (0..100)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let result = ljung_box(&residuals, None, 0);
assert_eq!(result.lags, 10); }
#[test]
fn ljung_box_lags_clamped_to_n_minus_1() {
let residuals = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = ljung_box(&residuals, Some(100), 0);
assert_eq!(result.lags, 4); }
#[test]
fn ljung_box_three_values() {
let residuals = vec![1.0, -1.0, 0.5];
let result = ljung_box(&residuals, Some(1), 0);
assert!(!result.statistic.is_nan());
assert!(result.statistic >= 0.0);
}
#[test]
fn durbin_watson_two_values() {
let result = durbin_watson(&[1.0, 2.0]);
assert!(!result.statistic.is_nan());
assert!(result.statistic >= 0.0 && result.statistic <= 4.0);
}
#[test]
fn durbin_watson_empty() {
let result = durbin_watson(&[]);
assert!(result.statistic.is_nan());
assert_eq!(result.interpretation, AutocorrelationType::None);
}
#[test]
fn durbin_watson_all_interpretation_ranges() {
let mut strong_pos = vec![0.0; 100];
strong_pos[0] = 10.0;
for i in 1..100 {
strong_pos[i] = strong_pos[i - 1] * 0.999;
}
let result = durbin_watson(&strong_pos);
assert!(result.statistic < 0.5);
assert_eq!(result.interpretation, AutocorrelationType::PositiveStrong);
let result_none = DurbinWatsonResult {
statistic: 2.0,
interpretation: AutocorrelationType::None,
};
assert_eq!(result_none.interpretation, AutocorrelationType::None);
let alt: Vec<f64> = (0..100)
.map(|i| if i % 2 == 0 { 100.0 } else { -100.0 })
.collect();
let result = durbin_watson(&alt);
assert!(result.statistic > 3.5);
assert_eq!(result.interpretation, AutocorrelationType::NegativeStrong);
}
#[test]
fn box_pierce_empty() {
let result = box_pierce(&[], Some(5));
assert!(result.statistic.is_nan());
}
#[test]
fn box_pierce_default_lags() {
let residuals: Vec<f64> = (0..50)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
let result = box_pierce(&residuals, None);
assert_eq!(result.lags, 10); }
#[test]
fn ljung_box_with_nan_values() {
let mut residuals: Vec<f64> = (0..50)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
residuals[10] = f64::NAN;
let result = ljung_box(&residuals, Some(5), 0);
assert!(result.statistic.is_nan());
}
#[test]
fn durbin_watson_with_nan() {
let residuals = vec![1.0, f64::NAN, 3.0, 4.0];
let result = durbin_watson(&residuals);
assert!(result.statistic.is_nan());
}
#[test]
fn jarque_bera_with_nan() {
let mut residuals: Vec<f64> = (0..50)
.map(|i| ((i * 17 + 13) % 97) as f64 / 50.0 - 1.0)
.collect();
residuals[5] = f64::NAN;
let result = jarque_bera(&residuals);
assert!(result.statistic.is_nan());
}
}