Advertisement
Guest User

premier league solver code

a guest
Jan 17th, 2020
165
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.67 KB | None | 0 0
  1. import datetime
  2. import json
  3. import concurrent.futures
  4.  
  5. import requests
  6. import dateutil.parser
  7. import minizinc
  8.  
  9. # file nname to cache match results
  10. MATCH_DATA_FILENAME = 'matchdata.json'
  11.  
  12. # teams to calculate extreme positions for (only part of name necessary, e.g. don't need AFC etc.)
  13. TARGETS = ['Liverpool', 'Manchester City', 'Leicester', 'Aston Villa', 'Bournemouth', 'Norwich']
  14.  
  15. # token for football-data.org
  16. TOKEN = 'INSERT YOUR TOKEN HERE'
  17.  
  18. URI = 'http://api.football-data.org/v2/competitions/PL/matches?season=2019'
  19. HEADERS = {'X-Auth-Token': TOKEN}
  20.  
  21. # timeout for solver, in seconds
  22. TIMEOUT = 10
  23.  
  24.  
  25. class Match:
  26.     def __init__(self, match_json):
  27.         self.time = dateutil.parser.parse(match_json['utcDate'])
  28.         self.last_updated = dateutil.parser.parse(match_json['lastUpdated'])
  29.         self.complete = match_json['status'] == 'FINISHED'
  30.  
  31.         # assume match results will be available approximately this many minutes after scheduled start time
  32.         ASSUMED_LENGTH = 107
  33.         self.expected = self.time + datetime.timedelta(minutes=ASSUMED_LENGTH) < datetime.datetime.now(datetime.timezone.utc)
  34.  
  35.         home = match_json['homeTeam']
  36.         away = match_json['awayTeam']
  37.  
  38.         self.home_id = home['id']
  39.         self.home_name = home['name']
  40.         self.away_id = away['id']
  41.         self.away_name = away['name']
  42.        
  43.         score = match_json['score']
  44.         if score['winner'] == 'HOME_TEAM':
  45.             self.result = 1
  46.         elif score['winner'] == 'AWAY_TEAM':
  47.             self.result = -1
  48.         else:
  49.             self.result = 0
  50.         self.home_goals = score['fullTime']['homeTeam']
  51.         self.away_goals = score['fullTime']['awayTeam']
  52.  
  53.     def __repr__(self):
  54.         return f'Match(<{self!s}>)'
  55.  
  56.     def __str__(self):
  57.         if self.complete:
  58.             return f'{self.home_name} {self.home_goals} - {self.away_goals} {self.away_name} ({self.time})'
  59.         else:
  60.             return f'{self.home_name} {self.home_goals}+? - {self.away_goals}+? {self.away_name} ({self.time})'
  61.  
  62.  
  63. def parse_match_data(data):
  64.     matches = []
  65.     teams = []
  66.     for match_json in data['matches']:
  67.         matches.append(Match(match_json))
  68.     for match in matches:
  69.         if match.home_name not in teams:
  70.             teams.append(match.home_name)
  71.         if match.away_name not in teams:
  72.             teams.append(match.away_name)
  73.     return teams, matches
  74.  
  75.  
  76. def run_model(additional_code, teams, matches):
  77.     solver = minizinc.Solver.lookup('coinbc')
  78.     model = minizinc.Model()
  79.     # set up 2D arrays for match results and 1D array for points per team
  80.     # slightly ugly because there are entries for teams playing themselves, but these aren't used anywhere and are constrained to be draws,
  81.     # so the solver should ignore them
  82.     common_code = f"""
  83.        int: TEAMS = {len(teams)};
  84.  
  85.        array[1..TEAMS, 1..TEAMS] of var {{0, 1, 3}}: home_points;
  86.        array[1..TEAMS, 1..TEAMS] of var {{0, 1, 3}}: away_points;
  87.        away_points = array2d(1..TEAMS, 1..TEAMS, [if home_points[i,j] == 1 then 1 else 3 - home_points[i,j] endif | j,i in 1..TEAMS]);
  88.        array[1..TEAMS] of var int: points = [sum([home_points[i, j] + away_points[i, j] | j in 1..TEAMS where i != j]) | i in 1..TEAMS];
  89.  
  90.        constraint forall(i in 1..TEAMS)(home_points[i, i] == 1);
  91.    """
  92.     model.add_string(common_code)
  93.  
  94.     # add match results as constraints
  95.     for match in matches:
  96.         if match.complete:
  97.             i = 1 + teams.index(match.home_name)
  98.             j = 1 + teams.index(match.away_name)
  99.             home_points = 3 if match.result == 1 else 1 if match.result == 0 else 0
  100.             model.add_string(f'constraint home_points[{i}, {j}] = {home_points};')
  101.  
  102.     model.add_string(additional_code)
  103.  
  104.     instance = minizinc.Instance(solver, model)
  105.     result = instance.solve(timeout=datetime.timedelta(seconds=TIMEOUT))
  106.     try:
  107.         return result['objective']
  108.     except KeyError:
  109.         return 'TIMED OUT'
  110.  
  111.  
  112. def min_win_points(teams, matches):
  113.     # solve for smallest points such that it's the largest of any team
  114.     return run_model('solve minimize max(points);', teams, matches)
  115.  
  116. def min_win_outright_points(teams, matches):
  117.     # solve for smallest possible points such that it's the unique largest of any team
  118.     additional = """
  119.        var 0..6*(TEAMS-1): max_points = max(points);
  120.        constraint sum([points[i] == max_points | i in 1..TEAMS]) == 1;
  121.        solve minimize max_points;
  122.    """
  123.     return run_model(additional, teams, matches)
  124.  
  125. def max_relegation_points(teams, matches):
  126.     # solve for largest number of points such that a team potentially in the relegation zone (depending on tiebreakers) has it
  127.     additional = """
  128.        var 0..6*(TEAMS-1): relegation_points;
  129.        constraint sum([points[i] == relegation_points | i in 1..TEAMS]) > 0;
  130.        constraint sum([points[i] >= relegation_points | i in 1..TEAMS]) > 17;
  131.        solve maximize relegation_points;
  132.    """
  133.     return run_model(additional, teams, matches)
  134.  
  135. def max_relegation_outright_points(teams, matches):
  136.     # solve for largest number of points such that a team definitely in the relegation zone has it
  137.     additional = """
  138.        var 0..6*(TEAMS-1): relegation_points;
  139.        constraint sum([points[i] == relegation_points | i in 1..TEAMS]) > 0;
  140.        constraint sum([points[i] > relegation_points | i in 1..TEAMS]) > 16;
  141.        solve maximize relegation_points;
  142.    """
  143.     return run_model(additional, teams, matches)
  144.  
  145. def highest_rank(team_index, teams, matches):
  146.     # minimise table position of the given team
  147.     additional = f"""
  148.        1..TEAMS: INDEX = {team_index+1};
  149.        var 1..TEAMS: highest_rank = 1 + TEAMS - sum([points[i] <= points[INDEX] | i in 1..TEAMS]);
  150.        solve minimize highest_rank;
  151.    """
  152.     return run_model(additional, teams, matches)
  153.  
  154. def lowest_rank(team_index, teams, matches):
  155.     # maximise table position of the given team
  156.     additional = f"""
  157.        1..TEAMS: INDEX = {team_index+1};
  158.        var 1..TEAMS: lowest_rank = sum([points[i] >= points[INDEX] | i in 1..TEAMS]);
  159.        solve maximize lowest_rank;
  160.    """
  161.     return run_model(additional, teams, matches)
  162.  
  163.  
  164. def main():
  165.     # try and load local cache
  166.     try:
  167.         with open(MATCH_DATA_FILENAME) as f:
  168.             match_data = json.load(f)
  169.     except:
  170.         print('INFO: match data missing or corrupted')
  171.         match_data = None
  172.  
  173.     # discard cache if any new results are expected
  174.     if match_data is not None:
  175.         teams, matches = parse_match_data(match_data)
  176.         for match in matches:
  177.             if match.expected and not match.complete:
  178.                 print(f'INFO: match data outdated - one missing match is {match}')
  179.                 match_data = None
  180.                 break
  181.  
  182.     # download new data and save cache, if necessary
  183.     if match_data is None:
  184.         print('INFO: downloading match data...')
  185.         response = requests.get(URI, headers=HEADERS)
  186.         match_data = response.json()
  187.         with open(MATCH_DATA_FILENAME, 'w') as f:
  188.             json.dump(match_data, f)
  189.         teams, matches = parse_match_data(match_data)
  190.     else:
  191.         print('INFO: match data restored')
  192.  
  193.     with concurrent.futures.ProcessPoolExecutor() as executor:
  194.         lr = {}
  195.         hr = {}
  196.        
  197.         for target in TARGETS:
  198.             for i, team in enumerate(teams):
  199.                 if target in team:
  200.                     index = i
  201.                     break
  202.             else:
  203.                 raise ValueError(f'team {target} not found')
  204.             lr[target] = executor.submit(lowest_rank, index, teams, matches)
  205.             hr[target] = executor.submit(highest_rank, index, teams, matches)
  206.  
  207.         mwp = executor.submit(min_win_points, teams, matches)
  208.         mrp = executor.submit(max_relegation_points, teams, matches)
  209.         mwop = executor.submit(min_win_outright_points, teams, matches)
  210.         mrop = executor.submit(max_relegation_outright_points, teams, matches)
  211.  
  212.         for target in TARGETS:
  213.             print(target)
  214.             print('=' * len(target))
  215.             print(f'  worst possible position: {lr[target].result()}')
  216.             print(f'  best possible position: {hr[target].result()}')
  217.  
  218.         print('Extras')
  219.         print('======')
  220.         print(f'minimum possible points for tied or outright first place team: {mwp.result()}')
  221.         print(f'minimum possible points for outright first place team: {mwop.result()}')
  222.         print(f'maximum possible points for tied or outright relegated team: {mrp.result()}')
  223.         print(f'maximum possible points for outright relegated team: {mrop.result()}')
  224.  
  225.  
  226. if __name__ == '__main__':
  227.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement