DAT-4313 - Data Viz in Model Development

NBA Example

Author

Gabriela CHajon

FURTHER INSTRUCTIONS

Use the dataset assigned to you in the LAB Instructions.

Use the depvar indicated for your dataset.

Select 5-8 independent variables from your dataset as predictors, and estimate a linear regression model.

  • Note that not all variables in your dataset make sense as predictors. For example, categorical variables with many levels or all components of a derived variable are unreasonable.

  • Work through the EDA to identified good candidates. Your model should be linear in the parameters. Consider alternative functional forms for your depvar or continuous variables.

Include interpretations and comments after each chunk.

Conclude with a final assessment of the model and a reflection of the value of data visualization to aid model development.


Example Code for FIFA with depvar=Wage

DATA

options(scipen=999)
library(socviz) # use for the round_df() function
load("NBA_viz.RData")
library(psych)

describe(NBA_viz)
         vars   n    mean     sd  median trimmed     mad min    max  range
PLAYER*     1 530  265.50 153.14  265.50  265.50  196.44   1  530.0  529.0
FORWARD*    2 530    1.49   0.50    1.00    1.49    0.00   1    2.0    1.0
CENTER*     3 530    1.19   0.39    1.00    1.11    0.00   1    2.0    1.0
GUARD*      4 530    1.49   0.50    1.00    1.49    0.00   1    2.0    1.0
ROOKIE*     5 530    1.20   0.40    1.00    1.12    0.00   1    2.0    1.0
TEAM*       6 530   15.41   8.60   15.00   15.38   10.38   1   30.0   29.0
AGE         7 530   26.14   4.18   25.00   25.85    4.45  19   42.0   23.0
GP          8 530   49.25  26.05   56.00   51.00   28.17   1   82.0   81.0
W           9 530   24.67  15.89   24.00   24.30   20.76   0   60.0   60.0
MIN        10 530 1121.63 838.67 1069.00 1078.70 1115.66   1 3028.0 3027.0
PTS        11 530   19.84   7.85   19.05   19.46    5.86   0  100.5  100.5
FGM        12 530    7.42   3.26    7.10    7.30    2.37   0   50.3   50.3
FGA        13 530   16.78   5.37   16.10   16.47    4.30   0   52.7   52.7
FTM        14 530    3.00   2.05    2.70    2.79    1.63   0   17.2   17.2
FTA        15 530    4.08   2.62    3.65    3.85    2.15   0   18.2   18.2
REB        16 530    9.06   5.14    7.90    8.52    3.85   0   52.7   52.7
AST        17 530    4.38   2.85    3.60    4.06    2.22   0   17.1   17.1
TOV        18 530    2.53   1.40    2.30    2.43    1.04   0   12.8   12.8
          skew kurtosis    se
PLAYER*   0.00    -1.21  6.65
FORWARD*  0.05    -2.00  0.02
CENTER*   1.59     0.52  0.02
GUARD*    0.02    -2.00  0.02
ROOKIE*   1.51     0.28  0.02
TEAM*     0.03    -1.18  0.37
AGE       0.65     0.08  0.18
GP       -0.48    -1.13  1.13
W         0.16    -1.12  0.69
MIN       0.28    -1.16 36.43
PTS       2.20    20.63  0.34
FGM       4.31    55.11  0.14
FGA       1.29     6.82  0.23
FTM       1.57     5.55  0.09
FTA       1.26     3.18  0.11
REB       2.20    11.90  0.22
AST       1.09     1.16  0.12
TOV       1.82     9.15  0.06
colnames(NBA_viz)
 [1] "PLAYER"  "FORWARD" "CENTER"  "GUARD"   "ROOKIE"  "TEAM"    "AGE"    
 [8] "GP"      "W"       "MIN"     "PTS"     "FGM"     "FGA"     "FTM"    
[15] "FTA"     "REB"     "AST"     "TOV"    
str(NBA_viz)
'data.frame':   530 obs. of  18 variables:
 $ PLAYER : Factor w/ 530 levels "Aaron Gordon",..: 1 2 3 5 4 6 7 8 9 10 ...
 $ FORWARD: Factor w/ 2 levels "No","Yes": 2 1 2 2 2 2 1 1 1 1 ...
 $ CENTER : Factor w/ 2 levels "No","Yes": 1 1 1 2 1 2 1 1 1 2 ...
 $ GUARD  : Factor w/ 2 levels "No","Yes": 1 2 1 1 1 1 2 2 2 1 ...
 $ ROOKIE : Factor w/ 2 levels "No","Yes": 1 2 1 1 1 1 1 1 1 1 ...
 $ TEAM   : Factor w/ 30 levels "ATL","BKN","BOS",..: 22 12 21 3 25 2 26 21 14 1 ...
 $ AGE    : int  23 22 25 32 28 26 27 25 25 25 ...
 $ GP     : int  78 50 61 68 81 5 64 31 25 77 ...
 $ W      : int  40 31 38 41 52 1 19 21 8 28 ...
 $ MIN    : int  2633 646 694 1973 2292 26 1375 588 531 1544 ...
 $ PTS    : num  22.7 21.9 16.7 22.5 15.9 33.8 19.6 13.5 20.7 26.5 ...
 $ FGM    : num  8.6 7.8 6.3 9.4 5.4 15 6.7 4.6 7 9.9 ...
 $ FGA    : num  19.1 19.5 14.9 17.6 12.4 24.4 16.5 12.8 15.6 20.1 ...
 $ FTM    : num  3.4 3 1.9 1.9 3.1 3.8 4 1 4.6 4.4 ...
 $ FTA    : num  4.6 3.7 2.5 2.3 3.6 7.5 4.9 1.1 5.8 6.7 ...
 $ REB    : num  10.5 5 8 11.1 12.8 35.7 8.2 3.9 6.1 13.2 ...
 $ AST    : num  5.3 6.5 1.4 6.9 2.2 5.6 4.5 1.6 7 2.7 ...
 $ TOV    : num  3 3 1.8 2.5 1.5 1.9 2.3 1.1 3.8 3 ...

Partition 60% Train / 40% Test

library(caret)
set.seed(1)
index <- createDataPartition(NBA_viz$GP, p=0.6, list=FALSE)
train <- NBA_viz[index,]
test  <- NBA_viz[-index,]

Training set number of observations: 319
Test dataset number of observations: 211


EDA

Descriptive Statistics

library(psych)
library(tidyverse)
library(flextable)
psych::describe(train, fast=TRUE)  # (class note --> remove flextable)
        vars   n    mean     sd min    max  range    se
PLAYER     1 319     NaN     NA Inf   -Inf   -Inf    NA
FORWARD    2 319     NaN     NA Inf   -Inf   -Inf    NA
CENTER     3 319     NaN     NA Inf   -Inf   -Inf    NA
GUARD      4 319     NaN     NA Inf   -Inf   -Inf    NA
ROOKIE     5 319     NaN     NA Inf   -Inf   -Inf    NA
TEAM       6 319     NaN     NA Inf   -Inf   -Inf    NA
AGE        7 319   26.16   4.27  19   39.0   20.0  0.24
GP         8 319   49.33  25.76   1   82.0   81.0  1.44
W          9 319   25.46  16.08   0   60.0   60.0  0.90
MIN       10 319 1098.37 814.22   1 2848.0 2847.0 45.59
PTS       11 319   19.39   6.42   0   42.8   42.8  0.36
FGM       12 319    7.27   2.48   0   17.1   17.1  0.14
FGA       13 319   16.54   4.93   0   52.7   52.7  0.28
FTM       14 319    2.82   1.74   0    9.1    9.1  0.10
FTA       15 319    3.85   2.36   0   18.2   18.2  0.13
REB       16 319    8.81   4.94   0   52.7   52.7  0.28
AST       17 319    4.24   2.84   0   17.1   17.1  0.16
TOV       18 319    2.43   1.19   0    7.2    7.2  0.07

Boxplots – All Numeric

data_long <- train %>%
  select(where(is.numeric)) %>%
  pivot_longer(cols = everything(), names_to = "Variable", values_to = "Value")

ggplot(data_long, aes(x = Variable, y = Value)) +
  geom_boxplot() +
  coord_flip() + 
  facet_wrap(~ Variable, scales = "free", ncol=3) +
  theme_minimal() +
  theme(axis.text.x = element_blank()) + 
  labs(title = "Horizontal Boxplot for Each Numeric Variable")

library(DataExplorer)
plot_boxplot(train, by="AGE") 

Histograms

plot_histogram(train)

Scatterplots (depvar ~ all x)

Look at the shape of the relationship between the dependent variable and all of the continuous potential independent variables.

plot_scatterplot(test, by="GP")

Correlation Matrix

plot_correlation(train, type="continuous")

Variables with stronger correlation are FGM AND W which can make us infer that will be more significant in the model. The plot help us see that some variables are correlated between them like FTM and FTA and also FGM and PTS


MODEL

Linear Regression Model

From the above EDA, we chose the following variables to start. Note that, given the shape of the relationship between Wage and Age, we entered Age as a quadratic.
From experience and prior research, it is common to specify a dependent variable that is a currency variable (e.g., sales, revenue, wages) in log form.

Estimate the following model:
\(log(gp) = Age + Age^2 + Potential + International.Reputation + Value.n + Special + Height + RightFoot + Skill.Moves\) \(log(gp)= PTS + FGM + FGA + FTM + FTA\)

Estimate Coefficients and show coefficients table

model <- lm(log(GP) ~ PTS + W + MIN + FGM + AGE, data=train)
model %>% as_flextable

Estimate

Standard Error

t value

Pr(>|t|)

(Intercept)

2.253

0.231

9.737

0.0000

***

PTS

0.009

0.019

0.483

0.6293

W

0.030

0.003

9.025

0.0000

***

MIN

0.000

0.000

6.361

0.0000

***

FGM

-0.016

0.049

-0.322

0.7476

AGE

0.002

0.008

0.226

0.8217

Signif. codes: 0 <= '***' < 0.001 < '**' < 0.01 < '*' < 0.05

Residual standard error: 0.59 on 313 degrees of freedom

Multiple R-squared: 0.6496, Adjusted R-squared: 0.644

F-statistic: 116.1 on 313 and 5 DF, p-value: 0.0000

Among the factors considered, both “W” (wins) and “MIN” (minutes played) significantly influence the number of games played. For instance, for every additional win, the expected increase in games played is about 0.030, and for each extra minute played, there’s a rise in games played. On the other hand, indicators like “PTS” (points), “FGM” (field goals made), and “AGE” (age) show that were not statistically significant. The model shows a good ability to explain the variation in games played, with an adjusted R-squared value of 0.644, meaning that about 64.4% of the variability in games played can be understood by the predictors. Moreover, the low p-value of 0.0000 for the F-statistic (116.1) indicates that the overall model is highly statistically significant.

Coefficient Magnitude Plot

# note this simple code using coefplot package produces chart like Figure 6.6 in one short line. 
library(coefplot)
coefplot(model, soft="magnitude", intercept=FALSE)

The chart helps visualize the 3 predictors that were not statistically significant because they are the ones that has the line that pass on zero, it means that the predictor variable does not have a significant impact on the outcome variable in the model.

Check for predictor independence

Using Variance Inflation Factors (VIF)

library(car)
vif(model)
      PTS         W       MIN       FGM       AGE 
13.825569  2.570092  2.875727 13.286659  1.042462 

Since a VIF value greater than 10 indicates a high degree of multicollinearity. “PTS” and “FGM” have VIF values exceeding 10, indicating potential multicollinearity issues which mean that these variables are highly correlated with other predictor variables in the model, which can affect the reliability of coefficient estimates. However, the model was made with those predictors because they were the ones that helped the model to explain the variablity more.

Residual Analysis

Residual Range

quantile(round(residuals(model, type = "deviance"),2))
    0%    25%    50%    75%   100% 
-2.470 -0.275  0.130  0.385  1.000 

Residual Plots

We are looking for: - Random distribution of residuals vs fitted values - Normally distributed residuals : Normal Q-Q plot with values along line - Homoskedasticity with a Scale-Location line that is horizontal and no residual pattern - Minimal influential obs - that is, those outside the borders of Cook’s distance

plot(model)

Since the points closely follow the line, it can indicate that the model is performing well, and there are no apparent issues with heteroscedasticity or non-linearity. If the points in the Normal Q-Q plot deviate from the diagonal line, it suggests that the distribution of residuals does not perfectly follow a normal distribution.

Plot Fitted Value by Actual Value

library(broom)
train2 <- augment(model, data=train)  # this appends predicted to original dataset to build plots on your own

p <- ggplot(train2, mapping = aes(y=.fitted, x=GP))
p  + geom_point()

The model’s predictions can mean they are accurate, and there is a strong linear relationship between the predicted and actual values.

Plot Residuals by Fitted Values

p <- ggplot(train2, mapping = aes(y=.resid, x=.fitted))
p  + geom_point()

Since the points are not randomly distributed it can indicate heteroscedasticity ### Performance Evaluation

Use Model to Score test dataset (Display First 10 values - depvar and fitted values only)

library(dplyr)
pred <- predict(model, newdata = test, interval="predict")  #score test dataset wtih model
test_w_predict <- cbind(test, pred)  # append score to original test dataset

test_w_predict %>%
  head(10) %>%
    select(PLAYER, GP, fit, lwr,upr) %>% 
  as_flextable(show_coltype = FALSE) 

PLAYER

GP

fit

lwr

upr

Alan Williams

5

2.4

1.2

3.6

Alec Burks

64

3.5

2.4

4.7

Alex Poythress

21

2.7

1.5

3.9

Alize Johnson

14

2.6

1.5

3.8

Allen Crabbe

43

3.4

2.3

4.6

Allonzo Trier

64

3.4

2.2

4.6

Amile Jefferson

12

2.7

1.5

3.8

Amir Johnson

51

3.5

2.4

4.7

Andrew Harrison

17

2.6

1.4

3.8

Andrew Wiggins

73

4.4

3.2

5.6

n: 10

The model predicts the number

Plot Actual vs Fitted (test)

plot(test_w_predict$GP, test_w_predict$fit, pch=16, cex=1)

Performance Metrics

library(Metrics)
metric_label <- c("MAE","RMSE", "MAPE")
metrics <- c(round(mae(test_w_predict$GP, test_w_predict$fit),4),
                         round(rmse(test_w_predict$GP, test_w_predict$fit),4),
                         round(mape(test_w_predict$GP, test_w_predict$fit),4))
pmtable <- data.frame(Metric=metric_label, Value = metrics)
flextable(pmtable)

Metric

Value

MAE

45.6561

RMSE

52.3188

MAPE

0.8841

Model Fit by Continent

# more in depth plots of performance 
p <- ggplot(data = subset(test_w_predict, TEAM %in% c("LAC", "GSW")),
            aes(x = GP,
                y = fit, ymin = lwr, ymax = upr,
                color = TEAM,
                fill = TEAM,
                group = TEAM))
p +  geom_point(alpha = 0.5) + 
       geom_line() + geom_ribbon(alpha = 0.2, color = FALSE) +
       labs(title="Actual vs Fitted with Upper and Lower CI",
              subtitle="Golden State Warriors and Los Angeles Clippers",
              caption="NBA{datasetsICR}") +
       xlab(NULL) + ylab("fitted") +
       theme(legend.position = "bottom")

Since both lines in the plot (representing the teams Golden State Warriors and Los Angeles Clippers) are ascending or going up, it suggests a positive relationship between the number of games played (“GP”) and the fitted values generated by the model.

ggplot2 explore (Intro Section)

This is just replication of the first section within Chapter 6 (as FYI)

pip <- lm(log(GP) ~ W, data=train)
summary(pip)

Call:
lm(formula = log(GP) ~ W, data = train)

Residuals:
     Min       1Q   Median       3Q      Max 
-2.44684 -0.30764  0.07194  0.41148  1.23955 

Coefficients:
            Estimate Std. Error t value            Pr(>|t|)    
(Intercept) 2.399525   0.066373   36.15 <0.0000000000000002 ***
W           0.047317   0.002205   21.46 <0.0000000000000002 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.6323 on 317 degrees of freedom
Multiple R-squared:  0.5923,    Adjusted R-squared:  0.591 
F-statistic: 460.6 on 1 and 317 DF,  p-value: < 0.00000000000000022
# knitr does not like autoplot
#autoplot(lm(log(Wage.n) ~ Potential, data=train), data=train, colour = 'Nationality_Continent')


library(ggplot2)
p <- ggplot(data=train, mapping=aes(y=log(GP), x=W))

p +  geom_point(alpha = 0.2) + 
       geom_smooth(method = "lm", aes(color = "OLS", fill = "OLS"))

p + geom_point(alpha=0.1) +
    geom_smooth(color = "beige", fill="beige", method = MASS::rlm) +
    geom_smooth(color = "darkmagenta", fill="darkmagenta", method = "lm")

p + geom_point(alpha=0.1) +
    geom_smooth(color = "beige", method = "lm", linewidth = 1.2, 
                formula = y ~ splines::bs(x, 3), se = FALSE)

p + geom_point(alpha=0.1) +
    geom_quantile(color = "beige", size = 1.2, method = "rqss",
                  lambda = 1, quantiles = c(0.20, 0.5, 0.85))


SUMMARY ASSESSMENT AND EVALUATION OF THE MODEL

Include a reflection on the value of visualization for your model.

END