TOC
NHL API
In this tutorial we will use the NHL API to practice pulling data from an API and formatting for a downstream process.
The NHL API appears to be undocumented, but there are somed dedicated users that have published most of the functionality. Below are links to the most helpful source I found.
https://gitlab.com/dword4/nhlapi https://gitlab.com/dword4/nhlapi/-/blob/master/records-api.md
Endpoint examples
https://records.nhl.com/records/skater-records/goals/skater-most-goals-one-season https://api.nhle.com/stats/rest/en/franchise?sort=fullName&include=lastSeason.id&include=firstSeason.id
Before we get started let’s load the following libraries.
import requests
import json
import pandas as pd
Get Team Info
The first endpoint we will work with will give us infomation about the team:
https://statsapi.web.nhl.com/api/v1/teams
Get Request
teams_url = "https://statsapi.web.nhl.com/api/v1/teams"
team_response = requests.get(teams_url)
# check that reponse is valid
print(team_response.status_code)
200
API Status Code
200: Everything went okay, and the result has been returned (if any).
301: The server is redirecting you to a different endpoint. This can happen when a company switches domain names, or an endpoint name is changed.
400: The server thinks you made a bad request. This can happen when you don’t send along the right data, among other things.
401: The server thinks you’re not authenticated. Many APIs require login ccredentials, so this happens when you don’t send the right credentials to access an API.
403: The resource you’re trying to access is forbidden: you don’t have the right permissions to see it.
404: The resource you tried to access wasn’t found on the server.
503: The server is not ready to handle the request.
With 200 respone code we are good to start looking at the conent.
Pull out the content
To get the conents of our reposne object, we can use two methods:
- .content with json.loads
- .json()
# convert the content into a python dictionary
team_content = json.loads(team_response.content)
type(team_content)
dict
team_content.keys()
dict_keys(['copyright', 'teams'])
We can now access the contents of the data using dictionary actions.
team_content['teams'][0]
{'id': 1,
'name': 'New Jersey Devils',
'link': '/api/v1/teams/1',
'venue': {'name': 'Prudential Center',
'link': '/api/v1/venues/null',
'city': 'Newark',
'timeZone': {'id': 'America/New_York', 'offset': -5, 'tz': 'EST'}},
'abbreviation': 'NJD',
'teamName': 'Devils',
'locationName': 'New Jersey',
'firstYearOfPlay': '1982',
'division': {'id': 18,
'name': 'Metropolitan',
'nameShort': 'Metro',
'link': '/api/v1/divisions/18',
'abbreviation': 'M'},
'conference': {'id': 6, 'name': 'Eastern', 'link': '/api/v1/conferences/6'},
'franchise': {'franchiseId': 23,
'teamName': 'Devils',
'link': '/api/v1/franchises/23'},
'shortName': 'New Jersey',
'officialSiteUrl': 'http://www.newjerseydevils.com/',
'franchiseId': 23,
'active': True}
team_response.json() == json.loads(team_response.content)
True
Convert the content to a DataFrame
#convert the dictionary into a dataframe
df_team_content = pd.DataFrame(team_content['teams'])
df_team_content.head()
#df_team_content.info()
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
#convert the types of most columns in the dataframe
df_team_content2 = df_team_content.convert_dtypes()
df_team_content2.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 32 entries, 0 to 31
Data columns (total 15 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 32 non-null Int64
1 name 32 non-null string
2 link 32 non-null string
3 venue 32 non-null object
4 abbreviation 32 non-null string
5 teamName 32 non-null string
6 locationName 32 non-null string
7 firstYearOfPlay 32 non-null string
8 division 32 non-null object
9 conference 32 non-null object
10 franchise 32 non-null object
11 shortName 32 non-null string
12 officialSiteUrl 32 non-null string
13 franchiseId 32 non-null Int64
14 active 32 non-null boolean
dtypes: Int64(2), boolean(1), object(4), string(8)
memory usage: 3.8+ KB
# find the rangers info
df_team_content2.query("teamName == 'Rangers'")
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
json_normalize for DataFrame Conversion
Instead of placing the dicitonary in the DataFame function, we could also use the json_normalize function.
df_team_content_jn = pd.json_normalize(team_content['teams'])
df_team_content_jn.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 32 entries, 0 to 31
Data columns (total 29 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 32 non-null int64
1 name 32 non-null object
2 link 32 non-null object
3 abbreviation 32 non-null object
4 teamName 32 non-null object
5 locationName 32 non-null object
6 firstYearOfPlay 32 non-null object
7 shortName 32 non-null object
8 officialSiteUrl 32 non-null object
9 franchiseId 32 non-null int64
10 active 32 non-null bool
11 venue.name 32 non-null object
12 venue.link 32 non-null object
13 venue.city 32 non-null object
14 venue.timeZone.id 32 non-null object
15 venue.timeZone.offset 32 non-null int64
16 venue.timeZone.tz 32 non-null object
17 division.id 32 non-null int64
18 division.name 32 non-null object
19 division.nameShort 32 non-null object
20 division.link 32 non-null object
21 division.abbreviation 32 non-null object
22 conference.id 32 non-null int64
23 conference.name 32 non-null object
24 conference.link 32 non-null object
25 franchise.franchiseId 32 non-null int64
26 franchise.teamName 32 non-null object
27 franchise.link 32 non-null object
28 venue.id 26 non-null float64
dtypes: bool(1), float64(1), int64(6), object(21)
memory usage: 7.2+ KB
Notice that the json_normalize()
method flattend nested dictionaries by concatenating the keys.
df_team_content_jn.head()
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
If there are mulitple levles to the dictionary, each key will be concatenated. For examample, consider the dictionary in the venue column.
team_content['teams'][0]['venue']
{'name': 'Prudential Center',
'link': '/api/v1/venues/null',
'city': 'Newark',
'timeZone': {'id': 'America/New_York', 'offset': -5, 'tz': 'EST'}}
Since the value for the timezone
is a dict
the parent keys (venue
, timeZone
) are concatenated with each key within timeZone
(id
, offset
, tz
)
df_team_content_jn["venue.timeZone.tz"].head()
0 EST
1 EST
2 EST
3 EST
4 EST
Name: venue.timeZone.tz, dtype: object
We could also apply the .json_normalize()
methond to a dictionary column to return dataframes. Consider the venue
column from our previous df_team_content2
dataframe.
pd.json_normalize(df_team_content2.venue).head()
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
Now that we have looked at different approaches, lets use the result fromt he .json_normalize()
going forward.
df_team_content = df_team_content_jn
Request Roster Data
Now that we have collected the basic team info, let’s move down a level to collect roster information.
Extracting the team ID usign regex
# get the rangers link to their team site
rangers_link = df_team_content.query("teamName == 'Rangers'").link.values[0]
rangers_link
'/api/v1/teams/3'
type(rangers_link)
str
# get the rangers team id from the end of the url
import re
pattern = "\d$"
re.findall(pattern, rangers_link)
['3']
#get the base url of the api
teams_url
url_pattern = ".+com"
base_url = re.search(url_pattern, teams_url).group()
base_url
'https://statsapi.web.nhl.com'
#create the url to get the rangers team info
rangers_url = base_url + rangers_link
rangers_url
'https://statsapi.web.nhl.com/api/v1/teams/3'
Add Parameters to Team URL
In order to get the roster data, we need to use modifier or the roster endpoint.
- ?expand=team.roster
- /roster appended to the end of the url
We also need to specify a season
- yyyyYYYYY format
- yyyy:start year
- YYYY: end
# create a url to get the rangers team roster info
params = "?expand=team.roster&season=20182019"
rangers_roster_url = rangers_url + params
rangers_roster_url
'https://statsapi.web.nhl.com/api/v1/teams/3?expand=team.roster&season=20182019'
rangers_response = requests.get(rangers_roster_url)
rangers_response.status_code
200
#convert the content
rangers_content = rangers_response.json()
rangers_content.keys()
dict_keys(['copyright', 'teams'])
rangers_content['teams'][0]['roster']['roster'][1]
{'person': {'id': 8471686,
'fullName': 'Marc Staal',
'link': '/api/v1/people/8471686'},
'jerseyNumber': '18',
'position': {'code': 'D',
'name': 'Defenseman',
'type': 'Defenseman',
'abbreviation': 'D'}}
Make a function to get team info
Now that endpoints are leveraging parameters, it would be useful to create a function to simplify the creation of the API url.
For now, the urls we are creating require a team number and season.
test = 19992000
test = str(test)
pattern = re.compile('\d{8}')
bool(pattern.match(test))
True
def create_team_roster_url(team_number, season):
from sys import exit
base_url = "https://statsapi.web.nhl.com/api/v1/teams/"
#verify team number makes sense
if int(team_number) > 33:
raise ValueError("Please use a correct value. Team numbers must be less than 33")
# convert to strings
team_number = str(team_number) # convert to a string in case a number was supplied
season = str(season)
#verify the season is 8 digits
pattern = re.compile('\d{8}')
is_season_formatted = bool(pattern.match(season))
if not is_season_formatted:
raise ValueError("You did not provide the season with 8 digits. Specify start and end season with four digits and no spaces")
# combine the components into one url
url = base_url + team_number + "/roster/" + "?season=" + season
return url
create_team_roster_url(3,20192020)
'https://statsapi.web.nhl.com/api/v1/teams/3/roster/?season=20192020'
Checking Errors
create_team_roster_url(3,2019)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-63-f09f6485bbbd> in <module>
----> 1 create_team_roster_url(3,2019)
<ipython-input-61-803bcea8f4e1> in create_team_roster_url(team_number, season)
16 is_season_formatted = bool(pattern.match(season))
17 if not is_season_formatted:
---> 18 raise ValueError("You did not provide the season with 8 digits. Specify start and end season with four digits and no spaces")
19
20 # combine the components into one url
ValueError: You did not provide the season with 8 digits. Specify start and end season with four digits and no spaces
create_team_roster_url(100,20192020)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-64-7641c6e970a0> in <module>
----> 1 create_team_roster_url(100,20192020)
<ipython-input-61-803bcea8f4e1> in create_team_roster_url(team_number, season)
6 #verify team number makes sense
7 if int(team_number) > 33:
----> 8 raise ValueError("Please use a correct value. Team numbers must be less than 33")
9
10 # convert to strings
ValueError: Please use a correct value. Team numbers must be less than 33
Send Get Request for Rangers Data
# get the rangers roster from 2019-2020
rangers_roster_url = create_team_roster_url(3,20192020)
rangers_response = requests.get(rangers_roster_url)
rangers_response.status_code
200
# convert the roster
rangers_roster_content = rangers_response.json()["roster"]
rangers_roster_content[0]
{'person': {'id': 8471686,
'fullName': 'Marc Staal',
'link': '/api/v1/people/8471686'},
'jerseyNumber': '18',
'position': {'code': 'D',
'name': 'Defenseman',
'type': 'Defenseman',
'abbreviation': 'D'}}
# convert the roster to a dataframe
df_rangers_roster = pd.json_normalize(rangers_roster_content).astype(str)
df_rangers_roster.head()
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
Stat Types
stat_type_url = "https://statsapi.web.nhl.com/api/v1/statTypes"
response_stat_type = requests.get(stat_type_url)
response_stat_type.status_code
200
response_stat_type.json()
[{'displayName': 'yearByYear', 'gameType': None},
{'displayName': 'yearByYearRank', 'gameType': None},
{'displayName': 'yearByYearPlayoffs', 'gameType': None},
{'displayName': 'yearByYearPlayoffsRank', 'gameType': None},
{'displayName': 'careerRegularSeason', 'gameType': None},
{'displayName': 'careerPlayoffs', 'gameType': None},
{'displayName': 'gameLog', 'gameType': None},
{'displayName': 'playoffGameLog', 'gameType': None},
{'displayName': 'vsTeam', 'gameType': None},
{'displayName': 'vsTeamPlayoffs', 'gameType': None},
{'displayName': 'vsDivision', 'gameType': None},
{'displayName': 'vsDivisionPlayoffs', 'gameType': None},
{'displayName': 'vsConference', 'gameType': None},
{'displayName': 'vsConferencePlayoffs', 'gameType': None},
{'displayName': 'byMonth', 'gameType': None},
{'displayName': 'byMonthPlayoffs', 'gameType': None},
{'displayName': 'byDayOfWeek', 'gameType': None},
{'displayName': 'byDayOfWeekPlayoffs', 'gameType': None},
{'displayName': 'homeAndAway', 'gameType': None},
{'displayName': 'homeAndAwayPlayoffs', 'gameType': None},
{'displayName': 'winLoss', 'gameType': None},
{'displayName': 'winLossPlayoffs', 'gameType': None},
{'displayName': 'onPaceRegularSeason', 'gameType': None},
{'displayName': 'regularSeasonStatRankings', 'gameType': None},
{'displayName': 'playoffStatRankings', 'gameType': None},
{'displayName': 'goalsByGameSituation', 'gameType': None},
{'displayName': 'goalsByGameSituationPlayoffs', 'gameType': None},
{'displayName': 'statsSingleSeason',
'gameType': {'id': 'R',
'description': 'Regular season',
'postseason': False}},
{'displayName': 'statsSingleSeasonPlayoffs',
'gameType': {'id': 'P', 'description': 'Playoffs', 'postseason': True}}]
Player Stats
# create a url to get player stats
def create_player_stats_url(id, param = ""):
base_url = "https://statsapi.web.nhl.com/api/v1/people/"
if param == "":
url = base_url + id + "/"
else:
url = base_url + id + "/stats/?" + param
return url
# get Artemi's player id
artemi_id = df_rangers_roster[df_rangers_roster['person.fullName'].str.contains("Artemi")]['person.id'].values[0]
artemi_id
'8478550'
# create a url for every player
df_rangers_roster["player_stats_link"] = create_player_stats_url(df_rangers_roster["person.id"], "stats=statsSingleSeason&season=20182019")
df_rangers_roster["player_stats_link"][0]
'https://statsapi.web.nhl.com/api/v1/people/8471686/stats/?stats=statsSingleSeason&season=20182019'
# get players stats
def get_player_stats(url):
first_layer = "stats"
response = requests.get(url)
try:
content = json.loads(response.content)[first_layer][0]['splits'][0]
except:
content = {}
return content
# test the function on one player
test_url = df_rangers_roster["player_stats_link"][0]
test_return = get_player_stats(test_url)
test_return
#pd.json_normalize(test_return['people'])
{'season': '20182019',
'stat': {'timeOnIce': '1534:12',
'assists': 10,
'goals': 3,
'pim': 32,
'shots': 84,
'games': 79,
'hits': 94,
'powerPlayGoals': 0,
'powerPlayPoints': 0,
'powerPlayTimeOnIce': '02:23',
'evenTimeOnIce': '1303:27',
'penaltyMinutes': '32',
'faceOffPct': 0.0,
'shotPct': 3.57,
'gameWinningGoals': 0,
'overTimeGoals': 0,
'shortHandedGoals': 0,
'shortHandedPoints': 0,
'shortHandedTimeOnIce': '228:22',
'blocked': 119,
'plusMinus': -9,
'points': 13,
'shifts': 2061,
'timeOnIcePerGame': '19:25',
'evenTimeOnIcePerGame': '16:29',
'shortHandedTimeOnIcePerGame': '02:53',
'powerPlayTimeOnIcePerGame': '00:01'}}
Store the results in a data frame
# get stats for each player
df_rangers_roster["player_json"] = df_rangers_roster["player_stats_link"].apply(get_player_stats)
df_rangers_roster["player_json"][0]
{'season': '20182019',
'stat': {'timeOnIce': '1534:12',
'assists': 10,
'goals': 3,
'pim': 32,
'shots': 84,
'games': 79,
'hits': 94,
'powerPlayGoals': 0,
'powerPlayPoints': 0,
'powerPlayTimeOnIce': '02:23',
'evenTimeOnIce': '1303:27',
'penaltyMinutes': '32',
'faceOffPct': 0.0,
'shotPct': 3.57,
'gameWinningGoals': 0,
'overTimeGoals': 0,
'shortHandedGoals': 0,
'shortHandedPoints': 0,
'shortHandedTimeOnIce': '228:22',
'blocked': 119,
'plusMinus': -9,
'points': 13,
'shifts': 2061,
'timeOnIcePerGame': '19:25',
'evenTimeOnIcePerGame': '16:29',
'shortHandedTimeOnIcePerGame': '02:53',
'powerPlayTimeOnIcePerGame': '00:01'}}
df_rangers_stats_2018_2019 = pd.json_normalize(df_rangers_roster["player_json"])
df_rangers_stats_2018_2019.head()
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
#find the players from 2019-2020 roster that did have stats in 2018-2019
#they were all rookies in 2018-2019
df_rangers_roster[df_rangers_stats_2018_2019['season'].isnull().values]
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
df_rangers_stats = pd.concat([df_rangers_roster, df_rangers_stats_2018_2019], axis = 1)
df_rangers_stats[df_rangers_stats["person.id"] == artemi_id]["stat.powerPlayGoals"]
17 6.0
Name: stat.powerPlayGoals, dtype: float64
%md
Compare Players
example = "https://suggest.svc.nhl.com/svc/suggest/v1/minplayers/wayne%20gre/99999"
the number reflects the results to return
url1 = "https://suggest.svc.nhl.com/svc/suggest/v1/minplayers/wayne/2"
File "<ipython-input-82-aa338d895432>", line 1
https://suggest.svc.nhl.com/svc/suggest/v1/minplayers/wayne/2
^
SyntaxError: invalid syntax
url2 = "https://statsapi.web.nhl.com/api/v1/teams/?teamId=3&expand=team.stats&season=20132014"
Team Stats
def flatten_json(y):
out = {}
def flatten(x, name=''):
if type(x) is dict:
for a in x:
flatten(x[a], name + a + '_')
elif type(x) is list:
i = 0
for a in x:
flatten(a, name + str(i) + '_')
i += 1
else:
out[name[:-1]] = x
flatten(y)
return out
import time
def get_team_stats(start, stop):
base_url = "https://statsapi.web.nhl.com/api/v1/teams/?teamId=3&expand=team.stats&season="
start = int(start)
stop = int(stop)
stats = []
while start != stop:
url = base_url + str(start) + str(start + 1)
response = requests.get(url)
content = response.json()['teams']
try:
team_stats = [team_dict['teamStats'][0] for team_dict in content][0]['splits'][0]
if type(team_stats) is dict:
team_stats.update({"season_start":start, "season_stop" : start + 1})
stats.append(team_stats)
else:
for item in team_stats:
item.update({"season_start":start, "season_stop" : start + 1})
except:
pass
time.sleep(1)
start += 1
print(str(start) + " season complete")
#stats = [item for sublist in stats for item in sublist ]
df_stats = pd.json_normalize(stats, sep = "_")
return df_stats
#rangers_stats = get_team_stats(2012,2014)
rangers_stats
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-87-d4f0a941d66d> in <module>
----> 1 rangers_stats
NameError: name 'rangers_stats' is not defined
df_rangers = get_team_stats(1926,2020)
1927 season complete
1928 season complete
1929 season complete
1930 season complete
1931 season complete
1932 season complete
1933 season complete
1934 season complete
1935 season complete
1936 season complete
1937 season complete
1938 season complete
1939 season complete
1940 season complete
1941 season complete
1942 season complete
1943 season complete
1944 season complete
1945 season complete
1946 season complete
1947 season complete
1948 season complete
1949 season complete
1950 season complete
1951 season complete
1952 season complete
1953 season complete
1954 season complete
1955 season complete
1956 season complete
1957 season complete
1958 season complete
1959 season complete
1960 season complete
1961 season complete
1962 season complete
1963 season complete
1964 season complete
1965 season complete
1966 season complete
1967 season complete
1968 season complete
1969 season complete
1970 season complete
1971 season complete
1972 season complete
1973 season complete
1974 season complete
1975 season complete
1976 season complete
1977 season complete
1978 season complete
1979 season complete
1980 season complete
1981 season complete
1982 season complete
1983 season complete
1984 season complete
1985 season complete
1986 season complete
1987 season complete
1988 season complete
1989 season complete
1990 season complete
1991 season complete
1992 season complete
1993 season complete
1994 season complete
1995 season complete
1996 season complete
1997 season complete
1998 season complete
1999 season complete
2000 season complete
2001 season complete
2002 season complete
2003 season complete
2004 season complete
2005 season complete
2006 season complete
2007 season complete
2008 season complete
2009 season complete
2010 season complete
2011 season complete
2012 season complete
2013 season complete
2014 season complete
2015 season complete
2016 season complete
2017 season complete
2018 season complete
2019 season complete
2020 season complete
import re
re.sub("\\.", "_", "test this.out")
df_rangers.rename(columns = lambda x: re.sub("\\.", "_", x), inplace = True)
df_rangers.columns
Index(['season_start', 'season_stop', 'stat_gamesPlayed', 'stat_wins',
'stat_losses', 'stat_ot', 'stat_pts', 'stat_ptPctg',
'stat_goalsPerGame', 'stat_goalsAgainstPerGame', 'stat_evGGARatio',
'stat_powerPlayPercentage', 'stat_powerPlayGoals',
'stat_powerPlayGoalsAgainst', 'stat_powerPlayOpportunities',
'stat_penaltyKillPercentage', 'stat_shotsPerGame', 'stat_shotsAllowed',
'stat_winScoreFirst', 'stat_winOppScoreFirst', 'stat_winLeadFirstPer',
'stat_winLeadSecondPer', 'stat_winOutshootOpp', 'stat_winOutshotByOpp',
'stat_faceOffsTaken', 'stat_faceOffsWon', 'stat_faceOffsLost',
'stat_faceOffWinPercentage', 'stat_shootingPctg', 'stat_savePctg',
'team_id', 'team_name', 'team_link'],
dtype='object')
from bokeh.plotting import figure, output_file, show
from bokeh.io import output_notebook, show
from bokeh.models import ColumnDataSource
from bokeh.models.tools import HoverTool
from bokeh.models import SingleIntervalTicker, LinearAxis, Range1d, LabelSet, Label, \
Arrow, NormalHead, OpenHead, VeeHead
output_notebook()
p = figure(plot_width=400, plot_height=400,
x_axis_type=None, title = "Rangers Points by Season")
source = ColumnDataSource(df_rangers)
p.line(x = "season_stop", y = "stat_pts", source = source, line_width = 2,
legend_label = "Points")
p.line(x = "season_stop", y = "stat_gamesPlayed", source = source, line_color = "grey",
line_width = 4, line_dash = "dashed",
legend_label = "Games Played")
p.yaxis.minor_tick_line_color = None
# add annotations
labels = Label(x = 1995, y = 20, text = "Lockout", level = "overlay")
p.add_layout(Arrow(end=NormalHead(line_color="black", line_width=1),
x_start=2005, y_start=25, x_end=1995, y_end=45))
p.add_layout(Arrow(end=NormalHead(line_color="black", line_width=1),
x_start=2005, y_start=25, x_end=2013, y_end=45))
p.add_layout(labels)
hover = HoverTool()
hover.tooltips=[
("Year","@season_stop"),
('Points', "@stat_pts"),
('Games Played', '@stat_gamesPlayed')
]
ticker = SingleIntervalTicker(interval=10)
xaxis = LinearAxis(ticker=ticker)
p.add_layout(xaxis, 'below')
p.add_tools(hover)
p.legend.location = "top_left"
p.legend.title_text_font_style = "bold"
p.legend.title_text_font_size = "20px"
p.legend.background_fill_alpha = 0
p.legend.border_line_alpha = 0
p.legend.margin = -1
show(p)
p = figure(plot_width=400, plot_height=400,
x_axis_type=None, title = "Rangers Points by Season")
p.line(x = "season_stop", y = "stat_wins", source = source, line_color = "firebrick",
line_width = 4, line_dash = "dashed")
p.yaxis.minor_tick_line_color = None
# Setting the second y axis range name and range
p.extra_y_ranges = {"percent": Range1d(start=0, end=1)}
# Adding the second axis to the plot.
p.add_layout(LinearAxis(y_range_name="percent"), 'right')
p.line(x = "season_stop", y = "stat_winScoreFirst", source = source,
line_width = 2, line_color = "green",
y_range_name = "percent")
hover = HoverTool()
hover.tooltips=[
("Year","@season_stop"),
('% Win Score First', "@stat_winScoreFirst"),
('Wins', '@stat_wins')
]
ticker = SingleIntervalTicker(interval=10)
xaxis = LinearAxis(ticker=ticker)
p.add_layout(xaxis, 'below')
p.add_tools(hover)
show(p)
df_rangers.stat_winLeadFirstPer
0 0.938
1 0.875
2 0.833
3 0.917
4 0.923
...
88 0.806
89 0.741
90 0.625
91 0.526
92 0.741
Name: stat_winLeadFirstPer, Length: 93, dtype: float64