More Introduction to R: ggplot, importing and working with data

Author

Yurk

Introduction

Getting started

You should create a new RStudio/Quarto project that will house all of your code from the labs and applied problem sets. There are instructions for creating a new RStudio project in this tutorial. You should have worked through this tutorial already, so you should have a Projects directory on your computer, and it should already have a subdirectory for the project you created for the tutorial. Use the Files tab in RStudio to navigate to your Projects directory and follow the instructions from the tutorial to create a new project. Name your project MATH313_labs_APs or something similar.

Structure your project by creating code, raw_data, clean_data, and outputs subdirectories within the MATH313_labs_APs project directory you just created. This process is also described in the tutorial.

You will create a new Quarto document for each lab and applied problem set in this course. These documents should be saved in the code directory. Follow the instructions in the tutorial to create a Quarto document for this lab. Make sure you follow the instructions to set your document up so that a standalone html file is generated when you render your document. Give your document a descriptive title. For example, you may use the same title as I used for the document you are reading right now: More Introduction to R: ggplot2, importing and working with data.

Guidance and requirements for working through labs

The lab documents are themselves rendered Quarto documents. They include code blocks and the output produced by running the code blocks, along with explanatory text.

The minimum requirements for your Quarto document for any particular lab include:

  • Informative title
  • Author (your name)
  • Date
  • All code chunks from the lab (in the same order as the lab)
  • Structure your document using headings and subheadings (see this tutorial). An easy way to do this is to use the same headings and subheadings as in the lab document.

Run each block of code as you enter it, and make sure that the output is consistent with what you see in the lab document. At this point it is VERY important that you think about and understand what each line of code is doing. Investigate the things you do not understand by using the help features in R (e.g., entering ?function_name at the console to learn about a function), referring to previous labs and tutorials, reading the references suggested on the Moodle page, searching for answers on the web, asking other students in the class, or asking your instructor. You will learn nothing from a lab if you just enter the code, run it, and don’t think about what you are doing.

I strongly recommend including some explanatory text in your own words for each code chunk. This is especially useful when you encounter new commands or code that you need to investigate further to understand. Writing an explanation helps you to identify holes in your understanding, while helping you to retain what you learned. Your explanatory text can also help you later when you refer back to your labs as you write new R code.

I also recommend that you explore the code and the objects you are working with (data sets, etc.), by experimenting in the R console. Use glimpse and View to explore your data sets. Use class and str to explore the class and structure of an object. Create additional plots and summaries to help you understand the data and analyses more deeply. You may include some of this code in your Quarto document if you find it particularly informative.

Finally, once you have completed entering, exploring, and running the code for a particular lab and have written any explanatory text, you should render your document to create a report for the lab. You should render to HTML (not a Word document or PDF).

For each lab and applied problem set you will submit your Quarto file (with a .qmd extension) and your HTML report (with a .html extension). It is important that you structure your project exactly as indicated in this tutorial. This will allow me to put your Quarto file in my code directory and run it to replicate your results/report, since the structure of your project will match mine exactly.

New R topics

Functions and pipes (%>%)

R computations are performed using functions. You have already seen some examples of functions, including c, mean, sd, seq, and plot, in this tutorial. As another example, we can create a vector of random numbers drawn from a normal distribution using the function rnorm.

rnorm(50)
 [1]  0.037654250  0.325608543 -0.493369886 -1.323113223  0.357683706
 [6]  0.336067177  0.178315519  0.135794230 -0.494168295 -1.338718642
[11]  1.388787652  1.003434905  0.356491026 -1.005065132  0.810836591
[16] -0.974694133  0.962346718 -0.030776484  0.013036186 -1.139660017
[21]  0.547437102  0.553965113  0.005462344 -0.887030124  1.094200495
[26]  1.321168376 -2.220543307  2.547769052 -0.479948847  1.149976208
[31]  0.571087488  0.666285616  0.341443939 -0.146321517 -0.721819759
[36] -1.264039760 -0.808537890 -0.309727218  1.010118366 -0.705120053
[41] -0.767622204 -1.308154590 -0.062829862 -2.074195619 -1.060541057
[46]  0.248530215  0.839668206  0.264750794 -1.686495580 -0.457400382

If we type ?rnorm into the console, we see the line rnorm(n, mean = 0, sd = 1) listed under the heading Usage (try it). This gives us information about how we can use the command. The rnorm function can take 3 arguments (n, mean, and sd). The mean and sd arguments have default values (0 and 1). This means that we do not have to provide values for these arguments–if we do not, then R will just use the default values. The n argument does not have a default value, so we need to provide one. In the code above, we called the rnorm function with a single argument (50). Since R undersands that the rnorm function only requires one argument, it assumes that the value we provided (50) is for that argument. Thus, R assumes that we want to set n=50.

If we read about the meaning of the Arguments in the help document for rnorm we learn that n is the number of observations that the function will return. The values of the mean and sd arguments specify the mean and standard deviation of the normal distribution that we are drawing the values from. Hence, when we ran the command rnorm(50) R created a vector of 50 random numbers drawn from a normal distribution with mean 0 and standard deviation 1.

When you ran the previous code block you almost certainly got a different vector of random numbers than I did. We can ensure that we get the same set of random numbers if we seed the random number generator using the set.seed function. This function can be called with a single argument (an integer). If you use the same number as I do, you will get the same random results as I do. Seeding the random number generator makes it possible for others to repeat your analysis and get the same results.

Let’s seed the random number generator and create another set of random numbers. This time we will the numbers in a vector called x.

set.seed(8675309)
x <- rnorm(50)
x
 [1] -0.996582353  0.721824146 -0.617208815  2.029391575  1.065416053
 [6]  0.987219739  0.027453925  0.672872319  0.572066502  0.903677699
[11] -1.549552389  1.022637819  0.150083169 -0.659963960 -0.994589034
[16]  1.972458679 -0.441801643 -0.900637210 -0.150588198 -0.827894215
[21]  1.985825823  0.044005033 -0.404282312 -0.472998550 -0.414823240
[26]  0.683234228  0.690201104  0.533491920 -0.186104764  0.382945826
[31]  0.376184171  1.153530020  1.574902775  0.588527289 -0.615045184
[36] -0.228911177  0.063917036 -0.313473710 -0.248499403 -0.153795217
[41] -0.635256978 -0.003794307  0.045615260  0.684072008 -0.252042521
[46] -0.151038534 -0.874931437 -1.976901402  0.237663205  0.036090905

Let’s create another random vector. This time we will use non-default values for the mean and sd arguments. You can see that these numbers are centered around 50 instead of 0 and vary less than the elements of x.

z <- rnorm(50, mean = 50, sd = 0.1)
z
 [1] 50.01349 50.01176 49.91744 49.78648 50.02142 49.90529 49.93484 50.07287
 [9] 50.10498 50.01020 50.15970 50.05641 49.98446 49.88245 49.98579 50.09008
[17] 50.03582 49.90815 49.86780 50.02278 49.96521 50.13007 49.97754 49.74031
[25] 49.90783 50.14688 49.90605 49.91491 50.16609 49.86425 50.00860 50.12516
[33] 50.06101 50.00204 50.07225 49.88916 50.07698 50.10271 50.00729 49.86855
[41] 50.01947 50.05916 49.90740 49.92126 50.05868 50.05396 50.10870 50.01310
[49] 49.96068 50.20207

Let’s create one more vector by adding x and z. Recall that we can add two vectors that have the same length. We can use the length function to check the lengths of x and z before we add them.

length(x)
[1] 50
length(z)
[1] 50
y <- x + z
y
 [1] 49.01691 50.73358 49.30023 51.81587 51.08684 50.89251 49.96229 50.74574
 [9] 50.67704 50.91387 48.61015 51.07905 50.13455 49.22249 48.99120 52.06254
[17] 49.59402 49.00751 49.71721 49.19489 51.95104 50.17408 49.57325 49.26731
[25] 49.49300 50.83011 50.59626 50.44841 49.97999 50.24719 50.38478 51.27869
[33] 51.63591 50.59057 49.45721 49.66025 50.14090 49.78924 49.75879 49.71476
[41] 49.38422 50.05536 49.95301 50.60533 49.80664 49.90292 49.23377 48.03620
[49] 50.19834 50.23816

Somewhat recently, pipes have been introduced as a method for passing arguments to functions in R. These were introduced in the magrittr package, which is part of the tidyverse metapackage. In order to use a magrittr pipe, we need to load the package. We can do this by loading the entire tidyverse metapackage.

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.3     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.0
✔ ggplot2   3.4.3     ✔ tibble    3.2.1
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

The magrittr pipe uses the operator %>% which pipes whatever obect is on the left side of the pipe into the first argument of a function that is on the right side of the pipe. For example, here are two equivalent ways to find the mean of the elements of the vector x, one of which uses a pipe.

mean(x)
[1] 0.1026918
x %>% mean()
[1] 0.1026918

In the seecond case, we take the vector x and pipe it into the mean function. We can use the pipe with functions that have multiple arguments, too. The following are equivalent, because in the second case 8 is piped into the first argument of the function.

log(8, base = 2)
[1] 3
8 %>% log(base = 2)
[1] 3

We can also pipe values into other arguments of a function, but we need to use a period . to tell R where to pipe the value. This is another way to compute the same logarithm we just computed. The period indicates thag we want to pipe the 2 into the base argument.

2 %>% log(8, base = .)
[1] 3

Using pipes may seem like a boring and unnecessary way to complicate your R code, but it often results in more readable code that emphasizes the flow of data through different operations.

Pipes can also be chained together to create a pipeline that performs multiple successive operations. It is good practice to use a new line for each operation in the pipeline. If you end a line with a pipe, R realizes that you must want to continue the computation (you wouldn’t pipe into nothing, right), so it continues the computation on the next line.

The following pipeline will compute the geometric mean of the vector y (note that this would not work if y had non-positive values). First the (natural) logs of all of the elements of y are computed. Then the arithmetic mean of the resulting vector is computed. Finally the result is exponentiated.

y_gmean <- y %>%
  log() %>%
  mean() %>%
  exp()

y_gmean
[1] 50.09565

Data frames and tibbles

A data frame is the most common way to structure data in R. A tibble is a type of data frame. The primary difference between a tibble and a data frame is how R prints the data. You will encounter both tibbles and data frames as you work with R. I recommend converting any data frame to a tibble to take advantage of its better printing behavior.

Let’s create a very simple data frame, using the data.frame function. Note that the arguments that we provide the data.frame function have the form name = vector, so that a and b become the column names, and the data in the corresponding columns are defined by the vectors. Note that all columns must be the same length in a data frame (or tibble).

df <- data.frame(a = c(1, 2, 3), b = c(9, 8, 7))
df
  a b
1 1 9
2 2 8
3 3 7

We can create a tibble in a similar manner. The tibble and data frame are very similar, but they will be printed differently in the console or in your knitted document. Try printing both in the console to see the difference.

tib <- tibble(a = c(1, 2, 3), b = c(9, 8, 7))
tib
# A tibble: 3 × 2
      a     b
  <dbl> <dbl>
1     1     9
2     2     8
3     3     7

We can easily convert a data frame to a tibble using the as_tibble function. Here is another way to create the same tibble.

tib <- df %>%
  as_tibble()
tib
# A tibble: 3 × 2
      a     b
  <dbl> <dbl>
1     1     9
2     2     8
3     3     7

Next, let’s construct a data frame that has a column for each of the vectors we have created so far. This code is somewhat confusing, because we are using a name for each column that matches the name of the vector. However, the same pattern is followed as before–the name is on the left and the vector is on the right.

data <- tibble(x = x, y = y, z = z)
data
# A tibble: 50 × 3
         x     y     z
     <dbl> <dbl> <dbl>
 1 -0.997   49.0  50.0
 2  0.722   50.7  50.0
 3 -0.617   49.3  49.9
 4  2.03    51.8  49.8
 5  1.07    51.1  50.0
 6  0.987   50.9  49.9
 7  0.0275  50.0  49.9
 8  0.673   50.7  50.1
 9  0.572   50.7  50.1
10  0.904   50.9  50.0
# ℹ 40 more rows

We can determine the number of rows and the number of columns in a tibble (its dimensions) usin the dim function. Our tibble has 50 rows and 3 columns.

data %>% dim()
[1] 50  3

We can also use the nrow and ncol functions to count rows and columns independently.

data %>% nrow()
[1] 50
data %>% ncol()
[1] 3

Most of the operations that we perform in R will involve tibbles that contain data that we are interested in analyzing. Now we will see a simple example of an analysis of the data that we created.

Correlation

The tibble we just created includes three variables (columns). Are the columns correlated? They should be, given how we created them. Which variables do you expect to be correlated?

We can use the correlate function in the corrr package to find out. The function calculates the (Pearson) correlation coefficient (r) for each pair of variables. The results are stored in a tibble. We can see that \(x\) and \(y\) are highly correlated, but the other pairs of variables are not. Is this what you expected? By default the function reports the correlation of a variable with itself as having the value NA, which stands for “Not Applicable”, because such a computation is not interesting (the result is always 1).

library(corrr)
cor_xyz <- data %>%
  correlate()
Correlation computed with
• Method: 'pearson'
• Missing treated using: 'pairwise.complete.obs'
cor_xyz
# A tibble: 3 × 4
  term        x       y       z
  <chr>   <dbl>   <dbl>   <dbl>
1 x     NA       0.993  -0.0434
2 y      0.993  NA       0.0728
3 z     -0.0434  0.0728 NA     

The corrr package also includes a function rplot for visualizing correlation data. colors is one of the arguments that can be specified in this function. Here we use a vector of three colors that define a gradient, that is used to color the points in the plot. It is important to choose color-blind friendly colors for your plots. There is some information related to this on our Moodle page.

From the plot, we can clearly see that x and y are correlated.

cor_xyz %>% rplot(colors = c("darkkhaki", "black", "skyblue1"))

Extracting information from tibbles

If we have data stored in a tibble, we can extract a single column as a vector using the pull function or the pluck function. The pull function is from the dplyr package, and pluck is from the purrr package, both of which were loaded when we loaded the tidyverse packages earlier.

xvec <- data %>% pull(x)
xvec
 [1] -0.996582353  0.721824146 -0.617208815  2.029391575  1.065416053
 [6]  0.987219739  0.027453925  0.672872319  0.572066502  0.903677699
[11] -1.549552389  1.022637819  0.150083169 -0.659963960 -0.994589034
[16]  1.972458679 -0.441801643 -0.900637210 -0.150588198 -0.827894215
[21]  1.985825823  0.044005033 -0.404282312 -0.472998550 -0.414823240
[26]  0.683234228  0.690201104  0.533491920 -0.186104764  0.382945826
[31]  0.376184171  1.153530020  1.574902775  0.588527289 -0.615045184
[36] -0.228911177  0.063917036 -0.313473710 -0.248499403 -0.153795217
[41] -0.635256978 -0.003794307  0.045615260  0.684072008 -0.252042521
[46] -0.151038534 -0.874931437 -1.976901402  0.237663205  0.036090905
xvec <- data %>% pluck("x")
xvec
 [1] -0.996582353  0.721824146 -0.617208815  2.029391575  1.065416053
 [6]  0.987219739  0.027453925  0.672872319  0.572066502  0.903677699
[11] -1.549552389  1.022637819  0.150083169 -0.659963960 -0.994589034
[16]  1.972458679 -0.441801643 -0.900637210 -0.150588198 -0.827894215
[21]  1.985825823  0.044005033 -0.404282312 -0.472998550 -0.414823240
[26]  0.683234228  0.690201104  0.533491920 -0.186104764  0.382945826
[31]  0.376184171  1.153530020  1.574902775  0.588527289 -0.615045184
[36] -0.228911177  0.063917036 -0.313473710 -0.248499403 -0.153795217
[41] -0.635256978 -0.003794307  0.045615260  0.684072008 -0.252042521
[46] -0.151038534 -0.874931437 -1.976901402  0.237663205  0.036090905

Often we want to extract information from a tibble and return the results as a new tibble. We can use the select function from dplyr to select columns by name that we want to keep. Note that the following code does not change the tibble data, because we have not saved the results with the name data using the assignment operator <-. Thus, we have not overwritten data with the output of the operation. Instead the new tibble is just printed and not saved.

data %>% select(x, y)
# A tibble: 50 × 2
         x     y
     <dbl> <dbl>
 1 -0.997   49.0
 2  0.722   50.7
 3 -0.617   49.3
 4  2.03    51.8
 5  1.07    51.1
 6  0.987   50.9
 7  0.0275  50.0
 8  0.673   50.7
 9  0.572   50.7
10  0.904   50.9
# ℹ 40 more rows

We can also use select to drop specific columns from our tibble, indicating that we want to drop the column putting a - sign in front of it.

data %>%
  select(-x)
# A tibble: 50 × 2
       y     z
   <dbl> <dbl>
 1  49.0  50.0
 2  50.7  50.0
 3  49.3  49.9
 4  51.8  49.8
 5  51.1  50.0
 6  50.9  49.9
 7  50.0  49.9
 8  50.7  50.1
 9  50.7  50.1
10  50.9  50.0
# ℹ 40 more rows

We can extract certain rows using the filter function from dplyr. Suppose we wanted to keep only the rows in the data set for which the value of y is at least 51. We could use the following code:

data %>% filter(y >= 51)
# A tibble: 7 × 3
      x     y     z
  <dbl> <dbl> <dbl>
1  2.03  51.8  49.8
2  1.07  51.1  50.0
3  1.02  51.1  50.1
4  1.97  52.1  50.1
5  1.99  52.0  50.0
6  1.15  51.3  50.1
7  1.57  51.6  50.1

We can also extract columns and rows from a tibble using numerical indices. We can extract the first two columns using the following code. Note that numerical indexing is done using square brackets [ ] and has the form [row range, column range]. In this case we have not specified a row range, so R returns all of the rows.

data[,1:2]
# A tibble: 50 × 2
         x     y
     <dbl> <dbl>
 1 -0.997   49.0
 2  0.722   50.7
 3 -0.617   49.3
 4  2.03    51.8
 5  1.07    51.1
 6  0.987   50.9
 7  0.0275  50.0
 8  0.673   50.7
 9  0.572   50.7
10  0.904   50.9
# ℹ 40 more rows

We can extract the 7th through 11th row using the following code:

data[7:11,]
# A tibble: 5 × 3
        x     y     z
    <dbl> <dbl> <dbl>
1  0.0275  50.0  49.9
2  0.673   50.7  50.1
3  0.572   50.7  50.1
4  0.904   50.9  50.0
5 -1.55    48.6  50.2

We can combine these to return the first two columns for rows 7-11.

data[7:11,1:2]
# A tibble: 5 × 2
        x     y
    <dbl> <dbl>
1  0.0275  50.0
2  0.673   50.7
3  0.572   50.7
4  0.904   50.9
5 -1.55    48.6

We can also use a pipe to extract data using numerical indexing (I do this sometimes in my code). We need to use a period to tell R where to pipe the tibble (right before the square brackets).

data %>% .[7:11,1:2]
# A tibble: 5 × 2
        x     y
    <dbl> <dbl>
1  0.0275  50.0
2  0.673   50.7
3  0.572   50.7
4  0.904   50.9
5 -1.55    48.6

Create columns with mutate

Often we will want to add a new column to our tibble by performing an operation on existing columns. We can achieve this using the mutate command from dplyr. We can add a column named w to our data so that the elements of w are the squares of the elements of x.

data <- data %>% mutate(w = x^2)
data
# A tibble: 50 × 4
         x     y     z        w
     <dbl> <dbl> <dbl>    <dbl>
 1 -0.997   49.0  50.0 0.993   
 2  0.722   50.7  50.0 0.521   
 3 -0.617   49.3  49.9 0.381   
 4  2.03    51.8  49.8 4.12    
 5  1.07    51.1  50.0 1.14    
 6  0.987   50.9  49.9 0.975   
 7  0.0275  50.0  49.9 0.000754
 8  0.673   50.7  50.1 0.453   
 9  0.572   50.7  50.1 0.327   
10  0.904   50.9  50.0 0.817   
# ℹ 40 more rows

We can also use multiple columns to create a new column.

data <- data %>%
  mutate(u = x + z)
data
# A tibble: 50 × 5
         x     y     z        w     u
     <dbl> <dbl> <dbl>    <dbl> <dbl>
 1 -0.997   49.0  50.0 0.993     49.0
 2  0.722   50.7  50.0 0.521     50.7
 3 -0.617   49.3  49.9 0.381     49.3
 4  2.03    51.8  49.8 4.12      51.8
 5  1.07    51.1  50.0 1.14      51.1
 6  0.987   50.9  49.9 0.975     50.9
 7  0.0275  50.0  49.9 0.000754  50.0
 8  0.673   50.7  50.1 0.453     50.7
 9  0.572   50.7  50.1 0.327     50.7
10  0.904   50.9  50.0 0.817     50.9
# ℹ 40 more rows

Note that this new column was constructed in exactly the same way as y, so it’s values are identical to y.

The dplyr verbs mutate, select, filter, etc. are extremely helpful for performing common data manipulations that you may need to perform during an analysis.

Loading data into R

Many of the data sets we work with in this class are included with R or one of its packages (e.g., the ISLR2 package). Other data sets will need to be read into R from files. Some data sets we will read directly from websites.

First we will explore a built-in data set called mtcars. Enter ?mtcars at the console to learn about the data set. Since it is built into R we can just refer to it by name. Let’s convert it to a tibble to make it nicer to work with.

mtc_tib <- mtcars %>%
  as_tibble()
mtc_tib
# A tibble: 32 × 11
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1  21       6  160    110  3.9   2.62  16.5     0     1     4     4
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1
 7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4
 8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2
 9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2
10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4
# ℹ 22 more rows

We can also use the glimpse function to get a different view of the data.

mtc_tib %>% glimpse()
Rows: 32
Columns: 11
$ mpg  <dbl> 21.0, 21.0, 22.8, 21.4, 18.7, 18.1, 14.3, 24.4, 22.8, 19.2, 17.8,…
$ cyl  <dbl> 6, 6, 4, 6, 8, 6, 8, 4, 4, 6, 6, 8, 8, 8, 8, 8, 8, 4, 4, 4, 4, 8,…
$ disp <dbl> 160.0, 160.0, 108.0, 258.0, 360.0, 225.0, 360.0, 146.7, 140.8, 16…
$ hp   <dbl> 110, 110, 93, 110, 175, 105, 245, 62, 95, 123, 123, 180, 180, 180…
$ drat <dbl> 3.90, 3.90, 3.85, 3.08, 3.15, 2.76, 3.21, 3.69, 3.92, 3.92, 3.92,…
$ wt   <dbl> 2.620, 2.875, 2.320, 3.215, 3.440, 3.460, 3.570, 3.190, 3.150, 3.…
$ qsec <dbl> 16.46, 17.02, 18.61, 19.44, 17.02, 20.22, 15.84, 20.00, 22.90, 18…
$ vs   <dbl> 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0,…
$ am   <dbl> 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,…
$ gear <dbl> 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 4, 4, 4, 3, 3,…
$ carb <dbl> 4, 4, 1, 1, 2, 1, 4, 2, 2, 4, 4, 3, 3, 3, 4, 4, 4, 1, 2, 1, 1, 2,…

The authors of the ISLR text have made another automobile data set available on their website. Download the Auto.csv data set from this website, and save it in the raw_data folder within your MATH313_labs_APs project directory (or whatever you called it). You should be able to see the file in your raw_data folder if you navigate there using the Files tab in RStudio.

The file is a comma separated values (csv) file that can be opened with a text editor or spreadsheet program (like Excel). We can read the data into R as a tibble using the read_csv function from the readr package (also part of the tidyverse).

auto <- read_csv("raw_data/Auto.csv")
Rows: 397 Columns: 9
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): horsepower, name
dbl (7): mpg, cylinders, displacement, weight, acceleration, year, origin

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
auto
# A tibble: 397 × 9
     mpg cylinders displacement horsepower weight acceleration  year origin
   <dbl>     <dbl>        <dbl> <chr>       <dbl>        <dbl> <dbl>  <dbl>
 1    18         8          307 130          3504         12      70      1
 2    15         8          350 165          3693         11.5    70      1
 3    18         8          318 150          3436         11      70      1
 4    16         8          304 150          3433         12      70      1
 5    17         8          302 140          3449         10.5    70      1
 6    15         8          429 198          4341         10      70      1
 7    14         8          454 220          4354          9      70      1
 8    14         8          440 215          4312          8.5    70      1
 9    14         8          455 225          4425         10      70      1
10    15         8          390 190          3850          8.5    70      1
# ℹ 387 more rows
# ℹ 1 more variable: name <chr>

A third way we can read data into R is directly from a website. The Advertising.csv file can be accessed from the ISLR website using the following URL: https://www.statlearning.com/s/Advertising.csv. Instead of downloading the file, copying it into our raw_data folder, and then reading it into R. We can access it directly from the website using the URL. This is also straightforward using the read_csv function.

ads <- read_csv("https://www.statlearning.com/s/Advertising.csv")
New names:
Rows: 200 Columns: 5
── Column specification
──────────────────────────────────────────────────────── Delimiter: "," dbl
(5): ...1, TV, radio, newspaper, sales
ℹ Use `spec()` to retrieve the full column specification for this data. ℹ
Specify the column types or set `show_col_types = FALSE` to quiet this message.
• `` -> `...1`
ads
# A tibble: 200 × 5
    ...1    TV radio newspaper sales
   <dbl> <dbl> <dbl>     <dbl> <dbl>
 1     1 230.   37.8      69.2  22.1
 2     2  44.5  39.3      45.1  10.4
 3     3  17.2  45.9      69.3   9.3
 4     4 152.   41.3      58.5  18.5
 5     5 181.   10.8      58.4  12.9
 6     6   8.7  48.9      75     7.2
 7     7  57.5  32.8      23.5  11.8
 8     8 120.   19.6      11.6  13.2
 9     9   8.6   2.1       1     4.8
10    10 200.    2.6      21.2  10.6
# ℹ 190 more rows

Note that the first column simply contains row numbers, so there is no need to keep it. Let’s read the first column. One way to do this is using numeric indexing. We don’t want column 1, so we simply put -1 in the column position. Note: If you run this code again, you will drop the first column of the remaining data, and so on. You probably do not want to do this.

ads <- ads[,-1]
ads
# A tibble: 200 × 4
      TV radio newspaper sales
   <dbl> <dbl>     <dbl> <dbl>
 1 230.   37.8      69.2  22.1
 2  44.5  39.3      45.1  10.4
 3  17.2  45.9      69.3   9.3
 4 152.   41.3      58.5  18.5
 5 181.   10.8      58.4  12.9
 6   8.7  48.9      75     7.2
 7  57.5  32.8      23.5  11.8
 8 120.   19.6      11.6  13.2
 9   8.6   2.1       1     4.8
10 200.    2.6      21.2  10.6
# ℹ 190 more rows

One disadvantage of downloading data this way is that you will not be able to access it offline. So, if you load data this way, you may want to save a copy. We discuss saving data in R in the next section.

Saving data and loading in data you have saved

We can save data in a variety of formats using R. For example, you can write a .csv file using the write_csv function. The advantage of this method is that you can read the file outside of R.

However, more often we will use a binary format to save our R objects using the saveRDS function. This tends to produce smaller files that can be written faster by R and are faster to read back into R later (though they cannot be opened easily outside of R). A major advantage of saving data in this way is that the object retains its format (e.g., as a tibble), and it will have the same format when it is read back into R.

Let’s save the ads data in our clean_data folder. We save it to the clean_data folder instead of the raw_data folder, because we have modified it (we dropped the extra column). We will use the file name ads.rds.

ads %>% saveRDS("clean_data/ads.rds")

If you navigate to your clean_data folder in the Files tab in RStudio, you should now see the file there.

The ads tibble is still accessible within your workspace. However, you have now saved a copy on your that you can access later. You can view the variables in your workspace using the ls command. Note that ads is still there.

ls()
 [1] "ads"             "auto"            "cor_xyz"         "data"           
 [5] "df"              "has_annotations" "mtc_tib"         "tib"            
 [9] "x"               "xvec"            "y"               "y_gmean"        
[13] "z"              

We can remove a variable from the workspace using the rm command. Remove the ads object. Note that it is no longer present. Type ads into the console as if you wanted to see a print out of the data. Since R can no longer find ads, it prints an error message instead.

rm(ads)
ls()
 [1] "auto"            "cor_xyz"         "data"            "df"             
 [5] "has_annotations" "mtc_tib"         "tib"             "x"              
 [9] "xvec"            "y"               "y_gmean"         "z"              

Let’s load the data again, but this time we will use the readRDS function to read it in from the .rds file we just created. We can see that ads is now back in our workspace.

ads <- readRDS("clean_data/ads.rds")
ls()
 [1] "ads"             "auto"            "cor_xyz"         "data"           
 [5] "df"              "has_annotations" "mtc_tib"         "tib"            
 [9] "x"               "xvec"            "y"               "y_gmean"        
[13] "z"              

Numerical summaries

The summary function gives us a quick summary of a data set. It lists summary statistics for each numerical variable in the data, including maximum and minimum values, quartiles, and means. The number of NA values are also listed. Let’s create a summary of the auto data. Note that there seems to be a problem with the horsepower summary. We will return to that later. The summary for name is also not very helpful. However, this is simply due to the fact that name is not a numerical variable.

auto %>% summary()
      mpg          cylinders      displacement    horsepower       
 Min.   : 9.00   Min.   :3.000   Min.   : 68.0   Length:397        
 1st Qu.:17.50   1st Qu.:4.000   1st Qu.:104.0   Class :character  
 Median :23.00   Median :4.000   Median :146.0   Mode  :character  
 Mean   :23.52   Mean   :5.458   Mean   :193.5                     
 3rd Qu.:29.00   3rd Qu.:8.000   3rd Qu.:262.0                     
 Max.   :46.60   Max.   :8.000   Max.   :455.0                     
     weight      acceleration        year           origin     
 Min.   :1613   Min.   : 8.00   Min.   :70.00   Min.   :1.000  
 1st Qu.:2223   1st Qu.:13.80   1st Qu.:73.00   1st Qu.:1.000  
 Median :2800   Median :15.50   Median :76.00   Median :1.000  
 Mean   :2970   Mean   :15.56   Mean   :75.99   Mean   :1.574  
 3rd Qu.:3609   3rd Qu.:17.10   3rd Qu.:79.00   3rd Qu.:2.000  
 Max.   :5140   Max.   :24.80   Max.   :82.00   Max.   :3.000  
     name          
 Length:397        
 Class :character  
 Mode  :character  
                   
                   
                   

Another useful and more flexible way to create numerical summaries is using the summarize function. The result will be a tibble listing the particular summary statistics that we specify. For example, we can list the mean, standard deviation, and the 10th and 90th percentile for the mpg values using the following code. We have to give each summary a name (e.g., mpg_mean), and we have to specify how each summary is computed, using an R function that takes a vector as input and outputs a single numerical value. The summarize function is similar to the mutate function in that they both creat new columns from old ones, however summarize creates a new tibble, whereas mutate adds columns to an existing tibble.

mpg_summary <- auto %>%
  summarize(mpg_mean = mean(mpg), mpg_sd = sd(mpg),
            mpg_10 = quantile(mpg, 0.1), mpg_90 = quantile(mpg, 0.9))
mpg_summary
# A tibble: 1 × 4
  mpg_mean mpg_sd mpg_10 mpg_90
     <dbl>  <dbl>  <dbl>  <dbl>
1     23.5   7.83     14   34.3

Data cleaning

Real data sets often have problems that must be addressed prior to analysis. If we look carefully at the print out of the auto tibble, we see that almost all of the variables that we expect to be numerical are of type dbl, which stands for double. A double is a particular way to represent a floating point number on a computer. however, horsepower is listed as having type chr (short for character). Although we expect horsepower to be numerical it is being treated as text.

auto
# A tibble: 397 × 9
     mpg cylinders displacement horsepower weight acceleration  year origin
   <dbl>     <dbl>        <dbl> <chr>       <dbl>        <dbl> <dbl>  <dbl>
 1    18         8          307 130          3504         12      70      1
 2    15         8          350 165          3693         11.5    70      1
 3    18         8          318 150          3436         11      70      1
 4    16         8          304 150          3433         12      70      1
 5    17         8          302 140          3449         10.5    70      1
 6    15         8          429 198          4341         10      70      1
 7    14         8          454 220          4354          9      70      1
 8    14         8          440 215          4312          8.5    70      1
 9    14         8          455 225          4425         10      70      1
10    15         8          390 190          3850          8.5    70      1
# ℹ 387 more rows
# ℹ 1 more variable: name <chr>

Let’s investigate further. If we list all of the horsepower values, we see that there are some ? values, indicating missing data for those vehicles. The rest of the values can be interpreted as numbers. When the data were read into R, the horsepower variable was interpreted as text, because of the presence of ? characters.

auto %>% pull(horsepower)
  [1] "130" "165" "150" "150" "140" "198" "220" "215" "225" "190" "170" "160"
 [13] "150" "225" "95"  "95"  "97"  "85"  "88"  "46"  "87"  "90"  "95"  "113"
 [25] "90"  "215" "200" "210" "193" "88"  "90"  "95"  "?"   "100" "105" "100"
 [37] "88"  "100" "165" "175" "153" "150" "180" "170" "175" "110" "72"  "100"
 [49] "88"  "86"  "90"  "70"  "76"  "65"  "69"  "60"  "70"  "95"  "80"  "54" 
 [61] "90"  "86"  "165" "175" "150" "153" "150" "208" "155" "160" "190" "97" 
 [73] "150" "130" "140" "150" "112" "76"  "87"  "69"  "86"  "92"  "97"  "80" 
 [85] "88"  "175" "150" "145" "137" "150" "198" "150" "158" "150" "215" "225"
 [97] "175" "105" "100" "100" "88"  "95"  "46"  "150" "167" "170" "180" "100"
[109] "88"  "72"  "94"  "90"  "85"  "107" "90"  "145" "230" "49"  "75"  "91" 
[121] "112" "150" "110" "122" "180" "95"  "?"   "100" "100" "67"  "80"  "65" 
[133] "75"  "100" "110" "105" "140" "150" "150" "140" "150" "83"  "67"  "78" 
[145] "52"  "61"  "75"  "75"  "75"  "97"  "93"  "67"  "95"  "105" "72"  "72" 
[157] "170" "145" "150" "148" "110" "105" "110" "95"  "110" "110" "129" "75" 
[169] "83"  "100" "78"  "96"  "71"  "97"  "97"  "70"  "90"  "95"  "88"  "98" 
[181] "115" "53"  "86"  "81"  "92"  "79"  "83"  "140" "150" "120" "152" "100"
[193] "105" "81"  "90"  "52"  "60"  "70"  "53"  "100" "78"  "110" "95"  "71" 
[205] "70"  "75"  "72"  "102" "150" "88"  "108" "120" "180" "145" "130" "150"
[217] "68"  "80"  "58"  "96"  "70"  "145" "110" "145" "130" "110" "105" "100"
[229] "98"  "180" "170" "190" "149" "78"  "88"  "75"  "89"  "63"  "83"  "67" 
[241] "78"  "97"  "110" "110" "48"  "66"  "52"  "70"  "60"  "110" "140" "139"
[253] "105" "95"  "85"  "88"  "100" "90"  "105" "85"  "110" "120" "145" "165"
[265] "139" "140" "68"  "95"  "97"  "75"  "95"  "105" "85"  "97"  "103" "125"
[277] "115" "133" "71"  "68"  "115" "85"  "88"  "90"  "110" "130" "129" "138"
[289] "135" "155" "142" "125" "150" "71"  "65"  "80"  "80"  "77"  "125" "71" 
[301] "90"  "70"  "70"  "65"  "69"  "90"  "115" "115" "90"  "76"  "60"  "70" 
[313] "65"  "90"  "88"  "90"  "90"  "78"  "90"  "75"  "92"  "75"  "65"  "105"
[325] "65"  "48"  "48"  "67"  "67"  "67"  "?"   "67"  "62"  "132" "100" "88" 
[337] "?"   "72"  "84"  "84"  "92"  "110" "84"  "58"  "64"  "60"  "67"  "65" 
[349] "62"  "68"  "63"  "65"  "65"  "74"  "?"   "75"  "75"  "100" "74"  "80" 
[361] "76"  "116" "120" "110" "105" "88"  "85"  "88"  "88"  "88"  "85"  "84" 
[373] "90"  "92"  "74"  "68"  "68"  "63"  "70"  "88"  "75"  "70"  "67"  "67" 
[385] "67"  "110" "85"  "92"  "112" "96"  "84"  "90"  "86"  "52"  "84"  "79" 
[397] "82" 

Let’s convert the horsepower variable to a numeric type and use a more standard method for representing the missing data (as NA instead of ?). We can use the mutate and na_if commands to convert ? values to NA. Then we can use the mutate and as.numeric commands to convert the horsepower data to a numeric type.

auto <- auto %>% 
  mutate(horsepower = horsepower %>% na_if("?")) %>%
  mutate(horsepower = horsepower %>% as.numeric())
auto
# A tibble: 397 × 9
     mpg cylinders displacement horsepower weight acceleration  year origin
   <dbl>     <dbl>        <dbl>      <dbl>  <dbl>        <dbl> <dbl>  <dbl>
 1    18         8          307        130   3504         12      70      1
 2    15         8          350        165   3693         11.5    70      1
 3    18         8          318        150   3436         11      70      1
 4    16         8          304        150   3433         12      70      1
 5    17         8          302        140   3449         10.5    70      1
 6    15         8          429        198   4341         10      70      1
 7    14         8          454        220   4354          9      70      1
 8    14         8          440        215   4312          8.5    70      1
 9    14         8          455        225   4425         10      70      1
10    15         8          390        190   3850          8.5    70      1
# ℹ 387 more rows
# ℹ 1 more variable: name <chr>

Let’s look at the values of the horsepower variable again. We should now see NA values in place of ? characters.

auto %>% pull(horsepower)
  [1] 130 165 150 150 140 198 220 215 225 190 170 160 150 225  95  95  97  85
 [19]  88  46  87  90  95 113  90 215 200 210 193  88  90  95  NA 100 105 100
 [37]  88 100 165 175 153 150 180 170 175 110  72 100  88  86  90  70  76  65
 [55]  69  60  70  95  80  54  90  86 165 175 150 153 150 208 155 160 190  97
 [73] 150 130 140 150 112  76  87  69  86  92  97  80  88 175 150 145 137 150
 [91] 198 150 158 150 215 225 175 105 100 100  88  95  46 150 167 170 180 100
[109]  88  72  94  90  85 107  90 145 230  49  75  91 112 150 110 122 180  95
[127]  NA 100 100  67  80  65  75 100 110 105 140 150 150 140 150  83  67  78
[145]  52  61  75  75  75  97  93  67  95 105  72  72 170 145 150 148 110 105
[163] 110  95 110 110 129  75  83 100  78  96  71  97  97  70  90  95  88  98
[181] 115  53  86  81  92  79  83 140 150 120 152 100 105  81  90  52  60  70
[199]  53 100  78 110  95  71  70  75  72 102 150  88 108 120 180 145 130 150
[217]  68  80  58  96  70 145 110 145 130 110 105 100  98 180 170 190 149  78
[235]  88  75  89  63  83  67  78  97 110 110  48  66  52  70  60 110 140 139
[253] 105  95  85  88 100  90 105  85 110 120 145 165 139 140  68  95  97  75
[271]  95 105  85  97 103 125 115 133  71  68 115  85  88  90 110 130 129 138
[289] 135 155 142 125 150  71  65  80  80  77 125  71  90  70  70  65  69  90
[307] 115 115  90  76  60  70  65  90  88  90  90  78  90  75  92  75  65 105
[325]  65  48  48  67  67  67  NA  67  62 132 100  88  NA  72  84  84  92 110
[343]  84  58  64  60  67  65  62  68  63  65  65  74  NA  75  75 100  74  80
[361]  76 116 120 110 105  88  85  88  88  88  85  84  90  92  74  68  68  63
[379]  70  88  75  70  67  67  67 110  85  92 112  96  84  90  86  52  84  79
[397]  82

Next we will add a column to the auto data for the vehicle make. Currently the name column lists both the make and model for each vehicle. How can we extract just the make?

auto %>%
  select(name)
# A tibble: 397 × 1
   name                     
   <chr>                    
 1 chevrolet chevelle malibu
 2 buick skylark 320        
 3 plymouth satellite       
 4 amc rebel sst            
 5 ford torino              
 6 ford galaxie 500         
 7 chevrolet impala         
 8 plymouth fury iii        
 9 pontiac catalina         
10 amc ambassador dpl       
# ℹ 387 more rows

The make of each vehicle is indicated by the first word in the name. We can extract the first word in each name using the str_extract function from the stringr package (part of the tidyverse). The command is somewhat complicated because it uses a regular expression. We won’t go into detail regarding how it works, but the regular expression "[^\\s]+" specifies that we want to extract all of the characters before the first space.

auto <- auto %>% 
  mutate(make = name %>% str_extract("[^\\s]+"))
auto %>% select(make)
# A tibble: 397 × 1
   make     
   <chr>    
 1 chevrolet
 2 buick    
 3 plymouth 
 4 amc      
 5 ford     
 6 ford     
 7 chevrolet
 8 plymouth 
 9 pontiac  
10 amc      
# ℹ 387 more rows

We can use the count function to see how many vehicles are present in the data for each make. Even after viewing only the first few counts, we immediately notice a problem with the data. There are some misspellings, and there are represented in multiple ways.

auto %>% 
  count(make)
# A tibble: 37 × 2
   make          n
   <chr>     <int>
 1 amc          27
 2 audi          7
 3 bmw           2
 4 buick        17
 5 cadillac      2
 6 capri         1
 7 chevroelt     1
 8 chevrolet    43
 9 chevy         3
10 chrysler      6
# ℹ 27 more rows

To see how extensive the problem is, we can use the View function at the console to view the entire set of counts. Here we will use another approach that works better when knitting our document. We will pull the make column out of the data (as a vector), find the unique values, and sort them alphabetically.

auto %>% 
  pull(make) %>%
  unique() %>%
  sort()
 [1] "amc"           "audi"          "bmw"           "buick"        
 [5] "cadillac"      "capri"         "chevroelt"     "chevrolet"    
 [9] "chevy"         "chrysler"      "datsun"        "dodge"        
[13] "fiat"          "ford"          "hi"            "honda"        
[17] "maxda"         "mazda"         "mercedes"      "mercedes-benz"
[21] "mercury"       "nissan"        "oldsmobile"    "opel"         
[25] "peugeot"       "plymouth"      "pontiac"       "renault"      
[29] "saab"          "subaru"        "toyota"        "toyouta"      
[33] "triumph"       "vokswagen"     "volkswagen"    "volvo"        
[37] "vw"           

We will replace “chevroelt” and “chevy” with “chevrolet”, “maxda” with “mazda”, “mercedes-benz” with “mercedes”, “toyouta” with “toyota”, and “volkswagen” and “vokswagen” with “vw”. We can use the recode function to do this.

auto <- auto %>%
  mutate(make = make %>% recode(chevroelt = "chevrolet", chevy = "chevrolet",
                                maxda = "mazda",
                                `mercedes-benz` = "mercedes",
                                toyouta = "toyota",
                                vokswagen = "vw", volkswagen = "vw"))

Did we fix the problems?

auto %>% 
  pull(make) %>%
  unique() %>%
  sort()
 [1] "amc"        "audi"       "bmw"        "buick"      "cadillac"  
 [6] "capri"      "chevrolet"  "chrysler"   "datsun"     "dodge"     
[11] "fiat"       "ford"       "hi"         "honda"      "mazda"     
[16] "mercedes"   "mercury"    "nissan"     "oldsmobile" "opel"      
[21] "peugeot"    "plymouth"   "pontiac"    "renault"    "saab"      
[26] "subaru"     "toyota"     "triumph"    "volvo"      "vw"        

The process that we have just gone through is often described as cleaning the data. Our earlier work to convert horsepower to a numeric variable and to label missing values as NA is another example of cleaning.

Let’s look at the counts again. This time we will sort them in descending order, so that the makes with the most vehicles in the data set are at the top.

auto %>%
  count(make) %>%
  arrange(desc(n))
# A tibble: 30 × 2
   make          n
   <chr>     <int>
 1 ford         51
 2 chevrolet    47
 3 plymouth     31
 4 dodge        28
 5 amc          27
 6 toyota       26
 7 datsun       23
 8 vw           22
 9 buick        17
10 pontiac      16
# ℹ 20 more rows

Now, let’s look at one of the more mysterious columns in the auto data. The origin column labels each vehicle with 1, 2, or 3, but what does this mean?

auto %>% 
  count(origin)
# A tibble: 3 × 2
  origin     n
   <dbl> <int>
1      1   248
2      2    70
3      3    79

If we compare the origin and make columns we might realize (depending on how much we know about automobile manufacturers) that this column indicates the region in which the manufacturer was located (the United States, Europe, or Japan). The following code makes this easy to see. Do you understand what the code is doing? You may need to look at the help files for the paste function to understand it.

auto %>%
  mutate(origin_make = paste(origin, make, sep = "_")) %>%
  pull(origin_make) %>%
  unique() %>%
  sort()
 [1] "1_amc"        "1_buick"      "1_cadillac"   "1_capri"      "1_chevrolet" 
 [6] "1_chrysler"   "1_dodge"      "1_ford"       "1_hi"         "1_mercury"   
[11] "1_oldsmobile" "1_plymouth"   "1_pontiac"    "2_audi"       "2_bmw"       
[16] "2_fiat"       "2_mercedes"   "2_opel"       "2_peugeot"    "2_renault"   
[21] "2_saab"       "2_triumph"    "2_volvo"      "2_vw"         "3_datsun"    
[26] "3_honda"      "3_mazda"      "3_nissan"     "3_subaru"     "3_toyota"    

Let’s make this more clear in our data by converting the numerical origin labels to text data.

auto <- auto %>%
  mutate(origin = origin %>% recode(`1` = "usa", `2` = "europe", `3` = "japan"))
auto
# A tibble: 397 × 10
     mpg cylinders displacement horsepower weight acceleration  year origin
   <dbl>     <dbl>        <dbl>      <dbl>  <dbl>        <dbl> <dbl> <chr> 
 1    18         8          307        130   3504         12      70 usa   
 2    15         8          350        165   3693         11.5    70 usa   
 3    18         8          318        150   3436         11      70 usa   
 4    16         8          304        150   3433         12      70 usa   
 5    17         8          302        140   3449         10.5    70 usa   
 6    15         8          429        198   4341         10      70 usa   
 7    14         8          454        220   4354          9      70 usa   
 8    14         8          440        215   4312          8.5    70 usa   
 9    14         8          455        225   4425         10      70 usa   
10    15         8          390        190   3850          8.5    70 usa   
# ℹ 387 more rows
# ℹ 2 more variables: name <chr>, make <chr>

How many cars are there from each region?

auto %>% count(origin)
# A tibble: 3 × 2
  origin     n
  <chr>  <int>
1 europe    70
2 japan     79
3 usa      248

Since we’ve done a lot of work cleaning the auto data, let’s save a clean version as an RDS file in case we want to use it later.

auto %>% saveRDS("clean_data/auto.rds")

Visualizing data using ggplot2

Visualizations are extremely useful for exploring or describing patterns in a data set. The ggplot2 library allows you to create a variety of different types of plots using R. The RStudio ggplot2 cheat sheet lists several examples. We will explore a few common types of plots here using the auto data.

Scatterplots

Scatterplots can be used to visualize the relationship between pairs of quantitative variables. Let’s plot fuel efficiency against displacement (a measure of the size of an engine) using ggplot2.

ggplot(data = auto, mapping = aes(x = displacement, y = mpg)) +
  geom_point()

The preceding code is a good starting point for learning about how to construct a plot using ggplot2. The ggplot command initializes the plot. Here we also specify the data set that will be used in the plot using the data argument, and some plot aesthetics using the mapping argument. Broadly, aesthetics determine how the plot will look. Here we specify the variables that will be on the x and y axes of the plot. The aes command is used to construct the aesthetic mappings. We add elements to a ggplot plot using the + operator. Once we have initialized the plot, we add a scatterplot using the geom_point command.

We can change the overall appearance of a plot using themes. I often prefer the classic or minimal theme.

ggplot(data = auto, mapping = aes(x = displacement, y = mpg)) +
  geom_point() +
  theme_classic()

We can change the axis labels.

ggplot(data = auto, mapping = aes(x = displacement, y = mpg)) +
  geom_point() +
  xlab("displacement (cubic inches)") +
  ylab("fuel efficiency (miles per gallon)") +
  theme_classic()

We can also change the colors of the points to whatever we want. We can change all of the points to a different color using the color argument in the geom_point command.

ggplot(data = auto, mapping = aes(x = displacement, y = mpg)) +
  geom_point(color = "midnightblue") +
  xlab("displacement (cubic inches)") +
  ylab("fuel efficiency (miles per gallon)") +
  theme_classic()

We can also use color to incorporate information from another variable into our plot. However, this requires us to define a corresponding aesthetic mapping. For example, we can map color to region of origin.

ggplot(data = auto, mapping = aes(x = displacement, y = mpg, color = origin)) +
  geom_point() +
  xlab("displacement (cubic inches)") +
  ylab("fuel efficiency (miles per gallon)") +
  theme_classic()

We can also use a different color scale that more easily perceived by viewers with some common forms of color blindness.

ggplot(data = auto, mapping = aes(x = displacement, y = mpg, color = origin)) +
  geom_point() +
  xlab("displacement (cubic inches)") +
  ylab("fuel efficiency (miles per gallon)") +
  scale_color_viridis_d() +
  theme_classic()

We can further distinguish points from different regions by using different shapes for the points.

ggplot(data = auto, mapping = aes(x = displacement, y = mpg, color = origin, shape = origin)) +
  geom_point() +
  xlab("displacement (cubic inches)") +
  ylab("fuel efficiency (miles per gallon)") +
  scale_color_viridis_d() +
  theme_classic()

Jittering

The following code demonstrates a slightly more compact way to construct a plot using ggplot. You will often see less verbose approaches like this one in examples you find online. It uses the fact that the ggplot command expects data as its first argument and mapping as its second argument instead of assigning those arguments by name (the pipe puts the auto data into the first argument). It also uses the fact that aes expects x and y arguments in that order. I encourage you to use the more verbose version (using named arguments) while you are starting to learn ggplot.

auto %>%
  ggplot(aes(cylinders, mpg)) +
  geom_point() +
  ylab("fuel efficiency (miles per gallon)") +
  theme_classic()

The plot we just produced helps us to explore the relationship between the number of cylinders that a vehicle’s engine has and fuel efficiency. Since the number of cylinders is discrete, many points are plotted for each number of cylinders, and they tend to overlap. This makes it difficult to see the variation in the data. We can solve this problem by using geom_jitter to inject a small amount of random variation into the data so that the points are more spread out. By default, variation is added in both the x and y variables, but we can specify how much variation to add in the x and y directions using the width and height arguments to geom_jitter.

Let’s color and shape the points by region of origin again. We will also make it possible to see through overlapping points by making them somewhat transparent using the alpha argument (0 = completely transparent, 1 = not transparent at all). We specify alpha as an argument to geom_jitter rather than as an aesthetic mapping, because we want all the points to have the same alpha value.

auto %>%
  ggplot(aes(cylinders, mpg, color = origin, shape = origin)) +
  geom_jitter(width = 0.2, height = 0, alpha = 0.7) +
  ylab("fuel efficiency (miles per gallon)") +
  scale_color_viridis_d() +
  theme_classic()

Box plots

Box plots can be useful for visualizing the distribution of a quantitative variable. Parallel boxplots are particularly useful for understanding how the distribution varies for different subsets of the data. Thus, they are particularly useful if we want to examine the relationship between a quantitative variable and a categorical variable.

Let’s create a boxplot that shows the fuel efficiency distributions for the three regions of origin. We use the geom_boxplot function.

ggplot(data = auto, mapping = aes(x = origin, y = mpg)) +
  geom_boxplot() +
  ylab("fuel efficiency (miles per gallon)") +
  theme_classic()

We can also create visualizations that include different plot types on the same graph. In cases like this, I often find it helpful to plot the same data using both a boxplot and a scatterplot. The boxplot summarizes the variation in the data, and the scatterplot shows it in detail.

ggplot(data = auto, mapping = aes(x = origin, y = mpg)) +
  geom_boxplot(outlier.shape = NA) +
  geom_jitter(width = 0.2, height = 0, color = "midnightblue", alpha = 0.5) +
  ylab("fuel efficiency (miles per gallon)") +
  theme_classic()

Histograms

Histograms give us an alternative way to visualize the distribution of a quantitative variable. Let’s create a simple histogram showing the distribution of fuel efficiencies across the entire data set.

ggplot(data = auto, mapping = aes(x = mpg)) +
  geom_histogram(fill = "midnightblue") +
  xlab("fuel efficiency (miles per gallon)") +
  theme_classic()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

We can increase the number of bins (or decrease the bin width) to obtain higher resolution in the x direction.

ggplot(data = auto, mapping = aes(x = mpg)) +
  geom_histogram(fill = "midnightblue", bins = 50) +
  xlab("fuel efficiency (miles per gallon)") +
  theme_classic()

Faceted plots

Often we will want to create separate visualizations for different subsets of the data. These visualizations can then be arranged in a matrix of plots that facilitates comparisons between subsets. We can create such faceted plots using the facet_wrap or facet_grid functions from ggplot2. First we will create a faceted plot showing the fuel efficiency distributions (as histograms) for each region of origin. We indicate that we want to facet by origin by specifying the formula ~origin in the first argument of the facet_wrap function (~ can be read as “in terms of”). We indicate ncol = 1 so that the plots are arranged as a single column.

ggplot(data = auto, mapping = aes(x = mpg, fill = origin)) +
  geom_histogram() +
  xlab("fuel efficiency (miles per gallon)") +
  facet_wrap(~origin, ncol = 1) +
  scale_fill_viridis_d() +
  theme_classic()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

One limitation of this visualization is that the European and Japanese distributions are more difficult to see because there are smaller numbers of cars in the data set from those regions. By default, a faceted plot will use the same scales for all of the subplots. We can allow the y scales to differ from plot to plot by setting scales = “free_y”.

ggplot(data = auto, mapping = aes(x = mpg, fill = origin)) +
  geom_histogram() +
  xlab("fuel efficiency (miles per gallon)") +
  facet_wrap(~origin, ncol = 1, scales = "free_y") +
  scale_fill_viridis_d() +
  theme_classic()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Pairs plots

It can be helpful to compare multiple pairs of variables using the same visualization. This is particularly true as you begin to explore a new data set. A pairs plot is a tool for doing just this. The ggplot2 package does not have a function that allows us to create a pairs plot using a single command. However, the GGally package does. The GGally adds several functions to ggplot2. If you have not done so yet, you will need to install the GGally package to generate the following pairs plot.

We will create a pairs plot for the auto data using the ggpairs function from GGally. First we drop the name and make columns, because they have too many levels to visualize in a small plot. The result is a matrix of plots and correlation values.

library(GGally)
auto %>% 
  select(-name, -make) %>% 
  ggpairs()

Each cell in the matrix compares two variables (see labels across the top and the right side). Comparisons of two quantititive variables are made with scatterplots (below the diagonal) and correlation coefficients (above the diagonal). A comparison of a categorical variable and a quantitative variable results in boxplots (if the categorical variable is on the x axis) or histograms (if its on the y axis). The distribution of each variable is plotted along the diagonal as either a continuous approximation (quantitative variables) or as a bar graph (categorical variables).