multi_skill/
summary.rs

1use crate::data_processing::write_slice_to_file;
2use crate::systems::{Player, PlayerEvent};
3use serde::{Deserialize, Serialize};
4use std::cell::RefCell;
5use std::collections::HashMap;
6
7const NUM_TITLES: usize = 11;
8const TITLE_BOUND: [i32; NUM_TITLES] = [
9    -999, 1000, 1200, 1400, 1600, 1800, 2000, 2200, 2400, 2700, 3000,
10];
11const TITLE: [&str; NUM_TITLES] = [
12    "Ne", "Pu", "Ap", "Sp", "Ex", "CM", "Ma", "IM", "GM", "IG", "LG",
13];
14
15pub struct GlobalSummary {
16    mean_rating: f64,
17    title_count: Vec<usize>,
18}
19
20#[derive(Serialize, Deserialize)]
21pub struct PlayerSummary {
22    rank: Option<usize>,
23    display_rating: i32,
24    max_rating: i32,
25    cur_sigma: i32,
26    num_contests: usize,
27    last_contest_index: usize,
28    last_contest_time: u64,
29    last_perf: i32,
30    last_change: i32,
31    handle: String,
32}
33
34pub fn get_display_rating_from_ints(mu: i32, sig: i32) -> i32 {
35    // TODO: get rid of the magic numbers 2 and 80!
36    //       2.0 gives a conservative estimate: use 0 to get mean estimates
37    //       80 is EloR's default sig_lim
38    mu - 2 * (sig - 80)
39}
40
41pub fn get_display_rating(event: &PlayerEvent) -> i32 {
42    get_display_rating_from_ints(event.rating_mu, event.rating_sig)
43}
44
45pub fn make_leaderboard(
46    players: &HashMap<String, RefCell<Player>>,
47    rated_since: u64,
48) -> (GlobalSummary, Vec<PlayerSummary>) {
49    let mut rating_data = Vec::with_capacity(players.len());
50    let mut title_count = vec![0; NUM_TITLES];
51    let sum_ratings = {
52        let mut ratings: Vec<f64> = players
53            .iter()
54            .map(|(_, player)| player.borrow().approx_posterior.mu)
55            .collect();
56        ratings.sort_by(|a, b| a.partial_cmp(b).expect("NaN is unordered"));
57        ratings.into_iter().sum::<f64>()
58    };
59    for (handle, player) in players {
60        let player = player.borrow_mut();
61        let num_contests = player.event_history.len();
62        let last_event = player.event_history.last().unwrap();
63        let max_rating = player
64            .event_history
65            .iter()
66            .map(get_display_rating)
67            .max()
68            .unwrap();
69        let display_rating = get_display_rating(&last_event);
70        let prev_rating = if num_contests == 1 {
71            get_display_rating_from_ints(1500, 350)
72        } else {
73            get_display_rating(&player.event_history[num_contests - 2])
74        };
75        rating_data.push(PlayerSummary {
76            rank: None,
77            display_rating,
78            max_rating,
79            cur_sigma: player.approx_posterior.sig.round() as i32,
80            num_contests,
81            last_contest_index: last_event.contest_index,
82            last_contest_time: player.update_time,
83            last_perf: last_event.perf_score,
84            last_change: display_rating - prev_rating,
85            handle: handle.clone(),
86        });
87
88        if player.update_time > rated_since {
89            if let Some(title_id) = (0..NUM_TITLES)
90                .rev()
91                .find(|&i| get_display_rating(&last_event) >= TITLE_BOUND[i])
92            {
93                title_count[title_id] += 1;
94            }
95        }
96    }
97    rating_data.sort_unstable_by_key(|data| (-data.display_rating, data.handle.clone()));
98
99    let mut rank = 0;
100    for data in &mut rating_data {
101        if data.last_contest_time > rated_since {
102            rank += 1;
103            data.rank = Some(rank);
104        }
105    }
106
107    let global_summary = GlobalSummary {
108        mean_rating: sum_ratings / players.len() as f64,
109        title_count,
110    };
111
112    (global_summary, rating_data)
113}
114
115pub fn print_ratings(
116    players: &HashMap<String, RefCell<Player>>,
117    rated_since: u64,
118    dir: impl AsRef<std::path::Path>,
119) {
120    // TODO: refactor this "summary" section to instead print a distribution.[csv|json].
121    //       Somehow make the distribution aware of titles so we can keep printing titles.
122    let (summary, rating_data) = make_leaderboard(players, rated_since);
123
124    println!("Mean rating.mu = {}", summary.mean_rating);
125    for i in (0..NUM_TITLES).rev() {
126        println!(
127            "{} {} x{:6}",
128            TITLE_BOUND[i], TITLE[i], summary.title_count[i]
129        );
130    }
131
132    let filename = dir.as_ref().join("all_players.csv");
133    write_slice_to_file(&rating_data, &filename);
134}