irox_stats/
fitting.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2024 IROX Contributors
3//
4
5///
6/// Simple linear regression from provided data.
7///
8/// ```
9/// use irox_stats::sampling::Sample;
10/// use irox_time::epoch::UnixTimestamp;
11/// use irox_stats::fitting::LinearRegression;
12///
13/// let data = &[
14///             Sample::new(0., UnixTimestamp::from_seconds(0)),
15///             Sample::new(0.5, UnixTimestamp::from_seconds_f64(0.5)),
16///             Sample::new(1., UnixTimestamp::from_seconds(1)),
17///         ];
18///
19///         let reg = LinearRegression::from_data(
20///             data.iter(),
21///             |s| s.time.get_offset().as_seconds_f64(),
22///             |s| s.value,
23///         );
24///         assert!(reg.is_some());
25///         let Some(reg) = reg else {
26///             return;
27///         };
28///         assert_eq!(1.0, reg.slope);
29///         assert_eq!(0.5, reg.mean_x);
30///         assert_eq!(0.5, reg.mean_y);
31/// ```
32#[derive(Debug, Copy, Clone, PartialEq)]
33pub struct LinearRegression {
34    pub slope: f64,
35    pub mean_x: f64,
36    pub mean_y: f64,
37}
38
39impl LinearRegression {
40    pub fn from_data<
41        'a,
42        S: 'a,
43        I: Iterator<Item = S> + Clone,
44        X: Fn(&S) -> f64,
45        Y: Fn(&S) -> f64,
46    >(
47        data: I,
48        x_accessor: X,
49        y_accessor: Y,
50    ) -> Option<LinearRegression> {
51        let mut mean_x = 0f64;
52        let mut mean_y = 0f64;
53        let mut count = 0f64;
54
55        for s in data.clone() {
56            let x = x_accessor(&s);
57            let y = y_accessor(&s);
58            mean_y += y;
59            mean_x += x;
60            count += 1.0;
61        }
62
63        if count <= 1. {
64            return None;
65        }
66        mean_x /= count;
67        mean_y /= count;
68
69        let mut xsum = 0f64;
70        let mut linear = 0f64;
71        for s in data {
72            let x = x_accessor(&s);
73            let y = y_accessor(&s);
74
75            let dy = y - mean_y;
76            let dx = x - mean_x;
77
78            xsum += dx * dx;
79            linear += dx * dy;
80        }
81        if xsum <= 0.0 {
82            return None;
83        }
84
85        Some(LinearRegression {
86            slope: linear / xsum,
87            mean_x,
88            mean_y,
89        })
90    }
91}
92
93#[cfg(test)]
94mod test {
95    use crate::fitting::LinearRegression;
96    use crate::sampling::Sample;
97    use irox_time::epoch::UnixTimestamp;
98
99    #[test]
100    pub fn test() {
101        let data = &[
102            Sample::new(0., UnixTimestamp::from_seconds(0)),
103            Sample::new(0.5, UnixTimestamp::from_seconds_f64(0.5)),
104            Sample::new(1., UnixTimestamp::from_seconds(1)),
105        ];
106
107        let reg = LinearRegression::from_data(
108            data.iter(),
109            |s| s.time.get_offset().as_seconds_f64(),
110            |s| s.value,
111        );
112        assert!(reg.is_some());
113        let Some(reg) = reg else {
114            return;
115        };
116        assert_eq!(1.0, reg.slope);
117        assert_eq!(0.5, reg.mean_x);
118        assert_eq!(0.5, reg.mean_y);
119    }
120}