From f8b5f6445ffd4e1afbc9fc3a7bec46d8cccd4309 Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Wed, 24 Jul 2024 10:54:08 +0100 Subject: [PATCH 01/11] wip update max free transfers to 5 and separate max free transfers allowed in game rules and max transfers per week to consider in optimisation --- airsenal/framework/optimization_utils.py | 45 ++++--- .../scripts/fill_transfersuggestion_table.py | 13 +- airsenal/tests/test_optimization.py | 120 +++++++++--------- 3 files changed, 103 insertions(+), 75 deletions(-) diff --git a/airsenal/framework/optimization_utils.py b/airsenal/framework/optimization_utils.py index b4ee1e6b..55d910f5 100644 --- a/airsenal/framework/optimization_utils.py +++ b/airsenal/framework/optimization_utils.py @@ -29,6 +29,7 @@ positions = ["FWD", "MID", "DEF", "GK"] # front-to-back DEFAULT_SUB_WEIGHTS = {"GK": 0.03, "Outfield": (0.65, 0.3, 0.1)} +MAX_FREE_TRANSFERS = 5 # changed in 24/25 season (not accounted for in replay season) def check_tag_valid(pred_tag, gameweek_range, season=CURRENT_SEASON, dbsession=session): @@ -74,22 +75,24 @@ def calc_points_hit(num_transfers, free_transfers): raise RuntimeError(f"Unexpected argument for num_transfers {num_transfers}") -def calc_free_transfers(num_transfers, prev_free_transfers): +def calc_free_transfers( + num_transfers, prev_free_transfers, max_free_transfers=MAX_FREE_TRANSFERS +): """ We get one extra free transfer per week, unless we use a wildcard or free hit, but we can't have more than 2. So we should only be able to return 1 or 2. """ if num_transfers in ["W", "F"]: - return 1 + return prev_free_transfers # changed in 24/25 season, previously 1 elif isinstance(num_transfers, int): - return max(1, min(2, 1 + prev_free_transfers - num_transfers)) + return max(1, min(max_free_transfers, 1 + prev_free_transfers - num_transfers)) elif (num_transfers.startswith("B") or num_transfers.startswith("T")) and len( num_transfers ) == 2: # take the 'x' out of Bx or Tx num_transfers = int(num_transfers[-1]) - return max(1, min(2, 1 + prev_free_transfers - num_transfers)) + return max(1, min(max_free_transfers, 1 + prev_free_transfers - num_transfers)) else: raise RuntimeError(f"Unexpected argument for num_transfers {num_transfers}") @@ -429,8 +432,9 @@ def next_week_transfers( strat, max_total_hit=None, allow_unused_transfers=True, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": [], "chip_to_play": None}, + max_free_transfers=MAX_FREE_TRANSFERS, ): """Given a previous strategy and some optimisation constraints, determine the valid options for the number of transfers (or chip played) in the following gameweek. @@ -438,6 +442,12 @@ def next_week_transfers( strat is a tuple (free_transfers, hit_so_far, strat_dict) strat_dict must have key chips_played, which is a dict indexed by gameweek with possible values None, "wildcard", "free_hit", "bench_boost" or triple_captain" + + max_opt_transfers - maximum number of transfers to play each week as part of + strategy in optimisation + + max_free_transfers - maximum number of free transfers saved in the game rules + (2 before 2024/25, 5 from 2024/25 season) """ # check that the 'chips' dict we are given makes sense: if ( @@ -453,13 +463,13 @@ def next_week_transfers( ft_available, hit_so_far, strat_dict = strat chip_history = strat_dict["chips_played"] - if not allow_unused_transfers and ft_available == 2: - # Force at least 1 free transfer. + if not allow_unused_transfers and ft_available == max_free_transfers: + # Force at least 1 free transfer if a free transfer will be lost otherwise. # NOTE: This will exclude the baseline strategy when allow_unused_transfers # is False. Re-add it outside this function in that case. - ft_choices = list(range(1, max_transfers + 1)) + ft_choices = list(range(1, max_opt_transfers + 1)) else: - ft_choices = list(range(max_transfers + 1)) + ft_choices = list(range(max_opt_transfers + 1)) if max_total_hit is not None: ft_choices = [ @@ -515,7 +525,10 @@ def next_week_transfers( new_points_hits = [ hit_so_far + calc_points_hit(nt, ft_available) for nt in new_transfers ] - new_ft_available = [calc_free_transfers(nt, ft_available) for nt in new_transfers] + new_ft_available = [ + calc_free_transfers(nt, ft_available, max_free_transfers) + for nt in new_transfers + ] # return list of (num_transfers, free_transfers, hit_so_far) tuples for each new # strategy @@ -528,8 +541,9 @@ def count_expected_outputs( free_transfers=1, max_total_hit=None, allow_unused_transfers=True, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={}, + max_free_transfers=MAX_FREE_TRANSFERS, ): """ Count the number of possible transfer and chip strategies for gw_ahead gameweeks @@ -540,7 +554,7 @@ def count_expected_outputs( * Allow playing the chips which have their allow_xxx argument set True * Exclude strategies that waste free transfers (make 0 transfers if 2 free tramsfers are available), if allow_unused_transfers is False. - * Make a maximum of max_transfers transfers each gameweek. + * Make a maximum of max_opt_transfers transfers each gameweek. * Each chip only allowed once. """ @@ -559,9 +573,10 @@ def count_expected_outputs( possibilities = next_week_transfers( s, max_total_hit=max_total_hit, - max_transfers=max_transfers, + max_opt_transfers=max_opt_transfers, allow_unused_transfers=allow_unused_transfers, chips=chips_for_gw, + max_free_transfers=max_free_transfers, ) for n_transfers, new_free_transfers, new_hit in possibilities: @@ -596,13 +611,13 @@ def count_expected_outputs( # if allow_unused_transfers is False baseline of no transfers will be removed above, # add it back in here, apart from edge cases where it's already included. if not allow_unused_transfers and ( - gw_ahead > 1 or (gw_ahead == 1 and init_free_transfers == 2) + gw_ahead > 1 or (gw_ahead == 1 and init_free_transfers == max_free_transfers) ): baseline_strat_dict = { "players_in": {gw: [] for gw in range(next_gw, next_gw + gw_ahead)}, "chips_played": {}, } - baseline_dict = (2, 0, baseline_strat_dict) + baseline_dict = (max_free_transfers, 0, baseline_strat_dict) strategies.insert(0, baseline_dict) return len(strategies) diff --git a/airsenal/scripts/fill_transfersuggestion_table.py b/airsenal/scripts/fill_transfersuggestion_table.py index b5912bdb..af8037b2 100644 --- a/airsenal/scripts/fill_transfersuggestion_table.py +++ b/airsenal/scripts/fill_transfersuggestion_table.py @@ -39,6 +39,7 @@ ) from airsenal.framework.optimization_transfers import make_best_transfers from airsenal.framework.optimization_utils import ( + MAX_FREE_TRANSFERS, calc_free_transfers, calc_points_hit, check_tag_valid, @@ -95,6 +96,7 @@ def optimize( updater: Optional[Callable] = None, resetter: Optional[Callable] = None, profile: bool = False, + max_free_transfers: int = MAX_FREE_TRANSFERS, ) -> None: """ Queue is the multiprocessing queue, @@ -213,7 +215,9 @@ def optimize( strat_dict["discount_factor"][gw] = discount_factor strat_dict["players_in"][gw] = transfers["in"] strat_dict["players_out"][gw] = transfers["out"] - free_transfers = calc_free_transfers(num_transfers, free_transfers) + free_transfers = calc_free_transfers( + num_transfers, free_transfers, max_free_transfers + ) depth += 1 @@ -235,8 +239,9 @@ def optimize( (free_transfers, hit_so_far, strat_dict), max_total_hit=max_total_hit, allow_unused_transfers=allow_unused_transfers, - max_transfers=max_transfers, + max_opt_transfers=max_transfers, chips=chips_gw_dict[gw + 1], + max_free_transfers=max_free_transfers, ) for strat in strategies: @@ -412,6 +417,7 @@ def run_optimization( num_thread: int = 4, profile: bool = False, is_replay: bool = False, # for replaying seasons + max_free_transfers: int = MAX_FREE_TRANSFERS, ): """ This is the actual main function that sets up the multiprocessing @@ -511,8 +517,9 @@ def run_optimization( free_transfers=num_free_transfers, max_total_hit=max_total_hit, allow_unused_transfers=allow_unused_transfers, - max_transfers=max_transfers, + max_opt_transfers=max_transfers, chip_gw_dict=chip_gw_dict, + max_free_transfers=max_free_transfers, ) total_progress = tqdm(total=num_expected_outputs, desc="Total progress") diff --git a/airsenal/tests/test_optimization.py b/airsenal/tests/test_optimization.py index 8ec24758..2ebdb81d 100644 --- a/airsenal/tests/test_optimization.py +++ b/airsenal/tests/test_optimization.py @@ -282,7 +282,7 @@ def test_next_week_transfers_no_chips_no_constraints(): strat, max_total_hit=None, allow_unused_transfers=True, - max_transfers=2, + max_opt_transfers=2, ) # (no. transfers, free transfers following week, points hit) expected = [(0, 2, 0), (1, 1, 0), (2, 1, 4)] @@ -295,7 +295,7 @@ def test_next_week_transfers_any_chip_no_constraints(): actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, chips={ "chips_allowed": ["wildcard", "free_hit", "bench_boost", "triple_captain"], "chip_to_play": None, @@ -324,7 +324,7 @@ def test_next_week_transfers_no_chips_zero_hit(): strat, max_total_hit=0, allow_unused_transfers=True, - max_transfers=2, + max_opt_transfers=2, ) expected = [(0, 2, 0), (1, 1, 0)] assert actual == expected @@ -337,7 +337,7 @@ def test_next_week_transfers_2ft_no_unused(): strat, max_total_hit=None, allow_unused_transfers=False, - max_transfers=2, + max_opt_transfers=2, ) expected = [(1, 2, 0), (2, 1, 0)] assert actual == expected @@ -361,7 +361,7 @@ def test_next_week_transfers_chips_already_used(): actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, ) expected = [(0, 2, 0), (1, 1, 0), (2, 1, 4)] assert actual == expected @@ -372,7 +372,7 @@ def test_next_week_transfers_play_wildcard(): actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": [], "chip_to_play": "wildcard"}, ) expected = [("W", 1, 0)] @@ -384,10 +384,10 @@ def test_next_week_transfers_2ft_allow_wildcard(): actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": ["wildcard"], "chip_to_play": None}, ) - expected = [(0, 2, 0), (1, 2, 0), (2, 1, 0), ("W", 1, 0)] + expected = [(0, 2, 0), (1, 2, 0), (2, 1, 0), ("W", 2, 0)] assert actual == expected @@ -397,10 +397,10 @@ def test_next_week_transfers_2ft_allow_wildcard_no_unused(): strat, max_total_hit=None, allow_unused_transfers=False, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": ["wildcard"], "chip_to_play": None}, ) - expected = [(1, 2, 0), (2, 1, 0), ("W", 1, 0)] + expected = [(1, 2, 0), (2, 1, 0), ("W", 2, 0)] assert actual == expected @@ -409,10 +409,10 @@ def test_next_week_transfers_2ft_play_wildcard(): actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": [], "chip_to_play": "wildcard"}, ) - expected = [("W", 1, 0)] + expected = [("W", 2, 0)] assert actual == expected @@ -422,7 +422,7 @@ def test_next_week_transfers_2ft_play_bench_boost_no_unused(): strat, max_total_hit=None, allow_unused_transfers=False, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": [], "chip_to_play": "bench_boost"}, ) expected = [("B1", 2, 0), ("B2", 1, 0)] @@ -435,7 +435,7 @@ def test_next_week_transfers_play_triple_captain_max_transfers_3(): strat, max_total_hit=None, allow_unused_transfers=True, - max_transfers=3, + max_opt_transfers=3, chips={"chips_allowed": [], "chip_to_play": "triple_captain"}, ) expected = [("T0", 2, 0), ("T1", 1, 0), ("T2", 1, 4), ("T3", 1, 8)] @@ -450,65 +450,68 @@ def test_count_expected_outputs_no_chips_no_constraints(): max_total_hit=None, allow_unused_transfers=True, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={}, ) assert count == 3**3 - # Max hit 0 - # Include: - # (0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), - # (0, 2, 0), (0, 2, 1), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (1, 1, 1) - # Exclude: - # (0, 2, 2), (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 0), (2, 0, 1), - # (2, 0, 2), (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2) - def test_count_expected_outputs_no_chips_zero_hit(): + """ + Max hit 0 + Include: + (0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), + (0, 2, 0), (0, 2, 1), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (1, 1, 1) + Exclude: + (0, 2, 2), (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 0), (2, 0, 1), + (2, 0, 2), (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2) + """ count = count_expected_outputs( 3, free_transfers=1, max_total_hit=0, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={}, ) assert count == 13 - # Start with 2 FT and no unused - # Include: - # (0, 0, 0), (1, 1, 1), (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 1), - # (2, 0, 2), (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2) - # Exclude: - # (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), (0, 2, 0), (0, 2, 1), - # (0, 2, 2), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (2, 0, 0) - def test_count_expected_outputs_no_chips_2ft_no_unused(): + """ + Start with 2 FT and no unused + Include: + (0, 0, 0), (1, 1, 1), (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 1), + (2, 0, 2), (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2) + Exclude: + (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), (0, 2, 0), (0, 2, 1), + (0, 2, 2), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (2, 0, 0) + """ count = count_expected_outputs( 3, free_transfers=2, max_total_hit=None, allow_unused_transfers=False, next_gw=1, - max_transfers=2, + max_opt_transfers=2, ) assert count == 14 - # Wildcard, 2 weeks, no constraints - # Strategies: - # (0, 0), (0, 1), (0, 2), (0, 'W'), (1, 0), (1, 1), (1, 2), (1, 'W'), (2, 0), - # (2, 1), (2, 2), (2, 'W'), ('W', 0), ('W', 1), ('W', 2) - def test_count_expected_wildcard_allowed_no_constraints(): + """ + Wildcard, 2 weeks, no constraints + Strategies: + (0, 0), (0, 1), (0, 2), (0, 'W'), (1, 0), (1, 1), (1, 2), (1, 'W'), (2, 0), + (2, 1), (2, 2), (2, 'W'), ('W', 0), ('W', 1), ('W', 2) + """ count = count_expected_outputs( 2, free_transfers=1, max_total_hit=None, allow_unused_transfers=True, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={ 1: {"chips_allowed": ["wildcard"]}, 2: {"chips_allowed": ["wildcard"]}, @@ -517,22 +520,23 @@ def test_count_expected_wildcard_allowed_no_constraints(): ) assert count == 15 - # Bench boost, 2 weeks, no constraints - # Strategies: - # (0, 0), (0, 1), (0, 2), (0, 'B0'), (0, 'B1'), (0, 'B2'), (1, 0), (1, 1), (1, 2), - # (1, 'B0'), (1, 'B1'), (1, 'B2'), (2, 0), (2, 1), (2, 2), (2, 'B0'), (2, 'B1'), - # (2, 'B2'), ('B0', 0), ('B0', 1), ('B0', 2), ('B1', 0), ('B1', 1), ('B1', 2), - # ('B2', 0), ('B2', 1), ('B2', 2), - def count_expected_bench_boost_allowed_no_constraints(): + """ + Bench boost, 2 weeks, no constraints + Strategies: + (0, 0), (0, 1), (0, 2), (0, 'B0'), (0, 'B1'), (0, 'B2'), (1, 0), (1, 1), (1, 2), + (1, 'B0'), (1, 'B1'), (1, 'B2'), (2, 0), (2, 1), (2, 2), (2, 'B0'), (2, 'B1'), + (2, 'B2'), ('B0', 0), ('B0', 1), ('B0', 2), ('B1', 0), ('B1', 1), ('B1', 2), + ('B2', 0), ('B2', 1), ('B2', 2), + """ count = count_expected_outputs( 2, free_transfers=1, max_total_hit=None, allow_unused_transfers=True, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={ 1: {"chips_allowed": ["bench_boost"]}, 2: {"chips_allowed": ["bench_boost"]}, @@ -541,19 +545,20 @@ def count_expected_bench_boost_allowed_no_constraints(): ) assert count == 27 - # Force playing wildcard in first week - # Strategies: - # ("W",0), ("W,1), ("W",2) - def count_expected_play_wildcard_no_constraints(): + """ + Force playing wildcard in first week + Strategies: + ("W",0), ("W,1), ("W",2) + """ count = count_expected_outputs( 2, free_transfers=1, max_total_hit=None, allow_unused_transfers=True, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={ 1: {"chip_to_play": "wildcard", "chips_allowed": []}, 2: {"chip_to_play": None, "chips_allowed": []}, @@ -561,19 +566,20 @@ def count_expected_play_wildcard_no_constraints(): ) assert count == 3 - # Force playing free hit in first week, 2FT, don't allow unused - # Strategies: - # (0,0), ("F",1), ("F",2) - def count_expected_play_free_hit_no_unused(): + """ + Force playing free hit in first week, 2FT, don't allow unused + Strategies: + (0,0), ("F",1), ("F",2) + """ count = count_expected_outputs( 2, free_transfers=2, max_total_hit=None, allow_unused_transfers=False, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={ 1: {"chip_to_play": "free_hit", "chips_allowed": []}, 2: {"chip_to_play": None, "chips_allowed": []}, From 03f5ab8bd7a305e7a1e12d2b2411addc6cda4478 Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Wed, 24 Jul 2024 13:14:36 +0100 Subject: [PATCH 02/11] fix tests by setting 2 max free transfers (not added 5 free transfer tests) --- airsenal/tests/test_optimization.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/airsenal/tests/test_optimization.py b/airsenal/tests/test_optimization.py index 2ebdb81d..03b53c0b 100644 --- a/airsenal/tests/test_optimization.py +++ b/airsenal/tests/test_optimization.py @@ -338,6 +338,7 @@ def test_next_week_transfers_2ft_no_unused(): max_total_hit=None, allow_unused_transfers=False, max_opt_transfers=2, + max_free_transfers=2, ) expected = [(1, 2, 0), (2, 1, 0)] assert actual == expected @@ -386,6 +387,7 @@ def test_next_week_transfers_2ft_allow_wildcard(): max_total_hit=None, max_opt_transfers=2, chips={"chips_allowed": ["wildcard"], "chip_to_play": None}, + max_free_transfers=2, ) expected = [(0, 2, 0), (1, 2, 0), (2, 1, 0), ("W", 2, 0)] assert actual == expected @@ -399,6 +401,7 @@ def test_next_week_transfers_2ft_allow_wildcard_no_unused(): allow_unused_transfers=False, max_opt_transfers=2, chips={"chips_allowed": ["wildcard"], "chip_to_play": None}, + max_free_transfers=2, ) expected = [(1, 2, 0), (2, 1, 0), ("W", 2, 0)] assert actual == expected @@ -424,6 +427,7 @@ def test_next_week_transfers_2ft_play_bench_boost_no_unused(): allow_unused_transfers=False, max_opt_transfers=2, chips={"chips_allowed": [], "chip_to_play": "bench_boost"}, + max_free_transfers=2, ) expected = [("B1", 2, 0), ("B2", 1, 0)] assert actual == expected @@ -494,6 +498,7 @@ def test_count_expected_outputs_no_chips_2ft_no_unused(): allow_unused_transfers=False, next_gw=1, max_opt_transfers=2, + max_free_transfers=2, ) assert count == 14 From 4787bae7308156a235c5d366a444fadb506cfc5b Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Wed, 24 Jul 2024 21:52:20 +0100 Subject: [PATCH 03/11] add some basic tests with more than 2 transfers --- airsenal/framework/optimization_utils.py | 2 + airsenal/tests/test_optimization.py | 170 +++++++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/airsenal/framework/optimization_utils.py b/airsenal/framework/optimization_utils.py index 55d910f5..5440b11e 100644 --- a/airsenal/framework/optimization_utils.py +++ b/airsenal/framework/optimization_utils.py @@ -448,6 +448,8 @@ def next_week_transfers( max_free_transfers - maximum number of free transfers saved in the game rules (2 before 2024/25, 5 from 2024/25 season) + + Returns (new_transfers, new_ft_available, new_points_hits) tuples. """ # check that the 'chips' dict we are given makes sense: if ( diff --git a/airsenal/tests/test_optimization.py b/airsenal/tests/test_optimization.py index 03b53c0b..003ddfb1 100644 --- a/airsenal/tests/test_optimization.py +++ b/airsenal/tests/test_optimization.py @@ -289,6 +289,21 @@ def test_next_week_transfers_no_chips_no_constraints(): assert actual == expected +def test_next_week_transfers_no_chips_no_constraints_max5(): + # First week (blank starting strat with 1 free transfer available) + strat = (1, 0, {"players_in": {}, "chips_played": {}}) + # No chips or constraints + actual = next_week_transfers( + strat, + max_total_hit=None, + allow_unused_transfers=True, + max_opt_transfers=5, + ) + # (no. transfers, free transfers following week, points hit) + expected = [(0, 2, 0), (1, 1, 0), (2, 1, 4), (3, 1, 8), (4, 1, 12), (5, 1, 16)] + assert actual == expected + + def test_next_week_transfers_any_chip_no_constraints(): # All chips, no constraints strat = (1, 0, {"players_in": {}, "chips_played": {}}) @@ -317,6 +332,43 @@ def test_next_week_transfers_any_chip_no_constraints(): assert actual == expected +def test_next_week_transfers_any_chip_no_constraints_max5(): + # All chips, no constraints + strat = (1, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=None, + max_opt_transfers=5, + chips={ + "chips_allowed": ["wildcard", "free_hit", "bench_boost", "triple_captain"], + "chip_to_play": None, + }, + ) + expected = [ + (0, 2, 0), + (1, 1, 0), + (2, 1, 4), + (3, 1, 8), + (4, 1, 12), + (5, 1, 16), + ("W", 1, 0), + ("F", 1, 0), + ("B0", 2, 0), + ("B1", 1, 0), + ("B2", 1, 4), + ("B3", 1, 8), + ("B4", 1, 12), + ("B5", 1, 16), + ("T0", 2, 0), + ("T1", 1, 0), + ("T2", 1, 4), + ("T3", 1, 8), + ("T4", 1, 12), + ("T5", 1, 16), + ] + assert actual == expected + + def test_next_week_transfers_no_chips_zero_hit(): # No points hits strat = (1, 0, {"players_in": {}, "chips_played": {}}) @@ -330,6 +382,19 @@ def test_next_week_transfers_no_chips_zero_hit(): assert actual == expected +def test_next_week_transfers_no_chips_zero_hit_max5(): + # No points hits + strat = (1, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=0, + allow_unused_transfers=True, + max_opt_transfers=5, + ) + expected = [(0, 2, 0), (1, 1, 0)] + assert actual == expected + + def test_next_week_transfers_2ft_no_unused(): # 2 free transfers available, no wasted transfers strat = (2, 0, {"players_in": {}, "chips_played": {}}) @@ -344,6 +409,34 @@ def test_next_week_transfers_2ft_no_unused(): assert actual == expected +def test_next_week_transfers_5ft_no_unused_max5(): + # 2 free transfers available, no wasted transfers + strat = (5, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=None, + allow_unused_transfers=False, + max_opt_transfers=5, + max_free_transfers=5, + ) + expected = [(1, 5, 0), (2, 4, 0), (3, 3, 0), (4, 2, 0), (5, 1, 0)] + assert actual == expected + + +def test_next_week_transfers_3ft_no_hit_max5(): + # 2 free transfers available, no wasted transfers + strat = (3, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=0, + allow_unused_transfers=False, + max_opt_transfers=5, + max_free_transfers=5, + ) + expected = [(0, 4, 0), (1, 3, 0), (2, 2, 0), (3, 1, 0)] + assert actual == expected + + def test_next_week_transfers_chips_already_used(): # Chips allowed but previously used strat = ( @@ -393,6 +486,27 @@ def test_next_week_transfers_2ft_allow_wildcard(): assert actual == expected +def test_next_week_transfers_5ft_allow_wildcard(): + strat = (5, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=None, + max_opt_transfers=5, + chips={"chips_allowed": ["wildcard"], "chip_to_play": None}, + max_free_transfers=5, + ) + expected = [ + (0, 5, 0), + (1, 5, 0), + (2, 4, 0), + (3, 3, 0), + (4, 2, 0), + (5, 1, 0), + ("W", 5, 0), + ] + assert actual == expected + + def test_next_week_transfers_2ft_allow_wildcard_no_unused(): strat = (2, 0, {"players_in": {}, "chips_played": {}}) actual = next_week_transfers( @@ -460,6 +574,21 @@ def test_count_expected_outputs_no_chips_no_constraints(): assert count == 3**3 +def test_count_expected_outputs_no_chips_no_constraints_max5(): + # No constraints or chips, expect 6**num_gameweeks strategies (0 to 5 transfers + # each week) + count = count_expected_outputs( + 3, + free_transfers=1, + max_total_hit=None, + allow_unused_transfers=True, + next_gw=1, + max_opt_transfers=5, + chip_gw_dict={}, + ) + assert count == 5**3 + + def test_count_expected_outputs_no_chips_zero_hit(): """ Max hit 0 @@ -481,6 +610,24 @@ def test_count_expected_outputs_no_chips_zero_hit(): assert count == 13 +def test_count_expected_outputs_no_chips_zero_hit_max5(): + """ + Max hit 0 + Max 5 transfers + Adds (0, 0, 3) to valid strategies compared to + test_count_expected_outputs_no_chips_zero_hit above + """ + count = count_expected_outputs( + 3, + free_transfers=1, + max_total_hit=0, + next_gw=1, + max_opt_transfers=5, + chip_gw_dict={}, + ) + assert count == 13 + + def test_count_expected_outputs_no_chips_2ft_no_unused(): """ Start with 2 FT and no unused @@ -503,6 +650,29 @@ def test_count_expected_outputs_no_chips_2ft_no_unused(): assert count == 14 +def test_count_expected_outputs_no_chips_5ft_no_unused_max5(): + """ + Start with 5 FT and no unused over 2 weeks + Include: + (0, 0), + (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), + (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), + (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), + (4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), + (5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), + """ + count = count_expected_outputs( + 2, + free_transfers=5, + max_total_hit=None, + allow_unused_transfers=False, + next_gw=1, + max_opt_transfers=5, + max_free_transfers=5, + ) + assert count == 30 + + def test_count_expected_wildcard_allowed_no_constraints(): """ Wildcard, 2 weeks, no constraints From d8f9953ce5529406730915f8590dc914375c2cec Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Wed, 24 Jul 2024 21:56:28 +0100 Subject: [PATCH 04/11] fix previous commit --- airsenal/tests/test_optimization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airsenal/tests/test_optimization.py b/airsenal/tests/test_optimization.py index 003ddfb1..347122aa 100644 --- a/airsenal/tests/test_optimization.py +++ b/airsenal/tests/test_optimization.py @@ -586,7 +586,7 @@ def test_count_expected_outputs_no_chips_no_constraints_max5(): max_opt_transfers=5, chip_gw_dict={}, ) - assert count == 5**3 + assert count == 6**3 def test_count_expected_outputs_no_chips_zero_hit(): @@ -625,7 +625,7 @@ def test_count_expected_outputs_no_chips_zero_hit_max5(): max_opt_transfers=5, chip_gw_dict={}, ) - assert count == 13 + assert count == 14 def test_count_expected_outputs_no_chips_2ft_no_unused(): From bb27240e3fa8f55f4f60bd3b91b17ff4d35a3953 Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Fri, 26 Jul 2024 08:59:55 +0100 Subject: [PATCH 05/11] add max transfers arg to opt/replay/pipeline scripts --- airsenal/scripts/airsenal_run_pipeline.py | 9 ++++++--- .../scripts/fill_transfersuggestion_table.py | 17 +++++++++++++---- airsenal/scripts/replay_season.py | 12 ++++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/airsenal/scripts/airsenal_run_pipeline.py b/airsenal/scripts/airsenal_run_pipeline.py index 48dda293..35ce28f0 100644 --- a/airsenal/scripts/airsenal_run_pipeline.py +++ b/airsenal/scripts/airsenal_run_pipeline.py @@ -110,8 +110,11 @@ ) @click.option( "--max_transfers", - help="specify maximum number of transfers to be made each gameweek (defaults to 2)", - type=click.IntRange(min=0, max=2), + help=( + "specify maximum number of transfers to consider each gameweek [EXPERIMENTAL: " + "increasing this value above 2 will make the optimisation extremely slow!]" + ), + type=click.IntRange(min=0, max=5), default=2, ) @click.option( @@ -361,7 +364,7 @@ def run_optimize_squad( fpl_team_id=fpl_team_id, num_thread=num_thread, chip_gameweeks=chips_played, - max_transfers=max_transfers, + max_opt_transfers=max_transfers, max_total_hit=max_hit, allow_unused_transfers=allow_unused, ) diff --git a/airsenal/scripts/fill_transfersuggestion_table.py b/airsenal/scripts/fill_transfersuggestion_table.py index af8037b2..2a19177d 100644 --- a/airsenal/scripts/fill_transfersuggestion_table.py +++ b/airsenal/scripts/fill_transfersuggestion_table.py @@ -412,7 +412,7 @@ def run_optimization( num_free_transfers: Optional[int] = None, max_total_hit: Optional[int] = None, allow_unused_transfers: bool = False, - max_transfers: int = 2, + max_opt_transfers: int = 2, num_iterations: int = 100, num_thread: int = 4, profile: bool = False, @@ -517,7 +517,7 @@ def run_optimization( free_transfers=num_free_transfers, max_total_hit=max_total_hit, allow_unused_transfers=allow_unused_transfers, - max_opt_transfers=max_transfers, + max_opt_transfers=max_opt_transfers, chip_gw_dict=chip_gw_dict, max_free_transfers=max_free_transfers, ) @@ -575,7 +575,7 @@ def update_progress(increment=1, index=None): chip_gw_dict, max_total_hit, allow_unused_transfers, - max_transfers, + max_opt_transfers, num_iterations, update_progress, reset_progress, @@ -758,6 +758,15 @@ def main(): help="if set, include strategies that waste free transfers", action="store_true", ) + parser.add_argument( + "--max_transfers", + help=( + "maximum number of transfers to consider each gameweek [EXPERIMENTAL: " + "increasing this value above 2 will make the optimisation extremely slow!]" + ), + type=int, + default=2, + ) parser.add_argument( "--num_iterations", help="how many iterations to use for Wildcard/Free Hit optimization", @@ -842,7 +851,7 @@ def main(): num_free_transfers, max_total_hit, allow_unused_transfers, - 2, + args.max_transfers, num_iterations, num_thread, profile, diff --git a/airsenal/scripts/replay_season.py b/airsenal/scripts/replay_season.py index aabb39a3..b8d45528 100644 --- a/airsenal/scripts/replay_season.py +++ b/airsenal/scripts/replay_season.py @@ -64,6 +64,7 @@ def replay_season( team_model: str = "extended", team_model_args: dict = {"epsilon": 0.0}, fpl_team_id: Optional[int] = None, + max_opt_transfers: int = 2, ) -> None: start = datetime.now() if gameweek_end is None: @@ -130,6 +131,7 @@ def replay_season( fpl_team_id=fpl_team_id, num_thread=num_thread, is_replay=True, + max_opt_transfers=max_opt_transfers, ) gw_result["starting_11"] = [] gw_result["subs"] = [] @@ -227,6 +229,15 @@ def main(): type=float, default=0.0, ) + parser.add_argument( + "--max_transfers", + help=( + "maximum number of transfers to consider each gameweek [EXPERIMENTAL: " + "increasing this value above 2 will make the optimisation extremely slow!]" + ), + type=int, + default=2, + ) args = parser.parse_args() if args.resume and not args.fpl_team_id: @@ -251,6 +262,7 @@ def main(): fpl_team_id=args.fpl_team_id, team_model=args.team_model, team_model_args={"epsilon": args.epsilon}, + max_opt_transfers=args.max_transfers, ) n_completed += 1 From 95ac6372040a364fcfb8e629d8fe14fb1fcb6039 Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Wed, 21 Aug 2024 11:43:54 +0100 Subject: [PATCH 06/11] allow more than 2 transfers in make_best_transfers --- airsenal/framework/optimization_transfers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/airsenal/framework/optimization_transfers.py b/airsenal/framework/optimization_transfers.py index ac754359..0e8e8700 100644 --- a/airsenal/framework/optimization_transfers.py +++ b/airsenal/framework/optimization_transfers.py @@ -388,6 +388,21 @@ def make_best_transfers( players_out = [p for p in _out if p not in _in] # remove duplicates transfer_dict = {"in": players_in, "out": players_out} + elif isinstance(num_transfers, int) and num_transfers > 2: + new_squad, players_out, players_in = make_random_transfers( + squad, + tag, + nsubs=num_transfers, + gw_range=gameweeks, + root_gw=root_gw, + num_iter=num_iter, + update_func_and_args=update_func_and_args, + season=season, + bench_boost_gw=bench_boost_gw, + triple_captain_gw=triple_captain_gw, + ) + transfer_dict = {"in": players_in, "out": players_out} + else: raise RuntimeError(f"Unrecognized value for num_transfers: {num_transfers}") From 24a15346f94f319315eb62e38826f6ea8bc7567d Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Sun, 15 Sep 2024 22:07:40 +0100 Subject: [PATCH 07/11] hotfix counting baseline strategy relies on baseline strategy being first if present --- airsenal/framework/optimization_utils.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/airsenal/framework/optimization_utils.py b/airsenal/framework/optimization_utils.py index 5440b11e..c28ad9d6 100644 --- a/airsenal/framework/optimization_utils.py +++ b/airsenal/framework/optimization_utils.py @@ -467,8 +467,8 @@ def next_week_transfers( if not allow_unused_transfers and ft_available == max_free_transfers: # Force at least 1 free transfer if a free transfer will be lost otherwise. - # NOTE: This will exclude the baseline strategy when allow_unused_transfers - # is False. Re-add it outside this function in that case. + # NOTE: This can cause the baseline strategy to be excluded. Re-add it outside + # this function in that case. ft_choices = list(range(1, max_opt_transfers + 1)) else: ft_choices = list(range(max_opt_transfers + 1)) @@ -610,17 +610,19 @@ def count_expected_outputs( strategies = new_strategies - # if allow_unused_transfers is False baseline of no transfers will be removed above, - # add it back in here, apart from edge cases where it's already included. - if not allow_unused_transfers and ( - gw_ahead > 1 or (gw_ahead == 1 and init_free_transfers == max_free_transfers) - ): - baseline_strat_dict = { - "players_in": {gw: [] for gw in range(next_gw, next_gw + gw_ahead)}, - "chips_played": {}, - } + # if allow_unused_transfers is False baseline of no transfers can be removed above. + # Check whether 1st strategy is the baseline and if not add it back in here + baseline_strat_dict = { + "players_in": {gw: [] for gw in range(next_gw, next_gw + gw_ahead)}, + "chips_played": {}, + } + if strategies[0][2] != baseline_strat_dict: baseline_dict = (max_free_transfers, 0, baseline_strat_dict) strategies.insert(0, baseline_dict) + # print(strategies) + # for s in strategies: + # print("-".join([str(sum(p)) for p in s[2]["players_in"].values()])) + # raise RuntimeError("stop here") return len(strategies) From ae37a786b493442ce79edf68ce86d3bd205e543e Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Tue, 17 Sep 2024 22:09:35 +0100 Subject: [PATCH 08/11] tidy baseline counting/queueing --- airsenal/framework/optimization_utils.py | 71 +++++++++++-------- airsenal/scripts/airsenal_run_pipeline.py | 2 +- .../scripts/fill_transfersuggestion_table.py | 14 ++-- airsenal/scripts/replay_season.py | 2 +- airsenal/tests/test_optimization.py | 20 +++--- 5 files changed, 59 insertions(+), 50 deletions(-) diff --git a/airsenal/framework/optimization_utils.py b/airsenal/framework/optimization_utils.py index c28ad9d6..9333f229 100644 --- a/airsenal/framework/optimization_utils.py +++ b/airsenal/framework/optimization_utils.py @@ -64,16 +64,17 @@ def calc_points_hit(num_transfers, free_transfers): """ if num_transfers in ["W", "F"]: return 0 - elif isinstance(num_transfers, int): - return max(0, 4 * (num_transfers - free_transfers)) - elif (num_transfers.startswith("B") or num_transfers.startswith("T")) and len( - num_transfers - ) == 2: + if ( + isinstance(num_transfers, str) + and num_transfers.startswith(("B", "T")) + and len(num_transfers) == 2 + ): num_transfers = int(num_transfers[-1]) - return max(0, 4 * (num_transfers - free_transfers)) - else: + if not isinstance(num_transfers, int): raise RuntimeError(f"Unexpected argument for num_transfers {num_transfers}") + return max(0, 4 * (num_transfers - free_transfers)) + def calc_free_transfers( num_transfers, prev_free_transfers, max_free_transfers=MAX_FREE_TRANSFERS @@ -85,16 +86,19 @@ def calc_free_transfers( """ if num_transfers in ["W", "F"]: return prev_free_transfers # changed in 24/25 season, previously 1 - elif isinstance(num_transfers, int): - return max(1, min(max_free_transfers, 1 + prev_free_transfers - num_transfers)) - elif (num_transfers.startswith("B") or num_transfers.startswith("T")) and len( - num_transfers - ) == 2: - # take the 'x' out of Bx or Tx + + if ( + isinstance(num_transfers, str) + and num_transfers.startswith(("B", "T")) + and len(num_transfers) == 2 + ): + # take the 'x' out of Bx or Tx (bench boost or triple captain with x transfers) num_transfers = int(num_transfers[-1]) - return max(1, min(max_free_transfers, 1 + prev_free_transfers - num_transfers)) - else: - raise RuntimeError(f"Unexpected argument for num_transfers {num_transfers}") + + if not isinstance(num_transfers, int): + raise ValueError(f"Unexpected input for num_transfers {num_transfers}") + + return max(1, min(max_free_transfers, 1 + prev_free_transfers - num_transfers)) def get_starting_squad( @@ -538,15 +542,15 @@ def next_week_transfers( def count_expected_outputs( - gw_ahead, - next_gw=NEXT_GAMEWEEK, - free_transfers=1, - max_total_hit=None, - allow_unused_transfers=True, - max_opt_transfers=2, - chip_gw_dict={}, - max_free_transfers=MAX_FREE_TRANSFERS, -): + gw_ahead: int, + next_gw: int = NEXT_GAMEWEEK, + free_transfers: int = 1, + max_total_hit: Optional[int] = None, + allow_unused_transfers: bool = True, + max_opt_transfers: int = 2, + chip_gw_dict: dict = {}, + max_free_transfers: int = MAX_FREE_TRANSFERS, +) -> tuple[int, bool]: """ Count the number of possible transfer and chip strategies for gw_ahead gameweeks ahead, subject to: @@ -558,6 +562,13 @@ def count_expected_outputs( are available), if allow_unused_transfers is False. * Make a maximum of max_opt_transfers transfers each gameweek. * Each chip only allowed once. + + Returns + ------- + Tuple of int: number of strategies that will be computed, and bool: whether the + baseline strategy will be excluded from the main optimization tree and will need + to be computed separately (this can be the case if allow_unused_transfers is + False). Either way, the total count of strategies will include the baseline. """ init_strat_dict = { @@ -619,11 +630,11 @@ def count_expected_outputs( if strategies[0][2] != baseline_strat_dict: baseline_dict = (max_free_transfers, 0, baseline_strat_dict) strategies.insert(0, baseline_dict) - # print(strategies) - # for s in strategies: - # print("-".join([str(sum(p)) for p in s[2]["players_in"].values()])) - # raise RuntimeError("stop here") - return len(strategies) + baseline_excluded = True + else: + baseline_excluded = False + + return len(strategies), baseline_excluded def get_discount_factor(next_gw, pred_gw, discount_type="exp", discount=14 / 15): diff --git a/airsenal/scripts/airsenal_run_pipeline.py b/airsenal/scripts/airsenal_run_pipeline.py index 35ce28f0..f777a720 100644 --- a/airsenal/scripts/airsenal_run_pipeline.py +++ b/airsenal/scripts/airsenal_run_pipeline.py @@ -112,7 +112,7 @@ "--max_transfers", help=( "specify maximum number of transfers to consider each gameweek [EXPERIMENTAL: " - "increasing this value above 2 will make the optimisation extremely slow!]" + "increasing this value above 2 may make the optimisation very slow!]" ), type=click.IntRange(min=0, max=5), default=2, diff --git a/airsenal/scripts/fill_transfersuggestion_table.py b/airsenal/scripts/fill_transfersuggestion_table.py index 2a19177d..bc8182db 100644 --- a/airsenal/scripts/fill_transfersuggestion_table.py +++ b/airsenal/scripts/fill_transfersuggestion_table.py @@ -473,7 +473,7 @@ def run_optimization( # if we got to here, we can assume we are optimizing an existing squad. # How many free transfers are we starting with? - if not num_free_transfers: + if num_free_transfers is None: num_free_transfers = get_free_transfers( fpl_team_id, gameweeks[0], @@ -511,7 +511,7 @@ def run_optimization( # number of nodes in tree will be something like 3^num_weeks unless we allow # a "chip" such as wildcard or free hit, in which case it gets complicated num_weeks = len(gameweeks) - num_expected_outputs = count_expected_outputs( + num_expected_outputs, baseline_excluded = count_expected_outputs( num_weeks, next_gw=gameweeks[0], free_transfers=num_free_transfers, @@ -546,9 +546,7 @@ def update_progress(increment=1, index=None): progress_bars[index].update(increment) progress_bars[index].refresh() - if not allow_unused_transfers and ( - num_weeks > 1 or (num_weeks == 1 and num_free_transfers == 2) - ): + if baseline_excluded: # if we are excluding unused transfers the tree may not include the baseline # strategy. In those cases quickly calculate and save it here first. save_baseline_score(starting_squad, gameweeks, tag) @@ -704,8 +702,8 @@ def sanity_check_args(args: argparse.Namespace) -> bool: args.gameweek_end and not args.gameweek_start ): raise RuntimeError("Need to specify both gameweek_start and gameweek_end") - if args.num_free_transfers and args.num_free_transfers not in range(1, 3): - raise RuntimeError("Number of free transfers must be 1 or 2") + if args.num_free_transfers and args.num_free_transfers not in range(6): + raise RuntimeError("Number of free transfers must be 0 to 5") return True @@ -762,7 +760,7 @@ def main(): "--max_transfers", help=( "maximum number of transfers to consider each gameweek [EXPERIMENTAL: " - "increasing this value above 2 will make the optimisation extremely slow!]" + "increasing this value above 2 make the optimisation very slow!]" ), type=int, default=2, diff --git a/airsenal/scripts/replay_season.py b/airsenal/scripts/replay_season.py index b8d45528..881809cd 100644 --- a/airsenal/scripts/replay_season.py +++ b/airsenal/scripts/replay_season.py @@ -233,7 +233,7 @@ def main(): "--max_transfers", help=( "maximum number of transfers to consider each gameweek [EXPERIMENTAL: " - "increasing this value above 2 will make the optimisation extremely slow!]" + "increasing this value above 2 may make the optimisation very slow!]" ), type=int, default=2, diff --git a/airsenal/tests/test_optimization.py b/airsenal/tests/test_optimization.py index 347122aa..39388e4b 100644 --- a/airsenal/tests/test_optimization.py +++ b/airsenal/tests/test_optimization.py @@ -562,7 +562,7 @@ def test_next_week_transfers_play_triple_captain_max_transfers_3(): def test_count_expected_outputs_no_chips_no_constraints(): # No constraints or chips, expect 3**num_gameweeks strategies - count = count_expected_outputs( + count, _ = count_expected_outputs( 3, free_transfers=1, max_total_hit=None, @@ -577,7 +577,7 @@ def test_count_expected_outputs_no_chips_no_constraints(): def test_count_expected_outputs_no_chips_no_constraints_max5(): # No constraints or chips, expect 6**num_gameweeks strategies (0 to 5 transfers # each week) - count = count_expected_outputs( + count, _ = count_expected_outputs( 3, free_transfers=1, max_total_hit=None, @@ -599,7 +599,7 @@ def test_count_expected_outputs_no_chips_zero_hit(): (0, 2, 2), (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 0), (2, 0, 1), (2, 0, 2), (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2) """ - count = count_expected_outputs( + count, _ = count_expected_outputs( 3, free_transfers=1, max_total_hit=0, @@ -617,7 +617,7 @@ def test_count_expected_outputs_no_chips_zero_hit_max5(): Adds (0, 0, 3) to valid strategies compared to test_count_expected_outputs_no_chips_zero_hit above """ - count = count_expected_outputs( + count, _ = count_expected_outputs( 3, free_transfers=1, max_total_hit=0, @@ -638,7 +638,7 @@ def test_count_expected_outputs_no_chips_2ft_no_unused(): (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), (0, 2, 0), (0, 2, 1), (0, 2, 2), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (2, 0, 0) """ - count = count_expected_outputs( + count, _ = count_expected_outputs( 3, free_transfers=2, max_total_hit=None, @@ -661,7 +661,7 @@ def test_count_expected_outputs_no_chips_5ft_no_unused_max5(): (4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), """ - count = count_expected_outputs( + count, _ = count_expected_outputs( 2, free_transfers=5, max_total_hit=None, @@ -680,7 +680,7 @@ def test_count_expected_wildcard_allowed_no_constraints(): (0, 0), (0, 1), (0, 2), (0, 'W'), (1, 0), (1, 1), (1, 2), (1, 'W'), (2, 0), (2, 1), (2, 2), (2, 'W'), ('W', 0), ('W', 1), ('W', 2) """ - count = count_expected_outputs( + count, _ = count_expected_outputs( 2, free_transfers=1, max_total_hit=None, @@ -705,7 +705,7 @@ def count_expected_bench_boost_allowed_no_constraints(): (2, 'B2'), ('B0', 0), ('B0', 1), ('B0', 2), ('B1', 0), ('B1', 1), ('B1', 2), ('B2', 0), ('B2', 1), ('B2', 2), """ - count = count_expected_outputs( + count, _ = count_expected_outputs( 2, free_transfers=1, max_total_hit=None, @@ -727,7 +727,7 @@ def count_expected_play_wildcard_no_constraints(): Strategies: ("W",0), ("W,1), ("W",2) """ - count = count_expected_outputs( + count, _ = count_expected_outputs( 2, free_transfers=1, max_total_hit=None, @@ -748,7 +748,7 @@ def count_expected_play_free_hit_no_unused(): Strategies: (0,0), ("F",1), ("F",2) """ - count = count_expected_outputs( + count, _ = count_expected_outputs( 2, free_transfers=2, max_total_hit=None, From be3d6d9da5ab41a692e053ee60c004f6cef452de Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Tue, 17 Sep 2024 22:14:17 +0100 Subject: [PATCH 09/11] remove duplicate computation of new free transfers Already handled by `next_week_transfers` when computing new strategies to add to the queue. Doing this twice before is probably a long standing bug. Could have meant we were over-estimating free transfers available, and therefore under-counting hits. --- airsenal/scripts/fill_transfersuggestion_table.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/airsenal/scripts/fill_transfersuggestion_table.py b/airsenal/scripts/fill_transfersuggestion_table.py index bc8182db..94644a83 100644 --- a/airsenal/scripts/fill_transfersuggestion_table.py +++ b/airsenal/scripts/fill_transfersuggestion_table.py @@ -40,7 +40,6 @@ from airsenal.framework.optimization_transfers import make_best_transfers from airsenal.framework.optimization_utils import ( MAX_FREE_TRANSFERS, - calc_free_transfers, calc_points_hit, check_tag_valid, count_expected_outputs, @@ -215,9 +214,6 @@ def optimize( strat_dict["discount_factor"][gw] = discount_factor strat_dict["players_in"][gw] = transfers["in"] strat_dict["players_out"][gw] = transfers["out"] - free_transfers = calc_free_transfers( - num_transfers, free_transfers, max_free_transfers - ) depth += 1 From baa1039fa8537c457c5d543f8904e37a7ad38598 Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Tue, 17 Sep 2024 22:56:42 +0100 Subject: [PATCH 10/11] docstring --- airsenal/framework/optimization_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airsenal/framework/optimization_utils.py b/airsenal/framework/optimization_utils.py index 9333f229..bfbb63e8 100644 --- a/airsenal/framework/optimization_utils.py +++ b/airsenal/framework/optimization_utils.py @@ -81,8 +81,8 @@ def calc_free_transfers( ): """ We get one extra free transfer per week, unless we use a wildcard or - free hit, but we can't have more than 2. So we should only be able - to return 1 or 2. + free hit, but we can't have more than 5. So we should only be able + to return 1 to 5. """ if num_transfers in ["W", "F"]: return prev_free_transfers # changed in 24/25 season, previously 1 From 5e1a6309601cae5d5ab2b9c1d9c16bde3a9b4909 Mon Sep 17 00:00:00 2001 From: Jack Roberts Date: Wed, 18 Sep 2024 19:57:45 +0100 Subject: [PATCH 11/11] airsenal 1.9 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bfcad539..8f380b5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "airsenal" -version = "1.8.0" +version = "1.9.0" description = "AI manager for Fantasy Premier League" authors = [ "Angus Williams ",