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()
andcalculate_forward_points()
in order to do that in a single function for all the players. - using
Cython
for makingcalculate_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()
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\$