What my program does
I'm trying to estimate for how many minutes in average a football(soccer) match will be in different game states depending on implied goals of both teams.
For our purposes there are three possible game states:
- Home team is ahead, e.g. 1-0
- Both teams are drawing, e.g. 2-2
- Away team is ahead, e.g. 0-2
And implied goals means how many goals teams are expected to score in average, for example 1.80 for home team and 1.45 for away team.
For simplicity we may assume that:
- Teams' scoring rate doesn't depend on the current scoreline
- The probability to score is equal regardless of the part of the game
- If one team scored in a given minute, it doesn't affect the probability of the other team to score in the same minute
A typical football match consists of 90 minutes of regular time and usually 1 minute of injury time in the first half and 4 minutes in the second half, which gives us 95 minutes in total.
I use the following algorithm to accomplish this task:
- For each team calculate probability to score in a given minute.
- For each team make a bitmap with length of match duration in minutes (95 in our example) where 1 indicates that a team scored in a given minute of the match and 0 indicates it didn't score.
- Calculate cumulative score of a team by a particular minute of the match.
- By comparing cumulative scores of both teams, count the duration of each game state.
- Repeat this for the required number of trials.
- Calculate average duration of each game state.
I'm interested in what are the alternatives to my solution both conceptually and implementation wise. Even though I'm aware that my program can be speed up by orders of magnitude via incorporating numpy, it is not that relevant in this particular case since it takes only a few seconds to run and it isn't supposed to be invoked a large number of times in a quick succession.
from dataclasses import dataclass import random from statistics import mean from numpy import cumsum MATCH_LENGTH = 95 TRIALS = 100_000 @dataclass class MatchGameState: """ Represents for how many minutes of a particular game: - home team was ahead - teams were drawing - away team was ahead """ home_ahead: int draw: int away_ahead: int MatchGameStates = list[MatchGameState] def mean_game_state(home_implied_goals: float, away_implied_goals: float, match_length: int=MATCH_LENGTH, trials: int=TRIALS) -> tuple[float, float, float]: """ Given match length in minutes and implied goals for home and away teams, calculates for how many minutes per match in average home team will be ahead, there will be a draw and away team will be ahead. """ random.seed() sims = [single_match_game_state(home_implied_goals, away_implied_goals, match_length) for _ in range(trials)] home_ahead_mean = round(mean(s.home_ahead for s in sims), 2) draw_mean = round(mean(s.draw for s in sims), 2) away_ahead_mean = round(mean(s.away_ahead for s in sims), 2) return home_ahead_mean, draw_mean, away_ahead_mean def single_match_game_state(home_implied_goals: float, away_implied_goals: float, match_length: int=95) -> MatchGameState: """ Given match length in minutes and implied goals for home and away teams, simulates teams scoring minute by minute for a particular game. Returns MatchGameState for the game. """ # Probability to score in a given minute home_goals_per_min = home_implied_goals / match_length away_goals_per_min = away_implied_goals / match_length # For every minute in a game, 1 if a team scored in that minute, 0 otherwise home_outcomes = [random.random() for _ in range(match_length)] away_outcomes = [random.random() for _ in range(match_length)] home_goals_by_minute = [int(home_outcome < home_goals_per_min) for home_outcome in home_outcomes] away_goals_by_minute = [int(away_outcome < away_goals_per_min) for away_outcome in away_outcomes] # How many goals a team scored by a particular minute of the game home_cumulative_goals = cumsum(home_goals_by_minute) away_cumulative_goals = cumsum(away_goals_by_minute) home_ahead, draw, away_ahead = 0, 0, 0 for home_cumulative_score, away_cumulative_score in zip(home_cumulative_goals, away_cumulative_goals): if home_cumulative_score > away_cumulative_score: home_ahead += 1 elif home_cumulative_score == away_cumulative_score: draw += 1 else: away_ahead += 1 assert home_ahead + draw + away_ahead == match_length return MatchGameState(home_ahead, draw, away_ahead) if __name__ == '__main__': print(mean_game_state(2.15, 1.20))