-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoptimizer.py
More file actions
298 lines (263 loc) · 11.5 KB
/
Copy pathoptimizer.py
File metadata and controls
298 lines (263 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
"""
This module defines the Optimizer class, which provides methods to optimize FPL teams.
The Optimizer class includes methods to calculate optimal formations, discounted rewards, and optimized teams.
Available functions:
- calc_optimal_formation: Return the optimal formation of an FPL team for a particular gameweek.
- calc_discounted_reward_player: Calculate the discounted reward you can expect from a player over a particular horizon.
- calc_discounted_reward_team: Calculate the discounted reward you can expect from your team over a particular horizon.
- calc_optimal_teams: Find the top three optimized teams based on the given parameters.
"""
from fpl import Player, Team, Formation, ExpectedPointsCalculator, Loader
import heapq
class Optimizer:
"""Static class providing methods to optimize an FPL team."""
@staticmethod
def calc_optimal_formation(
team: Team, epc: ExpectedPointsCalculator, gameweek: int
) -> Formation:
"""Return the optimal formation of an FPL team for a particular gameweek.
:param team: The team for which the optimal formation is to be calculated.
:param epc: Expected points calculator.
:param gameweek: The gameweek for which the optimal formation is to be calculated.
:return: Optimal Formation object containing gkps, defs, mids, fwds, captain, expected points.
"""
def pop_n_elements(lst, n):
popped_elements = set()
for _ in range(n):
try:
popped_elements.add(lst.pop())
except IndexError: # The list is empty
break
return popped_elements
# for each position create a list in increasing size of expected points
gkps_sorted_list = sorted(
[
(player, epc.get_expected_points(player.element, gameweek))
for player in team.gkps
],
key=lambda x: x[1],
)
defs_sorted_list = sorted(
[
(player, epc.get_expected_points(player.element, gameweek))
for player in team.defs
],
key=lambda x: x[1],
)
mids_sorted_list = sorted(
[
(player, epc.get_expected_points(player.element, gameweek))
for player in team.mids
],
key=lambda x: x[1],
)
fwds_sorted_list = sorted(
[
(player, epc.get_expected_points(player.element, gameweek))
for player in team.fwds
],
key=lambda x: x[1],
)
# fill up the minimal number of players for each position
gkps, defs, mids, fwds = set(), set(), set(), set()
remaining_list = []
gkps = pop_n_elements(
gkps_sorted_list, Loader.get_position_info(1)["squad_min_play"]
)
defs = pop_n_elements(
defs_sorted_list, Loader.get_position_info(2)["squad_min_play"]
)
mids = pop_n_elements(
mids_sorted_list, Loader.get_position_info(3)["squad_min_play"]
)
fwds = pop_n_elements(
fwds_sorted_list, Loader.get_position_info(4)["squad_min_play"]
)
# for the remaining players we pick the best
remaining_list.extend(
gkps_sorted_list + defs_sorted_list + mids_sorted_list + fwds_sorted_list
)
remaining_list = sorted(remaining_list, key=lambda x: x[1])
i = len(remaining_list) - 1
while len(gkps) + len(defs) + len(mids) + len(fwds) < 11:
player, exp_points = remaining_list[i]
if (
player.position == 1
and len(gkps) < Loader.get_position_info(1)["squad_max_play"]
):
gkps.add((player, exp_points))
elif (
player.position == 2
and len(defs) < Loader.get_position_info(2)["squad_max_play"]
):
defs.add((player, exp_points))
elif (
player.position == 3
and len(mids) < Loader.get_position_info(3)["squad_max_play"]
):
mids.add((player, exp_points))
elif (
player.position == 4
and len(fwds) < Loader.get_position_info(4)["squad_max_play"]
):
fwds.add((player, exp_points))
i -= 1
# compute the total expected points and find the captain
total_exp_points, max_exp_points, captain = 0, 0, None
for player, exp_points in gkps | defs | mids | fwds:
if exp_points > max_exp_points:
captain = player
max_exp_points = exp_points
total_exp_points += exp_points
# need to double the captain's points
total_exp_points += max_exp_points
formation = Formation(
total_exp_points=total_exp_points,
gkps=frozenset(x[0] for x in gkps),
defs=frozenset(x[0] for x in defs),
mids=frozenset(x[0] for x in mids),
fwds=frozenset(x[0] for x in fwds),
captain=captain,
)
return formation
@staticmethod
def calc_discounted_reward_player(
player: Player,
epc: ExpectedPointsCalculator,
gameweek: int,
horizon: int,
gamma: float = 1,
):
"""Calculate the discounted reward you can expect from a player over a particular horizon.
:param player: The player for which the reward is to be calculated.
:param epc: Expected points calculator.
:param gameweek: The gameweek from which to start accumulating the reward.
:param horizon: The number of gameweeks over which to accumulate the reward.
:param gamma: Discount factor.
:return: Discounted reward of a player over a partcular horizon.
"""
discounted_reward = 0
discount_factor = 1
for h in range(horizon):
discounted_reward += discount_factor * epc.get_expected_points(
player.element, gameweek + h
)
discount_factor *= gamma
return discounted_reward
@staticmethod
def calc_discounted_reward_team(
team: Team,
epc: ExpectedPointsCalculator,
gameweek: int,
horizon: int,
gamma: float = 1,
wildcard: bool = False,
) -> float:
"""Calculate the discounted reward you can expect from your team over a particular horizon.
The discounted reward of a team is not the sum of the players.
This is because each week the team formation is changed to reflect the best player.
:param team: The team for which the reward is to be calculated.
:param epc: Expected points calculator.
:param gameweek: The gameweek from which to start accumulating the reward.
:param horizon: The number of gameweeks over which to accumulate the reward.
:param gamma: Discount factor.
:param wildcard: Whether you are wildcarding or not, this is a switch to turn off the transfer adjustment.
:return: Discounted reward of your team over a particular horizon, including a transfer adjustment.
"""
# draw a graph - each free transfer is roughly worth 0.8 points
transfer_adjustment = 0
if team.free_transfers <= -1:
transfer_adjustment = 4 * team.free_transfers
elif team.free_transfers >= 4:
transfer_adjustment = 0
else:
transfer_adjustment = -4 + (team.free_transfers + 1) * 0.8
# unlimited transfers
if wildcard:
transfer_adjustment = 0
discounted_reward = 0
discount_factor = 1
for h in range(horizon):
formation = Optimizer.calc_optimal_formation(team, epc, gameweek + h)
# transfer adjustment should be applied every week
# having one less transfer this week -> on average you'll have one less next week
discounted_reward += discount_factor * (
formation.total_exp_points + transfer_adjustment
)
discount_factor *= gamma
return discounted_reward
@staticmethod
def calc_optimal_teams(
team: Team,
candidates: list[Player],
epc: ExpectedPointsCalculator,
gameweek: int,
horizon: int,
max_transfers: int,
gamma: float = 1,
wildcard: bool = False,
) -> list[Team]:
"""Find the top three optimized teams.
:param team: The team you wish to optimize.
:param candidates: List of player candidates you wish to transfer in.
:param epc: Expected points calculator.
:param gameweek: The gameweek for which you want to start optimizing.
:param horizon: The number of gameweeks over which to optimize.
:param max_transfers: Maximum number of transfers you wish to take on.
:param gamma: Discount factor.
:param wildcard: Whether you are wildcarding or not.
:return: List of the top three teams based on score.
:raises ValueError: If one of the candidates is already in the team or has an invalid position.
"""
team_player_ids = set(
[p.element for p in team.gkps | team.defs | team.mids | team.fwds]
)
valid_positions = set([1, 2, 3, 4])
for candidate in candidates:
if candidate.element in team_player_ids:
raise ValueError("Candidate already in team.")
if candidate.position not in valid_positions:
raise ValueError("Invalid player position.")
top_three = [
(
Optimizer.calc_discounted_reward_team(
team, epc, gameweek, horizon, gamma, wildcard
),
team,
)
]
heapq.heapify(top_three)
heap_set = set([team])
currLayer = [team]
for i in range(max_transfers):
nextLayer = []
for u_team in currLayer:
for candidate in candidates:
out_players = None
if candidate.position == 1:
out_players = u_team.gkps
elif candidate.position == 2:
out_players = u_team.defs
elif candidate.position == 3:
out_players = u_team.mids
elif candidate.position == 4:
out_players = u_team.fwds
if candidate not in out_players:
for out_player in out_players:
v_team = u_team.transfer_player(out_player, candidate)
v_score = Optimizer.calc_discounted_reward_team(
v_team, epc, gameweek, horizon, gamma, wildcard
)
if v_team.is_feasible and (v_team not in heap_set):
if len(top_three) < 3:
heapq.heappush(top_three, (v_score, v_team))
heap_set.add(v_team)
else:
_, team_popped = heapq.heappushpop(
top_three, (v_score, v_team)
)
heap_set.add(v_team)
heap_set.remove(team_popped)
nextLayer.append(v_team)
currLayer = nextLayer
return top_three