The JPRESS Code

The JPRESS College Football Ranking system has been live for nearly a full season now, and the results speak for themselves. I honestly do believe the system works and works well. In another post, I detailed the mathematical underpinnings of the JPRESS system. In this post, I want to give the full code in Python for the algorithm.

I tried to comment the code generously, but there are still some parts that are probably confusing. In particular, a couple of the functions have very intricate logic switches that cause the functions to behave differently depending on the situation. I *by no means* consider myself a great coder, and an experienced coder would likely laugh at my code and make it much better. Nevertheless, I present it.

This is presented as totally open-source and free to use. I only ask if you use it publicly for any reason, including modifying it for your own system/reasons, that you give credit to me where due.

Finally, notice the API Key is not given. This is unique to me, and I do not want others using my API Key and potentially reaching my own personal limit of data calls. If you wish to use this system, you’ll need to get your own API Key, for free, at collegefootballdata.com and insert into the appropriate place in the code.

import numpy as np
import pandas as pd
import requests
import cfbd 
from datetime import datetime

# Load needed packages


# The newRankings function takes 7 different parameters to calculate the new rankings of the two teams playing in a game, based on the outcome of that game.
# The math is an Elo-based system. 
def newRankings(home_team_ranking,away_team_ranking, home_team_Prating, away_team_Prating,average_Prating, home_team_points, away_team_points):
    K= 20*(np.minimum(16,((np.asarray(home_team_Prating) + np.asarray(away_team_Prating))/np.asarray(average_Prating))))
    if (np.asarray(home_team_points) > np.asarray(away_team_points)):
        Outcome_home = 1
        Outcome_away = 0
    else:
        Outcome_home = 0
        Outcome_away = 1
    Expected_home = (10**(np.asarray(home_team_ranking)/800))/((10**(np.asarray(home_team_ranking)/800))+(10**(np.asarray(away_team_ranking)/800)))
    Expected_away = (10**(np.asarray(away_team_ranking)/800))/((10**(np.asarray(home_team_ranking)/800))+(10**(np.asarray(away_team_ranking)/800)))
    New_Ranking_home = np.asarray(home_team_ranking) + K*(Outcome_home - Expected_home)
    if (Outcome_home == 1) & ((np.asarray(home_team_points)-np.asarray(away_team_points))>=9):
        New_Ranking_home+=10
        if (np.asarray(home_team_points)-np.asarray(away_team_points))>=17:
              New_Ranking_home+=5
    New_Ranking_away = np.asarray(away_team_ranking) + K * (Outcome_away - Expected_away)
    if (Outcome_away == 1) & ((np.asarray(away_team_points)-np.asarray(home_team_points))>=9):
        New_Ranking_away+=10
        if (np.asarray(away_team_points)-np.asarray(home_team_points))>=17:
              New_Ranking_away+=5
    return New_Ranking_home, New_Ranking_away

# The newPratings function will update the P-Rating of every team based on the change in 
# their rating in the previous week of games. It uses an x^x type transformation
# and turns all ratings into a probability distribution weighted heavily
# towards the top, so the first 4-6 teams will usually take most of the
# available P-Rating. All P-Ratings add up to 1. 
def newPratings(TeamList, average_PRating):
    sumTeams = 0
    newTeamList = []
    for rating in TeamList:
        power = np.log(np.asarray(rating))
        SelectedTeam = (2.5*np.asarray(power))**(2.5*np.asarray(power))
        newTeamList.append(SelectedTeam)
        sumTeams += SelectedTeam
    for team in range(len(newTeamList)):
        newTeamList[team] = newTeamList[team]/sumTeams
        average_PRating = np.median(newTeamList)
  
    return newTeamList, average_PRating

# At the beginning of each new season, the ending ranks from the post-season of
# the previous year is taken and transformed. Instead of starting at the
# default rating of 1000 and P-Rating of 1/n (n being the number of teams),
# teams starting a new season will get a small boost (or nerf) to their
# P-Rating and Rating based on their performance last year. Whereas
# the default starting rank is 1000, teams will vary between around
# 800 - 1200 when the transformRanks function is applied. Most teams
# will be between 950 and 1050. 
def transformRanks(LastYearRankings):
    newRanks = []
    for rank in LastYearRankings:
        newRank = (1000*np.log(np.asarray(rank)))/(3*np.log(10))
        newRanks.append(newRank)
    return newRanks

# This function downloads the necessary data several times per program execution.
# The data comes from CollegeFootballData.com and uses their API to do so.
 Without developing a more convoluted
# program with calls to other files, this seemed the best way to implement currently.
def downloadData(input2, post = "regular"):
    cfb_api_key = "<<INSERT YOUR OWN API KEY HERE>>"
    configuration = cfbd.Configuration()
    configuration.api_key['Authorization'] = cfb_api_key
    configuration.api_key_prefix['Authorization'] = "Bearer"
    api_instance = cfbd.GamesApi(cfbd.ApiClient(configuration))
    year = userInput
    try: 
        cfbd_data = api_instance.get_games(input2, season_type=post)
    except ApiException as e:
        print("Error:" % e)
    for game in range(0,len(cfbd_data)):
        cfbd_data[game] = vars(cfbd_data[game])
    cfbd_df = pd.DataFrame(cfbd_data)
    my_data = cfbd_df[["_season","_week","_home_team","_home_points","_away_team","_away_points"]]
    return my_data

# This is the main function. It begins by downloading the appropriate data and manipulating
# the data into variables that will be used in the calculations. The core of the function
# iterates over each week in the season and also (within each week) iterates over each
# game from that week. During each game, the newRankings function is called to update
# the rating of each team based on the game's result. At the end of each week,
# new P-Ratings are also generated.This function behaves differently, depending
# on which other function called it.
def updateSeasonRankings (week,year, startingRank=1000, printResults=False):
    if (postSeasonRankings.has_been_called==False):
        data = downloadData(year)
    else:
        data= downloadData(year,post="postseason")
    weekNum = 0
    if(isinstance(startingRank,int)):
        team_list = np.unique(data["_home_team"])
        startingRank2 = pd.DataFrame(team_list,columns=["team_list"])
        startingRank2.insert(1, "Prating", 1/(data.shape[0]))
        startingRank2.insert(2,"rating",value=1000)
    else:
        startingRank2 = startingRank
    team_list_home = np.unique(data["_home_team"])
    team_list_away = np.unique(data["_away_team"])
    team_list_combined = list(team_list_home) + list(team_list_away)
    team_list = np.unique(team_list_combined)
    rankings = pd.DataFrame(team_list, columns=["team_list"])
    rankings.insert(1,"rating",value=1000)
    Pratings = pd.DataFrame(team_list, columns=["team_list"])
    Pratings.insert(1,"Prating",1/len(team_list))
    for team in range(0,len(team_list)):
        for team2 in range(0,startingRank2.shape[0]):
            if (rankings.iloc[team,0] == startingRank2.iloc[team2,0]):
                rankings.iat[team,1] = startingRank2.iloc[team2,2]   
    average_Prating = np.prod(Pratings["Prating"])**(1/len(Pratings["Prating"]))
    if not(np.mean(rankings["rating"]==1000)):
        NewPRatingScores = newPratings(rankings["rating"],average_Prating)
        Pratings.iloc[:,1] = NewPRatingScores[0]
        average_Prating = NewPRatingScores[1] 
    if ((preSeasonRankings.has_been_called) | (postSeasonRankings.has_been_called) | (justpreSeasonRankings.has_been_called)):
        week = np.max(data["_week"])
    while weekNum <= week:
        if weekNum > np.max(data["_week"]):
            break
        games_that_week = pd.DataFrame(data.loc[data["_week"]==weekNum,:])
        if (games_that_week.shape[0] != 0):   
            for game in range(0,(games_that_week.shape[0])):
                if (preSeasonRankings.has_been_called) & (switch==False):
                    continue
                home_team = games_that_week.iloc[game,2]
                away_team = games_that_week.iloc[game,4]
                if pd.isna(home_team) | pd.isna(away_team):
                    continue
                if (postSeasonRankings.has_been_called==False):
                    if not(home_team in team_list):
                        continue
                    if not(away_team in team_list):
                        continue
                home_team_current_ranking = rankings.loc[rankings["team_list"]==home_team]
                home_team_current_ranking = home_team_current_ranking.iloc[:,1]
                away_team_current_ranking = rankings.loc[rankings["team_list"]==away_team]
                away_team_current_ranking = away_team_current_ranking.iloc[:,1]
                home_team_Prating = Pratings.loc[Pratings["team_list"]==home_team]
                home_team_Prating = home_team_Prating.iloc[:,1]
                away_team_Prating = Pratings.loc[Pratings["team_list"]==away_team]
                away_team_Prating = away_team_Prating.iloc[:,1]
                home_team_points_scored = games_that_week.iloc[game,3]
                away_team_points_scored = games_that_week.iloc[game,5]
                if pd.isna(home_team_points_scored) | pd.isna(away_team_points_scored):
                    continue
                BothTeamsNewRankings = newRankings(home_team_current_ranking, away_team_current_ranking, home_team_Prating, away_team_Prating, average_Prating,home_team_points_scored, away_team_points_scored)
                which_home = rankings.index[rankings["team_list"]==home_team][0]
                rankings.iat[which_home,1] = np.array(BothTeamsNewRankings[0])
                which_away = rankings.index[rankings["team_list"]==away_team][0]
                rankings.iat[which_away,1]= np.array(BothTeamsNewRankings[1])
        weekNum = weekNum+1
        NewPRatingScores = newPratings(rankings["rating"],average_Prating)
        Pratings.iloc[:,1] = NewPRatingScores[0]
        average_Prating = NewPRatingScores[1]
    if (average_Prating == np.prod(Pratings["Prating"])**(1/len(Pratings["Prating"]))):
        NewPRatingScores = newPratings(rankings["rating"],average_Prating)
        Pratings.iloc[:,1] = NewPRatingScores[0]
    Top_25_table = pd.DataFrame(Pratings, columns=["team_list","Prating"])
    Top_25_table.insert(2,value=rankings.iloc[:,1],column="rankings") 
    if printResults == False:
        return Top_25_table
    else:
        Top_25 = Top_25_table.sort_values(by=["rankings"],ascending=False,inplace=False)
        Top_25_table2 = Top_25.iloc[:25,]
        final_Pratings = newPratings(Top_25_table2["rankings"],1)
        Top_25_table3 = Top_25_table2.replace(Top_25_table2["Prating"],pd.Series(final_Pratings[0]))
        return Top_25_table3

#The first of the three specialized functions. These functions intermingle with each
# other and operate on a convoluted logic system of T/F switches to control their
# behavior. Generally speaking, preSeasonRankings exists to generate the 
# rankings for Week 0 of the season and is later fed into a different
# function to calculate a desired season's values. This function, perhaps
# ironically, does NOT give "just the Pre-Season Rankings" - another function
# exists for that. This function is always a stepping stone to another answer.
def preSeasonRankings(year_num):
    preSeasonRankings.has_been_called = True
    if (justpreSeasonRankings.has_been_called):
        postRatings = postSeasonRankings(year_num)
    else:
        postRatings = postSeasonRankings(year_num-1)
    postSeasonRankings.has_been_called = False
    last_year_rankings = updateSeasonRankings(1,year_num, startingRank=postRatings)
    preseasonRanks = transformRanks(last_year_rankings.iloc[:,-1])
    last_year_rankings.at[:,2] = preseasonRanks
    preSeasonRankings.has_been_called = False
    return last_year_rankings

#The most logic-heavy function, postSeasonRankings has the primary
# goal of calculating final season rankings, after bowls and playoffs.
# If the user enters "20" as their week number, this function's
# output is what is returned. Otherwise, it is always used in
# calculating pre-season ratings (to find the previous year's final ratings). 
# This duality creates what is essentially two functions, 
# depending on the switches inside the function.
def postSeasonRankings(my_userInput):
    if justpreSeasonRankings.has_been_called== False:
        if (preSeasonRankings.has_been_called) == False:
            start = preSeasonRankings(my_userInput)
            this_year_rankings = updateSeasonRankings(1, my_userInput, startingRank = start)
        else:
            this_year_rankings = updateSeasonRankings(1, my_userInput)
            postSeasonRankings.has_been_called = True
    else: 
        this_year_rankings = 1000
    if  preSeasonRankings.has_been_called:
        switch = False
    else:
        switch = True
    if justpreSeasonRankings.has_been_called == False:
        postSeasonRankings.has_been_called = True
    post_year_rankings = updateSeasonRankings(1, my_userInput, startingRank = this_year_rankings,printResults=switch)
    postSeasonRankings.has_been_called = True
    if justpreSeasonRankings.has_been_called == True:
        post_year_rankings = updateSeasonRankings(1, my_userInput, startingRank = post_year_rankings, printResults=switch)
    if (preSeasonRankings.has_been_called == False):
        postSeasonRankings.has_been_called = False
    return post_year_rankings

# The last of the functions, this specialized function exists for only
# one purpose: calculating the pre-season rankings for teams
# before the season starts. This function calculates last
# year's ratings and P-Ratings, transforms them for
# the starting values for the upcoming year, and then
# bypasses the rest of the core calculations. This function
# executes when the user enters "0" for their Week number.
def justpreSeasonRankings(userInput3):
    justpreSeasonRankings.has_been_called = True
    post_year_rankings = postSeasonRankings(userInput3)
    preseasonRanks2 = transformRanks(post_year_rankings.iloc[:,-1])
    post_year_rankings["rankings"] = preseasonRanks2
    newP = newPratings(post_year_rankings["rankings"],1)
    post_year_rankings["Prating"] = newP[0]
    justpreSeasonRankings.has_been_called = False
    return post_year_rankings
    

### The main body of the program, this while loop continues until the user has
# satisfied the requirements asked. If the user enters erroneous data,
# the program will complain until the user either quits with "Q" or
# enters valid data. The program gives the option to 
# enter a Year and a Week, with Weeks 0 and 20 reserved for
# Pre- and Post- Season results, respectively. 
# The user also has the opportunity to download the results
# as an Excel file. The file will automatically download
# to the same folder as the Python file. 
while True:
    try:
        preSeasonRankings.has_been_called  = False
        postSeasonRankings.has_been_called = False
        justpreSeasonRankings.has_been_called = False
        switch = True
        userInput = input("Welcome to the J-Press Rankings program! To receive the rankings, you need to input a valid year from 2014 to the current year. Or enter Q to quit.")
        if userInput == "Q":
            break
        elif np.isnan(int(userInput)):
            raise Exception("You must enter a number!")
        else:
            userInput = int(userInput)
            if (userInput < 2014) | (userInput > datetime.now().year):
                raise Exception("Your number must be between 2014 and the current year!")
            userInput2 = input("Now, enter the Week of the season you want rankings for. For Preseason, choose 0 for your week. For Post-Season, choose 20 for your week. Or enter Q to quit.")
            if userInput2 == "Q":
                break
            elif np.isnan(int(userInput2)):
                raise Exception("You must enter a number!")
            userInput2 = int(userInput2)
            if (userInput == (datetime.now().year)) & (datetime.now().month <=8) & (userInput2 >1):
                raise Exception("Please choose a Week that has already occurred. For Preseason, choose 0 for your week.")
        userInputFile = input("Would you like to save the output to an Excel file? Type Y for Yes or N for No or Q to quit.")
        if(userInputFile == "Q"):
             break
        elif (userInputFile == "Y"):
            saveToFile = True
        elif (userInputFile == "N"):
            saveToFile = False
        else:
            raise Exception("Enter a correct input! You must enter Y for Yes, N for No, or Q to quit.")
    except:
        print("Error! Please follow the directions carefully. Make sure your numbers are the Year and Week correctly. Please try again.")
    else:
        if (userInput2 != 20) & (userInput2 !=0):
            print("The Top 25 for Week " + str(userInput2) + " of the " + str(userInput) + " season:" )
            startSeason = preSeasonRankings(userInput)
            update = updateSeasonRankings(week=int(userInput2),year=userInput, startingRank=startSeason, printResults=True)
            update.index = np.arange(1,len(update)+1)
            print(update)
            if(saveToFile == True):
                fileName = "JPressRankings_Week"+str(userInput2)+"_"+str(userInput)+".xlsx"
                update.to_excel(fileName)
            break
        elif userInput2 == 0:
            userInput3 = int(userInput) - 1
            print("The Top 25 for Pre-Season of the " + str(userInput) + " season:")
            update2 = justpreSeasonRankings(userInput3)
            update2.index = np.arange(1,len(update2)+1)
            print(update2)
            if(saveToFile == True):
                fileName = "JPressRankings_Preseason_"+str(userInput)+".xlsx"
                update2.to_excel(fileName)
            break
        else: 
            print("The Top 25 for the Post-Season of the " + str(userInput) + " season: ")
            update3 = postSeasonRankings(userInput)
            update3.index = np.arange(1,len(update3)+1)
            print(update3)
            if(saveToFile == True):
                fileName = "JPressRankings_Post_Season_"+str(userInput)+".xlsx"
                update3.to_excel(fileName)
            break
            
### Special Thanks to Peter Kosek for his encouragement, advice, and feedback on the development. It is appreciated! 
### Developed by Stuart Jones, 2021. All usage and/or modification of this program is allowed, for personal, private,
###  and/or commercial purposes. It is simply asked to give credit in the comments of the program (if the program is
### made public), or in a visible location adjacent to the results obtained via this program.