use plotters::prelude::*; use smallvec::smallvec; use time::{Date, Month}; use trueskill_tt::{Event, History, Member, Outcome, Team, drift::ConstantDrift}; fn main() { let mut csv = csv::Reader::open("examples/atp.csv").unwrap(); let from = Date::from_calendar_date(1900, Month::January, 1).unwrap(); let time_format = time::format_description::parse("[year]-[month]-[day]").unwrap(); let mut events: Vec> = Vec::new(); for row in csv.records() { let date = Date::parse(&row["time_start"], &time_format).unwrap(); let time = (date - from).whole_days(); if &row["double"] == "t" { events.push(Event { time, teams: smallvec![ Team::with_members([ Member::new(row["w1_id"].to_owned()), Member::new(row["w2_id"].to_owned()), ]), Team::with_members([ Member::new(row["l1_id"].to_owned()), Member::new(row["l2_id"].to_owned()), ]), ], outcome: Outcome::winner(0, 2), }); } else { events.push(Event { time, teams: smallvec![ Team::with_members([Member::new(row["w1_id"].to_owned())]), Team::with_members([Member::new(row["l1_id"].to_owned())]), ], outcome: Outcome::winner(0, 2), }); } } let mut hist: History = History::builder_with_key() .sigma(1.6) .drift(ConstantDrift(0.036)) .convergence(trueskill_tt::ConvergenceOptions { max_iter: 10, epsilon: 0.01, }) .build(); hist.add_events(events).unwrap(); hist.converge().unwrap(); let players = [ ("aggasi", "a092", 38800i64), ("borg", "b058", 30300), ("connors", "c044", 31250), ("courier", "c243", 35750), ("djokovic", "d643", i64::MAX), ("edberg", "e004", 34750), ("federer", "f324", i64::MAX), ("hewitt", "h432", 40750), ("mcenroe", "m047", 33000), ("lendl", "l018", 33750), ("murray", "mc10", 60750), ("nadal", "n409", i64::MAX), ("nastase", "n008", 28750), ("sampras", "s402", i64::MAX), ("wilander", "w023", 32600), ]; let mut x_spec = (f64::MAX, f64::MIN); let mut y_spec = (f64::MAX, f64::MIN); for &(_, id, cutoff) in &players { for (ts, gs) in hist.learning_curve(id) { if ts >= cutoff { continue; } let ts = ts as f64; if ts < x_spec.0 { x_spec.0 = ts; } if ts > x_spec.1 { x_spec.1 = ts; } let upper = gs.mu() + gs.sigma(); let lower = gs.mu() - gs.sigma(); if lower < y_spec.0 { y_spec.0 = lower; } if upper > y_spec.1 { y_spec.1 = upper; } } } let root = SVGBackend::new("plot.svg", (1280, 640)).into_drawing_area(); root.fill(&WHITE).unwrap(); let mut chart = ChartBuilder::on(&root) .margin(5) .x_label_area_size(30) .y_label_area_size(30) .build_cartesian_2d(x_spec.0..x_spec.1, y_spec.0..y_spec.1) .unwrap(); chart.configure_mesh().draw().unwrap(); for (idx, &(player, id, cutoff)) in players.iter().enumerate() { let mut data = Vec::new(); let mut upper = Vec::new(); let mut lower = Vec::new(); for (ts, gs) in hist.learning_curve(id) { if ts >= cutoff { continue; } data.push((ts as f64, gs.mu())); upper.push((ts as f64, gs.mu() + gs.sigma())); lower.push((ts as f64, gs.mu() - gs.sigma())); } let color = Palette99::pick(idx); let band = upper .into_iter() .chain(lower.into_iter().rev()) .collect::>(); chart .plotting_area() .draw(&Polygon::new(band, color.mix(0.15))) .unwrap(); chart .draw_series(LineSeries::new(data, &color)) .unwrap() .label(player) .legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &color)); } chart .configure_series_labels() .background_style(WHITE.mix(0.8)) .border_style(BLACK) .draw() .unwrap(); } mod csv { use std::{ fs::File, io::{self, BufRead, BufReader, Lines}, ops, path::Path, }; pub struct Reader { header_map: Vec, lines: Lines>, } impl Reader { pub fn open>(path: P) -> Result { let mut lines = File::open(path).map(BufReader::new)?.lines(); let header_map = if let Some(header) = lines.next() { let header = header?; header.split(',').map(Into::into).collect::>() } else { Vec::new() }; Ok(Self { header_map, lines }) } pub fn records(&mut self) -> Records<'_> { Records { header_map: &self.header_map, lines: &mut self.lines, } } } pub struct Records<'a> { header_map: &'a Vec, lines: &'a mut Lines>, } impl<'a> Iterator for Records<'a> { type Item = Record<'a>; fn next(&mut self) -> Option { let line = self.lines.next()?; Some(Record { header_map: self.header_map, columns: line.unwrap().split(',').map(Into::into).collect::>(), }) } } pub struct Record<'a> { header_map: &'a Vec, columns: Vec, } impl<'a> ops::Index<&str> for Record<'a> { type Output = str; fn index(&self, index: &str) -> &Self::Output { &self.columns[self .header_map .iter() .position(|header| header == index) .unwrap()] } } }