<p><div><div><div><div data-show-faces="false" data-height="21" data-width="100" data-layout="button_count" data-send="true" data-href="https://www.r-bloggers.com/scraping-nba-game-data-from-basketball-reference-com/"></div></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="https://statisticaloddsandends.wordpress.com/2018/12/11/scraping-nba-game-data-from-basketball-reference-com/"> R – Statistical Odds &amp; Ends</a></strong>, and kindly contributed to <a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers)</a> </div>
<p>I’m a casual NBA fan: I don’t have time to watch the games but enjoy viewing the highlights on Instagram/Youtube (especially <a rel="nofollow" href="http://www.nba.com/fool" target="_blank">Shaqtin’ A Fool</a>!); I sometimes read game articles and analyses (e.g. <a rel="nofollow" href="http://www.nba.com/blogtable" target="_blank">Blogtable</a>). Apart from the game being an amazing visual spectacle, it’s fun to drink in the deluge of stats that each game brings. I’m not even talking about advanced stats and “<a rel="nofollow" href="https://en.wikipedia.org/wiki/APBRmetrics" target="_blank">ABPRmetrics</a>“: there’s something exciting about seeing how many different statistical records can be broken on a given night.</p>
<p>As a data/stats person, I’ve been wanting to get my hands on NBA data and play around with it on my own. However, in my internet searching I didn’t come across any free easy-to-use datasets. The website <a rel="nofollow" href="https://www.basketball-reference.com/" target="_blank">Basketball-Reference.com</a> is an excellent compendium of all the data I would want, but it was embedded within the webpage, not made available in an analysis-ready format. (Or at least, I couldn’t find it, or it wasn’t free.)</p>
<div data-shortcode="caption" class="wp-caption"><img data-orig-file="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference.png?w=450&amp;h=415&amp;fit=584%2C415" data-orig-size="1900,1350" data-medium-file="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference.png?w=450&amp;h=415&amp;fit=584%2C415?w=300" data-attachment-id="693" data-image-meta="{" data-image-description="" src="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference.png?w=450&h=415&fit=584%2C415" data-permalink="https://statisticaloddsandends.wordpress.com/2018/12/11/scraping-nba-game-data-from-basketball-reference-com/basketball-reference/" data-large-file="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference.png?w=450&amp;h=415&amp;fit=584%2C415?w=584" alt="" data-image-title="basketball-reference" sizes="(max-width: 584px) 100vw, 584px" data-comments-opened="1" class="wp-image-693" data-recalc-dims="1">
<p class="wp-caption-text">A screenshot from basketball-reference.com.</p>
</div>
<p>I recently found some spare time on my hands and decided that it was time for me to learn how to scrape data from this website. And it was surprisingly easy! In this post, I will walk through the steps for scraping top-level game data for the 2017-2018 NBA season (i.e. data from the screenshot above).  Click <a rel="nofollow" href="https://github.com/kjytay/misc/blob/master/blog/2018-12-11_nba_game_data.R" target="_blank">here</a> to view the full R code. If you only want the data, you can download it <a rel="nofollow" href="https://github.com/kjytay/misc/blob/master/data/NBA-2018_game_data.rds" target="_blank">here</a> in RDS format.</p>
<p><strong>Scraping the data</strong></p>
<p>First, let’s load the packages we will use for the web scraping:</p>
<pre>
library(rvest)
library(lubridate)
</pre>
<p>From the screenshot above, you may notice that game data for the season is split over several pages, with one page for the games in a given month. As such we will need to loop over the months and scrape the webpage for each month. We do that in the full R script; the explanation below shows the code for scraping for the month of October.</p>
<p>We can get the webpage as an <code>xml_document</code> object by using <code>rvest</code>‘s <code>read_html</code> function:</p>
<pre>
year &lt;- &quot;2018&quot;
month &lt;- &quot;october&quot;
url &lt;- paste0(&quot;https://www.basketball-reference.com/leagues/NBA_&quot;, year, 
                  &quot;_games-&quot;, month, &quot;.html&quot;)
webpage &lt;- read_html(url)
</pre>
<p>To get the data we want from this object, we need to look for the CSS selectors of the data we want. This involves inspecting the raw HTML of the webpage and finding the unique path that gets your data (and nothing else). To get the column names for this dataset, we extract the HTML nodes with CSS selector <code>&quot;table#schedule > thead > tr > th&quot;</code>, and then pull out the value of the attribute <code>&quot;data-stat&quot;</code>:</p>
<pre>
col_names &lt;- webpage %>% 
        html_nodes(&quot;table#schedule > thead > tr > th&quot;) %>% 
        html_attr(&quot;data-stat&quot;)    
col_names &lt;- c(&quot;game_id&quot;, col_names)
</pre>
<p>Notice that pipes <code>%>%</code> work with <code>rvest</code>‘s functions. (The <code>game_id</code> column cannot be pulled out in this way, and so I’ve added it in manually.)</p>
<p>Next, I will extract the dates and game IDs in a similar manner. The only snag here is that the table in the month of April is slightly different, since the playoffs start that month:</p>
<div data-shortcode="caption" class="wp-caption"><img data-orig-file="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference2.png?w=450&amp;h=271&amp;fit=584%2C271" data-orig-size="1462,678" data-medium-file="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference2.png?w=450&amp;h=271&amp;fit=584%2C271?w=300" data-attachment-id="697" data-image-meta="{" data-image-description="" src="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference2.png?w=450&h=271&fit=584%2C271" data-permalink="https://statisticaloddsandends.wordpress.com/2018/12/11/scraping-nba-game-data-from-basketball-reference-com/basketball-reference2/" data-large-file="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference2.png?w=450&amp;h=271&amp;fit=584%2C271?w=584" alt="" data-image-title="basketball-reference2" sizes="(max-width: 584px) 100vw, 584px" data-comments-opened="1" class="wp-image-697" data-recalc-dims="1">
<p class="wp-caption-text">Part of the Apr 2018 table. The Playoffs row interferes with our web scraping.</p>
</div>
<p>We will need a bit of tinkering to remove the effects of that row:</p>
<pre>
dates &lt;- webpage %>% 
        html_nodes(&quot;table#schedule > tbody > tr > th&quot;) %>% 
        html_text()
dates &lt;- dates[dates != &quot;Playoffs&quot;]
    
game_id &lt;- webpage %>% 
    html_nodes(&quot;table#schedule > tbody > tr > th&quot;) %>%
    html_attr(&quot;csk&quot;)
game_id &lt;- game_id[!is.na(game_id)]
</pre>
<p>The rest of the data is fairly straight forward to pull out. We then combine this data along with <code>dates</code> and <code>game_id</code> into a single data frame:</p>
<pre>
data &lt;- webpage %>% 
        html_nodes(&quot;table#schedule > tbody > tr > td&quot;) %>% 
        html_text() %>%
        matrix(ncol = length(col_names) - 2, byrow = TRUE)
    
month_df &lt;- as.data.frame(cbind(game_id, dates, data), stringsAsFactors = FALSE)
names(month_df) &lt;- col_names
</pre>
<p>From here, assume that we did the above for all the months and combined them into one big data frame <code>df</code>. When web scraping, all the data is pulled out as character strings, so we need to do some typecasting to get the data into the correct type. I also added a column to indicate whether a game was a regular season game or a playoff game (this is where we need the <code>lubridate</code> package) and dropped the box score column.</p>
<pre>
# change columns to the correct types
df$visitor_pts &lt;- as.numeric(df$visitor_pts)
df$home_pts    &lt;- as.numeric(df$home_pts)
df$attendance  &lt;- as.numeric(gsub(&quot;,&quot;, &quot;&quot;, df$attendance))
df$date_game   &lt;- mdy(df$date_game)

# add column to indicate if regular season or playoff
playoff_startDate &lt;- ymd(&quot;2018-04-14&quot;)
df$game_type &lt;- with(df, ifelse(date_game >= playoff_startDate, 
                                &quot;Playoff&quot;, &quot;Regular&quot;))

# drop boxscore column
df$box_score_text &lt;- NULL
</pre>
<p><strong>Sanity check: Table standings</strong></p>
<p>Let’s perform a sanity check by recreating the regular season table standings for each conference. The code in this section could be more elegant by using functions from the <code>tidyverse</code>, but I’ll demonstrate that we can do what we want using just base R functions.</p>
<p>First we create columns indicating the winner and loser of each game, then pull out just the regular season games:</p>
<pre>
df$winner &lt;- with(df, ifelse(visitor_pts > home_pts, 
                             visitor_team_name, home_team_name))
df$loser &lt;- with(df, ifelse(visitor_pts &lt; home_pts, 
                             visitor_team_name, home_team_name))
regular_df &lt;- subset(df, game_type == &quot;Regular&quot;)
</pre>
<p>Next, we build up a new data frame where each row corresponds to one team. We manually input the conference and division for each team, since there are only 30 of them (getting them programmatically would probably take longer than manual data entry):</p>
<pre>
teams &lt;- sort(unique(regular_df$visitor_team_name))
standings &lt;- data.frame(team = teams, stringsAsFactors = FALSE)

standings$conf &lt;- c(&quot;East&quot;, &quot;East&quot;, &quot;East&quot;, &quot;East&quot;, &quot;East&quot;,
                    &quot;East&quot;, &quot;West&quot;, &quot;West&quot;, &quot;East&quot;, &quot;West&quot;,
                    &quot;West&quot;, &quot;East&quot;, &quot;West&quot;, &quot;West&quot;, &quot;West&quot;,
                    &quot;East&quot;, &quot;East&quot;, &quot;West&quot;, &quot;West&quot;, &quot;East&quot;,
                    &quot;West&quot;, &quot;East&quot;, &quot;East&quot;, &quot;West&quot;, &quot;West&quot;,
                    &quot;West&quot;, &quot;West&quot;, &quot;East&quot;, &quot;West&quot;, &quot;East&quot;)
standings$div &lt;- c(&quot;Southeast&quot;, &quot;Atlantic&quot;, &quot;Atlantic&quot;, &quot;Southeast&quot;, &quot;Central&quot;,
                   &quot;Central&quot;, &quot;Southwest&quot;, &quot;Northwest&quot;, &quot;Central&quot;, &quot;Pacific&quot;,
                   &quot;Southwest&quot;, &quot;Central&quot;, &quot;Pacific&quot;, &quot;Pacific&quot;, &quot;Southwest&quot;,
                   &quot;Southeast&quot;, &quot;Central&quot;, &quot;Northwest&quot;, &quot;Southwest&quot;, &quot;Atlantic&quot;,
                   &quot;Northwest&quot;, &quot;Southeast&quot;, &quot;Atlantic&quot;, &quot;Pacific&quot;, &quot;Northwest&quot;,
                   &quot;Pacific&quot;, &quot;Southwest&quot;, &quot;Atlantic&quot;, &quot;Northwest&quot;, &quot;Southeast&quot;)
</pre>
<p>We populate the win loss columns in the following way: for each team, find the number of times it appears in each of the <code>winner</code> and <code>loser</code> columns in <code>df</code>. I use a for loop here, which is not a big problem here since there are only 30 teams, but the code could probably be improved to avoid the loop.</p>
<pre>
standings$win &lt;- 0; standings$loss &lt;- 0
for (i in 1:nrow(standings)) {
    standings$win[i]  &lt;- sum(regular_df$winner == standings$team[i])
    standings$loss[i] &lt;- sum(regular_df$loser  == standings$team[i])
}
</pre>
<p>The win-loss percentage can be calculated easily:</p>
<pre>
standings$wl_pct &lt;- with(standings, win / (win + loss))
</pre>
<p>Now that our standings table is complete, we can compare them with the actual standings table. There are slightly differences because when teams tie in W-L percentage, we just list them in alphabetical order. In real life tiebreaking is quite a bit more complicated (see the basis for tiebreaking near the bottom of <a rel="nofollow" href="https://stats.nba.com/standings/" target="_blank">this page</a>).</p>
<pre>
# Eastern conference standings
east_standings &lt;- subset(standings, conf == &quot;East&quot;) east_standings[with(east_standings, order(-wl_pct, team)), c(&quot;team&quot;, &quot;win&quot;, &quot;loss&quot;)] #>                   team win loss
#> 28     Toronto Raptors  59   23
#> 2       Boston Celtics  55   27
#> 23  Philadelphia 76ers  52   30
#> 6  Cleveland Cavaliers  50   32
#> 12      Indiana Pacers  48   34
#> 16          Miami Heat  44   38
#> 17     Milwaukee Bucks  44   38
#> 30  Washington Wizards  43   39
#> 9      Detroit Pistons  39   43
#> 4    Charlotte Hornets  36   46
#> 20     New York Knicks  29   53
#> 3        Brooklyn Nets  28   54
#> 5        Chicago Bulls  27   55
#> 22       Orlando Magic  25   57
#> 1        Atlanta Hawks  24   58

# Western conference standings
west_standings &lt;- subset(standings, conf == &quot;West&quot;) west_standings[with(west_standings, order(-wl_pct, team)), c(&quot;team&quot;, &quot;win&quot;, &quot;loss&quot;)] #>                      team win loss
#> 11        Houston Rockets  65   17
#> 10  Golden State Warriors  58   24
#> 25 Portland Trail Blazers  49   33
#> 19   New Orleans Pelicans  48   34
#> 21  Oklahoma City Thunder  48   34
#> 29              Utah Jazz  48   34
#> 18 Minnesota Timberwolves  47   35
#> 27      San Antonio Spurs  47   35
#> 8          Denver Nuggets  46   36
#> 13   Los Angeles Clippers  42   40
#> 14     Los Angeles Lakers  35   47
#> 26       Sacramento Kings  27   55
#> 7        Dallas Mavericks  24   58
#> 15      Memphis Grizzlies  22   60
#> 24           Phoenix Suns  21   61
</pre>
<div data-shortcode="caption" class="wp-caption"><img data-orig-file="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference3.png?w=450&amp;h=282&amp;fit=584%2C282" data-orig-size="1680,810" data-medium-file="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference3.png?w=450&amp;h=282&amp;fit=584%2C282?w=300" data-attachment-id="700" data-image-meta="{" data-image-description="" src="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference3.png?w=450&h=282&fit=584%2C282" data-permalink="https://statisticaloddsandends.wordpress.com/2018/12/11/scraping-nba-game-data-from-basketball-reference-com/basketball-reference3/" data-large-file="https://statisticaloddsandends.files.wordpress.com/2018/12/basketball-reference3.png?w=450&amp;h=282&amp;fit=584%2C282?w=584" alt="" data-image-title="basketball-reference3" sizes="(max-width: 584px) 100vw, 584px" data-comments-opened="1" class="wp-image-700" data-recalc-dims="1">
<p class="wp-caption-text">2017-18 regular season conference standings from basketball-reference.com.</p>
</div>
	<p>
<div>
<div>To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://statisticaloddsandends.wordpress.com/2018/12/11/scraping-nba-game-data-from-basketball-reference-com/"> R – Statistical Odds &amp; Ends</a></strong>.</div>
<hr>
<a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers.com</a> offers <strong><a rel="nofollow" href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers">daily e-mail updates</a></strong> about <a rel="nofollow" href="https://www.r-project.org/" title="The R Project for Statistical Computing">R</a> news and <a rel="nofollow" href="https://www.r-bloggers.com/search/tutorial" title="R tutorials">tutorials</a> on topics such as: <a rel="nofollow" href="https://www.r-bloggers.com/search/data%20science" title="Data science">Data science</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Big%20Data" title="Big Data">Big Data, <a rel="nofollow" href="https://www.r-users.com/" title="R jobs">R jobs</a>, visualization (<a rel="nofollow" href="https://www.r-bloggers.com/search/ggplot2" title="ggplot and ggplot2 tutorials">ggplot2</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/boxplot" title="Boxplots using lattice and ggplot2 tutorials">Boxplots</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/map" title="Maps and gis">maps</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/animation" title="Animation in R">animation</a>), programming (<a rel="nofollow" href="https://www.r-bloggers.com/search/RStudio" title="RStudio IDE for R">RStudio</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/sweave" title="Sweave and literate programming">Sweave</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/LaTeX" title="LaTeX in R">LaTeX</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/SQL" title="SQL and databases">SQL</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/eclipse" title="Eclipse IDE for R">Eclipse</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/git" title="git and github, Version Control System">git</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/hadoop" title="Large data in R using Hadoop">hadoop</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Web+Scraping" title="Web Scraping of google, facebook, yahoo, twitter and more using R">Web Scraping</a>) statistics (<a rel="nofollow" href="https://www.r-bloggers.com/search/regression" title="Regressions and ANOVA analysis tutorials">regression</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/PCA" title="principal component analysis tutorial">PCA</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/time+series" title="Time series">time series</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/trading" title="finance trading">trading</a>) and more...
<img alt="" width="1" src="http://feeds.feedburner.com/~r/RBloggers/~4/0HXXU07fAq0" height="1"></a></div>