getwd()
## [1] "/cloud/project"
#setwd("cloud/project")
mlb_stats <- read.csv("MLB Players-hittingstats-ss.csv", header = TRUE)
#Data structure
str(mlb_stats)
## 'data.frame': 47 obs. of 17 variables:
## $ Player : chr "Trea Turner" "Bo Bichette" "Amed Rosario" "Xander Bogaerts" ...
## $ Pos : chr "SS" "SS" "SS" "SS" ...
## $ Team : chr "LAD" "TOR" "CLE" "BOS" ...
## $ GS : int 160 158 151 148 161 133 148 151 129 138 ...
## $ AB : int 652 652 637 557 630 522 591 593 481 563 ...
## $ H : int 194 189 180 171 170 152 150 145 135 134 ...
## $ X2B : int 39 43 26 38 25 24 31 24 22 31 ...
## $ X3B : int 4 1 9 0 5 1 6 1 5 0 ...
## $ HR : int 21 24 11 15 26 22 20 33 10 31 ...
## $ RBI : int 100 93 71 73 107 64 80 83 55 98 ...
## $ AVG : num 0.298 0.29 0.283 0.307 0.27 0.291 0.254 0.245 0.281 0.238 ...
## $ OBP : num 0.343 0.333 0.312 0.377 0.339 0.366 0.294 0.317 0.327 0.298 ...
## $ SLG : num 0.466 0.469 0.403 0.456 0.449 0.467 0.428 0.455 0.41 0.458 ...
## $ OPS : num 0.809 0.802 0.715 0.833 0.788 0.834 0.722 0.772 0.736 0.756 ...
## $ WAR : num 4.84 3.44 3.95 5.42 5.4 5.55 1.05 4.04 4.5 4.42 ...
## $ Cash2023: chr "$27,272,727 " "$6,100,000 " "$7,800,000 " "$30,000,000 " ...
## $ Age : int 29 24 26 29 28 27 22 28 25 26 ...
names(mlb_stats)
## [1] "Player" "Pos" "Team" "GS" "AB" "H"
## [7] "X2B" "X3B" "HR" "RBI" "AVG" "OBP"
## [13] "SLG" "OPS" "WAR" "Cash2023" "Age"
#Turn the variable Cash2023 numeric
mlb_stats$Cash2023 <- as.numeric(gsub("[$, ]", "", mlb_stats$Cash2023))
str(mlb_stats)
## 'data.frame': 47 obs. of 17 variables:
## $ Player : chr "Trea Turner" "Bo Bichette" "Amed Rosario" "Xander Bogaerts" ...
## $ Pos : chr "SS" "SS" "SS" "SS" ...
## $ Team : chr "LAD" "TOR" "CLE" "BOS" ...
## $ GS : int 160 158 151 148 161 133 148 151 129 138 ...
## $ AB : int 652 652 637 557 630 522 591 593 481 563 ...
## $ H : int 194 189 180 171 170 152 150 145 135 134 ...
## $ X2B : int 39 43 26 38 25 24 31 24 22 31 ...
## $ X3B : int 4 1 9 0 5 1 6 1 5 0 ...
## $ HR : int 21 24 11 15 26 22 20 33 10 31 ...
## $ RBI : int 100 93 71 73 107 64 80 83 55 98 ...
## $ AVG : num 0.298 0.29 0.283 0.307 0.27 0.291 0.254 0.245 0.281 0.238 ...
## $ OBP : num 0.343 0.333 0.312 0.377 0.339 0.366 0.294 0.317 0.327 0.298 ...
## $ SLG : num 0.466 0.469 0.403 0.456 0.449 0.467 0.428 0.455 0.41 0.458 ...
## $ OPS : num 0.809 0.802 0.715 0.833 0.788 0.834 0.722 0.772 0.736 0.756 ...
## $ WAR : num 4.84 3.44 3.95 5.42 5.4 5.55 1.05 4.04 4.5 4.42 ...
## $ Cash2023: num 27272727 6100000 7800000 30000000 27000000 ...
## $ Age : int 29 24 26 29 28 27 22 28 25 26 ...
#Eliminating non-numeric columns
mlb_stats_num <- mlb_stats[sapply(mlb_stats, is.numeric)]
str(mlb_stats_num)
## 'data.frame': 47 obs. of 14 variables:
## $ GS : int 160 158 151 148 161 133 148 151 129 138 ...
## $ AB : int 652 652 637 557 630 522 591 593 481 563 ...
## $ H : int 194 189 180 171 170 152 150 145 135 134 ...
## $ X2B : int 39 43 26 38 25 24 31 24 22 31 ...
## $ X3B : int 4 1 9 0 5 1 6 1 5 0 ...
## $ HR : int 21 24 11 15 26 22 20 33 10 31 ...
## $ RBI : int 100 93 71 73 107 64 80 83 55 98 ...
## $ AVG : num 0.298 0.29 0.283 0.307 0.27 0.291 0.254 0.245 0.281 0.238 ...
## $ OBP : num 0.343 0.333 0.312 0.377 0.339 0.366 0.294 0.317 0.327 0.298 ...
## $ SLG : num 0.466 0.469 0.403 0.456 0.449 0.467 0.428 0.455 0.41 0.458 ...
## $ OPS : num 0.809 0.802 0.715 0.833 0.788 0.834 0.722 0.772 0.736 0.756 ...
## $ WAR : num 4.84 3.44 3.95 5.42 5.4 5.55 1.05 4.04 4.5 4.42 ...
## $ Cash2023: num 27272727 6100000 7800000 30000000 27000000 ...
## $ Age : int 29 24 26 29 28 27 22 28 25 26 ...
#mean
average <- mean(mlb_stats_num$Cash2023, na.rm = TRUE)
paste("average:", format(round(average, 0), scientific = FALSE, big.mark = ","))
## [1] "average: 6,855,709"
#variance
variance <- var(mlb_stats_num$Cash2023, na.rm = TRUE)
paste("variance:", format(round(variance, 0), scientific = FALSE, big.mark = ","))
## [1] "variance: 93,571,304,657,024"
#standard deviation
stdv <- sd(mlb_stats_num$Cash2023, na.rm = TRUE)
paste("standard deviation:", format(round(stdv, 0), scientific = FALSE, big.mark = ","))
## [1] "standard deviation: 9,673,226"
#median
median1 <- median(mlb_stats_num$Cash2023, na.rm = TRUE)
paste("median:", format(round(median1, 0), scientific = FALSE, big.mark = ","))
## [1] "median: 2,000,000"
#min number
minimum <- min(mlb_stats_num$Cash2023, na.rm = TRUE)
paste("minimum:", format(round(minimum, 0), scientific = FALSE, big.mark = ","))
## [1] "minimum: 410,326"
#max number
maximum <- max(mlb_stats_num$Cash2023, na.rm = TRUE)
paste("maximum:", format(round(maximum, 0), scientific = FALSE, big.mark = ","))
## [1] "maximum: 36,000,000"
#range of the numbers
range1 <- range(mlb_stats_num$Cash2023, na.rm = TRUE)
paste("range:", format(round(range1, 0), scientific = FALSE, big.mark = ","))
## [1] "range: 410,326" "range: 36,000,000"
#difference between max and min
difference <- diff(range(mlb_stats_num$Cash2023, na.rm = TRUE))
paste("difference:", format(round(difference, 0), scientific = FALSE, big.mark = ","))
## [1] "difference: 35,589,674"
names(mlb_stats_num)
## [1] "GS" "AB" "H" "X2B" "X3B" "HR"
## [7] "RBI" "AVG" "OBP" "SLG" "OPS" "WAR"
## [13] "Cash2023" "Age"
#correlation of the variables
cor(mlb_stats_num)
## GS AB H X2B X3B HR
## GS 1.0000000 0.9878832 0.9387030 0.8804607 0.366752978 0.7339877
## AB 0.9878832 1.0000000 0.9741364 0.9156261 0.389106791 0.7840134
## H 0.9387030 0.9741364 1.0000000 0.9356879 0.375379829 0.7724508
## X2B 0.8804607 0.9156261 0.9356879 1.0000000 0.293089007 0.7399494
## X3B 0.3667530 0.3891068 0.3753798 0.2930890 1.000000000 0.1576920
## HR 0.7339877 0.7840134 0.7724508 0.7399494 0.157692045 1.0000000
## RBI 0.8833421 0.9243508 0.9298298 0.8896238 0.315943566 0.8973054
## AVG 0.2527911 0.3350714 0.4999607 0.4540903 0.115910827 0.2808246
## OBP 0.1430023 0.1935521 0.3283211 0.2978383 0.001986825 0.2055263
## SLG 0.1628489 0.2577958 0.3830350 0.3966754 0.102071214 0.5288822
## OPS 0.1672546 0.2520635 0.3908039 0.3880660 0.069776549 0.4423801
## WAR 0.7585507 0.7838553 0.8122353 0.7383154 0.308012465 0.7154940
## Cash2023 0.4710708 0.5099290 0.5628422 0.4634702 0.030049924 0.6053461
## Age 0.2663626 0.2643532 0.2483782 0.2137952 -0.098017716 0.1988217
## RBI AVG OBP SLG OPS WAR
## GS 0.8833421 0.2527911 0.143002251 0.1628489 0.16725465 0.7585507
## AB 0.9243508 0.3350714 0.193552094 0.2577958 0.25206353 0.7838553
## H 0.9298298 0.4999607 0.328321103 0.3830350 0.39080388 0.8122353
## X2B 0.8896238 0.4540903 0.297838268 0.3966754 0.38806597 0.7383154
## X3B 0.3159436 0.1159108 0.001986825 0.1020712 0.06977655 0.3080125
## HR 0.8973054 0.2808246 0.205526332 0.5288822 0.44238007 0.7154940
## RBI 1.0000000 0.3961871 0.285046646 0.4542913 0.42255309 0.7653890
## AVG 0.3961871 1.0000000 0.807340495 0.7975364 0.86254468 0.4335819
## OBP 0.2850466 0.8073405 1.000000000 0.7032172 0.87380311 0.3843565
## SLG 0.4542913 0.7975364 0.703217214 1.0000000 0.96018521 0.4288262
## OPS 0.4225531 0.8625447 0.873803113 0.9601852 1.00000000 0.4439624
## WAR 0.7653890 0.4335819 0.384356543 0.4288262 0.44396237 1.0000000
## Cash2023 0.5789837 0.3434524 0.373311756 0.3875104 0.41241558 0.6341681
## Age 0.2358260 0.1043517 0.054119374 0.1016015 0.09065536 0.1841738
## Cash2023 Age
## GS 0.47107079 0.26636259
## AB 0.50992895 0.26435320
## H 0.56284220 0.24837822
## X2B 0.46347021 0.21379517
## X3B 0.03004992 -0.09801772
## HR 0.60534606 0.19882166
## RBI 0.57898374 0.23582598
## AVG 0.34345236 0.10435169
## OBP 0.37331176 0.05411937
## SLG 0.38751040 0.10160148
## OPS 0.41241558 0.09065536
## WAR 0.63416813 0.18417379
## Cash2023 1.00000000 0.44225191
## Age 0.44225191 1.00000000
If pairs are correlating with each other at 0.7+, that’s multicollinearity, and including both in the same regression could make the coefficients unstable or misleading. In that case, it’s often better to pick just one or two representative predictors rather than throwing all four in together. WAR is a strong single choice since it already tries to summarize overall value in one number.
#box plot chart
options(scipen = 999)
boxplot(mlb_stats_num$Cash2023, main="Boxplot of Salaries", ylab="Price ($)")
#histogram chart
options(scipen = 999)
hist(mlb_stats_num$Cash2023, main = "Histogram of Player Prices", xlab = "Price ($)")
# scatterplot
plot(x = mlb_stats_num$HR, y = mlb_stats_num$Cash2023,
main = "Scatterplot of HR vs. Salary",
xlab = "HR",
ylab = "Price ($)")
#scatter plot against Cash2023
par(mfrow = c(3, 5))
for (col in names(mlb_stats_num)) {
if (col != "Cash2023") {
plot(mlb_stats_num[[col]], mlb_stats_num$Cash2023,
main = paste(col, "vs. Salary"),
xlab = col,
ylab = "Price ($)")
}
}
par(mfrow = c(1, 1))
library(rpart)
library(randomForest)
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
Check for multicollinearity among predictors
pairs(mlb_stats_num[, c("WAR", "OPS", "HR", "RBI", "AVG", "OBP", "Age")])
If two predictors are highly correlated with each other (like AVG and
OBP are), keeping both adds little and can destabilize the model.
Choose the best model
Examine model results using summary function
Best Subset Selection
#install.packages("leaps", repos = "https://cran.r-project.org")
library(leaps)
#Best Subset Selection on the train dataset
best_subset <- regsubsets(Cash2023 ~ H + HR + RBI + WAR + AVG + OBP + SLG + OPS + Age + GS + AB + X2B + X3B,
data = mlb_stats_num, nvmax = 10) # max number of predictors to consider
summary(best_subset)
## Subset selection object
## Call: regsubsets.formula(Cash2023 ~ H + HR + RBI + WAR + AVG + OBP +
## SLG + OPS + Age + GS + AB + X2B + X3B, data = mlb_stats_num,
## nvmax = 10)
## 13 Variables (and intercept)
## Forced in Forced out
## H FALSE FALSE
## HR FALSE FALSE
## RBI FALSE FALSE
## WAR FALSE FALSE
## AVG FALSE FALSE
## OBP FALSE FALSE
## SLG FALSE FALSE
## OPS FALSE FALSE
## Age FALSE FALSE
## GS FALSE FALSE
## AB FALSE FALSE
## X2B FALSE FALSE
## X3B FALSE FALSE
## 1 subsets of each size up to 10
## Selection Algorithm: exhaustive
## H HR RBI WAR AVG OBP SLG OPS Age GS AB X2B X3B
## 1 ( 1 ) " " " " " " "*" " " " " " " " " " " " " " " " " " "
## 2 ( 1 ) " " " " " " "*" " " " " " " " " "*" " " " " " " " "
## 3 ( 1 ) " " "*" " " "*" " " " " " " " " "*" " " " " " " " "
## 4 ( 1 ) " " "*" " " "*" " " " " " " " " "*" "*" " " " " " "
## 5 ( 1 ) "*" "*" " " "*" " " " " " " " " "*" " " "*" " " " "
## 6 ( 1 ) "*" "*" " " "*" " " " " "*" " " "*" " " "*" " " " "
## 7 ( 1 ) "*" "*" " " "*" " " " " "*" "*" "*" " " "*" " " " "
## 8 ( 1 ) "*" "*" " " "*" " " "*" "*" "*" "*" " " "*" " " " "
## 9 ( 1 ) "*" "*" " " "*" " " "*" "*" "*" "*" " " "*" "*" " "
## 10 ( 1 ) "*" "*" " " "*" " " "*" "*" "*" "*" " " "*" "*" "*"
#best values for the linear regression model
results <- summary(best_subset)
results$adjr2 # adjusted R-squared for each model size
## [1] 0.3888841 0.4896185 0.5145837 0.5361944 0.5609653 0.5847174 0.6133255
## [8] 0.6193453 0.6189771 0.6127989
which.max(results$adjr2) # which size is best
## [1] 8
coef(best_subset, which.max(results$adjr2)) # variables in that best model
## (Intercept) H HR WAR OBP
## -18514540.04 231054.94 912529.52 1981159.35 -2233572815.83
## SLG OPS Age AB
## -2387981259.41 2312583912.10 1050862.19 -85435.49
Liner regression equation: Salary = -18514540.04 + 231,054.94 * H + 912,529.52 * HR + 1,981,159.35 * WAR - 2,233,572,815.83 * OBP - 2,387,981,259.41 * SLG + 2,312,583,912.10 * OPS + 1,050,862.19 * AGE - 85,435.49 * AB
#training the liner regression on the train dataset
lm_best <- lm(Cash2023 ~ H + HR + WAR + SLG + OBP + OPS + Age + AB, data = mlb_stats_num)
summary(lm_best)
##
## Call:
## lm(formula = Cash2023 ~ H + HR + WAR + SLG + OBP + OPS + Age +
## AB, data = mlb_stats_num)
##
## Residuals:
## Min 1Q Median 3Q Max
## -11921778 -4000606 50686 3124390 13859983
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -18514540 12221823 -1.515 0.138079
## H 231055 106373 2.172 0.036154 *
## HR 912530 248104 3.678 0.000725 ***
## WAR 1981159 861754 2.299 0.027096 *
## SLG -2387981259 1755482544 -1.360 0.181751
## OBP -2233572816 1756618875 -1.272 0.211272
## OPS 2312583912 1756954086 1.316 0.195976
## Age 1050862 265040 3.965 0.000313 ***
## AB -85436 31280 -2.731 0.009513 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 5968000 on 38 degrees of freedom
## Multiple R-squared: 0.6855, Adjusted R-squared: 0.6193
## F-statistic: 10.36 on 8 and 38 DF, p-value: 0.0000001511
While best subset selection identified this 8-variable model as optimal by adjusted R² (0.619), VIF diagnostics revealed severe multicollinearity among OBP, SLG, and OPS (VIF > 1,000), making individual coefficients for these variables uninterpretable.
# Sum of Squared Errors
SSE = sum(lm_best$residuals^2)
SSE
## [1] 1353497423704957
# Decision Tree Regression Model
dt_model <- rpart(Cash2023 ~ H + HR + RBI + WAR + AVG + OBP + SLG + OPS + Age + GS + AB + X2B + X3B, data = mlb_stats_num)
summary(dt_model)
## Call:
## rpart(formula = Cash2023 ~ H + HR + RBI + WAR + AVG + OBP + SLG +
## OPS + Age + GS + AB + X2B + X3B, data = mlb_stats_num)
## n= 47
##
## CP nsplit rel error xerror xstd
## 1 0.46901173 0 1.0000000 1.043921 0.3149211
## 2 0.09138502 1 0.5309883 1.237885 0.3250304
## 3 0.02300795 2 0.4396032 1.177699 0.3417400
## 4 0.01000000 3 0.4165953 1.176274 0.3418513
##
## Variable importance
## SLG OPS RBI HR H AVG Age AB WAR GS
## 22 19 14 14 12 11 4 1 1 1
##
## Node number 1: 47 observations, complexity param=0.4690117
## mean=6855709, MSE=9.158043e+13
## left son=2 (39 obs) right son=3 (8 obs)
## Primary splits:
## SLG < 0.4455 to the left, improve=0.4690117, (0 missing)
## H < 140 to the left, improve=0.4634343, (0 missing)
## AB < 521.5 to the left, improve=0.4189003, (0 missing)
## OPS < 0.764 to the left, improve=0.4066030, (0 missing)
## WAR < 3.995 to the left, improve=0.4044566, (0 missing)
## Surrogate splits:
## OPS < 0.7535 to the left, agree=0.979, adj=0.875, (0 split)
## HR < 20.5 to the left, agree=0.936, adj=0.625, (0 split)
## RBI < 81.5 to the left, agree=0.936, adj=0.625, (0 split)
## H < 140 to the left, agree=0.915, adj=0.500, (0 split)
## AVG < 0.2865 to the left, agree=0.915, adj=0.500, (0 split)
##
## Node number 2: 39 observations, complexity param=0.09138502
## mean=3887422, MSE=2.351169e+13
## left son=4 (28 obs) right son=5 (11 obs)
## Primary splits:
## Age < 27.5 to the left, improve=0.4289702, (0 missing)
## H < 91 to the left, improve=0.1845738, (0 missing)
## AVG < 0.229 to the left, improve=0.1488001, (0 missing)
## GS < 140 to the left, improve=0.1253144, (0 missing)
## WAR < 2.64 to the left, improve=0.1248433, (0 missing)
## Surrogate splits:
## AB < 523.5 to the left, agree=0.744, adj=0.091, (0 split)
##
## Node number 3: 8 observations
## mean=2.132611e+07, MSE=1.710708e+14
##
## Node number 4: 28 observations, complexity param=0.02300795
## mean=1896873, MSE=6.169185e+12
## left son=8 (21 obs) right son=9 (7 obs)
## Primary splits:
## WAR < 2.645 to the left, improve=0.5733142, (0 missing)
## H < 122.5 to the left, improve=0.5119767, (0 missing)
## AB < 482 to the left, improve=0.4873323, (0 missing)
## GS < 133.5 to the left, improve=0.4865769, (0 missing)
## RBI < 41 to the left, improve=0.3287763, (0 missing)
## Surrogate splits:
## AB < 462.5 to the left, agree=0.964, adj=0.857, (0 split)
## H < 104 to the left, agree=0.929, adj=0.714, (0 split)
## GS < 125.5 to the left, agree=0.929, adj=0.714, (0 split)
## RBI < 41 to the left, agree=0.893, adj=0.571, (0 split)
## X2B < 19.5 to the left, agree=0.857, adj=0.429, (0 split)
##
## Node number 5: 11 observations
## mean=8954273, MSE=3.189745e+13
##
## Node number 8: 21 observations
## mean=811073.6, MSE=1.373617e+11
##
## Node number 9: 7 observations
## mean=5154271, MSE=1.011713e+13
Root (47 players, mean $6.86M) splits on SLG < 0.4455
High slugging (8 players): mean $21.3M — elite power hitters command top salaries
Lower slugging (39 players) splits further on Age < 27.5
Age 28+ (11 players): mean $8.95M — veteran deals/accumulated service time
Under 28 (28 players) splits further on WAR < 2.645
WAR ≥ 2.645 (7 players): mean $5.15M — solid young performers, likely arbitration-eligible
WAR < 2.645 (21 players): mean $811,074 — this is almost certainly your rookie/pre-arbitration bucket
printcp(dt_model)
##
## Regression tree:
## rpart(formula = Cash2023 ~ H + HR + RBI + WAR + AVG + OBP + SLG +
## OPS + Age + GS + AB + X2B + X3B, data = mlb_stats_num)
##
## Variables actually used in tree construction:
## [1] Age SLG WAR
##
## Root node error: 4304280014223093/47 = 91580425834534
##
## n= 47
##
## CP nsplit rel error xerror xstd
## 1 0.469012 0 1.00000 1.0439 0.31492
## 2 0.091385 1 0.53099 1.2379 0.32503
## 3 0.023008 2 0.43960 1.1777 0.34174
## 4 0.010000 3 0.41660 1.1763 0.34185
Variables actually used: Age, SLG, WAR — matches the three real splits (SLG at root, Age second, WAR third)
plotcp(dt_model)
# Random Forest Regression Model
rf_model <- randomForest(Cash2023 ~ H + HR + RBI + WAR + AVG + OBP + SLG + OPS + Age + GS + AB + X2B + X3B, data = mlb_stats_num)
rf_model
##
## Call:
## randomForest(formula = Cash2023 ~ H + HR + RBI + WAR + AVG + OBP + SLG + OPS + Age + GS + AB + X2B + X3B, data = mlb_stats_num)
## Type of random forest: regression
## Number of trees: 500
## No. of variables tried at each split: 4
##
## Mean of squared residuals: 59816595699813
## % Var explained: 34.68
Overall, this model explains 33.67% of the variation in player’s salary and maintains an average squared error of 60,741,476,193,600 on the test data.
500 trees, 4 variables tried at each split (this is mtry, R’s default for regression: roughly p/3 of the 13 predictors ≈ 4) Mean of squared residuals: 60,741,476,193,600 Taking the square root gives you an OOB RMSE for context: √60,741,476,193,600 ≈ $7,793,681 % Var explained: 33.67% this is Random Forest’s internal analog to R², based on OOB predictions
mlb_stats_test <- read.csv("MLB+Player-hittingstats-ss-updated.csv", header = TRUE)
str(mlb_stats_test)
## 'data.frame': 25 obs. of 17 variables:
## $ Player : chr "FranciscoLindor" "WillyAdames" "ZachNeto" "TrevorStory" ...
## $ Pos : chr "SS" "SS" "SS" "SS" ...
## $ Team : chr "NYM" "SF" "LAA" "BOS" ...
## $ GS : int 160 160 128 157 159 157 162 71 102 150 ...
## $ AB : int 644 591 502 612 590 623 629 255 380 589 ...
## $ H : int 172 133 129 161 144 184 166 61 103 152 ...
## $ X2B : int 35 22 29 29 24 47 31 9 19 23 ...
## $ X3B : int 0 2 1 0 3 6 7 1 0 2 ...
## $ HR : int 31 30 26 25 24 23 22 21 21 20 ...
## $ RBI : int 86 87 62 96 77 88 86 55 50 82 ...
## $ AVG : num 0.267 0.225 0.257 0.263 0.244 0.295 0.264 0.239 0.271 0.258 ...
## $ OBP : num 0.346 0.318 0.319 0.308 0.3 0.351 0.336 0.311 0.373 0.326 ...
## $ SLG : num 0.466 0.421 0.474 0.433 0.417 0.501 0.44 0.529 0.487 0.406 ...
## $ OPS : num 0.812 0.739 0.793 0.741 0.717 0.852 0.776 0.84 0.86 0.732 ...
## $ WAR : num 5.8 3.7 5.3 4.1 4.5 7.1 3.6 3.3 6.2 4.9 ...
## $ Cash2023: int 27050000 21000000 780000 22500000 27000000 7100000 770000 355483 32000000 22000000 ...
## $ Age : int 31 29 24 32 31 25 23 23 31 32 ...
#Eliminating non-numeric columns
mlb_stats_test <- read.csv("MLB+Player-hittingstats-ss-updated.csv", header = TRUE)
mlb_stats_test_num <- mlb_stats_test[sapply(mlb_stats_test, is.numeric)]
str(mlb_stats_test_num)
## 'data.frame': 25 obs. of 14 variables:
## $ GS : int 160 160 128 157 159 157 162 71 102 150 ...
## $ AB : int 644 591 502 612 590 623 629 255 380 589 ...
## $ H : int 172 133 129 161 144 184 166 61 103 152 ...
## $ X2B : int 35 22 29 29 24 47 31 9 19 23 ...
## $ X3B : int 0 2 1 0 3 6 7 1 0 2 ...
## $ HR : int 31 30 26 25 24 23 22 21 21 20 ...
## $ RBI : int 86 87 62 96 77 88 86 55 50 82 ...
## $ AVG : num 0.267 0.225 0.257 0.263 0.244 0.295 0.264 0.239 0.271 0.258 ...
## $ OBP : num 0.346 0.318 0.319 0.308 0.3 0.351 0.336 0.311 0.373 0.326 ...
## $ SLG : num 0.466 0.421 0.474 0.433 0.417 0.501 0.44 0.529 0.487 0.406 ...
## $ OPS : num 0.812 0.739 0.793 0.741 0.717 0.852 0.776 0.84 0.86 0.732 ...
## $ WAR : num 5.8 3.7 5.3 4.1 4.5 7.1 3.6 3.3 6.2 4.9 ...
## $ Cash2023: int 27050000 21000000 780000 22500000 27000000 7100000 770000 355483 32000000 22000000 ...
## $ Age : int 31 29 24 32 31 25 23 23 31 32 ...
lm_pred <- predict(lm_best, newdata = mlb_stats_test_num)
prediction_examples <- data.frame(Player = mlb_stats_test$Player, Actual = mlb_stats_test_num$Cash2023,
Predicted = round(lm_pred, 0),
Difference = round(mlb_stats_test_num$Cash2023 - lm_pred, 0)
)
prediction_examples
# Function to calculate RMSE
rmse <- function(actual, predicted) {sqrt(mean((actual - predicted)^2))}
# Generate Predictions
dt_pred <- predict(dt_model, newdata = mlb_stats_test_num)
rf_pred <- predict(rf_model, newdata = mlb_stats_test_num)
cat("Linear Regression RMSE: $", rmse(mlb_stats_test_num$Cash2023, lm_pred), "\n")
## Linear Regression RMSE: $ 9636697
cat("Decision Tree RMSE: $", rmse(mlb_stats_test_num$Cash2023, dt_pred), "\n")
## Decision Tree RMSE: $ 11420270
cat("Random Forest RMSE: $", rmse(mlb_stats_test_num$Cash2023, rf_pred), "\n")
## Random Forest RMSE: $ 10463361
Linear Regression RMSE: $9,636,697 prediction error Decision Tree: $11,420,270 average prediction error Random Forest: $10,836,440 average prediction error
Decision Tree likely makes big absolute-dollar mistakes on a few high-salary players (it only splits on AB, Age and WAR, grouping players into a handful of buckets and if a superstar with unusual stats gets bucketed with much cheaper players, that’s a massive dollar miss, which RMSE punishes heavily).
Linear Regression likely does reasonably on high earners but performs proportionally worse on low-salary players, where the straight-line model badly mishandles the low end of the salary range.
comparison <- data.frame(
Player = mlb_stats_test$Player,
Actual = mlb_stats_test_num$Cash2023,
LM_Pred = round(lm_pred, 0),
DT_Pred = round(dt_pred, 0),
RF_Pred = round(rf_pred, 0),
LM_PctErr = round(abs((mlb_stats_test_num$Cash2023 - lm_pred) / mlb_stats_test_num$Cash2023) * 100, 1),
DT_PctErr = round(abs((mlb_stats_test_num$Cash2023 - dt_pred) / mlb_stats_test_num$Cash2023) * 100, 1),
RF_PctErr = round(abs((mlb_stats_test_num$Cash2023 - rf_pred) / mlb_stats_test_num$Cash2023) * 100, 1)
)
comparison[order(comparison$Actual), ]
Linear Regression and Random Forest show a different failure pattern here — they’re not catastrophically wrong on just one or two players, they’re consistently, moderately-to-severely overpredicting nearly every player on this page (190%-2200%+ errors across the board). That’s a systematic bias toward overpredicting low earners, rather than a few isolated misclassifications.
Decision Tree’s coarse bucketing correctly captured most rookie-scale salaries (5-7% error) but catastrophically misclassified high-slugging young players like ZachNeto, whose SLG exceeded the tree’s elite-player threshold despite his rookie-scale contract — producing an isolated but massive error that dominated RMSE. Linear Regression and Random Forest, by contrast, showed consistent, systematic overprediction across nearly all low-salary players, reflecting their inability to represent the discontinuous jump between rookie and market-rate pay.
# Analysis of Variance (ANOVA) for linear regression model
anova(lm_best)
No single model dominates across all metrics.
Linear Regression minimizes typical dollar-error (RMSE);
Decision Tree minimizes typical percentage-error for most players but is vulnerable to catastrophic misclassification of edge cases;
Random Forest offers the most balanced, moderate performance without excelling on any single metric.
All three models share a common, structural weakness: none can fully capture the discontinuous jump between rookie-scale and market-rate salaries, since this is governed by service-time rules rather than on-field performance.
# Save the trained model
saveRDS(lm_best, "linear_regression_model.rds")