3
\$\begingroup\$

What my program does

My program does simulation for fantasy football. Its inputs are teams and teams results in a particular match and its outputs are how many points each team earned in each match according to given scoring rules.

Here is an example of a team:

team1 = [
    calculate_points.Player(p_id=204597, full_name='Jordan Pickford', club='ENG', 
                              position='goalkeeper', is_captain=True, 
                              is_vice_captain=False, points=0.0),          
    calculate_points.Player(p_id=213442, full_name='Luke Shaw', club='ENG', 
                              position='defender', is_captain=False, 
                              is_vice_captain=True, points=0.0), 
    calculate_points.Player(p_id=204614, full_name='Harry Maguire', club='ENG', 
                              position='defender', is_captain=False, 
                              is_vice_captain=False, points=0.0), 
    calculate_points.Player(p_id=204615, full_name='Mason Mount', club='ENG', 
                              position='midfielder', is_captain=False, 
                              is_vice_captain=False, points=0.0), 
    calculate_points.Player(p_id=204617, full_name='Harry Kane', club='ENG',
                              position='forward', is_captain=False, 
                              is_vice_captain=False, points=0.0),
    ]

And an example of a team result:

team_result1 = calculate_points.TeamResult(
    team='ENG', 
    goals_for=1, 
    goals_against=0, 
    scored_goals=['Harry Kane'], 
    made_assist=['Kalvin Phillips'], 
    played60=['Jordan Pickford', 'Luke Shaw', 'John Stones', 'Harry Maguire',  
               'Kieran Trippier', 'Kyle Walker', 'Kalvin Phillips', 
               'Raheem Sterling', 'Declan Rice', 'Harry Kane'], 
    finished_game=['Jordan Pickford', 'Luke Shaw', 'John Stones', 'Harry Maguire', 
                    'Kieran Trippier', 'Kyle Walker', 'Kalvin Phillips', 
                    'Mason Mount', 'Harry Kane'], 
    got_booked=['Kyle Walker'], 
    made_shots_on_tg=['Harry Kane', 'Harry Kane', 'Raheem Sterling', 
                       'Mason Mount', 'Harry Kane'], 
    saves=3)

What kind of feedback I would like to get

The heart of the program is get_team_points() function which calculates how many points a team earned in a given match by adding points earned by each player in the team.

To be able to perform high-throughput simulations it has to work as fast as possible. With that in mind I'm looking for a way to achieve that.

Since I don't have any limitations of what can be used to do that, I'm open to any suggestions including refactoring existing code, using third-party libraries, writing C/C++ extensions or even rewriting the program in another language.

Any other feedback beyond performance is also appreciated.

What I have already tried

  • reducing the size of TeamResult object, I tried to use list of players ids instead of their fullnames.
  • eliminating some function calls, I tried removing functions for calculating player points per position, such as calculate_goalkeeper_points(), calculate_defender_points(), calculate_midfielder_points() and calculate_forward_points() in order to do that in a single function for all the players.
  • using Cython for making calculate_team_points() function precompiled and importing it as C extension.

Though these steps didn't gain any significant speed up.

My code, including a few unit tests

calculate_points.py

from dataclasses import dataclass
from typing import List

CAPTAIN_BONUS = 2.0
VICE_CAPTAIN_BONUS = 1.5

@dataclass
class Scoring:
    """
    Represents football scoring.
    """
    defender_goal: int=6
    midfielder_goal: int=5
    forward_goal: int=4
    assist: int=3
    defender_shot_on_tg: float=0.6
    midfielder_or_forward_shot_on_tg: float=0.4
    goalkeeper_save: float=0.5
    pos_impact: float=0.3
    neg_impact: float=-pos_impact
    yellow_card: int=-1
    defender_or_goalkeeper_clean_sheet: int=4
    midfielder_clean_sheet: int=1
    played60: int=2
    midfielder_or_forward_finished_game: int=1
    
scoring = Scoring()    

PlayerId = int
PlayerName = str
PlayerClub = str
PlayerPosition = str
Points = float

@dataclass
class Player:
    """
    Represents a player in Fantasy Football.
    """
    p_id: PlayerId
    full_name: PlayerName
    club: PlayerClub
    position: PlayerPosition
    is_captain: bool=False
    is_vice_captain: bool=False
    points: Points=0.0
    
Team = List[Player]
Teams = List[Team]    
    
@dataclass
class TeamResult:
    """
    Represents the result of a particular team with its name, goals scored,
    goals conceded, players who scored goals and made assists and the number of
    saves made by the goalkeeper.
    """
    team: PlayerClub
    goals_for: int
    goals_against: int
    scored_goals: List[PlayerName]
    made_assist: List[PlayerName]
    played60: List[PlayerName]
    finished_game: List[PlayerName]
    got_booked: List[PlayerName]
    made_shots_on_tg: List[PlayerName]
    saves: int

TeamResults = List[TeamResult]    

def get_team_points(team: Team, team_result: TeamResult) -> Points:
    """
    Returns how many points the given team earned with the given team result.
    """
    team = calculate_team_points(team, team_result)
    team = apply_captaincy(team)
    return sum(p.points for p in team)

def calculate_team_points(team: Team, team_result: TeamResult) -> Team:
    """
    Assigns earned points for each player in the team.
    """
    points_factory = {
        'goalkeeper': calculate_goalkeeper_points,
        'defender': calculate_defender_points,
        'midfielder': calculate_midfielder_points,
        'forward': calculate_forward_points,
        }
    
    for player in team:
        player.points = points_factory[player.position](player, team_result)
    return team

def calculate_goalkeeper_points(goalkeeper: Player, team_result: TeamResult) -> Points:
    """
    Returns points earned by a goalkeeper.
    """
    got_booked = scoring.yellow_card if goalkeeper.full_name in team_result.got_booked else 0
    goals_conceded = scoring.defender_or_goalkeeper_clean_sheet if team_result.goals_against == 0 else team_result.goals_against // 2 * -1
    impact = calculate_impact(team_result)
    saves = team_result.saves * scoring.goalkeeper_save
    playing_time = scoring.played60
    return sum((goals_conceded, impact, saves, playing_time, got_booked))

def calculate_defender_points(defender: Player, team_result: TeamResult) -> Points:
    """
    Returns points earned by a defender.
    """
    got_booked = scoring.yellow_card if defender.full_name in team_result.got_booked else 0
    goals_conceded = scoring.defender_or_goalkeeper_clean_sheet if team_result.goals_against == 0 else team_result.goals_against // 2 * -1
    impact = calculate_impact(team_result)
    playing_time = scoring.played60
    goals = team_result.scored_goals.count(defender.full_name) * scoring.defender_goal
    assists = team_result.made_assist.count(defender.full_name) * scoring.assist
    shots_on_tg = ((team_result.made_shots_on_tg.count(defender.full_name) - 
                   team_result.scored_goals.count(defender.full_name)) * scoring.defender_shot_on_tg)
    return sum((goals_conceded, impact, playing_time, goals, assists, 
                got_booked, shots_on_tg))

def calculate_midfielder_points(midfielder: Player, team_result: TeamResult) -> Points:
    """
    Returns points earned by a midfielder.
    """    
    got_booked = scoring.yellow_card if midfielder.full_name in team_result.got_booked else 0
    clean_sheet = scoring.midfielder_clean_sheet if team_result.goals_against == 0 else 0
    impact = calculate_impact(team_result)
    playing_time = scoring.midfielder_or_forward_finished_game + scoring.played60 if midfielder.full_name in team_result.finished_game else scoring.played60
    goals = team_result.scored_goals.count(midfielder.full_name) * scoring.midfielder_goal
    assists = team_result.made_assist.count(midfielder.full_name) * scoring.assist
    shots_on_tg = ((team_result.made_shots_on_tg.count(midfielder.full_name) - 
                   team_result.scored_goals.count(midfielder.full_name)) * scoring.midfielder_or_forward_shot_on_tg)    
    return sum((clean_sheet, impact, playing_time, goals, assists, got_booked, 
                shots_on_tg))

def calculate_forward_points(forward: Player, team_result: TeamResult) -> Points:
    """
    Returns points earned by a forward.
    """    
    got_booked = scoring.yellow_card if forward.full_name in team_result.got_booked else 0
    impact = calculate_impact(team_result)
    playing_time = scoring.midfielder_or_forward_finished_game + scoring.played60 if forward.full_name in team_result.finished_game else scoring.played60
    goals = team_result.scored_goals.count(forward.full_name) * scoring.forward_goal
    assists = team_result.made_assist.count(forward.full_name) * scoring.assist
    shots_on_tg = ((team_result.made_shots_on_tg.count(forward.full_name) - 
                   team_result.scored_goals.count(forward.full_name)) * scoring.midfielder_or_forward_shot_on_tg)    
    return sum((impact, playing_time, goals, assists, got_booked, shots_on_tg))

def calculate_impact(team_result: TeamResult) -> Points:
    """
    Calculates impact for a team based on whether it won, lost or drew.
    """
    if team_result.goals_for > team_result.goals_against:
        return scoring.pos_impact
    elif team_result.goals_for < team_result.goals_against:
        return scoring.neg_impact
    return 0

def apply_captaincy(team: Team) -> Team:
    """
    Multiplies points of captain and vice_captain.
    """
    for player in team:
        if player.is_captain:
            player.points *= CAPTAIN_BONUS
        elif player.is_vice_captain:
            player.points *= VICE_CAPTAIN_BONUS
    return team    

test_points.py

import unittest
import calculate_points

team1 = [
    calculate_points.Player(p_id=204597, full_name='Jordan Pickford', club='ENG', 
                              position='goalkeeper', is_captain=True, 
                              is_vice_captain=False, points=0.0),          
    calculate_points.Player(p_id=213442, full_name='Luke Shaw', club='ENG', 
                              position='defender', is_captain=False, 
                              is_vice_captain=True, points=0.0), 
    calculate_points.Player(p_id=204614, full_name='Harry Maguire', club='ENG', 
                              position='defender', is_captain=False, 
                              is_vice_captain=False, points=0.0), 
    calculate_points.Player(p_id=204615, full_name='Mason Mount', club='ENG', 
                              position='midfielder', is_captain=False, 
                              is_vice_captain=False, points=0.0), 
    calculate_points.Player(p_id=204617, full_name='Harry Kane', club='ENG',
                              position='forward',  is_captain=False, 
                              is_vice_captain=False, points=0.0),
    ]

team_result1 = calculate_points.TeamResult(
    team='ENG', 
    goals_for=1, 
    goals_against=0, 
    scored_goals=['Harry Kane'], 
    made_assist=['Kalvin Phillips'], 
    played60=['Jordan Pickford', 'Luke Shaw', 'John Stones', 'Harry Maguire',  
               'Kieran Trippier', 'Kyle Walker', 'Kalvin Phillips', 
               'Raheem Sterling', 'Declan Rice', 'Harry Kane'], 
    finished_game=['Jordan Pickford', 'Luke Shaw', 'John Stones', 'Harry Maguire', 
                    'Kieran Trippier', 'Kyle Walker', 'Kalvin Phillips', 
                    'Mason Mount', 'Harry Kane'], 
    got_booked=['Kyle Walker'], 
    made_shots_on_tg=['Harry Kane', 'Harry Kane', 'Raheem Sterling', 
                       'Mason Mount', 'Harry Kane'], 
    saves=3)

team_result2 = calculate_points.TeamResult(
    team='ENG', 
    goals_for=2, 
    goals_against=1, 
    scored_goals=['Raheem Sterling', 'Mason Mount'], 
    made_assist=['Substitutes', 'Luke Shaw'], 
    played60=['Jordan Pickford', 'Luke Shaw', 'John Stones', 'Harry Maguire', 
              'Kieran Trippier', 'Kyle Walker', 'Kalvin Phillips', 
              'Raheem Sterling', 'Mason Mount', 'Harry Kane'], 
    finished_game=['Jordan Pickford', 'Luke Shaw', 'John Stones', 
                   'Harry Maguire', 'Kieran Trippier', 'Kyle Walker', 
                   'Raheem Sterling'], 
    got_booked=['Jordan Pickford'], 
    made_shots_on_tg=['Harry Kane', 'Mason Mount', 'Mason Mount', 
                      'Substitutes', 'Harry Kane'], saves=2)

team_result3 = calculate_points.TeamResult(
    team='ENG', 
    goals_for=0, 
    goals_against=2, 
    scored_goals=[], 
    made_assist=[], 
    played60=['Jordan Pickford', 'Luke Shaw', 'John Stones', 'Harry Maguire', 
              'Kieran Trippier', 'Kyle Walker', 'Kalvin Phillips', 
              'Declan Rice', 'Harry Kane'], 
    finished_game=['Jordan Pickford', 'Luke Shaw', 'John Stones', 
                   'Harry Maguire', 'Kieran Trippier', 'Kyle Walker', 
                   'Kalvin Phillips', 'Declan Rice', 'Harry Kane'], 
    got_booked=['Luke Shaw', 'John Stones', 'Raheem Sterling', 'Mason Mount'], 
    made_shots_on_tg=['Harry Kane'], saves=2)

class TestGetTeamPoints(unittest.TestCase):
    def test_won_game_with_cleansheet(self):
        self.assertAlmostEqual(calculate_points.get_team_points(team1, team_result1), 44.15)
        
    def test_won_game_with_conceded_goal(self):
        self.assertAlmostEqual(calculate_points.get_team_points(team1, team_result2), 25.65)
        
    def test_lost_game_with_no_goals_scored(self):
        self.assertAlmostEqual(calculate_points.get_team_points(team1, team_result3), 7.45)
        
if __name__ == '__main__':
    unittest.main()        
\$\endgroup\$
2
  • \$\begingroup\$ Have you run a profiler on the code to find where to optimize? It seems like there is more code that is not shown. Presumably there is code to get the player and game stats from a web site, database, or file. Could that code be slow? How many players, teams, and games are there? \$\endgroup\$
    – RootTwo
    Aug 4, 2021 at 17:05
  • \$\begingroup\$ I have run a profiler, it spends almost all the time in calculate_team_points() function and its calls to calculate player points for each position, since it is called so many times. Game stats are generated by another program, they aren't retrieved from anywhere. Usually I run ~10,000 teams over ~50,000 game stats, that's why it takes some reasonable amount of time, which I would like to reduce. The program has no I/O at all. \$\endgroup\$ Aug 4, 2021 at 17:15

1 Answer 1

3
+100
\$\begingroup\$

I don't think that your Scoring class is a great place to store constants. (Even if it was, a regular class with statics would be a better fit than a singleton dataclass.) If you wanted to do something like this, make a module instead of a class to get a cheap namespace. Given how these figures are used I'm going to propose that they just be moved inline.

It's nice to see that you're aliasing types for PlayerId etc. If you want to go one step further in type safety, instead of a simple alias you can use NewType.

I don't understand why club is a property on the player. Isn't that a property of the team? Since club is supposed to be a property on the player, why is it also associated with the result object?

Having is_captain and is_vice_captain as booleans on the player is problematic because you could make multiple team captains, which is probably not supposed to be possible. The more reliable thing to do is just have captain and vice members on the team object.

I find "TeamResult" to be more understandable as "TeamGame", and for it to refer to a team object rather than keep a club string. Also, some of those lists should actually be sets given how they're used.

The way that you're running in-place mutation for apply_captaincy introduces problems - more difficult-to-debug code, and objects whose validity is more difficult to guarantee. Instead, return point numbers from methods and don't store them on any of the class objects.

Your points_factory is a somewhat-awkward stab at polymorphism where a more traditional polymorphic class inheritance model will make things easier, and also cut down on a lot of the repeated code in the calculate_* methods. Even if you were to keep this pattern, passing around positions as strings is not a good idea and these should be enums instead.

team1 etc. in your test module can be kept out of the global namespace and moved to e.g. statics on your test class. The result variables should be moved to the individual test methods as they're only used once.

Your player references, rather than being full-name strings, would be better-represented as IDs - that's what IDs are for.

There's an element of your logic that I don't understand: you always add scoring.played60 regardless of whether the given player actually appeared in your played60 collection. I've shown a (commented-out) implementation that does not add this score element unless the player appeared in that collection. While this is commented out the results I've shown are equal to yours.

Your test data are a little strange - you have big gaps in your team definition and are missing many of the players referenced in your results. Maybe this is OK; until you validate that the references in your results objects appear in the team instance and then it's not OK.

Suggested

from dataclasses import dataclass
from numbers import Real
from typing import NewType, Iterable, Tuple, ClassVar, Set, Sequence
import unittest
from unittest import TestCase


PlayerId = NewType('PlayerId', int)
PlayerName = NewType('PlayerName', str)
Club = NewType('Club', str)
Points = NewType('Points', Real)


@dataclass
class Player:
    """
    Represents a player in Fantasy Football.
    """
    p_id: PlayerId
    full_name: PlayerName
    shot_bonus: ClassVar[Points] = 0.4
    clean_sheet_bonus: ClassVar[Points] = 4
    assist_bonus: ClassVar[Points] = 3
    goal_bonus: ClassVar[Points]

    @staticmethod
    def booked_points(was_booked: bool) -> Points:
        if was_booked:
            return -1  # yellow_card
        return 0

    @classmethod
    def clean_sheet_points(cls, goals_against: int) -> Points:
        if goals_against == 0:
            return cls.clean_sheet_bonus
        return -(goals_against//2)

    @staticmethod
    def playtime_points(played_60: bool, finished_game: bool) -> Points:
        # if played_60:
        return 2
        # return 0

    @classmethod
    def goal_points(cls, goals: int) -> Points:
        return cls.goal_bonus * goals

    @classmethod
    def assist_points(cls, assists: int) -> Points:
        return assists * cls.assist_bonus

    @classmethod
    def shot_points(cls, shots_on: int, scored_goals: int) -> Points:
        return cls.shot_bonus*(shots_on - scored_goals)

    def captaincy_bonus(self, team: 'Team') -> Points:
        if team.captain is self:
            return 2
        if team.vice_captain is self:
            return 1.5
        return 1

    def get_points_before_captaincy(self, game: 'TeamGame') -> Points:
        return (
            self.booked_points(self.p_id in game.got_booked)
            + self.clean_sheet_points(game.goals_against)
            + game.impact
            + self.playtime_points(
                self.p_id in game.played60,
                self.p_id in game.finished_game,
            )
            + self.goal_points(
                game.scored_goals.count(self.p_id)
            )
            + self.assist_points(
                game.made_assist.count(self.p_id)
            )
            + self.shot_points(
                game.made_shots_on_tg.count(self.p_id),
                game.scored_goals.count(self.p_id),
            )
        )

    def get_points(self, game: 'TeamGame') -> Points:
        return self.get_points_before_captaincy(game) * self.captaincy_bonus(game.team)


class GoalKeeper(Player):
    shot_bonus = 0
    goal_bonus = 0
    assist_bonus = 0

    def get_points_before_captaincy(self, game: 'TeamGame') -> Points:
        return (
            super().get_points_before_captaincy(game)
            + game.saves * 0.5
        )


class Defender(Player):
    shot_bonus = 0.6
    goal_bonus = 6


class FrontLine(Player):
    @staticmethod
    def playtime_points(played_60: bool, finished_game: bool) -> Points:
        p = Player.playtime_points(played_60, finished_game)
        if finished_game:
            p += 1
        return p


class Midfielder(FrontLine):
    clean_sheet_bonus = 1
    goal_bonus = 5

    @classmethod
    def clean_sheet_points(cls, goals_against: int) -> Points:
        if goals_against == 0:
            return cls.clean_sheet_bonus
        return 0


class Forward(FrontLine):
    goal_bonus = 4

    @classmethod
    def clean_sheet_points(cls, goals_against: int) -> Points:
        return 0


class Team:
    def __init__(
        self,
        club: Club,
        players: Iterable[Player],
        vice_captain: Player,
        captain: Player,
    ) -> None:
        self.club, self.vice_captain, self.captain = club, vice_captain, captain
        self.players = {p.p_id: p for p in players}


@dataclass
class TeamGame:
    """
    Represents the result of a particular team with its name, goals scored,
    goals conceded, players who scored goals and made assists and the number of
    saves made by the goalkeeper.
    """
    team: Team
    goals_for: int
    goals_against: int
    saves: int
    scored_goals: Sequence[PlayerId]
    made_assist: Sequence[PlayerId]
    made_shots_on_tg: Sequence[PlayerId]
    played60: Set[PlayerId]
    finished_game: Set[PlayerId]
    got_booked: Set[PlayerId]

    @property
    def points_by_player(self) -> Iterable[Tuple[Player, Points]]:
        for player in self.team.players.values():
            yield player, player.get_points(self)

    @property
    def won(self) -> bool: return self.goals_for > self.goals_against
    @property
    def lost(self) -> bool: return self.goals_for < self.goals_against
    @property
    def draw(self) -> bool: return self.goals_for == self.goals_against

    @property
    def impact(self) -> Points:
        """
        Calculates impact for a team based on whether it won, lost or drew.
        """
        if self.won: return 0.3
        if self.lost: return -0.3
        return 0

    @property
    def points(self) -> Points:
        """
        Returns how many points the given team earned with the given team result.
        """
        return sum(points for player, points in self.points_by_player)


class TestGetTeamPoints(TestCase):
    pickford_id, shaw_id, maguire_id, mount_id, kane_id = (
        PlayerId(i) for i in (
            204597, 213442, 204614, 204615, 204617,
        )
    )
    phillips_id = PlayerId(99999)  # ???

    pickford = GoalKeeper(
        p_id=pickford_id, full_name=PlayerName('Jordan Pickford'),
    )
    shaw = Defender(
        p_id=shaw_id, full_name=PlayerName('Luke Shaw'),
    )

    team1 = Team(
        club=Club('ENG'),
        captain=pickford,
        vice_captain=shaw,
        players=(
            pickford, shaw,
            Defender(p_id=maguire_id, full_name=PlayerName('Harry Maguire')),
            Midfielder(p_id=mount_id, full_name=PlayerName('Mason Mount')),
            Forward(p_id=kane_id, full_name=PlayerName('Harry Kane')),
        ),
    )

    def test_won_game_with_cleansheet(self) -> None:
        team_result1 = TeamGame(
            team=self.team1,
            goals_for=1,
            goals_against=0,
            saves=3,
            scored_goals=[self.kane_id],
            made_assist=[self.phillips_id],
            played60={
                self.pickford_id, self.shaw_id, self.maguire_id, self.kane_id, self.phillips_id,
            },
            finished_game={
                self.pickford_id, self.shaw_id, self.maguire_id, self.kane_id, self.phillips_id, self.mount_id,
            },
            got_booked=set(),
            made_shots_on_tg=[
                self.kane_id, self.kane_id, self.mount_id, self.kane_id,
            ],
        )

        self.assertAlmostEqual(team_result1.points, Points(44.15))

    def test_won_game_with_conceded_goal(self) -> None:
        team_result2 = TeamGame(
            team=self.team1,
            goals_for=2,
            goals_against=1,
            scored_goals=[self.mount_id],
            made_assist=[self.shaw_id],
            played60={self.pickford_id, self.shaw_id, self.maguire_id, self.phillips_id, self.mount_id, self.kane_id},
            finished_game={self.pickford_id, self.shaw_id, self.maguire_id},
            got_booked={self.pickford_id},
            made_shots_on_tg=[self.kane_id, self.mount_id, self.mount_id, self.kane_id],
            saves=2,
        )

        self.assertAlmostEqual(team_result2.points, Points(25.65))

    def test_lost_game_with_no_goals_scored(self) -> None:
        team_result3 = TeamGame(
            team=self.team1,
            goals_for=0,
            goals_against=2,
            scored_goals=[],
            made_assist=[],
            played60={self.pickford_id, self.shaw_id, self.maguire_id, self.phillips_id, self.kane_id},
            finished_game={self.pickford_id, self.shaw_id, self.maguire_id, self.phillips_id, self.kane_id},
            got_booked={self.shaw_id, self.mount_id},
            made_shots_on_tg=[self.kane_id],
            saves=2,
        )

        self.assertAlmostEqual(team_result3.points, Points(7.45))


if __name__ == '__main__':
    unittest.main()

Though it's really outside of the scope of your question, given your priorities you shouldn't be using generators or classes at all. This is an example that should get you started down the path to vectorization; it's not exactly the same as what you do because you have piecewise operations and floor division in some places. The less of that you do the better.

    positions = np.array(
        (
            # Goalkeep defender midfield forward
            (-1.0, -1.0, -1.0, -1.0),  # yellow-card booked
            (-0.5, -0.5,  0.0,  0.0),  # goals-against
            ( 0.0,  0.0,  1.0,  1.0),  # finished game
            ( 0.0,  6-0.6, 5-0.4, 4-0.4),  # goals, subtracting shot coefficient
            ( 0.5,  0.0,  0.0,  0.0),  # saves
            ( 0.0,  3.0,  3.0,  3.0),  # assists
            ( 0.0,  0.6,  0.4,  0.4),  # shots
        )
    )

    players = np.array(
        (
            # pickford shaw maguire mount kane
            (1.0, 0.0, 0.0, 0.0, 0.0),  # goalkeep
            (0.0, 1.0, 1.0, 0.0, 0.0),  # defender
            (0.0, 0.0, 0.0, 1.0, 0.0),  # midfield
            (0.0, 0.0, 0.0, 0.0, 1.0),  # forward
        )
    )

    result2 = np.array(
        (
            # pickford shaw maguire mount kane
            (1, 0, 0, 0, 0),  # yellow-card booked
            (1, 1, 1, 1, 1),  # goals against (whole team, broadcast)
            (1, 1, 1, 0, 0),  # finished game
            (0, 0, 0, 1, 0),  # goals
            (2, 2, 2, 2, 2),  # saves (whole team, broadcast)
            (0, 1, 0, 0, 0),  # assists
            (0, 0, 0, 2, 2),  # shots
        )
    )

    impact = 0.3
    played60 = 2
    captain = np.array((2, 1.5, 1, 1, 1))
    points = (positions @ players) * result2
    player_points = captain*(np.sum(points, axis=0) + impact + played60)
    game_points = np.sum(player_points)
\$\endgroup\$
7
  • \$\begingroup\$ Thanks for the review! 1. club is a property of a player because a fantasy team may consist of players from different clubs; 2. I had captaincy as Enum in another version of this program, though it is not a major concern; 3. I used in-place mutation in apply_captaincy to save some function calls and had it as a class method in another version as well; 4. The same with 'points_factory', just tried to save some function call overhead; 5. test_points.py is for demonstration purposes only; \$\endgroup\$ Jul 31, 2021 at 20:42
  • \$\begingroup\$ 6. scoring.played60 is for future use, it can be left for now; 7. Many players are missing because I created only one team for demonstration purposes. \$\endgroup\$ Jul 31, 2021 at 20:42
  • \$\begingroup\$ If you care so much about micro-optimization that function call overhead is a problem, you're using entirely the wrong stack, and would want to move to numpy. \$\endgroup\$
    – Reinderien
    Jul 31, 2021 at 20:45
  • \$\begingroup\$ I'm going to propose that that's premature optimization. \$\endgroup\$
    – Reinderien
    Jul 31, 2021 at 20:45
  • \$\begingroup\$ In fact it is not, I have been successfully using this for a few months, nevertheless I would like to make it faster in order to run larger simulations within a few minutes instead of within an hour or two. \$\endgroup\$ Jul 31, 2021 at 20:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.