From 72765d648f4984c3438cb6af01e7161e5832faa9 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Fri, 8 Nov 2024 08:14:04 +0100 Subject: [PATCH] Revert "choose initial rating dynamically" This reverts commit af6d50a8667b0e078053f5fd6703cf9ff567d848. --- research/Cargo.lock | 32 ++--- research/src/bin/replay_vanilla_glicko2.rs | 160 ++++----------------- 2 files changed, 40 insertions(+), 152 deletions(-) diff --git a/research/Cargo.lock b/research/Cargo.lock index 06ead78..0d35014 100644 --- a/research/Cargo.lock +++ b/research/Cargo.lock @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -86,9 +86,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cc" -version = "1.1.36" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "shlex", ] @@ -163,7 +163,7 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compensated-summation" version = "0.3.0" -source = "git+https://github.com/niklasf/compensated-summation?branch=feat/default#0503675082edbe199fd9af02f37bbb3fe309d07a" +source = "git+https://github.com/niklasf/compensated-summation?branch=feat/default#bc09da76bb26fcb29fe2721c11fe80a1bb937b38" dependencies = [ "num-traits", ] @@ -307,9 +307,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heck" @@ -370,7 +370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.0", "serde", ] @@ -460,9 +460,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "ordered-float" -version = "4.5.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c65ee1f9701bf938026630b455d5315f490640234259037edb259798b3bcf85e" +checksum = "83e7ccb95e240b7c9506a3d544f10d935e142cc90b0a1d56954fb44d89ad6b97" dependencies = [ "num-traits", ] @@ -599,9 +599,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -610,18 +610,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.68" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.68" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", diff --git a/research/src/bin/replay_vanilla_glicko2.rs b/research/src/bin/replay_vanilla_glicko2.rs index 8f632bd..beb7b46 100644 --- a/research/src/bin/replay_vanilla_glicko2.rs +++ b/research/src/bin/replay_vanilla_glicko2.rs @@ -1,18 +1,17 @@ use std::{error::Error as StdError, f64::consts::PI, io}; use compensated_summation::KahanBabuskaNeumaier; -use glicko2::{GameResult, Glicko2Rating, GlickoRating}; +use glicko2::{GameResult, Glicko2Rating}; use liglicko2::{deviance, Score}; use liglicko2_research::{ - encounter::{BySpeed, PgnResult, RawEncounter, Speed, UtcDateTime}, + encounter::{BySpeed, PgnResult, RawEncounter, UtcDateTime}, player::{ByPlayerId, PlayerIds}, }; -use ordered_float::OrderedFloat; #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -const WHITE_ADVANTAGE: f64 = 0.0; // 12.0 / 173.7178 +const WHITE_ADVANTAGE: f64 = 12.0 / 173.7178; #[derive(Debug, Default)] struct PlayerState { @@ -24,7 +23,7 @@ impl PlayerState { fn live_rating(&self) -> Glicko2Rating { let unbounded = glicko2::new_rating(self.rating, &self.pending, 0.2).unwrap_or_else(|err| { - eprintln!("{}: {:?}", err, self); + println!("{}: {:?}", err, self); Glicko2Rating::unrated() }); @@ -39,28 +38,19 @@ impl PlayerState { self.rating = self.live_rating(); self.pending.clear(); } - - fn default_with_rating(rating: f64) -> Self { - Self { - rating: Glicko2Rating { - value: rating, - ..Glicko2Rating::unrated() - }, - pending: Vec::new(), - } - } } -fn _expectation_value(white: Glicko2Rating, black: Glicko2Rating) -> Score { +fn expectation_value(white: Glicko2Rating, black: Glicko2Rating) -> Score { Score( 1.0 / (1.0 + f64::exp( - -_g(f64::hypot(white.deviation, black.deviation)) * (white.value - black.value), + -g(f64::hypot(white.deviation, black.deviation)) + * (white.value - black.value), )), ) } -fn _g(deviation: f64) -> f64 { +fn g(deviation: f64) -> f64 { 1.0 / f64::sqrt(1.0 + 3.0 * deviation.powi(2) / PI.powi(2)) } @@ -71,70 +61,6 @@ fn with_offset(rating: Glicko2Rating, offset: f64) -> Glicko2Rating { } } -fn glicko_g(rd: f64) -> f64 { - let q = f64::ln(10.0) / 400.0; - 1.0 / f64::sqrt(1.0 + 3.0 * q.powi(2) * rd.powi(2) / PI.powi(2)) -} - -fn glicko_expectation_value(white: GlickoRating, black: GlickoRating) -> Score { - Score( - 1.0 / (1.0 + f64::powf(10.0, -glicko_g(f64::hypot(white.deviation, black.deviation)) * (white.value - black.value) / 400.0)) - ) -} - -#[derive(Default)] -struct Stats { - values: Vec, -} - -impl Stats { - pub fn add(&mut self, value: f64) { - self.values.push(value); - } - - pub fn prepare(&mut self) { - self.values.sort_by_key(|&value| OrderedFloat(value)); - } - - pub fn mean(&self) -> f64 { - let mut sum = KahanBabuskaNeumaier::default(); - for &value in &self.values { - sum += value; - } - sum.total() / self.values.len() as f64 - } - - pub fn percentile(&self, percentile: usize) -> f64 { - let index = self.values.len() * percentile / 100; - self.values - .get(index) - .copied() - .unwrap_or_else(|| self.values.last().copied().unwrap_or(f64::NAN)) - } - - pub fn csv_header(prefix: &str) -> String { - format!("{prefix}_mean,{prefix}_p0,{prefix}_p10,{prefix}_p20,{prefix}_p30,{prefix}_p40,{prefix}_p50,{prefix}_p60,{prefix}_p70,{prefix}_p80,{prefix}_p90,{prefix}_p100") - } - - pub fn csv(&self) -> String { - format!( - "{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6}", - self.mean(), - self.percentile(0), - self.percentile(10), - self.percentile(20), - self.percentile(30), - self.percentile(40), - self.percentile(50), - self.percentile(60), - self.percentile(70), - self.percentile(80), - self.percentile(90), - self.percentile(100), - ) - } -} - fn main() -> Result<(), Box> { let mut reader = csv::Reader::from_reader(io::stdin().lock()); @@ -143,80 +69,44 @@ fn main() -> Result<(), Box> { let mut last_rating_period = UtcDateTime::default(); let mut total_encounters: u64 = 0; let mut total_deviance = KahanBabuskaNeumaier::default(); - let mut initial_rating = 0.0; - - println!( - "rating_period,avg_deviance,encounters,players,{},{},{}", - Stats::csv_header("rating"), - Stats::csv_header("deviation"), - Stats::csv_header("volatility") - ); for encounter in reader.deserialize() { let encounter: RawEncounter = encounter?; - let speed = encounter.time_control.speed(); - - if speed != Speed::Blitz { - continue; - } // Commit rating period - if encounter.utc_date_time.as_seconds() > last_rating_period.as_seconds() + 7 * 24 * 60 * 60 { - let mut rating_stats = Stats::default(); - let mut deviation_stats = Stats::default(); - let mut volatility_stats = Stats::default(); - + if encounter.utc_date_time.as_seconds() > last_rating_period.as_seconds() + 24 * 60 * 60 { for states in states.values_mut() { for state in states.values_mut() { if let Some(state) = state { state.commit(); - - rating_stats.add(state.rating.value); - deviation_stats.add(state.rating.deviation); - volatility_stats.add(state.rating.volatility); } } } - last_rating_period = encounter.utc_date_time; // Close enough, because encounters are dense - - rating_stats.prepare(); - deviation_stats.prepare(); - volatility_stats.prepare(); - println!( - "{},{:.6},{},{},{},{},{}", + "Rating period ending at {}: avg deviance {:.6} over {} encounters", last_rating_period, total_deviance.total() / total_encounters as f64, - total_encounters, - players.len(), - rating_stats.csv(), - deviation_stats.csv(), - volatility_stats.csv(), + total_encounters ); - - initial_rating = rating_stats.percentile(50); - if initial_rating.is_nan() { - initial_rating = 0.0; - } } // Update deviance using live ratings let white = players.get_or_insert(encounter.white); let black = players.get_or_insert(encounter.black); - let states = states.get_mut(speed); + let states = states.get_mut(encounter.time_control.speed()); total_deviance += deviance( - glicko_expectation_value( + expectation_value( states .get(white) - .map_or_else(|| PlayerState::default_with_rating(initial_rating).rating, |state| state.live_rating()).into(), + .map_or_else(Glicko2Rating::unrated, |state| state.live_rating()), with_offset( states .get(black) - .map_or_else(|| PlayerState::default_with_rating(initial_rating).rating, |state| state.live_rating()), + .map_or_else(Glicko2Rating::unrated, |state| state.live_rating()), -WHITE_ADVANTAGE, - ).into(), + ), ), if let Some(actual) = encounter.result.white_score() { actual @@ -228,22 +118,20 @@ fn main() -> Result<(), Box> { // Record game result as pending in rating period let white_rating = with_offset( - states.get(white).map_or_else( - || PlayerState::default_with_rating(initial_rating).rating, - |state| state.rating, - ), + states + .get(white) + .map_or_else(Glicko2Rating::unrated, |state| state.rating), WHITE_ADVANTAGE, ); let black_rating = with_offset( - states.get(black).map_or_else( - || PlayerState::default_with_rating(initial_rating).rating, - |state| state.rating, - ), + states + .get(black) + .map_or_else(Glicko2Rating::unrated, |state| state.rating), -WHITE_ADVANTAGE, ); states - .get_mut_or_insert_with(white, || PlayerState::default_with_rating(initial_rating)) + .get_mut_or_insert_with(white, PlayerState::default) .pending .push(match encounter.result { PgnResult::WhiteWins => GameResult::win(black_rating), @@ -253,7 +141,7 @@ fn main() -> Result<(), Box> { }); states - .get_mut_or_insert_with(black, || PlayerState::default_with_rating(initial_rating)) + .get_mut_or_insert_with(black, PlayerState::default) .pending .push(match encounter.result { PgnResult::WhiteWins => GameResult::loss(white_rating),