Skip to main content
added 744 characters in body
Source Link
Reinderien
  • 65.6k
  • 5
  • 70
  • 190

Even if you were to skip vectorisation (which you shouldn't), the other minor things you could change:

Insufficient inter-function spacing for PEP8; you need two blank lines.

MatchGameStates is unused so delete it.

int=MATCH_LENGTH is also non-PEP8-compliant and needs spacing.

random.seed() is not the responsibility of mean_game_state; it's a top-level program concern whether the results should be repeatable or not. Similarly, round is a display concern and shouldn't be baked into your business logic function.

MatchGameState, rather than a dataclass, is simpler as a NamedTuple.

Even without Numpy, the home_ahead, draw and away_ahead variables can be calculated with sum() on generators.

Even if you were to skip vectorisation (which you shouldn't), the other minor things you could change:

Insufficient inter-function spacing for PEP8; you need two blank lines.

MatchGameStates is unused so delete it.

int=MATCH_LENGTH is also non-PEP8-compliant and needs spacing.

random.seed() is not the responsibility of mean_game_state; it's a top-level program concern whether the results should be repeatable or not. Similarly, round is a display concern and shouldn't be baked into your business logic function.

MatchGameState, rather than a dataclass, is simpler as a NamedTuple.

Even without Numpy, the home_ahead, draw and away_ahead variables can be calculated with sum() on generators.

Source Link
Reinderien
  • 65.6k
  • 5
  • 70
  • 190

It's clear that you've been working at generic Python quality, and from that perspective this code is half-decent. Where it falls over is Numpy use.

In one sense, well-written Numpy code doesn't look very much like what we imagine to be Pythonic code. Your dataclass must go away, and your "single_" method must go away. The operations from "single_" must be pulled out to "mean_" and vectorised over the number of trials. References to the built-in math and random libraries will go away. With this done, you can run the code in much less time, and/or run many more trials.

Suggested

import numpy as np
from numpy.random import default_rng

MATCH_LENGTH = 95
TRIALS = 100_000


rand = default_rng()


def mean_game_state(
    home_implied_goals: float,
    away_implied_goals: float,
    match_length_minutes: 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.
    """

    implied_goals = np.array((home_implied_goals, away_implied_goals))

    # Probability to score in a given minute
    goals_per_min = implied_goals / match_length_minutes

    # For every minute in a game, 1 if a team scored in that minute, 0 otherwise
    outcomes = rand.uniform(size=(trials, match_length_minutes, 2))
    goals_by_minute = outcomes < goals_per_min

    # How many goals a team scored by a particular minute of the game
    home_cumulative_goals, away_cumulative_goals = goals_by_minute.cumsum(axis=1).T
    diff = home_cumulative_goals - away_cumulative_goals

    home_ahead = np.count_nonzero(diff > 0, axis=0)
    away_ahead = np.count_nonzero(diff < 0, axis=0)
    draw = np.count_nonzero(diff == 0, axis=0)

    return home_ahead.mean(), draw.mean(), away_ahead.mean()


if __name__ == '__main__':
    print(mean_game_state(home_implied_goals=2.15, away_implied_goals=1.20))