12 Diagnosing and Addressing Problems in Linear Regression

This lab helps us understand how to diagnose and address potential problems in OLS regression. In the last lab, we addressed the OLS assumptions of linearity, multicollinearity, and normality of residuals. This lab addresses outliers and heteroscedasticity while also providing a refresher on exploring and visualizing your data. The following packages are required for this lab:

  1. tidyverse
  2. psych
  3. car
  4. stargazer
  5. reshape2
  6. stargazer
  7. MASS
  8. plotly
  9. sandwich
  10. broom

12.1 Introduction to the Data

For this lab we will use a data set that contains information on movies from the IMDB website. The initial data set contains almost 60,000 observations, so we will filter the data to make it a little smaller:

ds <- filter(ds, year>=1995 & votes>1000 & Short!=1) %>% dplyr::select(rating, length, budget,
                                                                votes, title, year) %>%
  na.omit()

Now explore the data. Start with examining the structure of the data set:

str(ds)
## Classes 'tbl_df', 'tbl' and 'data.frame':    1359 obs. of  6 variables:
##  $ rating: num  6.7 4.7 6.4 6.1 6.1 5.1 5.4 2.5 7.6 8 ...
##  $ length: int  97 100 98 102 120 107 101 99 129 124 ...
##  $ budget: int  16000000 85000000 37000000 85000000 42000000 76000000 6000000 26000000 12000000 20000000 ...
##  $ votes : int  19095 1987 7859 14344 10866 9556 4514 2023 2663 21857 ...
##  $ title : chr  "10 Things I Hate About You" "102 Dalmatians" "13 Going On 30" "13th Warrior, The" ...
##  $ year  : int  1999 2000 2004 1999 2001 2003 1999 2000 2004 2003 ...
##  - attr(*, "na.action")= 'omit' Named int  1 3 4 6 10 16 17 23 28 30 ...
##   ..- attr(*, "names")= chr  "1" "3" "4" "6" ...

Look at the first five observations in the data set:

head(ds)
## # A tibble: 6 x 6
##   rating length   budget votes title                       year
##    <dbl>  <int>    <int> <int> <chr>                      <int>
## 1    6.7     97 16000000 19095 10 Things I Hate About You  1999
## 2    4.7    100 85000000  1987 102 Dalmatians              2000
## 3    6.4     98 37000000  7859 13 Going On 30              2004
## 4    6.1    102 85000000 14344 13th Warrior, The           1999
## 5    6.1    120 42000000 10866 15 Minutes                  2001
## 6    5.1    107 76000000  9556 2 Fast 2 Furious            2003

To make analysis easier, we can name each row by the title of the movie:

row.names(ds) <- ds$title
## Warning: Setting row names on a tibble is deprecated.

Now look at the first observations:

head(ds)
## # A tibble: 6 x 6
##   rating length   budget votes title                       year
##    <dbl>  <int>    <int> <int> <chr>                      <int>
## 1    6.7     97 16000000 19095 10 Things I Hate About You  1999
## 2    4.7    100 85000000  1987 102 Dalmatians              2000
## 3    6.4     98 37000000  7859 13 Going On 30              2004
## 4    6.1    102 85000000 14344 13th Warrior, The           1999
## 5    6.1    120 42000000 10866 15 Minutes                  2001
## 6    5.1    107 76000000  9556 2 Fast 2 Furious            2003

Explore some descriptive statistics:

describe(ds)
## Warning in describe(ds): NAs introduced by coercion
##        vars    n        mean          sd     median     trimmed
## rating    1 1359        6.17        1.16        6.3        6.22
## length    2 1359      109.26       20.54      105.0      107.12
## budget    3 1359 37038211.77 32450599.29 28000000.0 32614123.05
## votes     4 1359    11552.81    15216.95     6728.0     8413.21
## title*    5 1359     1050.00     1408.56     1050.0     1050.00
## year      6 1359     1999.99        2.76     2000.0     2000.04
##                mad    min       max       range  skew kurtosis        se
## rating        1.19    1.7         9         7.3 -0.55     0.53      0.03
## length       17.79   69.0       275       206.0  1.89     7.96      0.56
## budget 26686800.00 6000.0 200000000 199994000.0  1.36     2.15 880264.11
## votes      6594.60 1007.0    157608    156601.0  3.77    20.75    412.78
## title*     1476.67   54.0      2046      1992.0  0.00    -2.75     38.21
## year          2.97 1995.0      2005        10.0 -0.16    -1.02      0.07

We will want to start by cleaning up a couple variables. First, we can scale the budget variable by millions of dollars, scale votes by thousands, and factor the year variable:

ds %>%
  mutate(budget_1m = budget/1000000,
         votes_1k = votes/1000,
         f.year = factor(year)) -> ds

The next step should be to look at the univariate distributions. Create histograms for the length, budget, user ratings, and votes variables. First you’ll need to melt the data:

melt.ds <- melt(ds, measure.vars = c("length", "budget_1m", "rating", "votes_1k"))
ggplot(melt.ds, aes(value)) +
  geom_histogram(fill="#0000FF") +
  facet_wrap(~variable, scale="free")
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Now let’s look at the bivariate plots for the relationship between length, budget, votes, and rating. This is where we might find potential outliers. Build each visualization and then use ggplotly() to create an interactive interface that will allow you to identify individual observations.

vote <- ggplot(ds, aes(votes_1k, rating, label = title)) +
  geom_point(color = "#0000FF50") +
  geom_smooth(method = "loess", se = FALSE, color = "green") +
  geom_smooth(method = "lm", se = FALSE, color = "red") +
  ggtitle("# of Votes and Rating")
ggplotly(vote)
length <- ggplot(ds, aes(length, rating, label = title)) +
  geom_point(color = "#0000FF50") +
  geom_smooth(method = "loess", se = FALSE, color = "green") +
  geom_smooth(method = "lm", se = FALSE, color = "red") +
  ggtitle("Length and Rating")
ggplotly(length)
budget <- ggplot(ds, aes(budget_1m, rating, label = title)) +
  geom_point(color = "#0000FF50") +
  geom_smooth(method = "loess", se = FALSE, color = "green") +
  geom_smooth(method = "lm", se = FALSE, color = "red") +
  ggtitle("Budget and Rating")
ggplotly(budget)

12.2 Outliers

The next step is to construct a model:

fit1 <- lm(rating ~ length + budget_1m + votes_1k, data = ds)
stargazer(fit1, type = "text", single.row = TRUE)
## 
## ===============================================
##                         Dependent variable:    
##                     ---------------------------
##                               rating           
## -----------------------------------------------
## length                   0.016*** (0.001)      
## budget_1m                -0.010*** (0.001)     
## votes_1k                 0.033*** (0.002)      
## Constant                 4.407*** (0.147)      
## -----------------------------------------------
## Observations                   1,359           
## R2                             0.312           
## Adjusted R2                    0.311           
## Residual Std. Error      0.962 (df = 1355)     
## F Statistic          205.216*** (df = 3; 1355) 
## ===============================================
## Note:               *p<0.1; **p<0.05; ***p<0.01

Normally we are primarily interested in how this model explains our data. For this section, we are more interested in the observations that are not explained by this model. Let’s identify some outliers. First create predicted ratings based on our model:

ds$predicted_rating <- predict(fit1)

One simple way to find some possible outliers is to use the outlierTest() function:

outlierTest(fit1)
##       rstudent unadjusted p-value Bonferonni p
## 476  -4.444751       0.0000095178     0.012935
## 1353 -4.343521       0.0000150670     0.020476
## 1313 -4.212031       0.0000269810     0.036667

We see four potential outliers… let’s compare their predicted rating based on their budget, length, and number of votes, to their actual rating:

ds["From Justin to Kelly",c("rating", "predicted_rating")]
## # A tibble: 1 x 2
##   rating predicted_rating
##    <dbl>            <dbl>
## 1     NA               NA
ds["You Got Served",c("rating", "predicted_rating")]
## # A tibble: 1 x 2
##   rating predicted_rating
##    <dbl>            <dbl>
## 1     NA               NA
ds["Werewolf",c("rating", "predicted_rating")]
## # A tibble: 1 x 2
##   rating predicted_rating
##    <dbl>            <dbl>
## 1     NA               NA
ds["Glitter",c("rating", "predicted_rating")]
## # A tibble: 1 x 2
##   rating predicted_rating
##    <dbl>            <dbl>
## 1     NA               NA

We can see that there is a large discrepancy between these movies’ ratings and their predicted ratings.

There are a variety of ways to visually inspect outliers. Let’s start with an influence plot:

influencePlot(fit1)

##        StudRes         Hat       CookD
## 476  -4.444751 0.001567730 0.007649222
## 742  -3.499031 0.071956182 0.235367503
## 782  -2.391247 0.062564978 0.095075700
## 1091 -1.506006 0.065411451 0.039647906
## 1353 -4.343521 0.001451519 0.006766880

An influence plot shows the residuals by their hat-values. This identifies the Matrix and the first Lord of the Rings as potential outliers, so let’s compare their ratings to predicted ratings:

ds["Lord of the Rings: The Fellowship of the Ring, The",c("rating", "predicted_rating")]
## # A tibble: 1 x 2
##   rating predicted_rating
##    <dbl>            <dbl>
## 1     NA               NA
ds["Matrix, The",c("rating", "predicted_rating")]
## # A tibble: 1 x 2
##   rating predicted_rating
##    <dbl>            <dbl>
## 1     NA               NA

These movies are flagged outliers for a different reason than the movies we found earlier. These two movies are rated high, but their predicted ratings are even higher (above the maximum possible value of 10).

Another way to examine outliers is to look at the DFBetas. DFBetas measure the influence of case i on the j estimated coefficients. Put another way, measuring DFBetas asks how many standard errors a particular beta changes when case i is removed. The rule of thumb is if the absolute value of a DFBETA is greater than 2 divided by the square root of n, there could be cause for concern. Let’s calculate that value:

df <- 2/sqrt(1261)
df
## [1] 0.05632127

We can find the DFBetas easily:

dfbs.fit1 <- dfbetas(fit1)
head(dfbs.fit1)
##    (Intercept)       length     budget_1m      votes_1k
## 1  0.006806296 -0.005084189 -0.0050276250  0.0070121773
## 2 -0.008979320  0.009910553 -0.0275525338  0.0130756825
## 3  0.010690935 -0.008032946  0.0032307871 -0.0011083722
## 4  0.009230482 -0.010909459  0.0210387730 -0.0003584795
## 5  0.001794457 -0.002610743 -0.0001487667  0.0012840947
## 6 -0.005795187  0.006463715 -0.0220719600  0.0064232528

With a large data set, listing every DFBeta value is not efficient. Instead, we should plot the DFBetas with lines at the calculated value. We will use the identify() function to mark specific observations. Each of the coefficients will have its own plot. Note: The windows() (or quartz()) function does not knit with RMarkdown; however, the function is used to build the plot on the screen as you follow along within the RMD document.

windows()
plot(dfbs.fit1[,"length"])
abline(h = c(2/sqrt(1261), -2/sqrt(1261)), lty = 2, col = "red")

identify(dfbs.fit1[,"length"],labels = ds$title)
## Error in identify.default(dfbs.fit1[, "length"], labels = ds$title): graphics device closed during call to locator or identify
windows()
plot(dfbs.fit1[,"budget_1m"])
abline(h = c(2/sqrt(1261), -2/sqrt(1261)), lty = 2, col = "red")

identify(dfbs.fit1[,"budget_1m"],labels = ds$title)
## Error in identify.default(dfbs.fit1[, "budget_1m"], labels = ds$title): graphics device closed during call to locator or identify
windows()
plot(dfbs.fit1[,"votes_1k"])
abline(h = c(2/sqrt(1261), -2/sqrt(1261)), lty = 2, col = "red")

identify(dfbs.fit1[,"votes_1k"],labels = ds$title)
## Error in identify.default(dfbs.fit1[, "votes_1k"], labels = ds$title): graphics device closed during call to locator or identify

All of the diagnostics so far indicate that there are outliers to address. There are a few ways to deal with this: First, you can keep them in the model. This is a perfectly viable method, especially if you don’t have a technical or theoretical reason to remove them. Another method of dealing with outliers is to omit them and re-run the model. Let’s look at the outliers identified by the outlier test again:

outlierTest(fit1)
##       rstudent unadjusted p-value Bonferonni p
## 476  -4.444751       0.0000095178     0.012935
## 1353 -4.343521       0.0000150670     0.020476
## 1313 -4.212031       0.0000269810     0.036667

Omit these using the following operator. Essentially this tells R not to include the rows with the titles of the outlier movies.

ds.omit <- ds[ !(ds$title %in% c("From Justin to Kelly", "You Got Served", "Werewolf", "Glitter")),]

Next make a new model with the ds.omit data:

fit.omit <- lm(rating ~ length + budget_1m + votes_1k, data = ds.omit)

Compare the two models side by side:

stargazer(fit1, fit.omit, type = "text", single.row = TRUE)
## 
## =======================================================================
##                                     Dependent variable:                
##                     ---------------------------------------------------
##                                           rating                       
##                                (1)                       (2)           
## -----------------------------------------------------------------------
## length                  0.016*** (0.001)          0.016*** (0.001)     
## budget_1m               -0.010*** (0.001)         -0.010*** (0.001)    
## votes_1k                0.033*** (0.002)          0.033*** (0.002)     
## Constant                4.407*** (0.147)          4.450*** (0.143)     
## -----------------------------------------------------------------------
## Observations                  1,359                     1,355          
## R2                            0.312                     0.322          
## Adjusted R2                   0.311                     0.321          
## Residual Std. Error     0.962 (df = 1355)         0.937 (df = 1351)    
## F Statistic         205.216*** (df = 3; 1355) 214.265*** (df = 3; 1351)
## =======================================================================
## Note:                                       *p<0.1; **p<0.05; ***p<0.01

Notice the minimal changes. The omitted observations changed 3 of the four coefficients, and increased the adjusted R squared value.

Another option when dealing with outliers is to use robust regression, which weights the observations based on influence. Make a new model using robust regression using the rlm() function. There are two methods, “M” and “MM”, and both should be evaluated to determine which model best represents your needs.

fit.m <- rlm(rating ~ length + budget_1m + votes_1k, data = ds, method = "M")
fit.mm <- rlm(rating ~ length + budget_1m + votes_1k, data = ds, method = "MM")

Compare the four models:

stargazer(fit1, fit.omit, fit.m, fit.mm, type = "text",single.row = TRUE)
## 
## ===========================================================================================================
##                                                       Dependent variable:                                  
##                     ---------------------------------------------------------------------------------------
##                                                             rating                                         
##                                             OLS                                       robust               
##                                                                                       linear               
##                                (1)                       (2)                   (3)               (4)       
## -----------------------------------------------------------------------------------------------------------
## length                  0.016*** (0.001)          0.016*** (0.001)      0.017*** (0.001)  0.016*** (0.001) 
## budget_1m               -0.010*** (0.001)         -0.010*** (0.001)     -0.011*** (0.001) -0.012*** (0.001)
## votes_1k                0.033*** (0.002)          0.033*** (0.002)      0.035*** (0.002)  0.036*** (0.002) 
## Constant                4.407*** (0.147)          4.450*** (0.143)      4.432*** (0.138)  4.472*** (0.138) 
## -----------------------------------------------------------------------------------------------------------
## Observations                  1,359                     1,355                 1,359             1,359      
## R2                            0.312                     0.322                                              
## Adjusted R2                   0.311                     0.321                                              
## Residual Std. Error     0.962 (df = 1355)         0.937 (df = 1351)     0.864 (df = 1355) 0.836 (df = 1355)
## F Statistic         205.216*** (df = 3; 1355) 214.265*** (df = 3; 1351)                                    
## ===========================================================================================================
## Note:                                                                           *p<0.1; **p<0.05; ***p<0.01

The biggest difference here is the residual standard error for the robust models is quite a bit lower. There are also differences in the coefficients. With outliers, there is not a one-size-fits-all solution. Let your theory contribute to what solution you use.

12.3 Heteroscedasticity

One of the key assumptions of OLS is homoscedasticity (constant error variance). One way to check for this is by making a spread level plot, which allows us to see the spread of the residuals:

spreadLevelPlot(fit1)

## 
## Suggested power transformation:  1.771347

There does not appear to be constant spread of residuals, which could indicate a problem with heteroscedasticity. We can further investigate this by doing a Non-constant Variance Test. This tests the null hypothesis that error variance changes (heteroscedasticity). That is, if you fail to reject the null there exists heteroscedasticity:

ncvTest(fit1)
## Non-constant Variance Score Test 
## Variance formula: ~ fitted.values 
## Chisquare = 3.205979    Df = 1     p = 0.0733696

Based on the test and visualization, it is clear there is an issue with heteroscedasticity. There are a couple ways to deal with heteroscedasticity. One method is robust standard errors. Robust standard errors don’t change the beta estimates, but rather affect the value of the standard errors, which improve the p-values accuracy. To use robust standard errors for the model:

se.fit1 <- fit1 %>% vcov() %>% diag() %>% sqrt()
vcov.fit1 <- vcovHC(fit1, method = "white1",type = "HC1")
rse.fit1 <- vcov.fit1 %>% diag() %>% sqrt()

Now compare the original model to the model using robust standard errors. Use se=list(se.fit1,rse.fit1) for R to use the original standard errors for the first model and robust for the second.

stargazer(fit1, fit1, type = "text", single.row = TRUE, se = list(se.fit1, rse.fit1))
## 
## ===================================================================
##                                         Dependent variable:        
##                                 -----------------------------------
##                                               rating               
##                                        (1)               (2)       
## -------------------------------------------------------------------
## length                          0.016*** (0.001)  0.016*** (0.002) 
## budget_1m                       -0.010*** (0.001) -0.010*** (0.001)
## votes_1k                        0.033*** (0.002)  0.033*** (0.003) 
## Constant                        4.407*** (0.147)  4.407*** (0.168) 
## -------------------------------------------------------------------
## Observations                          1,359             1,359      
## R2                                    0.312             0.312      
## Adjusted R2                           0.311             0.311      
## Residual Std. Error (df = 1355)       0.962             0.962      
## F Statistic (df = 3; 1355)         205.216***        205.216***    
## ===================================================================
## Note:                                   *p<0.1; **p<0.05; ***p<0.01

12.4 Revisiting Linearity

Let’s revisit addressing the assumption of linearity by constructing the residual plots that we made in the last lab. Recall that these are made by using augment() to predict values and calculate residuals, melt the data into long form and identify the independent variables and fitted values are your measure variables, then pipe it all into ggplot2 and use facet_wrap() to create a visualization for each variable.

fit1 %>%
  augment() %>%
  melt(measure.vars = c("length", "budget_1m", "votes_1k", ".fitted")) %>%
  ggplot(., aes(value, .std.resid)) +
  geom_point(shape=1) +
  geom_smooth(method = loess) +
  geom_hline(yintercept = 0) +
  facet_wrap(~variable, scales = "free")

There appears to be a linearity problem. The budget graphics appears to be the most linear, and the others suggest non-linear relationships. Let’s examine some more information about the variables:

describe(ds$length)
##    vars    n   mean    sd median trimmed   mad min max range skew kurtosis
## X1    1 1359 109.26 20.54    105  107.12 17.79  69 275   206 1.89     7.96
##      se
## X1 0.56
describe(ds$budget_1m)
##    vars    n  mean    sd median trimmed   mad  min max  range skew
## X1    1 1359 37.04 32.45     28   32.61 26.69 0.01 200 199.99 1.36
##    kurtosis   se
## X1     2.15 0.88
describe(ds$votes_1k)
##    vars    n  mean    sd median trimmed  mad  min    max range skew
## X1    1 1359 11.55 15.22   6.73    8.41 6.59 1.01 157.61 156.6 3.77
##    kurtosis   se
## X1    20.75 0.41

There is skew for all three variables, so let’s respecify the model by using the log of each variable, then create the same visualization as before:

fit.log <- lm(rating ~ log(length) + log(budget_1m) + log(votes_1k), data = ds)
fit.log %>%
  augment() %>% 
  melt(measure.vars = c("log.length.", "log.budget_1m.", "log.votes_1k.", ".fitted")) %>%
ggplot(., aes(value, .std.resid)) +
  geom_point(shape = 1) +
  geom_smooth(aes(value, .std.resid), method = "loess") +
  geom_hline(yintercept = 0) +
  facet_wrap(~variable, scales = "free")

This method fixed some problems and created new ones. The votes graphic suggests a more linear relationship, but problems persist. Perhaps a polynomial model is more appropriate. Let’s square every IV in the next model:

fit.poly <- lm(rating ~ poly(length, 2) + poly(budget_1m, 2) + poly(votes_1k, 2), data = ds)

Compare the last three models:

stargazer(fit1, fit.log, fit.poly, single.row = TRUE, type = "text")
## 
## =================================================================================================
##                                                  Dependent variable:                             
##                     -----------------------------------------------------------------------------
##                                                        rating                                    
##                                (1)                       (2)                       (3)           
## -------------------------------------------------------------------------------------------------
## length                  0.016*** (0.001)                                                         
## budget_1m               -0.010*** (0.001)                                                        
## votes_1k                0.033*** (0.002)                                                         
## log(length)                                       2.235*** (0.155)                               
## log(budget_1m)                                    -0.390*** (0.020)                              
## log(votes_1k)                                     0.551*** (0.026)                               
## poly(length, 2)1                                                            12.399*** (1.028)    
## poly(length, 2)2                                                            -3.986*** (0.945)    
## poly(budget_1m, 2)1                                                        -14.445*** (1.021)    
## poly(budget_1m, 2)2                                                         5.735*** (0.917)     
## poly(votes_1k, 2)1                                                          19.283*** (1.025)    
## poly(votes_1k, 2)2                                                          -8.690*** (0.962)    
## Constant                4.407*** (0.147)          -4.132*** (0.703)         6.165*** (0.025)     
## -------------------------------------------------------------------------------------------------
## Observations                  1,359                     1,359                     1,359          
## R2                            0.312                     0.411                     0.383          
## Adjusted R2                   0.311                     0.410                     0.381          
## Residual Std. Error     0.962 (df = 1355)         0.890 (df = 1355)         0.912 (df = 1352)    
## F Statistic         205.216*** (df = 3; 1355) 315.495*** (df = 3; 1355) 140.073*** (df = 6; 1352)
## =================================================================================================
## Note:                                                                 *p<0.1; **p<0.05; ***p<0.01

The log model has the highest adjusted R squared and lowest residual standard error.

12.4.1 Normality

Let’s look at the normality of the residuals for the models:

ds %>%
  mutate(fit1.r = residuals(fit1)) ->ds

ggplot(ds,aes(fit1.r)) +
  geom_density() +
  stat_function(fun = dnorm, args = list(mean = mean(ds$fit1.r), 
                                         sd = sd(ds$fit1.r)), color = "red") +
  ggtitle("First Linear Model")

ds %>%
  mutate(log.r = residuals(fit.log)) -> ds
ggplot(ds, aes(log.r)) +
  geom_density() +
  stat_function(fun = dnorm, args = list(mean = mean(ds$log.r),
                                         sd = sd(ds$log.r)), color = "red") +
  ggtitle("Log Linear Model")

ds %>%
  mutate(poly.r = residuals(fit.poly)) -> ds

ggplot(ds, aes(poly.r)) +
  geom_density() +
  stat_function(fun = dnorm, args = list(mean = mean(ds$poly.r),
                                         sd = sd(ds$poly.r)), color="red") +
  ggtitle("Polynomial Model")

The log model has the highest adjusted R squared value, the lowest residual standard error, and its residuals appear to approximate the normal distribution better than the other two models. Let’s use it to make predictions and create visualizations.

First create predicted values for movie ratings by holding all IVs constant at their means except one at at time, using the augment() function. Then use mutate() to calculate the upper and lower bounds of the confidence interval. Create separate data frames for length, budget, and votes.

df.length <- fit.log %>%
  augment(newdata = data.frame(length = 89:134,
                               budget_1m = mean(ds$budget_1m),
                               votes_1k = mean(ds$votes_1k))) %>%
  mutate(upper = .fitted + 1.96 * .se.fit,
         lower = .fitted - 1.96 * .se.fit)

df.budget <- fit.log %>% 
  augment(newdata = data.frame(length = mean(ds$length),
                               budget_1m = 5:80,
                               votes_1k = mean(ds$votes_1k))) %>%
  mutate(upper = .fitted + 1.96 * .se.fit,
         lower = .fitted - 1.96 * .se.fit)

df.votes <- fit.log %>% 
  augment(newdata = data.frame(length = mean(ds$length),
                                        budget_1m = mean(ds$budget_1m),
                                        votes_1k = 1.645:25.964)) %>%
  mutate(upper = .fitted + 1.96 * .se.fit,
         lower = .fitted - 1.96 * .se.fit)

Now make the visualization for each data frame~

ggplot(df.length, aes(length, .fitted)) +
  geom_line(size = 1, color = "royalblue") +
  geom_ribbon(aes(ymin = lower, ymax = upper), alpha = .2) +
  coord_cartesian(ylim = c(4:8), xlim = c(89:134)) +
  ggtitle("Movie Length and Rating") +
  xlab("Length") +
  ylab("Rating") +
  theme_bw()

Now do the same for thse next two IVs:

ggplot(df.votes, aes(votes_1k, .fitted)) +
  geom_line(size = 1, color = "royalblue") +
  geom_ribbon(aes(ymin = lower, ymax = upper), alpha = .2) +
  ggtitle("IMDB Votes and Rating") +
  xlab("Votes (Thousands)") +
  ylab("Rating") +
  theme_bw()

ggplot(df.budget, aes(budget_1m, .fitted)) +
  geom_line(size = 1, color = "royalblue") +
  geom_ribbon(aes(ymin = lower, ymax = upper), alpha = .2) +
  ggtitle("Movie Budget and Rating") +
  xlab("Budget (Millions)") +
  ylab("Rating") +
  theme_bw()