diff --git a/benches/cri_interval.rs b/benches/cri_interval.rs index 0fa0006..0fd67f6 100644 --- a/benches/cri_interval.rs +++ b/benches/cri_interval.rs @@ -1,7 +1,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use brumby::interval; -use brumby::interval::IntervalConfig; +use brumby::interval::{IntervalConfig, other_player}; fn criterion_benchmark(c: &mut Criterion) { fn run(intervals: u8) -> usize { @@ -11,7 +11,9 @@ fn criterion_benchmark(c: &mut Criterion) { away_prob: 0.25, common_prob: 0.25, max_total_goals: u16::MAX, - }).scenarios.len() + home_scorers: other_player(), + away_scorers: other_player(), + }).prospects.len() } // sanity check diff --git a/src/bin/soccer.rs b/src/bin/soccer.rs index 72e9a96..4fadc64 100644 --- a/src/bin/soccer.rs +++ b/src/bin/soccer.rs @@ -12,7 +12,8 @@ use brumby::market::{Market, Overround, OverroundMethod, PriceBounds}; use brumby::opt::{hypergrid_search, HypergridSearchConfig, HypergridSearchOutcome}; use brumby::probs::SliceExt; use brumby::scoregrid; -use brumby::scoregrid::{MarketType, OutcomeType, Over, Score, Side}; +use brumby::scoregrid::{MarketType, OutcomeType, Over, Player, Score, Side}; +use Player::Named; const OVERROUND_METHOD: OverroundMethod = OverroundMethod::OddsRatio; const SINGLE_PRICE_BOUNDS: PriceBounds = 1.04..=200.0; @@ -34,44 +35,44 @@ fn verona_vs_leece() -> HashMap { let correct_score = HashMap::from([ // home wins - (OutcomeType::Exact(Score::new(1, 0)), 7.0), - (OutcomeType::Exact(Score::new(2, 0)), 12.0), - (OutcomeType::Exact(Score::new(2, 1)), 10.0), - (OutcomeType::Exact(Score::new(3, 0)), 26.0), - (OutcomeType::Exact(Score::new(3, 1)), 23.0), - (OutcomeType::Exact(Score::new(3, 2)), 46.0), - (OutcomeType::Exact(Score::new(4, 0)), 101.0), - (OutcomeType::Exact(Score::new(4, 1)), 81.0), - (OutcomeType::Exact(Score::new(4, 2)), 126.0), - (OutcomeType::Exact(Score::new(4, 3)), 200.0), - (OutcomeType::Exact(Score::new(5, 0)), 200.0), - (OutcomeType::Exact(Score::new(5, 1)), 200.0), - (OutcomeType::Exact(Score::new(5, 2)), 200.0), - (OutcomeType::Exact(Score::new(5, 3)), 200.0), - (OutcomeType::Exact(Score::new(5, 4)), 200.0), + (OutcomeType::Score(Score::new(1, 0)), 7.0), + (OutcomeType::Score(Score::new(2, 0)), 12.0), + (OutcomeType::Score(Score::new(2, 1)), 10.0), + (OutcomeType::Score(Score::new(3, 0)), 26.0), + (OutcomeType::Score(Score::new(3, 1)), 23.0), + (OutcomeType::Score(Score::new(3, 2)), 46.0), + (OutcomeType::Score(Score::new(4, 0)), 101.0), + (OutcomeType::Score(Score::new(4, 1)), 81.0), + (OutcomeType::Score(Score::new(4, 2)), 126.0), + (OutcomeType::Score(Score::new(4, 3)), 200.0), + (OutcomeType::Score(Score::new(5, 0)), 200.0), + (OutcomeType::Score(Score::new(5, 1)), 200.0), + (OutcomeType::Score(Score::new(5, 2)), 200.0), + (OutcomeType::Score(Score::new(5, 3)), 200.0), + (OutcomeType::Score(Score::new(5, 4)), 200.0), // draws - (OutcomeType::Exact(Score::new(0, 0)), 6.75), - (OutcomeType::Exact(Score::new(1, 1)), 5.90), - (OutcomeType::Exact(Score::new(2, 2)), 16.00), - (OutcomeType::Exact(Score::new(3, 3)), 101.00), - (OutcomeType::Exact(Score::new(4, 4)), 200.00), - (OutcomeType::Exact(Score::new(5, 5)), 200.00), + (OutcomeType::Score(Score::new(0, 0)), 6.75), + (OutcomeType::Score(Score::new(1, 1)), 5.90), + (OutcomeType::Score(Score::new(2, 2)), 16.00), + (OutcomeType::Score(Score::new(3, 3)), 101.00), + (OutcomeType::Score(Score::new(4, 4)), 200.00), + (OutcomeType::Score(Score::new(5, 5)), 200.00), // away wins - (OutcomeType::Exact(Score::new(0, 1)), 7.25), - (OutcomeType::Exact(Score::new(0, 2)), 12.0), - (OutcomeType::Exact(Score::new(1, 2)), 10.5), - (OutcomeType::Exact(Score::new(0, 3)), 31.0), - (OutcomeType::Exact(Score::new(1, 3)), 23.0), - (OutcomeType::Exact(Score::new(2, 3)), 41.0), - (OutcomeType::Exact(Score::new(0, 4)), 101.0), - (OutcomeType::Exact(Score::new(1, 4)), 81.0), - (OutcomeType::Exact(Score::new(2, 4)), 151.0), - (OutcomeType::Exact(Score::new(3, 4)), 200.0), - (OutcomeType::Exact(Score::new(0, 5)), 200.0), - (OutcomeType::Exact(Score::new(1, 5)), 200.0), - (OutcomeType::Exact(Score::new(2, 5)), 200.0), - (OutcomeType::Exact(Score::new(3, 5)), 200.0), - (OutcomeType::Exact(Score::new(4, 5)), 200.0), + (OutcomeType::Score(Score::new(0, 1)), 7.25), + (OutcomeType::Score(Score::new(0, 2)), 12.0), + (OutcomeType::Score(Score::new(1, 2)), 10.5), + (OutcomeType::Score(Score::new(0, 3)), 31.0), + (OutcomeType::Score(Score::new(1, 3)), 23.0), + (OutcomeType::Score(Score::new(2, 3)), 41.0), + (OutcomeType::Score(Score::new(0, 4)), 101.0), + (OutcomeType::Score(Score::new(1, 4)), 81.0), + (OutcomeType::Score(Score::new(2, 4)), 151.0), + (OutcomeType::Score(Score::new(3, 4)), 200.0), + (OutcomeType::Score(Score::new(0, 5)), 200.0), + (OutcomeType::Score(Score::new(1, 5)), 200.0), + (OutcomeType::Score(Score::new(2, 5)), 200.0), + (OutcomeType::Score(Score::new(3, 5)), 200.0), + (OutcomeType::Score(Score::new(4, 5)), 200.0), ]); HashMap::from([ @@ -93,50 +94,99 @@ fn atlanta_vs_sporting_lisbon() -> HashMap { let correct_score = HashMap::from([ // home wins - (OutcomeType::Exact(Score::new(1, 0)), 9.25), - (OutcomeType::Exact(Score::new(2, 0)), 11.5), - (OutcomeType::Exact(Score::new(2, 1)), 8.75), - (OutcomeType::Exact(Score::new(3, 0)), 19.0), - (OutcomeType::Exact(Score::new(3, 1)), 15.0), - (OutcomeType::Exact(Score::new(3, 2)), 21.0), - (OutcomeType::Exact(Score::new(4, 0)), 46.0), - (OutcomeType::Exact(Score::new(4, 1)), 34.0), - (OutcomeType::Exact(Score::new(4, 2)), 61.0), - (OutcomeType::Exact(Score::new(4, 3)), 126.0), - (OutcomeType::Exact(Score::new(5, 0)), 126.0), - (OutcomeType::Exact(Score::new(5, 1)), 126.0), - (OutcomeType::Exact(Score::new(5, 2)), 176.0), - (OutcomeType::Exact(Score::new(5, 3)), 200.0), - (OutcomeType::Exact(Score::new(5, 4)), 200.0), + (OutcomeType::Score(Score::new(1, 0)), 9.25), + (OutcomeType::Score(Score::new(2, 0)), 11.5), + (OutcomeType::Score(Score::new(2, 1)), 8.75), + (OutcomeType::Score(Score::new(3, 0)), 19.0), + (OutcomeType::Score(Score::new(3, 1)), 15.0), + (OutcomeType::Score(Score::new(3, 2)), 21.0), + (OutcomeType::Score(Score::new(4, 0)), 46.0), + (OutcomeType::Score(Score::new(4, 1)), 34.0), + (OutcomeType::Score(Score::new(4, 2)), 61.0), + (OutcomeType::Score(Score::new(4, 3)), 126.0), + (OutcomeType::Score(Score::new(5, 0)), 126.0), + (OutcomeType::Score(Score::new(5, 1)), 126.0), + (OutcomeType::Score(Score::new(5, 2)), 176.0), + (OutcomeType::Score(Score::new(5, 3)), 200.0), + (OutcomeType::Score(Score::new(5, 4)), 200.0), // draws - (OutcomeType::Exact(Score::new(0, 0)), 13.0), - (OutcomeType::Exact(Score::new(1, 1)), 7.0), - (OutcomeType::Exact(Score::new(2, 2)), 12.5), - (OutcomeType::Exact(Score::new(3, 3)), 51.00), - (OutcomeType::Exact(Score::new(4, 4)), 200.00), - (OutcomeType::Exact(Score::new(5, 5)), 200.00), + (OutcomeType::Score(Score::new(0, 0)), 13.0), + (OutcomeType::Score(Score::new(1, 1)), 7.0), + (OutcomeType::Score(Score::new(2, 2)), 12.5), + (OutcomeType::Score(Score::new(3, 3)), 51.00), + (OutcomeType::Score(Score::new(4, 4)), 200.00), + (OutcomeType::Score(Score::new(5, 5)), 200.00), // away wins - (OutcomeType::Exact(Score::new(0, 1)), 14.0), - (OutcomeType::Exact(Score::new(0, 2)), 21.0), - (OutcomeType::Exact(Score::new(1, 2)), 12.5), - (OutcomeType::Exact(Score::new(0, 3)), 41.0), - (OutcomeType::Exact(Score::new(1, 3)), 26.0), - (OutcomeType::Exact(Score::new(2, 3)), 34.0), - (OutcomeType::Exact(Score::new(0, 4)), 126.0), - (OutcomeType::Exact(Score::new(1, 4)), 81.0), - (OutcomeType::Exact(Score::new(2, 4)), 101.0), - (OutcomeType::Exact(Score::new(3, 4)), 151.0), - (OutcomeType::Exact(Score::new(0, 5)), 200.0), - (OutcomeType::Exact(Score::new(1, 5)), 200.0), - (OutcomeType::Exact(Score::new(2, 5)), 200.0), - (OutcomeType::Exact(Score::new(3, 5)), 200.0), - (OutcomeType::Exact(Score::new(4, 5)), 200.0), + (OutcomeType::Score(Score::new(0, 1)), 14.0), + (OutcomeType::Score(Score::new(0, 2)), 21.0), + (OutcomeType::Score(Score::new(1, 2)), 12.5), + (OutcomeType::Score(Score::new(0, 3)), 41.0), + (OutcomeType::Score(Score::new(1, 3)), 26.0), + (OutcomeType::Score(Score::new(2, 3)), 34.0), + (OutcomeType::Score(Score::new(0, 4)), 126.0), + (OutcomeType::Score(Score::new(1, 4)), 81.0), + (OutcomeType::Score(Score::new(2, 4)), 101.0), + (OutcomeType::Score(Score::new(3, 4)), 151.0), + (OutcomeType::Score(Score::new(0, 5)), 200.0), + (OutcomeType::Score(Score::new(1, 5)), 200.0), + (OutcomeType::Score(Score::new(2, 5)), 200.0), + (OutcomeType::Score(Score::new(3, 5)), 200.0), + (OutcomeType::Score(Score::new(4, 5)), 200.0), + ]); + + let first_goalscorer = HashMap::from([ + (OutcomeType::Player(Named(Side::Home, "Muriel".into())), 5.25), + (OutcomeType::Player(Named(Side::Home, "Scamacca".into())), 5.8), + (OutcomeType::Player(Named(Side::Home, "Lookman".into())), 6.25), + (OutcomeType::Player(Named(Side::Home, "Miranchuk".into())), 7.75), + (OutcomeType::Player(Named(Side::Home, "Pasalic".into())), 9.0), + (OutcomeType::Player(Named(Side::Home, "Koopmeiners".into())), 9.25), + (OutcomeType::Player(Named(Side::Home, "Ederson".into())), 9.5), + (OutcomeType::Player(Named(Side::Home, "Cisse".into())), 9.5), + (OutcomeType::Player(Named(Side::Home, "Bakker".into())), 9.75), + (OutcomeType::Player(Named(Side::Home, "Holm".into())), 11.0), + (OutcomeType::Player(Named(Side::Home, "Toloi".into())), 16.0), + (OutcomeType::Player(Named(Side::Home, "Hateboer".into())), 17.0), + (OutcomeType::Player(Named(Side::Home, "Mendicino".into())), 18.0), + (OutcomeType::Player(Named(Side::Home, "Scalvini".into())), 21.0), + (OutcomeType::Player(Named(Side::Home, "Bonfanti".into())), 21.0), + (OutcomeType::Player(Named(Side::Home, "Adopo".into())), 23.0), + (OutcomeType::Player(Named(Side::Home, "Zortea".into())), 23.0), + (OutcomeType::Player(Named(Side::Home, "Kolasinac".into())), 23.0), + (OutcomeType::Player(Named(Side::Home, "Djimsiti".into())), 26.0), + (OutcomeType::Player(Named(Side::Home, "De Roon".into())), 26.0), + (OutcomeType::Player(Named(Side::Home, "Ruggeri".into())), 31.0), + (OutcomeType::Player(Named(Side::Home, "Del Lungo".into())), 61.0), + (OutcomeType::Player(Named(Side::Away, "Gyokeres".into())), 6.25), + (OutcomeType::Player(Named(Side::Away, "Santos".into())), 8.5), + (OutcomeType::Player(Named(Side::Away, "Paulinho".into())), 8.75), + (OutcomeType::Player(Named(Side::Away, "Pote".into())), 8.75), + (OutcomeType::Player(Named(Side::Away, "Edwards".into())), 9.75), + (OutcomeType::Player(Named(Side::Away, "Ribeiro".into())), 10.5), + (OutcomeType::Player(Named(Side::Away, "Trincao".into())), 11.0), + (OutcomeType::Player(Named(Side::Away, "Moreira".into())), 13.0), + (OutcomeType::Player(Named(Side::Away, "Morita".into())), 15.0), + (OutcomeType::Player(Named(Side::Away, "Braganca".into())), 21.0), + (OutcomeType::Player(Named(Side::Away, "Catamo".into())), 29.0), + (OutcomeType::Player(Named(Side::Away, "Essugo".into())), 31.0), + (OutcomeType::Player(Named(Side::Away, "Reis".into())), 31.0), + (OutcomeType::Player(Named(Side::Away, "Esgaio".into())), 31.0), + (OutcomeType::Player(Named(Side::Away, "St. Juste".into())), 34.0), + (OutcomeType::Player(Named(Side::Away, "Hjulmand".into())), 34.0), + (OutcomeType::Player(Named(Side::Away, "Coates".into())), 34.0), + (OutcomeType::Player(Named(Side::Away, "Diomande".into())), 41.0), + (OutcomeType::Player(Named(Side::Away, "Quaresma".into())), 51.0), + (OutcomeType::Player(Named(Side::Away, "Inacio".into())), 51.0), + (OutcomeType::Player(Named(Side::Away, "Fresneda".into())), 61.0), + (OutcomeType::Player(Named(Side::Away, "Neto".into())), 71.0), + (OutcomeType::None, 11.5), ]); HashMap::from([ (MarketType::HeadToHead, h2h), (MarketType::TotalGoalsOverUnder(Over(2)), goals_ou), (MarketType::CorrectScore, correct_score), + (MarketType::FirstGoalscorer, first_goalscorer), ]) } @@ -145,6 +195,7 @@ pub fn main() { let correct_score_prices = ext_markets[&MarketType::CorrectScore].clone(); let h2h_prices = ext_markets[&MarketType::HeadToHead].clone(); let goals_ou_prices = ext_markets[&MarketType::TotalGoalsOverUnder(Over(2))].clone(); + let first_gs = ext_markets[&MarketType::FirstGoalscorer].clone(); let h2h = fit_market(&h2h_prices); println!("h2h: {h2h:?}"); @@ -152,6 +203,8 @@ pub fn main() { println!("goals_ou: {goals_ou:?}"); let correct_score = fit_market(&correct_score_prices); println!("correct_score: {correct_score:?}"); + let first_gs = fit_market(&first_gs); + println!("first_gs: {first_gs:?}"); println!("*** fitting scoregrid ***"); let start = Instant::now(); diff --git a/src/interval.rs b/src/interval.rs index a2cd1be..c5bf242 100644 --- a/src/interval.rs +++ b/src/interval.rs @@ -1,24 +1,29 @@ +use std::collections::BTreeMap; + use rustc_hash::FxHashMap; use strum::IntoEnumIterator; -use crate::scoregrid::{GoalEvent, Score}; + +use crate::scoregrid::{GoalEvent, Player, Score}; #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct Scenario { +pub struct Prospect { pub score: Score, + pub scorers: BTreeMap } -impl Default for Scenario { +impl Default for Prospect { fn default() -> Self { Self { score: Score { home: 0, away: 0}, + scorers: Default::default() } } } -pub type Scenarios = FxHashMap; +pub type Prospects = FxHashMap; -pub fn init_scenarios(capacity: usize) -> Scenarios { - Scenarios::with_capacity_and_hasher(capacity, Default::default()) +pub fn init_prospects(capacity: usize) -> Prospects { + Prospects::with_capacity_and_hasher(capacity, Default::default()) } #[derive(Debug)] @@ -27,131 +32,167 @@ pub struct IntervalConfig { pub home_prob: f64, pub away_prob: f64, pub common_prob: f64, - pub max_total_goals: u16 + pub max_total_goals: u16, + // pub to_score: Option<(Player, f64)> + pub home_scorers: Vec<(Player, f64)>, + pub away_scorers: Vec<(Player, f64)> } -// fn explore_interval(config: &IntervalConfig) -> Scenarios { -// let mut scenarios = init_scenarios(4); -// scenarios.insert( -// Scenario { -// score: Score { home: 0, away: 0 }, -// }, -// 1.0 - config.home_prob - config.away_prob - config.common_prob, -// ); -// scenarios.insert( -// Scenario { -// score: Score { home: 1, away: 0 }, -// }, -// config.home_prob, -// ); -// scenarios.insert( -// Scenario { -// score: Score { home: 0, away: 1 }, -// }, -// config.away_prob, -// ); -// scenarios.insert( -// Scenario { -// score: Score { home: 1, away: 1 }, -// }, -// config.common_prob, -// ); -// scenarios -// } -// -// fn fold_intervals(a: &Scenarios, b: &Scenarios) -> Scenarios { -// let mut mutations = init_scenarios(a.len() * b.len()); -// for (a_scenario, a_prob) in a { -// for (b_scenario, b_prob) in b { -// let mutation = Scenario { -// score: Score { -// home: a_scenario.score.home + b_scenario.score.home, -// away: a_scenario.score.away + b_scenario.score.away, -// }, -// }; -// let mutation_prob = a_prob * b_prob; -// mutations -// .entry(mutation) -// .and_modify(|prob| *prob += mutation_prob) -// .or_insert(mutation_prob); -// } -// } -// mutations -// } -// -// pub fn explore_all(config: &IntervalConfig) -> Scenarios { -// let mut scenarios = explore_interval(config); -// for _ in 1..config.intervals { -// let next_scenarios = explore_interval(config); -// scenarios = fold_intervals(&scenarios, &next_scenarios); -// } -// scenarios -// } +pub fn other_player() -> Vec<(Player, f64)> { + vec![(Player::Other, 1.0)] +} #[derive(Debug)] pub struct Exploration { - pub scenarios: Scenarios, + pub prospects: Prospects, pub pruned: f64 } +#[derive(Debug)] +struct PartialProspect<'a> { + home_scorer: Option<&'a Player>, + away_scorer: Option<&'a Player>, + prob: f64, +} + pub fn explore_all(config: &IntervalConfig) -> Exploration { - let mut current_scenarios = init_scenarios(1); - current_scenarios.insert(Scenario::default(), 1.0); + let mut current_prospects = init_prospects(1); + current_prospects.insert(Prospect::default(), 1.0); let neither_prob = 1.0 - config.home_prob - config.away_prob - config.common_prob; + // let home_other_prob = 1.0 - config.to_score.filter(|(player, prob)| matches!(player, Player::Named(Side::Home, _))).map(|(_, prob)| prob).unwrap_or(0.0); + // let away_other_prob = 1.0 - config.to_score.filter(|(player, prob)| matches!(player, Player::Named(Side::Away, _))).map(|(_, prob)| prob).unwrap_or(0.0); let mut pruned = 0.0; + // let home_scorers = match &config.to_score { + // None => { + // vec![(&Player::Other, 1.0)] + // }, + // Some((player, prob)) => { + // match player { + // Player::Named(Side::Home, _) => { + // vec![(player, *prob), (&Player::Other, 1.0 - *prob)] + // } + // _ => { + // vec![(&Player::Other, 1.0)] + // } + // } + // } + // }; + // let away_scorers = match &config.to_score { + // None => { + // vec![(&Player::Other, 1.0)] + // }, + // Some((player, prob)) => { + // match player { + // Player::Named(Side::Away, _) => { + // vec![(player, *prob), (&Player::Other, 1.0 - *prob)] + // } + // _ => { + // vec![(&Player::Other, 1.0)] + // } + // } + // } + // }; for _ in 0..config.intervals { - let mut next_scenarios = init_scenarios(current_scenarios.len() * 4); + let mut next_prospects = init_prospects(current_prospects.len() * 4); for goal_event in GoalEvent::iter() { - let (next, next_prob) = match goal_event { + match goal_event { GoalEvent::Neither => { - (Scenario { - score: Score { home: 0, away: 0 }, - }, neither_prob) + let partial = PartialProspect { + home_scorer: None, + away_scorer: None, + prob: neither_prob + }; + pruned += merge(config, ¤t_prospects, partial, &mut next_prospects); } GoalEvent::Home => { - (Scenario { - score: Score { home: 1, away: 0 }, - }, config.home_prob) + for (player, player_prob) in &config.home_scorers { + let partial = PartialProspect { + home_scorer: Some(player), + away_scorer: None, + prob: config.home_prob * player_prob + }; + pruned += merge(config, ¤t_prospects, partial, &mut next_prospects); + } } GoalEvent::Away => { - (Scenario { - score: Score { home: 0, away: 1 }, - }, config.away_prob) + for (player, player_prob) in &config.away_scorers { + let partial = PartialProspect { + home_scorer: None, + away_scorer: Some(player), + prob: config.away_prob * player_prob + }; + pruned += merge(config, ¤t_prospects, partial, &mut next_prospects); + } } GoalEvent::Both => { - (Scenario { - score: Score { home: 1, away: 1 }, - }, config.common_prob) + for (home_player, home_player_prob) in &config.home_scorers { + for (away_player, away_player_prob) in &config.away_scorers { + let partial = PartialProspect { + home_scorer: Some(home_player), + away_scorer: Some(away_player), + prob: config.common_prob * home_player_prob * away_player_prob + }; + pruned += merge(config, ¤t_prospects, partial, &mut next_prospects); + } + } } }; - for (current, current_prob) in ¤t_scenarios { - let merged_prob = current_prob * next_prob; - if current.score.total() + next.score.total() > config.max_total_goals { - pruned += merged_prob; - continue; - } - let merged = Scenario { - score: Score { - home: current.score.home + next.score.home, - away: current.score.away + next.score.away, - }, - }; - next_scenarios - .entry(merged) - .and_modify(|prob| *prob += merged_prob) - .or_insert(merged_prob); - } + // for (current, current_prob) in ¤t_prospects { + // let merged_prob = current_prob * next_prob; + // if current.score.total() + next.score.total() > config.max_total_goals { + // pruned += merged_prob; + // continue; + // } + // let merged = Prospect { + // score: Score { + // home: current.score.home + next.score.home, + // away: current.score.away + next.score.away, + // }, + // }; + // next_prospects + // .entry(merged) + // .and_modify(|prob| *prob += merged_prob) + // .or_insert(merged_prob); + // } } - current_scenarios = next_scenarios; + current_prospects = next_prospects; } Exploration { - scenarios: current_scenarios, + prospects: current_prospects, pruned } } +#[must_use] +fn merge(config: &IntervalConfig, current_prospects: &Prospects, partial: PartialProspect, next_prospects: &mut Prospects) -> f64 { + let mut pruned = 0.0; + for (current, current_prob) in current_prospects { + let merged_prob = *current_prob * partial.prob; + let partial_goals = partial.home_scorer.map(|_| 1).unwrap_or(0) + partial.away_scorer.map(|_| 1).unwrap_or(0); + if current.score.total() + partial_goals > config.max_total_goals { + pruned += merged_prob; + continue; + } + + let mut merged = current.clone(); + if let Some(scorer) = partial.home_scorer { + merged.scorers.entry(scorer.clone()).and_modify(|count| *count += 1).or_insert(1); + merged.score.home += 1; + } + if let Some(scorer) = partial.away_scorer { + merged.scorers.entry(scorer.clone()).and_modify(|count| *count += 1).or_insert(1); + merged.score.away += 1; + } + next_prospects + .entry(merged) + .and_modify(|prob| *prob += merged_prob) + .or_insert(merged_prob); + } + pruned +} + #[cfg(test)] mod tests; \ No newline at end of file diff --git a/src/interval/tests.rs b/src/interval/tests.rs index ebbeac1..9a2f29a 100644 --- a/src/interval/tests.rs +++ b/src/interval/tests.rs @@ -8,71 +8,98 @@ fn explore_all_2x2() { away_prob: 0.25, common_prob: 0.25, max_total_goals: u16::MAX, + home_scorers: other_player(), + away_scorers: other_player(), }); - assert_eq!(9, exploration.scenarios.len()); - assert_eq!(1.0, exploration.scenarios.values().sum::()); + assert_eq!(9, exploration.prospects.len()); + assert_eq!(1.0, exploration.prospects.values().sum::()); let expected = [ ( - Scenario { + Prospect { score: Score { home: 0, away: 0 }, + scorers: Default::default(), }, 0.0625f64, ), ( - Scenario { + Prospect { score: Score { home: 2, away: 0 }, + scorers: BTreeMap::from([ + (Player::Other, 2) + ]), }, 0.0625, ), ( - Scenario { + Prospect { score: Score { home: 1, away: 1 }, + scorers: BTreeMap::from([ + (Player::Other, 2) + ]), }, 0.25, ), ( - Scenario { + Prospect { score: Score { home: 1, away: 2 }, + scorers: BTreeMap::from([ + (Player::Other, 3) + ]), }, 0.125, ), ( - Scenario { + Prospect { score: Score { home: 0, away: 2 }, + scorers: BTreeMap::from([ + (Player::Other, 2) + ]), }, 0.0625, ), ( - Scenario { + Prospect { score: Score { home: 2, away: 2 }, + scorers: BTreeMap::from([ + (Player::Other, 4) + ]), }, 0.0625, ), ( - Scenario { + Prospect { score: Score { home: 1, away: 0 }, + scorers: BTreeMap::from([ + (Player::Other, 1) + ]), }, 0.125, ), ( - Scenario { + Prospect { score: Score { home: 0, away: 1 }, + scorers: BTreeMap::from([ + (Player::Other, 1) + ]), }, 0.125, ), ( - Scenario { + Prospect { score: Score { home: 2, away: 1 }, + scorers: BTreeMap::from([ + (Player::Other, 3) + ]), }, 0.125, ), ]; - assert_eq!(expected.len(), exploration.scenarios.len()); + assert_eq!(expected.len(), exploration.prospects.len()); assert_eq!(0.0, exploration.pruned); - for (expected_scenario, expected_probability) in expected { + for (expected_prospect, expected_probability) in expected { assert_eq!( &expected_probability, - exploration.scenarios.get(&expected_scenario).unwrap() + exploration.prospects.get(&expected_prospect).expect(&format!("missing {expected_prospect:?}")) ); } } @@ -85,52 +112,71 @@ fn explore_all_2x2_pruned() { away_prob: 0.25, common_prob: 0.25, max_total_goals: 2, + home_scorers: other_player(), + away_scorers: other_player(), }); let expected = [ ( - Scenario { + Prospect { score: Score { home: 0, away: 0 }, + scorers: Default::default() }, 0.0625f64, ), ( - Scenario { + Prospect { score: Score { home: 2, away: 0 }, + scorers: BTreeMap::from([ + (Player::Other, 2) + ]), }, 0.0625, ), ( - Scenario { + Prospect { score: Score { home: 1, away: 1 }, + scorers: BTreeMap::from([ + (Player::Other, 2) + ]), }, 0.25, ), ( - Scenario { + Prospect { score: Score { home: 0, away: 2 }, + scorers: BTreeMap::from([ + (Player::Other, 2) + ]), }, 0.0625, ), ( - Scenario { + Prospect { score: Score { home: 1, away: 0 }, + scorers: BTreeMap::from([ + (Player::Other, 1) + ]), }, 0.125, ), ( - Scenario { + Prospect { score: Score { home: 0, away: 1 }, + scorers: BTreeMap::from([ + (Player::Other, 1) + ]), }, 0.125, ), ]; - assert_eq!(1.0 - 0.3125, exploration.scenarios.values().sum::()); - assert_eq!(expected.len(), exploration.scenarios.len()); + println!("exploration: {exploration:?}"); + assert_eq!(1.0 - 0.3125, exploration.prospects.values().sum::()); + assert_eq!(expected.len(), exploration.prospects.len()); assert_eq!(0.3125, exploration.pruned); - for (expected_scenario, expected_probability) in expected { + for (expected_prospect, expected_probability) in expected { assert_eq!( &expected_probability, - exploration.scenarios.get(&expected_scenario).expect(&format!("missing {expected_scenario:?}")) + exploration.prospects.get(&expected_prospect).expect(&format!("missing {expected_prospect:?}")) ); } } @@ -143,9 +189,11 @@ fn explore_all_3x3() { away_prob: 0.25, common_prob: 0.25, max_total_goals: u16::MAX, + home_scorers: other_player(), + away_scorers: other_player(), }); - assert_eq!(16, exploration.scenarios.len()); - assert_eq!(1.0, exploration.scenarios.values().sum::()); + assert_eq!(16, exploration.prospects.len()); + assert_eq!(1.0, exploration.prospects.values().sum::()); assert_eq!(0.0, exploration.pruned); } @@ -157,8 +205,10 @@ fn explore_all_4x4() { away_prob: 0.25, common_prob: 0.25, max_total_goals: u16::MAX, + home_scorers: other_player(), + away_scorers: other_player(), }); - assert_eq!(25, exploration.scenarios.len()); - assert_eq!(1.0, exploration.scenarios.values().sum::()); + assert_eq!(25, exploration.prospects.len()); + assert_eq!(1.0, exploration.prospects.values().sum::()); assert_eq!(0.0, exploration.pruned); } \ No newline at end of file diff --git a/src/scoregrid.rs b/src/scoregrid.rs index 8c55d69..e6fc384 100644 --- a/src/scoregrid.rs +++ b/src/scoregrid.rs @@ -5,7 +5,7 @@ use multinomial::binomial; use crate::{factorial, multinomial, poisson}; use crate::comb::{count_permutations, pick}; -use crate::interval::{explore_all, IntervalConfig}; +use crate::interval::{explore_all, IntervalConfig, other_player}; use crate::linear::matrix::Matrix; use crate::multinomial::bivariate_binomial; use crate::probs::SliceExt; @@ -199,9 +199,11 @@ pub fn from_interval( home_prob, away_prob, common_prob, - max_total_goals + max_total_goals, + home_scorers: other_player(), + away_scorers: other_player(), }); - for (scenario, prob) in exploration.scenarios { + for (scenario, prob) in exploration.prospects { scoregrid[(scenario.score.home as usize, scenario.score.away as usize)] += prob; } } @@ -296,15 +298,20 @@ pub enum MarketType { AnytimeAssist } +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum Player { + Named(Side, String), + Other +} + #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum OutcomeType { Win(Side), Draw, Under(u8), Over(u8), - Exact(Score), - Named(String), - Other, + Score(Score), + Player(Player), None, } impl OutcomeType { @@ -314,7 +321,7 @@ impl OutcomeType { OutcomeType::Draw => Self::gather_draw(scoregrid), OutcomeType::Under(goals) => Self::gather_goals_under(*goals, scoregrid), OutcomeType::Over(goals) => Self::gather_goals_over(*goals, scoregrid), - OutcomeType::Exact(score) => Self::gather_correct_score(score, scoregrid), + OutcomeType::Score(score) => Self::gather_correct_score(score, scoregrid), _ => unimplemented!() } } diff --git a/src/scoregrid/tests.rs b/src/scoregrid/tests.rs index f31186d..b05ec12 100644 --- a/src/scoregrid/tests.rs +++ b/src/scoregrid/tests.rs @@ -57,11 +57,11 @@ pub fn outcome_correct_score_gather() { let scoregrid = create_test_4x4_scoregrid(); assert_eq!( 0.04, - OutcomeType::Exact(Score::new(0, 0)).gather(&scoregrid) + OutcomeType::Score(Score::new(0, 0)).gather(&scoregrid) ); assert_eq!( 0.08, - OutcomeType::Exact(Score::new(3, 2)).gather(&scoregrid) + OutcomeType::Score(Score::new(3, 2)).gather(&scoregrid) ); }