diff --git a/README.md b/README.md index 29678a9..d3a9168 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,12 @@ Alternatively, you can provide a `numpy.array` where the columns correspond to p import numpy as np from fantasy_ga import LineupGenerator, read_csv +model = LineupGenerator(m, n_pop, n_gen, n_breed, n_mutate, n_compound) +lineups, scores = model.fit() +optimal_lineups, top_n_scores = model.get_top_n_lineups(1) + # load data from DraftKings salary csv -id_to_name, m = read_csv("examples/DraftKings/DKSalaries.csv", site="DraftKings") +id_to_name, id_to_salary, m = read_csv("examples/DraftKings/DKSalaries.csv", site="DraftKings") # initial population of random lineups n_pop = 1000 @@ -33,19 +37,29 @@ n_mutate = 30 n_compound = 5 model = LineupGenerator(m, n_pop, n_gen, n_breed, n_mutate, n_compound) -lineups, scores = model.fit() -optimal_lineups, top_n_scores = model.get_top_n_lineups(lineups, scores, 1) +model.fit() +optimal_lineups, top_n_scores = model.get_top_n_lineups(1) + +print( + f"Players: {[id_to_name[id] for id in optimal_lineups[0]]}\nSalary Total: {sum([id_to_salary[id] for id in optimal_lineups[0]])}\nExpected FPTS: {top_n_scores[0]}" +) ``` ### CLI -As a module +As a Python module ``` $ python -m fantasy_ga --filepath=examples/DraftKings/DKSalaries.csv --site=DraftKings --n_pop=100 --n_gen=5 --n_breed=100 --n_mutate=100 --n_compound=10 --top_n_lineups=1 -> PlayerIDs: [ 17. 106. 70. 0. 63. 33. 1. 108.], FPTS: 308.62082 ``` -or a command +or a CLI command +``` +$ fantasy-ga --filepath=examples/DraftKings/DKSalaries.csv --site=DraftKings --n_pop=100 --n_gen=5 --n_breed=100 --n_mutate=100 --n_compound=10 --top_n_lineups=1 ``` -$ fantasy-ga --filepath=examples/DraftKings/DKSalaries.csv --site=DraftKings --n_pop=100 --n_gen=5 --n_breed=100 --n_mutate=100 --n_compound=10 --top_n_lineups=1 -> PlayerIDs: [ 17. 106. 70. 0. 63. 33. 1. 108.], FPTS: 308.62082 +which generates +``` +Generated Top 1 lineups + +Players: ['Russell Westbrook', 'Bruce Brown', 'Michael Porter Jr.', 'Jerami Grant', 'Mason Plumlee', 'Paul George', 'Aaron Gordon', 'Marcus Morris Sr.'] +Salary Total: 49100 +Expected FPTS: 254.11 ``` \ No newline at end of file diff --git a/examples/generate_lineups.py b/examples/generate_lineups.py index 530ebe9..3d90f83 100644 --- a/examples/generate_lineups.py +++ b/examples/generate_lineups.py @@ -2,18 +2,19 @@ if __name__ == "__main__": # load data from DraftKings salary csv - id_to_name, m = read_csv("examples/DraftKings/DKSalaries.csv", site="DraftKings") + id_to_name, id_to_salary, m = read_csv( + "examples/DraftKings/DKSalaries.csv", site="DraftKings" + ) n_pop = 1000 n_breed = 30 n_mutate = 30 n_gen = 16 n_compound = 5 - pos_start_idx = 3 model = LineupGenerator(m, n_pop, n_gen, n_breed, n_mutate, n_compound) - lineups, scores = model.fit() - optimal_lineups, top_n_scores = model.get_top_n_lineups(lineups, scores, 1) + model.fit() + optimal_lineups, top_n_scores = model.get_top_n_lineups(1) print( - f"Players: {[id_to_name[id] for id in optimal_lineups[0]]}\nFPTS: {top_n_scores[0]}" + f"Players: {[id_to_name[id] for id in optimal_lineups[0]]}\nSalary Total: {sum([id_to_salary[id] for id in optimal_lineups[0]])}\nExpected FPTS: {top_n_scores[0]}" ) diff --git a/fantasy_ga/__init__.py b/fantasy_ga/__init__.py index d9c4a84..91a4798 100644 --- a/fantasy_ga/__init__.py +++ b/fantasy_ga/__init__.py @@ -2,4 +2,4 @@ from fantasy_ga.utils import read_csv from fantasy_ga.constants import * -__version__ = "0.1.0" +__version__ = "0.2.0" diff --git a/fantasy_ga/__main__.py b/fantasy_ga/__main__.py index 955fd76..bca5908 100644 --- a/fantasy_ga/__main__.py +++ b/fantasy_ga/__main__.py @@ -5,19 +5,22 @@ def main(): parser = argparse.ArgumentParser() - parser.add_argument("--filepath", help="filepath") - parser.add_argument("--site", help="site") - parser.add_argument("--top_n_lineups", help="filepath") - parser.add_argument("--n_pop", help="Do the bar option") - parser.add_argument("--n_gen", help="Foo the program") - parser.add_argument("--n_breed", help="Foo the program") - parser.add_argument("--n_mutate", help="Foo the program") - parser.add_argument("--n_compound", help="Foo the program") - parser.add_argument("--sal_cap", help="Foo the program") + parser.add_argument( + "--filepath", + help="File path to the csv file exported from daily fantasy platforms", + ) + parser.add_argument("--site", help="Site type. Currently supports (DraftKings)") + parser.add_argument("--top_n_lineups", help="top N lineups to display") + parser.add_argument("--n_pop", help="Number of initial population to sample from") + parser.add_argument("--n_gen", help="Number of generations to run evolution cycles") + parser.add_argument("--n_breed", help="Number of children lineups to generate") + parser.add_argument("--n_mutate", help="Number of mutations for lineups") + parser.add_argument("--n_compound", help="Number of compound evolution cycles") + parser.add_argument("--sal_cap", help="Salary cap for the lineup.") args = parser.parse_args() - id_to_name, m = read_csv(args.filepath, args.site) + id_to_name, id_to_salary, m = read_csv(args.filepath, args.site) model = LineupGenerator( m=m, n_pop=int(args.n_pop), @@ -27,12 +30,13 @@ def main(): n_compound=int(args.n_compound), ) - lineups, scores = model.fit() - lineups, scores = model.get_top_n_lineups(lineups, scores, int(args.top_n_lineups)) - print(f"generated top {args.top_n_lineups} lineups") + model.fit() + lineups, scores = model.get_top_n_lineups(int(args.top_n_lineups)) + print(f"Generated Top {args.top_n_lineups} lineups") for lineup, score in zip(lineups, scores): - print(f"Players: {[id_to_name[id] for id in lineup]}, FPTS: {score}") - + print( + f"\nPlayers: {[id_to_name[id] for id in lineup]}\nSalary Total: {sum([id_to_salary[id] for id in lineup])}\nExpected FPTS: {score}" + ) if __name__ == "__main__": main() diff --git a/fantasy_ga/lineup_generator.py b/fantasy_ga/lineup_generator.py index 8cc3037..a57e12e 100644 --- a/fantasy_ga/lineup_generator.py +++ b/fantasy_ga/lineup_generator.py @@ -21,24 +21,25 @@ def __init__( self.n_compound = n_compound self.pos_start_idx = pos_start_idx self.sal_cap = sal_cap + self.lineups = [] + self.scores = [] - @staticmethod - def get_top_n_lineups(lineups, scores, n): - top_n_scores = (-scores).argsort()[:n] - return lineups.take(top_n_scores.astype(int), 0), scores.take( + def get_top_n_lineups(self, n): + top_n_scores = (-self.scores).argsort()[:n] + return self.lineups.take(top_n_scores.astype(int), 0), self.scores.take( top_n_scores.astype(int), 0 ) - def calc_scores(self, lineups: np.array): + def calc_scores(self): scores = [] - for lineup in lineups: + for lineup in self.lineups: sal, fpts = self.m[np.in1d(self.m[:, 0], lineup.astype(int))][ :, [1, 2] ].sum(axis=0) scores.append(fpts) if sal <= self.sal_cap and len( np.unique(lineup) ) == 8 else scores.append(-1) - return np.array(scores) + self.scores = np.array(scores) def create_random_lineups(self): lineups = [] @@ -56,48 +57,45 @@ def create_random_lineups(self): lineups.append(lineup) return np.array(lineups) - def breed(self, lineups, scores): + def breed(self): new_lineups = [] - parents_idx = (-scores).argsort()[:2] - parents = lineups.take(parents_idx, 0) + parents_idx = (-self.scores).argsort()[:2] + parents = self.lineups.take(parents_idx, 0) for _ in range(self.n_breed): if np.array_equal(*parents): - return parents + self.lineups = parents + return else: breed_idx = np.random.choice(2, 8) new_lineups.append([parents[p, idx] for idx, p in enumerate(breed_idx)]) - return np.vstack([parents, np.array(new_lineups)]) + self.lineups = np.vstack([parents, np.array(new_lineups)]) - def mutate(self, lineups): - mutate_idx = np.random.choice(lineups.shape[0], self.n_mutate) + def mutate(self): + mutate_idx = np.random.choice(self.lineups.shape[0], self.n_mutate) for idx in mutate_idx: mutant = self.m[np.random.choice(self.m.shape[0]), :] original = self.m[ - self.m[:, 0] == np.random.choice(lineups[idx]).astype(int), : + self.m[:, 0] == np.random.choice(self.lineups[idx]).astype(int), : ][0] eligible_pos = np.where( mutant[self.pos_start_idx :].astype(bool) & original[self.pos_start_idx :].astype(bool) )[0] swap_pos = np.random.choice(eligible_pos) - lineups = np.vstack([lineups, lineups[idx]]) - lineups[-1, swap_pos] = mutant[0] - return lineups + self.lineups = np.vstack([self.lineups, self.lineups[idx]]) + self.lineups[-1, swap_pos] = mutant[0] - def evolve(self, lineups, scores): + def evolve(self): for _ in range(self.n_gen): - lineups = self.breed(lineups, scores) - lineups = self.mutate(lineups) - scores = self.calc_scores(lineups) - return lineups, scores + self.breed() + self.mutate() + self.calc_scores() def fit(self): - lineups = self.create_random_lineups() - scores = self.calc_scores(lineups) + self.lineups = self.create_random_lineups() for _ in range(self.n_compound): - lineups, scores = self.evolve(lineups, scores) - lineups = np.vstack([lineups, self.create_random_lineups()]) - scores = self.calc_scores(lineups) - return lineups, scores + self.calc_scores() + self.evolve() + self.lineups = np.vstack([self.lineups, self.create_random_lineups()]) diff --git a/fantasy_ga/utils.py b/fantasy_ga/utils.py index 1af0b86..64aaa95 100644 --- a/fantasy_ga/utils.py +++ b/fantasy_ga/utils.py @@ -19,13 +19,15 @@ def read_csv(filepath: str, site: str) -> tuple[dict, list]: _ = next(reader) players = [] id_to_name = {} + id_to_salary = {} if site == "DraftKings": for row in reader: id_to_name[int(row[3])] = row[2] + id_to_salary[int(row[3])] = int(row[5]) players.append( [int(row[3]), int((row[5])), float(row[8])] + encode_position(row[4]) ) elif site == "FanDuel": raise NotImplementedError - return id_to_name, np.array(players) + return id_to_name, id_to_salary, np.array(players) diff --git a/pyproject.toml b/pyproject.toml index bb62fd9..0bf022d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "fantasy-ga" -version = "0.1.0" +version = "0.2.0" description = "Genetic algorithm library to generate fantasy basketball lineups" readme = "README.md" authors = [{ name = "Kengo Arao", email = "kengo@hey.com" }] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup()