This RMarkdown shows how I come up with my daily projections for MR Play. See here for how MR Play contests work.
# Load libraries
library(tidyverse)
library(stringi)
library(nbastatR)
library(reshape2)
library(googlesheets4)
library(knitr)
# Load file that contains helper functions
source("Dropbox/Data_Science/TS_Functions.R")
The last 10 day stats are needed for every player as that is how the boosts are calculated based upon being compared to.
# Load last 10 game averages
setwd("/Users/billy/Downloads")
tenday <- read_csv("MRplayer10gameavg102822.csv", show_col_types = FALSE) #EDIT THIS LINE HERE
# Shorten the names of the stat columns and apply fixbballnames() name cleaning function
tenday2 <- tenday %>%
select(Name = YahooName, Points, Rebounds, Assists, Threes = ThreePointersMade, Steals, Blocks = BlockedShots, Games, Minutes, Started) %>%
mutate(Points = Points/Games,
Rebounds = Rebounds/Games,
Assists = Assists/Games,
Threes = Threes/Games,
Steals = Steals/Games,
Blocks = Blocks/Games) %>%
fix_bball_names() %>%
na.omit()
# Melt the individual stats variables into 1 new column w/ that variable name as the row
tenday_melt <- melt(tenday2, id = "Name", measure.vars = c("Points", "Assists", "Threes", "Steals", "Blocks"), variable.name = "Stat", value.name = "last10")
Load TopShot moment data from OTMNFT.com to determine which players have boosts in which stats.
# Load all TopShot moment data
topshotmoments <- read.csv("https://otmnftapi.com/nbatopshot/create_moments_csv", encoding = "Latin-1") %>% rename(Name = Player.Name)
# Convert European characters into English and perform the name cleaning function
topshotmoments <- topshotmoments %>%
mutate(Name = stri_trans_general(Name, 'latin-ascii')) %>%
fix_bball_names() %>%
I
# Function to convert TopShot moment stat categories to MR Play stat categories (e.g. "Layup" into "Points")
topshotmoments2 <- simplify_moments(topshotmoments)
First goal is to load daily stat projections from BasketballMonster (BBM), perform some cleaning, and compute the raw MR total points stat.
# Load BBM Projections from a csv file on the internet
bbmproj <- read_csv("https://basketballmonster.com/dailyprojections.aspx?exportcsv=TbRiHe8ctANfwMvqI6EetQdY4QsKbZvwhTJrUTJudBU=", show_col_types = FALSE)
# This below is for when I'm projecting for a specific day in advance
# setwd("/Users/billy/Downloads")
# bbmproj <- read_csv("Export_2022_03_15.csv", show_col_types = FALSE)
# Add column for the MR Play point system and sort by that column
bbmproj2 <- bbmproj %>%
mutate(MR_raw_proj = points + rebounds + 1.5*assists + 2*steals + 2*blocks, .before = value) %>%
# filter(MR_raw_proj > 0) %>%
arrange(desc(MR_raw_proj)) %>%
mutate(Name = paste(first_name, last_name, sep = " "), .before = MR_raw_proj) %>%
select(Name, Team = team, opponent, Injury = injury, MR_raw_proj, Minutes = minutes, Points = points, Rebounds = rebounds, Assists = assists, Threes = threes,
Steals = steals, Blocks = blocks) %>%
fix_bball_names()
bbm_melt <- melt(bbmproj2, id = c("Name", "Team", "opponent", "Injury", "MR_raw_proj"), measure.vars = c("Points", "Assists", "Threes", "Steals", "Blocks"), variable.name = "Stat", value.name = "projection")
# Join Last 10 day stats on to projection table
df1 <- bbm_melt %>%
left_join(tenday_melt, by = c("Name", "Stat")) %>%
replace(is.na(.), 0)
To determine the variability (standard devaition) of each player’s
statistical performance in each stat category, I’ll use their game logs
from the 2021-22 season. These are available through the package
nbastatR.
# Increase size of the connection buffer due to large data set about to be loaded (> 50k rows)
Sys.setenv("VROOM_CONNECTION_SIZE" = 131072*2)
# Get game logs from 2021, 2022, 2023 seasons
gamelog <- game_logs(seasons = c(2021:2023),
league = "NBA",
season_types = "Regular Season",
assign_to_environment = FALSE,
result_types = "player")
## Acquiring NBA basic player game logs for the 2020-21 Regular Season
## Acquiring NBA basic player game logs for the 2021-22 Regular Season
## Acquiring NBA basic player game logs for the 2022-23 Regular Season
# Some cleaning to match the BBM Projections and Last 10 day stat data frames
gamelog <- gamelog %>%
rename(c(Name = namePlayer, Points = pts, Rebounds = treb, Assists = ast, Threes = fg3m, Steals = stl, Blocks = blk)) %>%
data.frame() %>%
fix_bball_names() %>%
I
# Get the standard deviation for every stat for every player
gamelog_std <- gamelog %>%
group_by(Name) %>%
summarise(Points = sd(Points), Assists = sd(Assists), Threes = sd(Threes), Steals = sd(Steals), Blocks = sd(Blocks))
# Melt stat variable to prepare for joining
gamelog_std_melt <- melt(gamelog_std, id = "Name", measure.vars = c("Points", "Assists", "Threes", "Steals", "Blocks"), variable.name = "Stat", value.name = "sd")
# Join standard deviations onto the projection table
df2 <- df1 %>%
left_join(gamelog_std_melt, by = c("Name", "Stat")) %>%
replace(is.na(.), 0) %>%
inner_join(topshotmoments2, by = c("Name", "Stat"))
# Calculate player ceilings for every stat
df3 <- df2 %>%
mutate(ceiling = projection + 3*sd, .after = sd) %>%
mutate(Stat = as.character(Stat))
# Function to get boosted projections
boosted_projection <- function(stat, b, c, d, e){
boost <- NULL
if(stat == "Points"){boost = 0.5}
else if(stat == "Assists"){boost = 5}
else if(stat == "Threes"){boost = 7}
else if(stat == "Steals"){boost = 12}
else if(stat == "Blocks"){boost = 16}
#EV
boostoutcomes <- c((floor(c)+1):e)
probs <- dnorm(boostoutcomes, b, d) # b = projection, d = sd
probs <- round(probs, 3)
values <- (boostoutcomes - c)*boost + 5
xpx <- values*probs
sum(xpx)
}
# Calculate and attach boosts on to projections
df4 <- df3
for (row in 1:nrow(df3)){
df4$boost[row] <- boosted_projection(df4$Stat[row], df4$projection[row], df4$last10[row], df4$sd[row], df4$ceiling[row])
}
# Calculate final boosted projection, select relevant variables, sort, and round
df5 <- df4 %>%
mutate(MR_boosted_proj = MR_raw_proj + boost, .before = MR_raw_proj) %>%
arrange(desc(MR_boosted_proj)) %>%
select(Name, Team, opponent, Stat, MR_boosted_proj, MR_raw_proj, boost, projection, last10, Low.Ask, Injury) %>%
mutate_at(vars(MR_boosted_proj:last10), funs(round(., 1)))
# Write to google sheets for consumption
mrplay <- "https://docs.google.com/spreadsheets/d/1KBcepiBDe-6YXLc_9HX4LaXto4fu8wzBrhtqPFC2bto/edit?usp=sharing"
df5 %>% sheet_write(ss = mrplay, sheet = "oct-28") %>% range_autofit()
# Preview of the output
kable(head(df5, 30))
| Name | Team | opponent | Stat | MR_boosted_proj | MR_raw_proj | boost | projection | last10 | Low.Ask | Injury |
|---|---|---|---|---|---|---|---|---|---|---|
| Nikola Jokic | DEN | UTA | Blocks | 68.2 | 56.5 | 11.7 | 0.9 | 0.6 | 170 | , |
| Nikola Jokic | DEN | UTA | Assists | 66.4 | 56.5 | 9.9 | 8.0 | 7.6 | 3 | , |
| Rudy Gobert | MIN | LAL | Blocks | 66.3 | 39.0 | 27.3 | 2.4 | 1.1 | 2 | , |
| Trae Young | ATL | @ DET | Threes | 65.3 | 49.7 | 15.5 | 3.2 | 1.8 | 3 | , |
| James Harden | PHI | @ TOR | Assists | 64.7 | 48.6 | 16.2 | 10.4 | 8.4 | 3 | , |
| Trae Young | ATL | @ DET | Assists | 63.8 | 49.7 | 14.1 | 9.8 | 8.2 | 2 | , |
| Nikola Jokic | DEN | UTA | Threes | 63.8 | 56.5 | 7.3 | 1.0 | 0.8 | 4 | , |
| Giannis Antetokounmpo | MIL | NYK | Blocks | 63.2 | 55.6 | 7.6 | 1.2 | 1.5 | 4 | , |
| Kristaps Porzingis | WAS | IND | Blocks | 61.5 | 42.0 | 19.5 | 2.2 | 1.4 | 48 | , |
| Jakob Poeltl | SAS | CHI | Blocks | 61.3 | 35.5 | 25.8 | 2.0 | 0.8 | 39 | , |
| LeBron James | LAL | @ MIN | Steals | 61.1 | 49.5 | 11.7 | 1.1 | 0.7 | 9 | Probable, |
| Nikola Jokic | DEN | UTA | Points | 60.7 | 56.5 | 4.2 | 26.8 | 26.7 | 3 | , |
| Giannis Antetokounmpo | MIL | NYK | Threes | 59.4 | 55.6 | 3.8 | 1.0 | 1.1 | 500 | , |
| Giannis Antetokounmpo | MIL | NYK | Assists | 59.2 | 55.6 | 3.6 | 5.0 | 6.6 | 10 | , |
| Giannis Antetokounmpo | MIL | NYK | Points | 58.5 | 55.6 | 2.9 | 31.6 | 34.5 | 3 | , |
| Joel Embiid | PHI | @ TOR | Blocks | 58.1 | 46.2 | 11.9 | 1.5 | 1.3 | 199 | , |
| LeBron James | LAL | @ MIN | Assists | 57.8 | 49.5 | 8.3 | 7.1 | 6.9 | 7 | Probable, |
| Trae Young | ATL | @ DET | Points | 57.3 | 49.7 | 7.6 | 28.6 | 22.2 | 3 | , |
| Donovan Mitchell | CLE | @ BOS | Threes | 56.3 | 40.7 | 15.6 | 3.7 | 2.2 | 2 | , |
| James Harden | PHI | @ TOR | Blocks | 55.5 | 48.6 | 7.0 | 0.4 | 0.6 | 38 | , |
| LeBron James | LAL | @ MIN | Threes | 55.4 | 49.5 | 6.0 | 2.3 | 2.6 | 39 | Probable, |
| Chris Paul | PHO | NOR | Assists | 55.4 | 36.7 | 18.7 | 10.2 | 7.8 | 2 | , |
| Dejounte Murray | ATL | @ DET | Steals | 55.2 | 44.8 | 10.3 | 2.3 | 2.0 | 5 | , |
| Jayson Tatum | BOS | CLE | Assists | 54.6 | 47.0 | 7.6 | 5.8 | 5.6 | 3 | , |
| LeBron James | LAL | @ MIN | Blocks | 54.3 | 49.5 | 4.9 | 0.8 | 1.0 | 7 | Probable, |
| Anthony Edwards | MIN | LAL | Steals | 54.2 | 40.0 | 14.2 | 1.6 | 0.9 | 320 | , |
| Cade Cunningham | DET | ATL | Assists | 54.2 | 41.5 | 12.7 | 7.1 | 5.8 | 3900 | , |
| James Harden | PHI | @ TOR | Threes | 54.0 | 48.6 | 5.5 | 2.4 | 2.8 | 4 | , |
| Joel Embiid | PHI | @ TOR | Assists | 54.0 | 46.2 | 7.8 | 2.8 | 2.4 | 30 | , |
| Joel Embiid | PHI | @ TOR | Threes | 54.0 | 46.2 | 7.8 | 1.1 | 0.8 | 5 | , |