Transition to Glicko-based scoring system
This commit is contained in:
parent
0477d76322
commit
71b81789f3
|
@ -104,7 +104,10 @@ pub fn new_dataset(
|
|||
r#"CREATE TABLE "{0}_players" (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT,
|
||||
prefix TEXT
|
||||
prefix TEXT,
|
||||
deviation REAL NOT NULL,
|
||||
volatility REAL NOT NULL,
|
||||
last_played INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "{0}_network" (
|
||||
|
@ -196,9 +199,10 @@ pub fn add_players(
|
|||
connection: &Connection,
|
||||
dataset: &str,
|
||||
teams: &Teams<PlayerData>,
|
||||
time: Timestamp,
|
||||
) -> sqlite::Result<()> {
|
||||
let query = format!(
|
||||
r#"INSERT OR IGNORE INTO "{}_players" VALUES (?, ?, ?)"#,
|
||||
r#"INSERT OR IGNORE INTO "{}_players" VALUES (?, ?, ?, ?, ?, ?)"#,
|
||||
dataset
|
||||
);
|
||||
|
||||
|
@ -208,11 +212,56 @@ pub fn add_players(
|
|||
statement.bind((1, id.0 as i64))?;
|
||||
statement.bind((2, name.as_ref().map(|x| &x[..])))?;
|
||||
statement.bind((3, prefix.as_ref().map(|x| &x[..])))?;
|
||||
statement.bind((4, 2.01))?;
|
||||
statement.bind((5, 0.06))?;
|
||||
statement.bind((6, time.0 as i64))?;
|
||||
statement.into_iter().try_for_each(|x| x.map(|_| ()))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_player_data(
|
||||
connection: &Connection,
|
||||
dataset: &str,
|
||||
player: PlayerId,
|
||||
) -> sqlite::Result<(f64, f64, Timestamp)> {
|
||||
let query = format!(
|
||||
r#"SELECT deviation, volatility, last_played FROM "{}_players" WHERE id = ?"#,
|
||||
dataset
|
||||
);
|
||||
|
||||
let mut statement = connection.prepare(&query)?;
|
||||
statement.bind((1, player.0 as i64))?;
|
||||
statement.next()?;
|
||||
Ok((
|
||||
statement.read::<f64, _>("deviation")?,
|
||||
statement.read::<f64, _>("volatility")?,
|
||||
Timestamp(statement.read::<i64, _>("last_played")? as u64),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_player_data(
|
||||
connection: &Connection,
|
||||
dataset: &str,
|
||||
player: PlayerId,
|
||||
last_played: Timestamp,
|
||||
deviation: f64,
|
||||
volatility: f64,
|
||||
) -> sqlite::Result<()> {
|
||||
let query = format!(
|
||||
r#"UPDATE "{}_players" SET deviation = ?, volatility = ?, last_played = ? WHERE id = ?"#,
|
||||
dataset
|
||||
);
|
||||
|
||||
let mut statement = connection.prepare(&query)?;
|
||||
statement.bind((1, deviation))?;
|
||||
statement.bind((2, volatility))?;
|
||||
statement.bind((3, last_played.0 as i64))?;
|
||||
statement.bind((4, player.0 as i64))?;
|
||||
statement.next()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_advantage(
|
||||
connection: &Connection,
|
||||
dataset: &str,
|
||||
|
@ -412,11 +461,11 @@ pub fn initialize_edge(
|
|||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
// Mock a database file in transient memory
|
||||
fn mock_datasets() -> sqlite::Result<Connection> {
|
||||
pub fn mock_datasets() -> sqlite::Result<Connection> {
|
||||
let query = "
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
|
@ -435,7 +484,7 @@ mod tests {
|
|||
|
||||
// Functions to generate placeholder data
|
||||
|
||||
fn metadata() -> DatasetMetadata {
|
||||
pub fn metadata() -> DatasetMetadata {
|
||||
DatasetMetadata {
|
||||
last_sync: Timestamp(1),
|
||||
game_id: VideogameId(0),
|
||||
|
@ -444,7 +493,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn players(num: u64) -> Vec<PlayerData> {
|
||||
pub fn players(num: u64) -> Vec<PlayerData> {
|
||||
(1..=num)
|
||||
.map(|i| PlayerData {
|
||||
id: PlayerId(i),
|
||||
|
@ -477,7 +526,7 @@ mod tests {
|
|||
let connection = mock_datasets()?;
|
||||
new_dataset(&connection, "test", metadata())?;
|
||||
|
||||
add_players(&connection, "test", &vec![players(2)])?;
|
||||
add_players(&connection, "test", &vec![players(2)], Timestamp(0))?;
|
||||
|
||||
let mut statement =
|
||||
connection.prepare("SELECT * FROM dataset_test_players WHERE id = 1")?;
|
||||
|
@ -493,7 +542,7 @@ mod tests {
|
|||
fn edge_insert_get() -> sqlite::Result<()> {
|
||||
let connection = mock_datasets()?;
|
||||
new_dataset(&connection, "test", metadata())?;
|
||||
add_players(&connection, "test", &vec![players(2)])?;
|
||||
add_players(&connection, "test", &vec![players(2)], Timestamp(0))?;
|
||||
|
||||
insert_advantage(&connection, "test", PlayerId(2), PlayerId(1), 1.0)?;
|
||||
|
||||
|
@ -513,7 +562,7 @@ mod tests {
|
|||
fn player_all_edges() -> sqlite::Result<()> {
|
||||
let connection = mock_datasets()?;
|
||||
new_dataset(&connection, "test", metadata())?;
|
||||
add_players(&connection, "test", &vec![players(3)])?;
|
||||
add_players(&connection, "test", &vec![players(3)], Timestamp(0))?;
|
||||
|
||||
insert_advantage(&connection, "test", PlayerId(2), PlayerId(1), 1.0)?;
|
||||
insert_advantage(&connection, "test", PlayerId(1), PlayerId(3), 5.0)?;
|
||||
|
@ -539,7 +588,12 @@ mod tests {
|
|||
|
||||
let connection = mock_datasets()?;
|
||||
new_dataset(&connection, "test", metadata())?;
|
||||
add_players(&connection, "test", &vec![players(num_players)])?;
|
||||
add_players(
|
||||
&connection,
|
||||
"test",
|
||||
&vec![players(num_players)],
|
||||
Timestamp(0),
|
||||
)?;
|
||||
|
||||
for i in 1..=num_players {
|
||||
for j in 1..=num_players {
|
||||
|
@ -557,7 +611,7 @@ mod tests {
|
|||
fn hypoth_adv1() -> sqlite::Result<()> {
|
||||
let connection = mock_datasets()?;
|
||||
new_dataset(&connection, "test", metadata())?;
|
||||
add_players(&connection, "test", &vec![players(2)])?;
|
||||
add_players(&connection, "test", &vec![players(2)], Timestamp(0))?;
|
||||
|
||||
insert_advantage(&connection, "test", PlayerId(1), PlayerId(2), 1.0)?;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::{EntrantId, EventId, PlayerData, PlayerId, QueryUnwrap};
|
||||
use super::{EntrantId, EventId, PlayerData, PlayerId, QueryUnwrap, Timestamp};
|
||||
use cynic::GraphQlResponse;
|
||||
use schema::schema;
|
||||
|
||||
|
@ -42,6 +42,7 @@ struct PageInfo {
|
|||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
struct Set {
|
||||
start_at: Option<Timestamp>,
|
||||
#[arguments(includeByes: true)]
|
||||
#[cynic(flatten)]
|
||||
slots: Vec<SetSlot>,
|
||||
|
@ -81,6 +82,7 @@ pub struct EventSetsResponse {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct SetData {
|
||||
pub time: Timestamp,
|
||||
pub teams: Teams<PlayerData>,
|
||||
pub winner: usize,
|
||||
}
|
||||
|
@ -122,7 +124,11 @@ impl QueryUnwrap<EventSetsVars> for EventSets {
|
|||
.try_collect()
|
||||
})
|
||||
.try_collect()?;
|
||||
Some(SetData { teams, winner })
|
||||
Some(SetData {
|
||||
time: set.start_at?,
|
||||
teams,
|
||||
winner,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
|
177
src/sync.rs
177
src/sync.rs
|
@ -1,3 +1,4 @@
|
|||
use std::f64::consts::PI;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -6,6 +7,77 @@ use crate::error;
|
|||
use crate::queries::*;
|
||||
use sqlite::*;
|
||||
|
||||
// Glicko-2 system calculation
|
||||
|
||||
fn time_adjust(periods: f64, old_dev_sq: f64, volatility: f64) -> f64 {
|
||||
(old_dev_sq + periods * volatility * volatility).sqrt()
|
||||
}
|
||||
|
||||
fn illinois_optimize(fun: impl Fn(f64) -> f64, mut a: f64, mut b: f64) -> f64 {
|
||||
let mut f_a = fun(a);
|
||||
let mut f_b = fun(b);
|
||||
|
||||
while (b - a).abs() > 1e-6 {
|
||||
let c = a + (a - b) * f_a / (f_b - f_a);
|
||||
let f_c = fun(c);
|
||||
if f_c * f_b > 0.0 {
|
||||
f_a = f_a / 2.0;
|
||||
} else {
|
||||
a = b;
|
||||
f_a = f_b;
|
||||
}
|
||||
b = c;
|
||||
f_b = f_c;
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
fn glicko_adjust(
|
||||
advantage: f64,
|
||||
deviation: f64,
|
||||
volatility: f64,
|
||||
other_deviation: f64,
|
||||
won: bool,
|
||||
time: u64,
|
||||
) -> (f64, f64, f64) {
|
||||
// TODO: Turn this into dataset metadata
|
||||
let tau = 0.2;
|
||||
let period = (3600 * 24 * 30) as f64;
|
||||
|
||||
let g_val = 1.0 / (1.0 + 3.0 * other_deviation * other_deviation / PI / PI).sqrt();
|
||||
let exp_val = 1.0 / (1.0 + f64::exp(-g_val * advantage));
|
||||
|
||||
let variance = 1.0 / (g_val * g_val * exp_val * (1.0 - exp_val));
|
||||
|
||||
let score = if won { 1.0 } else { 0.0 };
|
||||
let delta = variance * g_val * (score - exp_val);
|
||||
|
||||
let delta_sq = delta * delta;
|
||||
let dev_sq = deviation * deviation;
|
||||
let a = (volatility * volatility).ln();
|
||||
let vol_fn = |x| {
|
||||
let ex = f64::exp(x);
|
||||
let subf = dev_sq + variance + ex;
|
||||
((ex * (delta_sq - dev_sq - variance - ex)) / 2.0 / subf / subf) - (x - a) / tau / tau
|
||||
};
|
||||
|
||||
let initial_b = if delta_sq > dev_sq + variance {
|
||||
(delta_sq - dev_sq - variance).ln()
|
||||
} else {
|
||||
(1..)
|
||||
.map(|k| vol_fn(a - k as f64 * tau))
|
||||
.find(|x| x >= &0.0)
|
||||
.unwrap()
|
||||
};
|
||||
let vol_new = f64::exp(illinois_optimize(vol_fn, a, initial_b) / 2.0);
|
||||
|
||||
let dev_time = time_adjust(time as f64 / period as f64, dev_sq, vol_new);
|
||||
let dev_new = 1.0 / (1.0 / dev_time / dev_time + 1.0 / variance).sqrt();
|
||||
let adjust = dev_new * dev_new * g_val * (score - exp_val);
|
||||
|
||||
(adjust, dev_new, vol_new)
|
||||
}
|
||||
|
||||
// Extract set data
|
||||
|
||||
fn get_event_sets(event: EventId, auth: &str) -> Option<Vec<SetData>> {
|
||||
|
@ -105,9 +177,9 @@ fn get_tournament_events(metadata: &DatasetMetadata, auth: &str) -> Option<Vec<E
|
|||
|
||||
fn update_from_set(connection: &Connection, dataset: &str, results: SetData) -> sqlite::Result<()> {
|
||||
let players_data = results.teams;
|
||||
add_players(connection, dataset, &players_data)?;
|
||||
add_players(connection, dataset, &players_data, results.time)?;
|
||||
|
||||
// Singles matches are currently not supported
|
||||
// Non-singles matches are currently not supported
|
||||
if players_data.len() != 2 || players_data[0].len() != 1 || players_data[1].len() != 1 {
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -115,36 +187,58 @@ fn update_from_set(connection: &Connection, dataset: &str, results: SetData) ->
|
|||
let mut it = players_data.into_iter();
|
||||
let player1 = it.next().unwrap()[0].id;
|
||||
let player2 = it.next().unwrap()[0].id;
|
||||
drop(it);
|
||||
|
||||
let (deviation1, volatility1, last_played1) = get_player_data(connection, dataset, player1)?;
|
||||
let (deviation2, volatility2, last_played2) = get_player_data(connection, dataset, player1)?;
|
||||
let advantage = match get_advantage(connection, dataset, player1, player2) {
|
||||
Err(e) => Err(e)?,
|
||||
Ok(None) => initialize_edge(connection, dataset, player1, player2)?,
|
||||
Ok(Some(adv)) => adv,
|
||||
};
|
||||
let adjust = 30.0 * (1.0 - 1.0 / (1.0 + 10_f64.powf(advantage / 400.0)));
|
||||
let (adjust1, dev_new1, vol_new1) = glicko_adjust(
|
||||
-advantage,
|
||||
deviation1,
|
||||
volatility1,
|
||||
deviation2,
|
||||
results.winner == 0,
|
||||
results.time.0 - last_played1.0,
|
||||
);
|
||||
let (adjust2, dev_new2, vol_new2) = glicko_adjust(
|
||||
advantage,
|
||||
deviation2,
|
||||
volatility2,
|
||||
deviation1,
|
||||
results.winner == 1,
|
||||
results.time.0 - last_played2.0,
|
||||
);
|
||||
|
||||
if results.winner == 0 {
|
||||
adjust_advantages(connection, dataset, player1, 0.5 * adjust)?;
|
||||
adjust_advantages(connection, dataset, player2, -0.5 * adjust)?;
|
||||
adjust_advantage(
|
||||
connection,
|
||||
dataset,
|
||||
player1,
|
||||
player2,
|
||||
-2.0 * (1.0 - 0.5) * adjust,
|
||||
)?;
|
||||
} else {
|
||||
adjust_advantages(connection, dataset, player1, -0.5 * adjust)?;
|
||||
adjust_advantages(connection, dataset, player2, 0.5 * adjust)?;
|
||||
adjust_advantage(
|
||||
connection,
|
||||
dataset,
|
||||
player1,
|
||||
player2,
|
||||
2.0 * (1.0 - 0.5) * adjust,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
set_player_data(
|
||||
connection,
|
||||
dataset,
|
||||
player1,
|
||||
results.time,
|
||||
dev_new1,
|
||||
vol_new1,
|
||||
)?;
|
||||
set_player_data(
|
||||
connection,
|
||||
dataset,
|
||||
player2,
|
||||
results.time,
|
||||
dev_new2,
|
||||
vol_new2,
|
||||
)?;
|
||||
|
||||
adjust_advantages(connection, dataset, player1, 0.5 * adjust1)?;
|
||||
adjust_advantages(connection, dataset, player2, 0.5 * adjust2)?;
|
||||
adjust_advantage(
|
||||
connection,
|
||||
dataset,
|
||||
player1,
|
||||
player2,
|
||||
(1.0 - 0.5) * (adjust2 - adjust1),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sync_dataset(
|
||||
|
@ -175,3 +269,36 @@ pub fn sync_dataset(
|
|||
}
|
||||
connection.execute("COMMIT;")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::datasets::tests::*;
|
||||
|
||||
#[test]
|
||||
fn glicko_single() -> sqlite::Result<()> {
|
||||
let connection = mock_datasets()?;
|
||||
new_dataset(&connection, "test", metadata())?;
|
||||
let players = players(2).into_iter().map(|x| vec![x]).collect();
|
||||
add_players(&connection, "test", &players, Timestamp(0))?;
|
||||
|
||||
update_from_set(
|
||||
&connection,
|
||||
"test",
|
||||
SetData {
|
||||
time: Timestamp(0),
|
||||
teams: players,
|
||||
winner: 0,
|
||||
},
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"{:?}",
|
||||
get_advantage(&connection, "test", PlayerId(1), PlayerId(2))?.unwrap()
|
||||
);
|
||||
println!("{:?}", get_player_data(&connection, "test", PlayerId(1)));
|
||||
println!("{:?}", get_player_data(&connection, "test", PlayerId(2)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue