def test_get_discount_factor(): """ Discount factor discounts future gameweek score predictions based on the number of gameweeks ahead. It uses two discount types based on a discount of 14/15, exponential ({14/15}^{weeks ahead}) and constant (1-{14/15}*weeks ahead) """ assert get_discount_factor(1, 4) == (14 / 15) ** (4 - 1) assert get_discount_factor(1, 4, "constant") == 1 - ((1 / 15) * (4 - 1)) assert get_discount_factor(1, 20, "const") == 0 assert get_discount_factor(1, 1, "const") == 1 assert get_discount_factor(1, 1, "exp") == 1
def save_baseline_score(squad, gameweeks, tag, season=CURRENT_SEASON): """When strategies with unused transfers are excluded the baseline strategy will normally not be part of the tree. In that case save it first with this function. """ # TODO: use season argument root_gw = gameweeks[0] strat_dict = { "total_score": 0, "points_per_gw": {}, "players_in": {}, "players_out": {}, "chips_played": {}, "root_gw": root_gw, } for gw in gameweeks: gw_score = squad.get_expected_points(gw, tag) * get_discount_factor( root_gw, gw) strat_dict["total_score"] += gw_score strat_dict["points_per_gw"][gw] = gw_score strat_dict["players_in"][gw] = [] strat_dict["players_out"][gw] = [] strat_dict["chips_played"][gw] = None num_gameweeks = len(gameweeks) zeros = ("0-" * num_gameweeks)[:-1] filename = os.path.join(OUTPUT_DIR, "strategy_{}_{}.json".format(tag, zeros)) with open(filename, "w") as f: json.dump(strat_dict, f)
def _get_gw_weight(self, **kwargs): """Weight for each gameweek (discount weeks further in the future). **kwargs passed to get_discount_factor """ return [ get_discount_factor(min(self.gw_range), gw, **kwargs) for gw in self.gw_range ]
def optimize( queue, pid, num_expected_outputs, gameweek_range, season, pred_tag, chips_gw_dict, max_total_hit=None, allow_unused_transfers=True, max_transfers=2, num_iterations=100, updater=None, resetter=None, profile=False, ): """ Queue is the multiprocessing queue, pid is the Process that will execute this func, gameweeks will be a list of gameweeks to consider, season and prediction_tag are hopefully self-explanatory. The rest of the parameters needed for prediction are from the queue. Things on the queue will either be "FINISHED", or a tuple: ( num_transfers, free_transfers, hit_so_far, current_team, strat_dict, strat_id ) """ while True: if queue.qsize() > 0: status = queue.get() else: if is_finished(num_expected_outputs): break time.sleep(5) continue # now assume we have set of parameters to do an optimization # from the queue. # turn on the profiler if requested if profile: profiler = cProfile.Profile() profiler.enable() num_transfers, free_transfers, hit_so_far, squad, strat_dict, sid = status # num_transfers will be 0, 1, 2, OR 'W' or 'F', OR 'T0', T1', 'T2', # OR 'B0', 'B1', or 'B2' (the latter six represent triple captain or # bench boost along with 0, 1, or 2 transfers). # sid (status id) is just a string e.g. "0-0-2" representing how many # transfers to be made in each gameweek. # Only exception is the root node, where sid is "starting" - this # node only exists to add children to the queue. if sid == "starting": sid = "" depth = 0 strat_dict["total_score"] = 0 strat_dict["points_per_gw"] = {} strat_dict["players_in"] = {} strat_dict["players_out"] = {} strat_dict["chips_played"] = {} new_squad = squad gw = gameweek_range[0] - 1 strat_dict["root_gw"] = gameweek_range[0] else: if len(sid) > 0: sid += "-" sid += str(num_transfers) resetter(pid, sid) # work out what gameweek we're in and how far down the tree we are. depth = len(strat_dict["points_per_gw"]) # gameweeks from this point in strategy to end of window gameweeks = gameweek_range[depth:] # upcoming gameweek: gw = gameweeks[0] root_gw = strat_dict["root_gw"] # check whether we're playing a chip this gameweek if isinstance(num_transfers, str): if num_transfers.startswith("T"): strat_dict["chips_played"][gw] = "triple_captain" elif num_transfers.startswith("B"): strat_dict["chips_played"][gw] = "bench_boost" elif num_transfers == "W": strat_dict["chips_played"][gw] = "wildcard" elif num_transfers == "F": strat_dict["chips_played"][gw] = "free_hit" else: strat_dict["chips_played"][gw] = None # calculate best transfers to make this gameweek (to maximise points across # remaining gameweeks) num_increments_for_updater = get_num_increments( num_transfers, num_iterations) increment = 100 / num_increments_for_updater new_squad, transfers, points = make_best_transfers( num_transfers, squad, pred_tag, gameweeks, root_gw, season, num_iterations, (updater, increment, pid), ) points -= calc_points_hit(num_transfers, free_transfers) * get_discount_factor( root_gw, gw) strat_dict["total_score"] += points strat_dict["points_per_gw"][gw] = points strat_dict["players_in"][gw] = transfers["in"] strat_dict["players_out"][gw] = transfers["out"] free_transfers = calc_free_transfers(num_transfers, free_transfers) depth += 1 if depth >= len(gameweek_range): with open( os.path.join(OUTPUT_DIR, "strategy_{}_{}.json".format(pred_tag, sid)), "w", ) as outfile: json.dump(strat_dict, outfile) # call function to update the main progress bar updater() if profile: profiler.dump_stats(f"process_strat_{pred_tag}_{sid}.pstat") else: # add children to the queue strategies = next_week_transfers( (free_transfers, hit_so_far, strat_dict), max_total_hit=max_total_hit, allow_unused_transfers=allow_unused_transfers, max_transfers=max_transfers, chips=chips_gw_dict[gw + 1], ) for strat in strategies: # strat: (num_transfers, free_transfers, hit_so_far) num_transfers, free_transfers, hit_so_far = strat queue.put(( num_transfers, free_transfers, hit_so_far, new_squad, strat_dict, sid, ))
def make_new_squad_iter( gw_range, tag, budget=1000, season=CURRENT_SEASON, num_iterations=100, update_func_and_args=None, verbose=False, bench_boost_gw=None, triple_captain_gw=None, **kwargs, ): """ Make a squad from scratch, i.e. for gameweek 1, or for wildcard, or free hit, by selecting high scoring players and then iteratively replacing them with cheaper options until we have a valid squad. """ transfer_gw = min(gw_range) # the gw we're making the new squad best_score = 0.0 best_squad = None for iteration in range(num_iterations): if verbose: print("Choosing new squad: iteration {}".format(iteration)) if update_func_and_args: # call function to update progress bar. # this was passed as a tuple (func, increment, pid) update_func_and_args[0](update_func_and_args[1], update_func_and_args[2]) predicted_points = {} t = Squad(budget, season=season) # first iteration - fill up from the front for pos in positions: predicted_points[pos] = get_predicted_points(gameweek=gw_range, position=pos, tag=tag, season=season) for pp in predicted_points[pos]: t.add_player(pp[0], gameweek=transfer_gw) if t.num_position[pos] == TOTAL_PER_POSITION[pos]: break # presumably we didn't get a complete squad now excluded_player_ids = [] while not t.is_complete(): # randomly swap out a player and replace with a cheaper one in the # same position player_to_remove = t.players[random.randint(0, len(t.players) - 1)] remove_cost = player_to_remove.purchase_price t.remove_player(player_to_remove.player_id, gameweek=transfer_gw) excluded_player_ids.append(player_to_remove.player_id) for pp in predicted_points[player_to_remove.position]: if (pp[0] not in excluded_player_ids or random.random() < 0.3): # some chance to put player back cp = CandidatePlayer(pp[0], gameweek=transfer_gw, season=season) if cp.purchase_price >= remove_cost: continue else: t.add_player(pp[0], gameweek=transfer_gw) # now try again to fill up the rest of the squad for pos in positions: num_missing = TOTAL_PER_POSITION[pos] - t.num_position[pos] if num_missing == 0: continue for pp in predicted_points[pos]: if pp[0] in excluded_player_ids: continue t.add_player(pp[0], gameweek=transfer_gw) if t.num_position[pos] == TOTAL_PER_POSITION[pos]: break # we have a complete squad score = 0.0 for gw in gw_range: if gw == bench_boost_gw: score += t.get_expected_points( gw, tag, bench_boost=True) * get_discount_factor( gw_range[0], gw) elif gw == triple_captain_gw: score += t.get_expected_points( gw, tag, triple_captain=True) * get_discount_factor( gw_range[0], gw) else: score += t.get_expected_points(gw, tag) * get_discount_factor( gw_range[0], gw) if score > best_score: best_score = score best_squad = t if verbose: print("====================================\n") print(best_squad) print(best_score) return best_squad
def make_optimum_double_transfer( squad, tag, gameweek_range=None, root_gw=None, season=CURRENT_SEASON, update_func_and_args=None, bench_boost_gw=None, triple_captain_gw=None, verbose=False, ): """ If we want to just make two transfers, it's not unfeasible to try all possibilities in turn. We will order the list of potential subs via the sum of expected points over a specified range of gameweeks. """ if not gameweek_range: gameweek_range = [NEXT_GAMEWEEK] root_gw = NEXT_GAMEWEEK transfer_gw = min(gameweek_range) # the week we're making the transfer best_score = 0.0 best_pid_out, best_pid_in = 0, 0 ordered_player_lists = { pos: get_predicted_points(gameweek=gameweek_range, position=pos, tag=tag) for pos in ["GK", "DEF", "MID", "FWD"] } for i in range(len(squad.players) - 1): positions_needed = [] pout_1 = squad.players[i] new_squad_remove_1 = fastcopy(squad) new_squad_remove_1.remove_player(pout_1.player_id, gameweek=transfer_gw) for j in range(i + 1, len(squad.players)): if update_func_and_args: # call function to update progress bar. # this was passed as a tuple (func, increment, pid) update_func_and_args[0](update_func_and_args[1], update_func_and_args[2]) pout_2 = squad.players[j] new_squad_remove_2 = fastcopy(new_squad_remove_1) new_squad_remove_2.remove_player(pout_2.player_id, gameweek=transfer_gw) if verbose: print("Removing players {} {}".format(i, j)) # what positions do we need to fill? positions_needed = [pout_1.position, pout_2.position] # now loop over lists of players and add players back in for pin_1 in ordered_player_lists[positions_needed[0]]: if pin_1[0].player_id in [pout_1.player_id, pout_2.player_id]: continue # no point in adding same player back in new_squad_add_1 = fastcopy(new_squad_remove_2) added_1_ok = new_squad_add_1.add_player(pin_1[0], gameweek=transfer_gw) if not added_1_ok: continue for pin_2 in ordered_player_lists[positions_needed[1]]: new_squad_add_2 = fastcopy(new_squad_add_1) if (pin_2[0] == pin_1[0] or pin_2[0].player_id == pout_1.player_id or pin_2[0].player_id == pout_2.player_id): continue # no point in adding same player back in added_2_ok = new_squad_add_2.add_player( pin_2[0], gameweek=transfer_gw) if added_2_ok: # calculate the score total_points = 0.0 for gw in gameweek_range: if gw == bench_boost_gw: total_points += new_squad_add_2.get_expected_points( gw, tag, bench_boost=True) * get_discount_factor( root_gw, gw) elif gw == triple_captain_gw: total_points += new_squad_add_2.get_expected_points( gw, tag, triple_captain=True) * get_discount_factor( root_gw, gw) else: total_points += new_squad_add_2.get_expected_points( gw, tag) * get_discount_factor( root_gw, gw) if total_points > best_score: best_score = total_points best_pid_out = [pout_1.player_id, pout_2.player_id] best_pid_in = [ pin_1[0].player_id, pin_2[0].player_id ] best_squad = new_squad_add_2 break return best_squad, best_pid_out, best_pid_in
def make_best_transfers( num_transfers, squad, tag, gameweeks, root_gw, season, num_iter=100, update_func_and_args=None, algorithm="genetic", ): """ Return a new squad and a dictionary {"in": [player_ids], "out":[player_ids]} """ transfer_dict = {} # deal with triple_captain or free_hit triple_captain_gw = None bench_boost_gw = None if isinstance(num_transfers, str): if num_transfers.startswith("T"): num_transfers = int(num_transfers[1]) triple_captain_gw = gameweeks[0] elif num_transfers.startswith("B"): num_transfers = int(num_transfers[1]) bench_boost_gw = gameweeks[0] if num_transfers == 0: # 0 or 'T0' or 'B0' (i.e. zero transfers, possibly with chip) new_squad = squad transfer_dict = {"in": [], "out": []} if update_func_and_args: # call function to update progress bar. # this was passed as a tuple (func, increment, pid) update_func_and_args[0](update_func_and_args[1], update_func_and_args[2]) elif num_transfers == 1: # 1 or 'T1' or 'B1' (i.e. 1 transfer, possibly with chip) new_squad, players_out, players_in = make_optimum_single_transfer( squad, tag, gameweeks, root_gw, season, triple_captain_gw=triple_captain_gw, bench_boost_gw=bench_boost_gw, update_func_and_args=update_func_and_args, ) transfer_dict = {"in": players_in, "out": players_out} elif num_transfers == 2: # 2 or 'T2' or 'B2' (i.e. 2 transfers, possibly with chip) new_squad, players_out, players_in = make_optimum_double_transfer( squad, tag, gameweeks, root_gw, season, triple_captain_gw=triple_captain_gw, bench_boost_gw=bench_boost_gw, update_func_and_args=update_func_and_args, ) transfer_dict = {"in": players_in, "out": players_out} elif num_transfers in ["W", "F"]: _out = [p.player_id for p in squad.players] budget = get_squad_value(squad) if num_transfers == "F": gameweeks = [gameweeks[0] ] # for free hit, only need to optimize this week new_squad = make_new_squad( gameweeks, tag=tag, budget=budget, season=season, verbose=0, bench_boost_gw=bench_boost_gw, triple_captain_gw=triple_captain_gw, algorithm=algorithm, population_size=num_iter, num_iter=num_iter, update_func_and_args=update_func_and_args, ) _in = [p.player_id for p in new_squad.players] players_in = [p for p in _in if p not in _out] # remove duplicates players_out = [p for p in _out if p not in _in] # remove duplicates transfer_dict = {"in": players_in, "out": players_out} else: raise RuntimeError( "Unrecognized value for num_transfers: {}".format(num_transfers)) # get the expected points total for next gameweek points = (new_squad.get_expected_points( gameweeks[0], tag, triple_captain=(triple_captain_gw is not None), bench_boost=(bench_boost_gw is not None), ) * get_discount_factor(root_gw, gameweeks[0])) if num_transfers == "F": # Free Hit changes don't apply to next gameweek, so return the original squad return squad, transfer_dict, points else: return new_squad, transfer_dict, points
def make_random_transfers( squad, tag, nsubs=1, gw_range=None, root_gw=None, num_iter=1, update_func_and_args=None, season=CURRENT_SEASON, bench_boost_gw=None, triple_captain_gw=None, ): """ choose nsubs random players to sub out, and then select players using a triangular PDF to preferentially select the replacements with the best expected score to fill their place. Do this num_iter times and choose the best total score over gw_range gameweeks. """ best_score = 0.0 best_squad = None best_pid_out = [] best_pid_in = [] max_tries = 100 for _ in range(num_iter): if update_func_and_args: # call function to update progress bar. # this was passed as a tuple (func, increment, pid) update_func_and_args[0](update_func_and_args[1], update_func_and_args[2]) new_squad = fastcopy(squad) if not gw_range: gw_range = [NEXT_GAMEWEEK] root_gw = NEXT_GAMEWEEK transfer_gw = min(gw_range) # the week we're making the transfer players_to_remove = [] # this is the index within the squad removed_players = [] # this is the player_ids # order the players in the squad by predicted_points - least-to-most player_list = [] for p in squad.players: p.calc_predicted_points(tag) player_list.append( (p.player_id, p.predicted_points[tag][gw_range[0]])) player_list.sort(key=itemgetter(1), reverse=False) while len(players_to_remove) < nsubs: index = int(random.triangular(0, len(player_list), 0)) if index not in players_to_remove: players_to_remove.append(index) positions_needed = [] for p in players_to_remove: positions_needed.append(squad.players[p].position) removed_players.append(squad.players[p].player_id) new_squad.remove_player(removed_players[-1], gameweek=transfer_gw) predicted_points = { pos: get_predicted_points(position=pos, gameweek=gw_range, tag=tag) for pos in set(positions_needed) } complete_squad = False added_players = [] attempt = 0 while not complete_squad: # sample with a triangular PDF - preferentially select players near # the start added_players = [] for pos in positions_needed: index = int(random.triangular(0, len(predicted_points[pos]), 0)) pid_to_add = predicted_points[pos][index][0] added_ok = new_squad.add_player(pid_to_add, gameweek=transfer_gw) if added_ok: added_players.append(pid_to_add) complete_squad = new_squad.is_complete() if not complete_squad: # try to avoid getting stuck in a loop attempt += 1 if attempt > max_tries: new_squad = fastcopy(squad) break # take those players out again. for ap in added_players: removed_ok = new_squad.remove_player(ap.player_id, gameweek=transfer_gw) if not removed_ok: print("Problem removing {}".format(ap.name)) added_players = [] # calculate the score total_points = 0.0 for gw in gw_range: if gw == bench_boost_gw: total_points += new_squad.get_expected_points( gw, tag, bench_boost=True) * get_discount_factor( root_gw, gw) elif gw == triple_captain_gw: total_points += new_squad.get_expected_points( gw, tag, triple_captain=True) * get_discount_factor( root_gw, gw) else: total_points += new_squad.get_expected_points( gw, tag) * get_discount_factor(root_gw, gw) if total_points > best_score: best_score = total_points best_pid_out = removed_players best_pid_in = [ap.player_id for ap in added_players] best_squad = new_squad # end of loop over n_iter return best_squad, best_pid_out, best_pid_in
def make_optimum_single_transfer( squad, tag, gameweek_range=None, root_gw=None, season=CURRENT_SEASON, update_func_and_args=None, bench_boost_gw=None, triple_captain_gw=None, verbose=False, ): """ If we want to just make one transfer, it's not unfeasible to try all possibilities in turn. We will order the list of potential transfers via the sum of expected points over a specified range of gameweeks. """ if not gameweek_range: gameweek_range = [NEXT_GAMEWEEK] root_gw = NEXT_GAMEWEEK transfer_gw = min(gameweek_range) # the week we're making the transfer best_score = -1.0 best_pid_out, best_pid_in = 0, 0 if verbose: print("Creating ordered player lists") ordered_player_lists = { pos: get_predicted_points(gameweek=gameweek_range, position=pos, tag=tag) for pos in ["GK", "DEF", "MID", "FWD"] } for p_out in squad.players: if update_func_and_args: # call function to update progress bar. # this was passed as a tuple (func, increment, pid) update_func_and_args[0](update_func_and_args[1], update_func_and_args[2]) new_squad = fastcopy(squad) position = p_out.position if verbose: print("Removing player {}".format(p_out.player_id)) new_squad.remove_player(p_out.player_id, gameweek=transfer_gw) for p_in in ordered_player_lists[position]: if p_in[0].player_id == p_out.player_id: continue # no point in adding the same player back in added_ok = new_squad.add_player(p_in[0], gameweek=transfer_gw) if added_ok: if verbose: print("Added player {}".format(p_in[0].name)) break else: if verbose: print("Failed to add {}".format(p_in[0].name)) total_points = 0.0 for gw in gameweek_range: if gw == bench_boost_gw: total_points += new_squad.get_expected_points( gw, tag, bench_boost=True) * get_discount_factor( root_gw, gw) elif gw == triple_captain_gw: total_points += new_squad.get_expected_points( gw, tag, triple_captain=True) * get_discount_factor( root_gw, gw) else: total_points += new_squad.get_expected_points( gw, tag) * get_discount_factor(root_gw, gw) if total_points > best_score: best_score = total_points best_pid_out = p_out.player_id best_pid_in = p_in[0].player_id best_squad = new_squad return best_squad, [best_pid_out], [best_pid_in]