use num_traits::Float;
use crate::rolling::RollingMoments;
type Kbn<T> = compensated_summation::KahanBabuskaNeumaier<T>;
#[derive(Debug, Clone)]
pub struct PairedStatistics<T> {
moments_x: RollingMoments<T>,
moments_y: RollingMoments<T>,
sum_xy: Kbn<T>,
ddof: bool,
}
impl<T> PairedStatistics<T>
where
T: Default + Clone + Float,
{
pub fn new(period: usize) -> Self {
Self {
moments_x: RollingMoments::new(period),
moments_y: RollingMoments::new(period),
sum_xy: Kbn::default(),
ddof: false,
}
}
pub fn period(&self) -> usize {
self.moments_x.period()
}
pub fn reset(&mut self) -> &mut Self {
self.moments_x.reset();
self.moments_y.reset();
self.sum_xy = Default::default();
self
}
pub fn recompute(&mut self) -> &mut Self {
self.moments_x.recompute();
self.moments_y.recompute();
for (&x, &y) in self.moments_x.iter().zip(self.moments_y.iter()) {
self.sum_xy += x * y;
}
self
}
pub fn next(&mut self, (x, y): (T, T)) -> &mut Self {
self.moments_x.next(x);
self.moments_y.next(y);
if self.moments_x.is_ready() {
if let Some((px, py)) = self.moments_x.popped().zip(self.moments_y.popped()) {
self.sum_xy -= px * py;
}
}
self.sum_xy += x * y;
self
}
pub const fn ddof(&self) -> bool {
self.ddof
}
pub const fn set_ddof(&mut self, ddof: bool) -> &mut Self {
self.ddof = ddof;
self
}
fn mean(&self) -> Option<(T, T)> {
self.moments_x.mean().zip(self.moments_y.mean())
}
fn mean_prod(&self) -> Option<(T, T)> {
if !self.moments_x.is_ready() {
return None;
}
let n = T::from(self.period())?;
let mp = self.sum_xy.total() / n;
Some((mp, mp))
}
fn variance(&self) -> Option<(T, T)> {
self.moments_x.variance().zip(self.moments_y.variance())
}
fn stddev(&self) -> Option<(T, T)> {
self.moments_x.stddev().zip(self.moments_y.stddev())
}
pub fn cov(&self) -> Option<T> {
let (mean_x, mean_y) = self.mean()?;
let (mean_xy, _) = self.mean_prod()?;
let cov = mean_xy - mean_x * mean_y;
let n = T::from(self.period())?;
if self.ddof() {
Some(cov * (n / (n - T::one())))
} else {
Some(cov)
}
}
pub fn corr(&self) -> Option<T> {
self.cov()
.zip(self.stddev())
.and_then(|(cov, (stddev_x, stddev_y))| {
if stddev_x.is_zero() || stddev_y.is_zero() {
None
} else {
Some(cov / (stddev_x * stddev_y))
}
})
}
pub fn beta(&self) -> Option<T> {
self.cov().zip(self.variance()).and_then(
|(cov, (_, var))| {
if var.is_zero() { None } else { Some(cov / var) }
},
)
}
}