portfolio_analyzer/sd2/
model.rs1use serde::{Deserialize, Serialize};
22
23fn solve_spd(a: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
25 let k = b.len();
26 let mut l = vec![vec![0.0; k]; k];
27 for i in 0..k {
28 for j in 0..=i {
29 let s: f64 = (0..j).map(|p| l[i][p] * l[j][p]).sum();
30 if i == j {
31 l[i][j] = (a[i][i] - s).max(1e-12).sqrt();
32 } else {
33 l[i][j] = (a[i][j] - s) / l[j][j];
34 }
35 }
36 }
37 let mut y = vec![0.0; k];
38 for i in 0..k {
39 y[i] = (b[i] - (0..i).map(|p| l[i][p] * y[p]).sum::<f64>()) / l[i][i];
40 }
41 let mut x = vec![0.0; k];
42 for i in (0..k).rev() {
43 x[i] = (y[i] - (i + 1..k).map(|p| l[p][i] * x[p]).sum::<f64>()) / l[i][i];
44 }
45 x
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Scaler {
51 pub mean: Vec<f64>,
52 pub std: Vec<f64>,
53}
54
55impl Scaler {
56 pub fn fit(x: &[Vec<f64>]) -> Self {
57 let k = x[0].len();
58 let n = x.len() as f64;
59 let mean: Vec<f64> = (0..k).map(|j| x.iter().map(|r| r[j]).sum::<f64>() / n).collect();
60 let std = (0..k)
61 .map(|j| {
62 let v = x.iter().map(|r| (r[j] - mean[j]).powi(2)).sum::<f64>() / n;
63 v.sqrt().max(1e-9)
64 })
65 .collect();
66 Self { mean, std }
67 }
68 pub fn apply(&self, r: &[f64]) -> Vec<f64> {
69 r.iter().enumerate().map(|(j, v)| (v - self.mean[j]) / self.std[j]).collect()
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Logistic {
76 pub scaler: Scaler,
77 pub intercept: f64,
78 pub weights: Vec<f64>,
79}
80
81pub fn sigmoid(x: f64) -> f64 {
82 1.0 / (1.0 + (-x).exp())
83}
84
85impl Logistic {
86 pub fn fit(x: &[Vec<f64>], y: &[f64], w: &[f64], lambda: f64) -> Self {
89 let scaler = Scaler::fit(x);
90 let z: Vec<Vec<f64>> = x.iter().map(|r| scaler.apply(r)).collect();
91 let k = z[0].len() + 1;
92 let total_w: f64 = w.iter().sum();
93 let mut beta = vec![0.0; k];
94 for _ in 0..25 {
95 let mut h = vec![vec![0.0; k]; k];
96 let mut g = vec![0.0; k];
97 for ((row, &yi), &wi) in z.iter().zip(y).zip(w) {
98 let eta = beta[0] + row.iter().zip(&beta[1..]).map(|(a, b)| a * b).sum::<f64>();
99 let p = sigmoid(eta);
100 let s = wi * p * (1.0 - p);
101 let r = wi * (yi - p);
102 let f = |j: usize| if j == 0 { 1.0 } else { row[j - 1] };
103 for a in 0..k {
104 g[a] += r * f(a);
105 for b in 0..=a {
106 h[a][b] += s * f(a) * f(b);
107 }
108 }
109 }
110 for a in 0..k {
111 for b in 0..a {
112 h[b][a] = h[a][b];
113 }
114 if a > 0 {
115 h[a][a] += lambda * total_w;
116 g[a] -= lambda * total_w * beta[a];
117 }
118 }
119 let step = solve_spd(&h, &g);
120 let size: f64 = step.iter().map(|s| s * s).sum::<f64>().sqrt();
121 for (b, s) in beta.iter_mut().zip(&step) {
122 *b += s;
123 }
124 if size < 1e-8 {
125 break;
126 }
127 }
128 Self { scaler, intercept: beta[0], weights: beta[1..].to_vec() }
129 }
130
131 pub fn logit(&self, r: &[f64]) -> f64 {
132 let z = self.scaler.apply(r);
133 self.intercept + z.iter().zip(&self.weights).map(|(a, b)| a * b).sum::<f64>()
134 }
135
136 pub fn predict(&self, r: &[f64]) -> f64 {
137 sigmoid(self.logit(r))
138 }
139
140 pub fn lean(&self, r: &[f64]) -> f64 {
143 self.logit(r) - self.intercept
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct Ridge {
150 pub scaler: Scaler,
151 pub intercept: f64,
152 pub weights: Vec<f64>,
153}
154
155impl Ridge {
156 pub fn fit(x: &[Vec<f64>], y: &[f64], lambda: f64) -> Self {
157 let scaler = Scaler::fit(x);
158 let k = x[0].len() + 1;
159 let n = x.len() as f64;
160 let mut a = vec![vec![0.0; k]; k];
161 let mut b = vec![0.0; k];
162 for (r, &yi) in x.iter().zip(y) {
163 let z = scaler.apply(r);
164 let f = |j: usize| if j == 0 { 1.0 } else { z[j - 1] };
165 for i in 0..k {
166 b[i] += f(i) * yi;
167 for j in 0..=i {
168 a[i][j] += f(i) * f(j);
169 }
170 }
171 }
172 for i in 0..k {
173 for j in 0..i {
174 a[j][i] = a[i][j];
175 }
176 if i > 0 {
177 a[i][i] += lambda * n;
178 }
179 }
180 let beta = solve_spd(&a, &b);
181 Self { scaler, intercept: beta[0], weights: beta[1..].to_vec() }
182 }
183
184 pub fn predict(&self, r: &[f64]) -> f64 {
185 let z = self.scaler.apply(r);
186 self.intercept + z.iter().zip(&self.weights).map(|(a, b)| a * b).sum::<f64>()
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn logistic_recovers_a_clear_signal() {
196 let x: Vec<Vec<f64>> = (0..2000).map(|i| vec![((i * 37) % 200) as f64 / 100.0 - 1.0]).collect();
197 let y: Vec<f64> = x.iter().map(|r| if r[0] > 0.0 { 1.0 } else { 0.0 }).collect();
198 let m = Logistic::fit(&x, &y, &vec![1.0; 2000], 1e-4);
199 assert!(m.predict(&[0.8]) > 0.9 && m.predict(&[-0.8]) < 0.1);
200 }
201
202 #[test]
203 fn ridge_recovers_a_line() {
204 let x: Vec<Vec<f64>> = (0..100).map(|i| vec![i as f64]).collect();
205 let y: Vec<f64> = x.iter().map(|r| 3.0 * r[0] + 2.0).collect();
206 let m = Ridge::fit(&x, &y, 0.0);
207 assert!((m.predict(&[10.0]) - 32.0).abs() < 1e-6);
208 }
209}