Skip to main content

pickems/reporting/
assess.rs

1use std::{iter::Sum, ops::Add};
2
3use anyhow::anyhow;
4
5use crate::{
6    datatypes::{Index, Name, Set, Teams},
7    reporting::Report,
8    simulation::{Simulation, SwissSystem},
9};
10
11/// Report for estimating how often a concrete pick set earns enough stars.
12#[derive(Debug, Clone, Copy, Default)]
13pub struct AssessReport {
14    /// Running mean of stars earned.
15    pub mean: f32,
16    /// Sum of squared differences for variance calculation.
17    pub ds: f32,
18    /// Number of simulations with at least five stars.
19    pub success: u64,
20    /// Number of simulated tournaments assessed.
21    pub n: u64,
22    /// Teams selected as 3-0 picks.
23    pub three_zero_picks: Set,
24    /// Teams selected as 3-1/3-2 advancement picks.
25    pub advancing_picks: Set,
26    /// Teams selected as 0-3 picks.
27    pub zero_three_picks: Set,
28}
29
30impl AssessReport {
31    /// Construct an assessment report from selected team indices.
32    pub fn new<
33        I1: IntoIterator<Item = Index>,
34        I2: IntoIterator<Item = Index>,
35        I3: IntoIterator<Item = Index>,
36    >(
37        three_zero_picks: I1,
38        advanced_picks: I2,
39        zero_three_picks: I3,
40    ) -> Self {
41        Self {
42            mean: 0.0,
43            ds: 0.0,
44            success: 0,
45            n: 0,
46            three_zero_picks: three_zero_picks.into_iter().collect(),
47            advancing_picks: advanced_picks.into_iter().collect(),
48            zero_three_picks: zero_three_picks.into_iter().collect(),
49        }
50    }
51
52    /// Resolve CLI team-name picks into simulation indices.
53    pub fn try_from_args(
54        teams: &Teams,
55        three_zero_str: &[Name; 2],
56        advancing_str: &[Name; 6],
57        zero_three_str: &[Name; 2],
58    ) -> anyhow::Result<Self> {
59        let mut three_zero_picks = Vec::new();
60        let mut advancing_picks = Vec::new();
61        let mut zero_three_picks = Vec::new();
62
63        for s in three_zero_str {
64            three_zero_picks.push(Index::try_new(
65                teams
66                    .names
67                    .iter()
68                    .position(|name| name == s)
69                    .ok_or_else(|| anyhow!("failed to find team \"{s}\" in the input file"))?
70                    as u16,
71            )?);
72        }
73
74        for s in advancing_str {
75            advancing_picks.push(Index::try_new(
76                teams
77                    .names
78                    .iter()
79                    .position(|name| name == s)
80                    .ok_or_else(|| anyhow!("failed to find team \"{s}\" in the input file"))?
81                    as u16,
82            )?);
83        }
84
85        for s in zero_three_str {
86            zero_three_picks.push(Index::try_new(
87                teams
88                    .names
89                    .iter()
90                    .position(|name| name == s)
91                    .ok_or_else(|| anyhow!("failed to find team \"{s}\" in the input file"))?
92                    as u16,
93            )?);
94        }
95
96        let mut seen = Set::new();
97
98        for pick in three_zero_picks
99            .iter()
100            .chain(advancing_picks.iter())
101            .chain(zero_three_picks.iter())
102        {
103            if !seen.insert(*pick) {
104                anyhow::bail!("duplicate pick: {}", teams.names[pick.to_usize()]);
105            }
106        }
107
108        Ok(Self::new(
109            three_zero_picks,
110            advancing_picks,
111            zero_three_picks,
112        ))
113    }
114}
115
116impl Add for AssessReport {
117    type Output = Self;
118
119    fn add(mut self, rhs: Self) -> Self::Output {
120        if self.n == 0 {
121            rhs
122        } else {
123            self.success += rhs.success;
124
125            let n_self = self.n as f32;
126            let n_rhs = rhs.n as f32;
127            let n = n_self + n_rhs;
128            self.n += rhs.n;
129
130            let delta = self.mean - rhs.mean;
131            self.mean = n_rhs.mul_add(rhs.mean, n_self * self.mean) / n;
132            self.ds += (delta * delta).mul_add(n_self * n_rhs / n, rhs.ds);
133
134            self
135        }
136    }
137}
138
139impl Sum for AssessReport {
140    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
141        iter.fold(Self::default(), |acc, report| acc + report)
142    }
143}
144
145impl Report for AssessReport {
146    fn update(&mut self, ss: &SwissSystem) {
147        // Update count before Welford's mean/variance step.
148        self.n += 1;
149
150        // Count stars under the current pick'em rules represented by this
151        // report: exact 3-0, non-3-0 advancement, and exact 0-3.
152        let stars = {
153            self.three_zero_picks
154                .iter()
155                .filter(|&pick| ss.wins[pick.to_usize()] == 3 && ss.losses[pick.to_usize()] == 0)
156                .count()
157                + self
158                    .advancing_picks
159                    .iter()
160                    .filter(|&pick| ss.wins[pick.to_usize()] == 3 && ss.losses[pick.to_usize()] > 0)
161                    .count()
162                + self
163                    .zero_three_picks
164                    .iter()
165                    .filter(|&pick| {
166                        ss.wins[pick.to_usize()] == 0 && ss.losses[pick.to_usize()] == 3
167                    })
168                    .count()
169        };
170
171        // Update success count and running distribution of stars.
172        if stars >= 5 {
173            self.success += 1;
174        }
175
176        let delta1 = stars as f32 - self.mean;
177        self.mean += delta1 / self.n as f32;
178        let delta2 = stars as f32 - self.mean;
179        self.ds = delta1.mul_add(delta2, self.ds);
180    }
181
182    fn format(&self, _sim: &Simulation) -> String {
183        let mean = self.mean;
184        let sd = (self.ds / self.n as f32).sqrt();
185        let success = self.success as f32 / self.n as f32 * 100.0;
186
187        format!(
188            "\nSimulated stars earned: {mean:.3} +/- {sd:.3}\nExpected success (>=5 stars): {success:.1}%"
189        )
190    }
191}