corescout_selfmodel/
uncertainty.rs1use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
23pub struct Interval {
24 #[serde(with = "corescout_core::serde_util::maybe_finite")]
25 pub point: f64,
26 #[serde(with = "corescout_core::serde_util::maybe_finite")]
27 pub low: f64,
28 #[serde(with = "corescout_core::serde_util::maybe_finite")]
29 pub high: f64,
30}
31
32impl Interval {
33 pub fn new(point: f64, half_width: f64) -> Interval {
34 let half_width = half_width.abs();
35 Interval {
36 point,
37 low: point - half_width,
38 high: point + half_width,
39 }
40 }
41
42 pub fn exact(point: f64) -> Interval {
44 Interval {
45 point,
46 low: point,
47 high: point,
48 }
49 }
50
51 pub fn unknown() -> Interval {
53 Interval {
54 point: f64::NAN,
55 low: f64::NEG_INFINITY,
56 high: f64::INFINITY,
57 }
58 }
59
60 pub fn width(&self) -> f64 {
61 self.high - self.low
62 }
63
64 pub fn contains(&self, value: f64) -> bool {
65 value >= self.low && value <= self.high
66 }
67
68 pub fn is_known(&self) -> bool {
69 self.point.is_finite() && self.width().is_finite()
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
78pub struct Confidence {
79 mean_error: f64,
81 baseline_error: f64,
83 mean_squared_error: f64,
85 samples: u64,
87 alpha: f64,
89}
90
91impl Default for Confidence {
92 fn default() -> Self {
93 Confidence::new(0.05)
94 }
95}
96
97impl Confidence {
98 pub fn new(alpha: f64) -> Confidence {
101 Confidence {
102 mean_error: 0.0,
103 baseline_error: 0.0,
104 mean_squared_error: 0.0,
105 samples: 0,
106 alpha: alpha.clamp(1e-4, 1.0),
107 }
108 }
109
110 pub fn observe(&mut self, predicted: f64, baseline: f64, actual: f64) {
112 if !predicted.is_finite() || !actual.is_finite() {
113 return;
114 }
115 let error = (predicted - actual).abs();
116 let baseline_error = (baseline - actual).abs();
117 if self.samples == 0 {
118 self.mean_error = error;
119 self.baseline_error = baseline_error;
120 self.mean_squared_error = error * error;
121 } else {
122 self.mean_error += self.alpha * (error - self.mean_error);
123 self.baseline_error += self.alpha * (baseline_error - self.baseline_error);
124 self.mean_squared_error += self.alpha * (error * error - self.mean_squared_error);
125 }
126 self.samples += 1;
127 }
128
129 pub fn samples(&self) -> u64 {
130 self.samples
131 }
132
133 pub fn mean_error(&self) -> f64 {
134 self.mean_error
135 }
136
137 pub fn sigma(&self) -> f64 {
139 self.mean_squared_error.max(0.0).sqrt()
140 }
141
142 pub fn skill(&self) -> f64 {
148 let scale = self.mean_error.abs().max(self.baseline_error.abs());
157 if self.baseline_error <= 1e-12 || self.baseline_error <= scale * 1e-9 {
158 return 0.0;
159 }
160 (1.0 - (self.mean_error / self.baseline_error)).max(-1.0)
165 }
166
167 pub fn score(&self) -> f64 {
173 if self.samples == 0 {
174 return 0.0;
175 }
176 let evidence = (self.samples as f64 / 50.0).min(1.0);
177 let skill = self.skill().clamp(0.0, 1.0);
178 (skill * evidence).clamp(0.0, 1.0)
179 }
180
181 pub fn interval(&self, point: f64) -> Interval {
188 if self.samples < 3 {
189 return Interval::unknown();
190 }
191 Interval::new(point, 2.0 * self.sigma())
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn an_interval_knows_what_it_contains() {
201 let interval = Interval::new(10.0, 2.0);
202 assert!(interval.contains(9.0));
203 assert!(!interval.contains(13.0));
204 assert_eq!(interval.width(), 4.0);
205 assert!(interval.is_known());
206 }
207
208 #[test]
209 fn ignorance_is_representable() {
210 let unknown = Interval::unknown();
211 assert!(!unknown.is_known());
212 assert!(
213 unknown.contains(1e300),
214 "an unknown interval excludes nothing"
215 );
216 }
217
218 #[test]
219 fn a_model_that_matches_the_baseline_has_no_skill() {
220 let mut confidence = Confidence::new(0.2);
221 for _ in 0..50 {
222 confidence.observe(10.0, 10.0, 12.0);
223 }
224 assert!(confidence.skill().abs() < 1e-6);
225 assert_eq!(confidence.score(), 0.0);
226 }
227
228 #[test]
229 fn a_model_that_beats_the_baseline_earns_confidence() {
230 let mut confidence = Confidence::new(0.2);
231 for _ in 0..100 {
232 confidence.observe(11.0, 8.0, 12.0);
234 }
235 assert!(confidence.skill() > 0.7, "skill was {}", confidence.skill());
236 assert!(confidence.score() > 0.7);
237 }
238
239 #[test]
240 fn a_model_that_is_worse_than_the_baseline_reports_negative_skill() {
241 let mut confidence = Confidence::new(0.2);
244 for _ in 0..50 {
245 confidence.observe(50.0, 11.0, 12.0);
246 }
247 assert!(confidence.skill() < 0.0);
248 assert_eq!(confidence.score(), 0.0, "score floors at zero");
249 }
250
251 #[test]
252 fn little_evidence_means_little_confidence_even_when_right() {
253 let mut sparse = Confidence::new(0.2);
254 let mut plentiful = Confidence::new(0.2);
255 for _ in 0..3 {
256 sparse.observe(11.0, 8.0, 12.0);
257 }
258 for _ in 0..100 {
259 plentiful.observe(11.0, 8.0, 12.0);
260 }
261 assert!(
262 sparse.score() < plentiful.score(),
263 "three correct predictions is not the same evidence as a hundred"
264 );
265 }
266
267 #[test]
268 fn confidence_falls_when_the_machine_changes_character() {
269 let mut confidence = Confidence::new(0.2);
270 for _ in 0..100 {
271 confidence.observe(11.0, 8.0, 12.0);
272 }
273 let before = confidence.skill();
274 for _ in 0..50 {
275 confidence.observe(11.0, 40.0, 40.0);
277 }
278 assert!(
279 confidence.skill() < before,
280 "a model must notice when it stops working"
281 );
282 }
283
284 #[test]
285 fn an_interval_needs_evidence_before_it_claims_a_width() {
286 let mut confidence = Confidence::new(0.2);
287 assert!(!confidence.interval(10.0).is_known());
288 for _ in 0..10 {
289 confidence.observe(10.0, 10.0, 10.5);
290 }
291 let interval = confidence.interval(10.0);
292 assert!(interval.is_known());
293 assert!(interval.contains(10.5));
294 }
295
296 #[test]
297 fn unobservable_values_are_ignored_rather_than_poisoning_the_estimate() {
298 let mut confidence = Confidence::new(0.2);
299 confidence.observe(f64::NAN, 1.0, 1.0);
300 confidence.observe(1.0, 1.0, f64::NAN);
301 assert_eq!(confidence.samples(), 0);
302 }
303}