Skip to content

Commit

Permalink
Derivation of goalscorer and assist
Browse files Browse the repository at this point in the history
  • Loading branch information
ekoutanov committed Dec 21, 2023
1 parent a35f7e5 commit 4cbe271
Show file tree
Hide file tree
Showing 16 changed files with 391 additions and 169 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ chrono = "0.4.31"
clap = { version = "4.4.6", features = ["derive"] }
racing_scraper = "0.0.18"
serde_json = "1.0.107"
stanza = "0.4.0"
stanza = "0.5.1"
tinyrand = "0.5.0"
tokio={version="1.32.0",features=["full"]}
tracing = "0.1.37"
Expand Down
51 changes: 49 additions & 2 deletions brumby-soccer/src/bin/soc_prices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use clap::Parser;
use rustc_hash::FxHashMap;
use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use stanza::style::Styles;
use stanza::table::{Cell, Content, Row, Table};
use tracing::{debug, info};

use brumby::hash_lookup::HashLookup;
Expand All @@ -16,6 +18,8 @@ use brumby::tables;
use brumby_soccer::data::{download_by_id, ContestSummary, SoccerFeedId};
use brumby_soccer::domain::{Offer, OfferType, OutcomeType};
use brumby_soccer::fit::{ErrorType, FittingErrors};
use brumby_soccer::model::player_assist_fitter::PlayerAssistFitter;
use brumby_soccer::model::player_goal_fitter::PlayerGoalFitter;
use brumby_soccer::model::score_fitter::ScoreFitter;
use brumby_soccer::model::{score_fitter, Model, Stub};
use brumby_soccer::{fit, model, print};
Expand Down Expand Up @@ -95,7 +99,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
| OfferType::PlayerShotsOnTarget(_)
| OfferType::AnytimeAssist => {
let implied_booksum = implied_booksum(prices.values());
let expected_overround = prices.len() as f64 * INCREMENTAL_OVERROUND;
let expected_overround = 1.0 + prices.len() as f64 * INCREMENTAL_OVERROUND;
implied_booksum / expected_overround
}
};
Expand All @@ -112,12 +116,23 @@ async fn main() -> Result<(), Box<dyn Error>> {
let score_fitter = ScoreFitter::try_from(score_fitter::Config::default())?;
score_fitter.fit(&mut model, &sample_offers)?;

let player_goal_fitter = PlayerGoalFitter;
player_goal_fitter.fit(&mut model, &sample_offers)?;

let player_assist_fitter = PlayerAssistFitter;
player_assist_fitter.fit(&mut model, &sample_offers)?;

let stubs = sample_offers
.iter()
.filter(|(offer_type, _)| {
matches!(
offer_type,
OfferType::HeadToHead(_) | OfferType::TotalGoals(_, _) | OfferType::CorrectScore(_)
OfferType::HeadToHead(_)
| OfferType::TotalGoals(_, _)
| OfferType::CorrectScore(_)
| OfferType::FirstGoalscorer
| OfferType::AnytimeGoalscorer
| OfferType::AnytimeAssist
)
})
.map(|(_, offer)| Stub {
Expand All @@ -129,6 +144,38 @@ async fn main() -> Result<(), Box<dyn Error>> {
.collect::<Vec<_>>();
model.derive(&stubs, &SINGLE_PRICE_BOUNDS)?;

{
let table = Table::default().with_rows({
const PER_ROW: usize = 4;
let sorted = sort_tuples(model.offers());
let mut rows = vec![];
loop {
let row = sorted
.iter()
.skip(rows.len() * PER_ROW)
.take(PER_ROW)
.map(|(_, offer)| {
let header = format!(
"{:?}\nΣ={:.3}, σ={:.3}, n={}\n",
offer.offer_type,
offer.market.fair_booksum(),
offer.market.offered_booksum(),
offer.market.probs.len(),
);
let nested = print::tabulate_offer(offer);
Cell::from(Content::Composite(vec![header.into(), nested.into()]))
})
.collect::<Vec<_>>();
if row.is_empty() {
break;
}
rows.push(Row::new(Styles::default(), row))
}
rows
});
info!("Derived prices:\n{}", Console::default().render(&table));
}

{
let fitting_errors = model
.offers
Expand Down
12 changes: 7 additions & 5 deletions brumby-soccer/src/bin/soc_prices2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
let anytime_gs = fit_offer(OfferType::AnytimeGoalscorer, &anytime_gs, 1.0);

// println!("scoregrid:\n{}sum: {}", scoregrid.verbose(), scoregrid.flatten().sum());
let draw_prob = isolate(
let nil_all_draw_prob = isolate(
&OfferType::CorrectScore(Period::FullTime),
&OutcomeType::Score(Score { home: 0, away: 0 }),
&exploration.prospects,
Expand Down Expand Up @@ -427,11 +427,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
// }
// let elapsed = start.elapsed();
// println!("player fitting took {elapsed:?}");

debug!("nil-all draw prob: {nil_all_draw_prob}");
let fitted_goalscorer_probs = fit::fit_first_goalscorer_all(
&BivariateProbs::from(adj_optimal_h1.as_slice()),
&BivariateProbs::from(adj_optimal_h2.as_slice()),
&first_gs,
draw_prob,
nil_all_draw_prob,
INTERVALS,
MAX_TOTAL_GOALS_FULL
);
Expand Down Expand Up @@ -559,7 +561,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
fitted_anytime_goalscorer_probs.push(isolated_prob);
}
fitted_anytime_goalscorer_outcomes.push(OutcomeType::None);
fitted_anytime_goalscorer_probs.push(draw_prob);
fitted_anytime_goalscorer_probs.push(nil_all_draw_prob);

let anytime_goalscorer_overround = Overround {
method: OVERROUND_METHOD,
Expand Down Expand Up @@ -623,7 +625,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
&BivariateProbs::from(adj_optimal_h2.as_slice()),
&assist_probs,
&anytime_assist,
draw_prob,
nil_all_draw_prob,
anytime_assist.market.fair_booksum(),
INTERVALS,
MAX_TOTAL_GOALS_FULL
Expand Down Expand Up @@ -677,7 +679,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
value: anytime_assist.market.offered_booksum() / fitted_anytime_assist_probs.sum(),
};
fitted_anytime_assist_outcomes.push(OutcomeType::None);
fitted_anytime_assist_probs.push(draw_prob);
fitted_anytime_assist_probs.push(nil_all_draw_prob);

let fitted_anytime_assist = Offer {
offer_type: OfferType::AnytimeAssist,
Expand Down
12 changes: 12 additions & 0 deletions brumby-soccer/src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ pub enum OutcomeType {
Player(Player),
None,
}
impl OutcomeType {
pub fn get_player(&self) -> Option<&Player> {
match self {
OutcomeType::Player(player) => Some(player),
_ => None
}
}
}

#[derive(Debug)]
pub struct Offer {
Expand All @@ -112,4 +120,8 @@ impl Offer {
pub fn filter_outcomes_with_probs<F>(&self, filter: F) -> Filter<Zip<Iter<OutcomeType>, Iter<f64>>, F> where F: FnMut(&(&OutcomeType, &f64)) -> bool{
self.outcomes.items().iter().zip(self.market.probs.iter()).filter(filter)
}

pub fn get_probability(&self, outcome: &OutcomeType) -> Option<f64> {
self.outcomes.index_of(outcome).map(|index| self.market.probs[index])
}
}
39 changes: 8 additions & 31 deletions brumby-soccer/src/domain/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub enum InvalidOffer {
impl Offer {
pub fn validate(&self) -> Result<(), InvalidOffer> {
OfferAlignmentAssertion::check(
&self.outcomes.items(),
self.outcomes.items(),
&self.market.probs,
&self.offer_type,
)?;
Expand All @@ -37,8 +37,9 @@ impl Offer {
}
}

pub type OfferCapture<'a> = Capture<'a, Offer, Offer>;
pub type OfferCapture<'a> = Capture<'a, Offer>;

/// Prevents accidental handling of an [Offer] before it has been validated.
#[derive(Debug)]
pub struct UnvalidatedOffer<'a>(OfferCapture<'a>);
impl<'a> UnvalidatedOffer<'a> {
Expand Down Expand Up @@ -81,12 +82,6 @@ impl OfferType {
}
}

// #[derive(Debug, Error)]
// pub enum FitError {
// #[error("missing offer {0:?}")]
// MissingOffer(OfferType),
// }

#[derive(Debug, Error)]
#[error("expected booksum in {assertion}, got {actual} for {offer_type:?}")]
pub struct WrongBooksum {
Expand All @@ -101,7 +96,7 @@ pub struct BooksumAssertion {
pub tolerance: f64,
}
impl BooksumAssertion {
const DEFAULT_TOLERANCE: f64 = 1e-6;
const DEFAULT_TOLERANCE: f64 = 1e-3;

pub fn with_default_tolerance(expected: RangeInclusive<f64>) -> Self {
Self {
Expand Down Expand Up @@ -162,8 +157,8 @@ impl OfferAlignmentAssertion {
#[derive(Debug, Error)]
#[error("{outcome_type:?} missing from {offer_type:?}")]
pub struct MissingOutcome {
outcome_type: OutcomeType,
offer_type: OfferType,
pub outcome_type: OutcomeType,
pub offer_type: OfferType,
}

#[derive(Debug)]
Expand Down Expand Up @@ -191,8 +186,8 @@ impl<'a> OutcomesIntactAssertion<'a> {
#[derive(Debug, Error)]
#[error("{outcome_type:?} does not belong in {offer_type:?}")]
pub struct ExtraneousOutcome {
outcome_type: OutcomeType,
offer_type: OfferType,
pub outcome_type: OutcomeType,
pub offer_type: OfferType,
}

pub struct OutcomesMatchAssertion<F: FnMut(&OutcomeType) -> bool> {
Expand All @@ -215,24 +210,6 @@ impl<F: FnMut(&OutcomeType) -> bool> OutcomesMatchAssertion<F> {
}
}

// #[derive(Debug, Error)]
// pub enum IncompleteOutcomes {
// #[error("{0}")]
// MissingOutcome(#[from] MissingOutcome),
//
// #[error("{0}")]
// ExtraneousOutcome(#[from] ExtraneousOutcome),
// }
//
// impl From<IncompleteOutcomes> for InvalidOutcome {
// fn from(value: IncompleteOutcomes) -> Self {
// match value {
// IncompleteOutcomes::MissingOutcome(nested) => nested.into(),
// IncompleteOutcomes::ExtraneousOutcome(nested) => nested.into(),
// }
// }
// }

#[derive(Debug)]
pub struct OutcomesCompleteAssertion<'a> {
pub outcomes: &'a [OutcomeType],
Expand Down
2 changes: 1 addition & 1 deletion brumby-soccer/src/domain/error/head_to_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ mod tests {
market: Market::frame(&Overround::fair(), vec![0.4, 0.4, 0.1], &PRICE_BOUNDS),
};
assert_eq!(
"expected booksum in 1.0..=1.0 ± 0.000001, got 0.9 for HeadToHead(FullTime)",
"expected booksum in 1.0..=1.0 ± 0.001, got 0.9 for HeadToHead(FullTime)",
offer.validate().unwrap_err().to_string()
);
}
Expand Down
2 changes: 1 addition & 1 deletion brumby-soccer/src/domain/error/total_goals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ mod tests {
market: Market::frame(&Overround::fair(), vec![0.4, 0.5], &PRICE_BOUNDS),
};
assert_eq!(
"expected booksum in 1.0..=1.0 ± 0.000001, got 0.9 for TotalGoals(FullTime, Over(2))",
"expected booksum in 1.0..=1.0 ± 0.001, got 0.9 for TotalGoals(FullTime, Over(2))",
offer.validate().unwrap_err().to_string()
);
}
Expand Down
Loading

0 comments on commit 4cbe271

Please sign in to comment.