```{r, echo=FALSE} is_static <- params$static ``` ```{r setup, include=FALSE} if (!is_static) library(learnr) library(tidyverse) knitr::opts_chunk$set(echo = is_static, fig.align = "center", cache = is_static, warning = FALSE, message = FALSE) library(nycflights13) pulseData <- read_tsv("https://raw.githubusercontent.com/aboland/TutoR/master/Tutorial_03_Transformation/pulse.txt") fastFoodData <- read_csv("https://raw.githubusercontent.com/aboland/TutoR/master/Tutorial_03_Transformation/fastfood_calories.csv") load(url("https://github.com/aboland/TutoR/raw/master/Tutorial_03_Transformation/olive.Rdata?raw=true")) missingData <- filter(pulseData, !complete.cases(pulseData)) cleanPulseData <- na.omit(pulseData) ``` ## Introduction {data-progressive=TRUE} This tutorial will show you how to read in datasets to R, save datasets as files and deal with missing data. We will also look at the `filter()`, `arrange()` and `select()` functions which are useful for data transformation. We will also look at the `group_by()` and `summarise()` functions which are useful for creating summary statistics. We will finish off looking at the concept of the pipe operator which helps to create *readable* code. ### Loading Datasets {data-allow-skip=TRUE} - The function used to read in the individual files depends on the file type. We will look at some functions included in the `readr` pacakge. - `read_csv()` reads comma delimited files, `read_csv2()` reads semicolon separated files (common in countries where , is used as the decimal place), `read_tsv()` reads tab delimited files, and `read_delim()` reads in files with any delimiter. - There are many arguments you can use dependin on the structure of your file. - You can use `col_names = FALSE` to tell `read_csv()` not to treat the first row as headings - `load()` is a base function used for reading in `.Rdata` files. - In the examples below we are reading directly from the Github repository online. - **In practice you will be reading from a location on your local machine.** - e.g. `read_tsv("user/downloads/pulse.txt")` ```{r readtable, exercise= TRUE, exercise.eval=FALSE} pulseData <- read_tsv("https://raw.githubusercontent.com/aboland/TutoR/master/Tutorial_03_Transformation/pulse.txt") pulseData ``` ```{r readcsv, exercise= TRUE, exercise.eval= FALSE} fastFoodData <- read_csv("https://raw.githubusercontent.com/aboland/TutoR/master/Tutorial_03_Transformation/fastfood_calories.csv", col_names = TRUE) fastFoodData ``` ```{r ex-load, exercise= TRUE, exercise.eval= FALSE} load(url("https://raw.githubusercontent.com/aboland/TutoR/master/Tutorial_03_Transformation/olive.Rdata?raw=true")) head(olive) ``` - When reading in data, it is useful to assign names to the input as then the datasets can easily be included in other functions. ### Saving Datasets {data-allow-skip=TRUE} - It is also possible to save datasets created or worked on in R as files. - The `write_delim()` function can be used to do this. The function takes the form `write_delim(x, path, delim)` where `x` is the name of the dataset in R and `path` is name of the file you wish to create. The `delim` parameter is the method by which each row of data is separated in the resulting file, with the default separator set to a space. - Since this is an interactive tutorial we won't run the write.table command. - `write_delim(pulseData, path = "savedPulse.csv", delim = "\t")` would save the `pulseData` data as a tab separated file. - The `write_csv()` is an alternative function which will save the data in a comma separated form. The function takes the form `write_csv(x, file,)` where `x` is the name of the dataset in R and `path` is name of the file you wish to create. ## Missing Data {data-progressive=TRUE} - Datasets often can contain missing data. - The `complete.cases()` function is used to identify the rows of data which are complete i.e. do not contain missing data. - Sometimes it is helpful to take a closer look at cases with missing data as opposed to just deleting them. - Run the following code to find any missing data in the `pulseData` dataframe and save them in a new dataframe called `missingData`. ```{r ex-pulse-complete, exercise= TRUE, exercise.eval= FALSE} missingData <- filter(pulseData, !complete.cases(pulseData)) ``` - The `!` in R is a negative operator, i.e. the code above is searching for cases that are **not** complete. - We will cover the `filter` function in the next section. ### Analysing the Missing Data - The `summary()` function is useful for quickly analysing a dataset. ```{r ex-pulse-summary, exercise= TRUE, exercise.eval= FALSE} summary(missingData) ``` ### Removing Missing Data - After analysing the cases which contain missing elements, sometimes we then wish to remove them from our original dataset. #### Exercise 1 **Use the `complete.cases()` function to create a new dataframe called `cleanPulseData` which contains no missing data.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 1 Solution] ") ``` ```{r ex01-pulse, exercise= TRUE, exercise.eval= FALSE, include=!is_static} cleanPulseData <- pulseData head(cleanPulseData) ``` ```{r ex01-pulse-solution, include=!is_static} cleanPulseData <- filter(pulseData, complete.cases(pulseData)) ``` ```{r ex01-static, include=!is_static, include=FALSE, eval=FALSE} cleanPulseData <- filter(pulseData, complete.cases(pulseData)) ```
- An alternative method of removing missing data is to use the `na.omit()` function: - Note how the use of `na.omit` differs to `complete.cases` - It is a standalone function which returns a dataset ```{r ex-pulse-naomit, exercise= TRUE, exercise.eval= FALSE} cleanPulseData <- na.omit(pulseData) cleanPulseData ```

## Data Transformation {data-progressive=TRUE} - The `filter()`, `arrange()` and `select()` functions are from the `dplyr` package which is a member of the `tidyverse` packages. ### `filter()` - The `filter()` function allows you to easily subset observations based on their values. - There are a number of different comparison operators which can be used. - `<`, `>` less than, greater than. - `<=`, `>=` less than or equal to, greater than or equal to. - `==` equal to. - `!=` not equal. - The data to be subsetted is first specified and the subsequent arguments are the expressions that filter the data frame. - For example, you may wish to create a subset of cases from `cleanPulseData` containing only individuals who smoke and weigh over 160. ```{r pulse-filter, exercise= TRUE, exercise.eval= FALSE} smokersOver160 <- filter(cleanPulseData, RestingPulse == "Low", Smokes == "Yes", Weight > 160) smokersOver160 ``` #### Exercise 2 **Create a subset from `cleanPulseData` which contains individuals with a low resting pulse who do not smoke and weight less than 180. Name the subset `lowRateNonSmokers`.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 2 Solution] ") ``` ```{r ex-pulse-filter, exercise= TRUE, exercise.eval= FALSE, include=!is_static} ``` ```{r ex-pulse-filter-solution, include=!is_static} lowRateNonSmokers <- filter(cleanPulseData, Smokes == "No", Weight < 180) lowRateNonSmokers ``` ```{r ex02-static, include=FALSE, eval=FALSE} lowRateNonSmokers <- filter(cleanPulseData, Smokes == "No", Weight < 180) lowRateNonSmokers ```
#### Exercise 3 **Create a subset containing individuals with a weight that is less than or equal to 170.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 3 Solution] ") ``` ```{r ex-pulse-subset, exercise= TRUE, exercise.eval= FALSE, include=!is_static} ``` ```{r ex-pulse-subset-solution, include=!is_static} lessThanOrEqualTo170 <- filter(cleanPulseData, Weight <= 170) head(lessThanOrEqualTo170) ``` ```{r ex03-static, include=FALSE, eval=FALSE} lessThanOrEqualTo170 <- filter(cleanPulseData, Weight <= 170) head(lessThanOrEqualTo170) ```
### `arrange()` - The `arrange()` function sorts and orders the contents of a dataframe. ```{r pulse-arrange1, exercise= TRUE, exercise.eval= FALSE} weightOrder <- arrange(cleanPulseData, Weight) weightOrder ``` - The data frame is arranged in ascending order by default. However, you can sort the data by descending order using the following code: ```{r pulse-arrange2, exercise= TRUE, exercise.eval= FALSE} weightOrderDesc <- arrange(cleanPulseData, desc(Weight)) weightOrderDesc ``` - It is also possible to include more than one column name in the `arrange()` function. ```{r pulse-arrange3, exercise= TRUE, exercise.eval= FALSE} smokesAndPulseOrder <- arrange(cleanPulseData, Smokes, RestingPulse) smokesAndPulseOrder ``` #### Exercise 4 **Run the above function again but this time input `RestingPulse` into the function before `Smokes`. What effect does this have on the resulting dataset.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 4 Solution] ") ``` ```{r ex-pulse-arrange, exercise= TRUE, exercise.eval= FALSE, include=!is_static} ``` ```{r ex-pulse-arrange-solution, include=!is_static} pulseAndSmokesOrder <- arrange(cleanPulseData, RestingPulse, Smokes) head(pulseAndSmokesOrder) ``` ```{r ex04-static, include=FALSE, echo=FALSE} pulseAndSmokesOrder <- arrange(cleanPulseData, RestingPulse, Smokes) head(pulseAndSmokesOrder) ```
### `select()` - The `select()` function allows you select only the variables you are interested in from a data frame. - For example, some datasets may contain hundreds of variables but you may only wish to analyse a few of them. - `fastFoodData` has 18 variables for each of its 515 observations. The code below shows how the `select()` function can be used to create a new dataset with less variables. ```{r pulse-select, exercise= TRUE, exercise.eval= FALSE} fastFoodDataSimplified <- select(fastFoodData, restaurant, item, calories) head(fastFoodDataSimplified) ``` #### Exercise 5 **Use the `select()` function to create a data frame called `fastFoodNutrition` which contains the variables `restaurant`, `item`, `calories`, `total_fat`, `sugar` and `protein`.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 5 Solution] ") ``` ```{r ex-pulse-select, exercise= TRUE, exercise.eval= FALSE, include=!is_static} ``` ```{r ex-pulse-select-solution, include=!is_static} fastFoodNutrition <- select(fastFoodData, restaurant, item, calories, total_fat, sugar, protein) fastFoodNutrition ``` ```{r ex05-static, inculde = FALSE, echo=FALSE} fastFoodNutrition <- select(fastFoodData, restaurant, item, calories, total_fat, sugar, protein) fastFoodNutrition ```

## More Transformations {data-progressive=TRUE} ```{r, include=!is_static} playerData <- read_csv("https://raw.githubusercontent.com/aboland/TutoR/master/Tutorial_03_Transformation/player_data.csv") cat("The `playerData` dataset has been pre-loaded. Explore the data below. \n") ``` ```{r ex-player_explore, exercise=TRUE, exercise.eval=TRUE, include=!is_static} playerData ``` ```{r, include=is_static, echo=F} cat("The `playerData` dataset can be read in as follows.") ``` ```{r, include=is_static} playerData <- read_csv("https://raw.githubusercontent.com/aboland/TutoR/master/Tutorial_03_Transformation/player_data.csv") playerData ``` There are further helpful functions for data transformation such as `mutate()`, `transmutate()`, `group_by()` and `ungroup()`. These functions will then be used in conjunction with functions from the previous section. ### `mutate()` - The `mutate()` function allows you create new columns (variables) that are functions of existing columns and adds them to the dataframe. - For example, the `playerData` dataset has two variables `year_start` and `year_end` which represent the year a player started their career and the year they stopped playing professionally. It is therefore possible to add a new column `years_active` to the existing dataset by doing the following: ```{r ex-player_mutate, exercise=TRUE, exercise.eval=FALSE} playerData <- mutate(playerData, years_active = year_end - year_start) playerData_select <- select(playerData, name, years_active) playerData_select ``` - It is possible to create multiple new variables within the same `mutate()` function using the following format: `mutate(data, newVariable1, newVariable2, newVariable3, ...)`. ### `transmute()` - If you only wish to keep the new variables you have created, you can do so using the `transmute()` function as shown below: ```{r ex-player_transmutate, exercise=TRUE, exercise.eval=FALSE} playerData_select <- transmute(playerData, years_active = year_end - year_start) playerData_select ```

```{r, include=FALSE} library(nycflights13) ``` ### NYC Flights {data-allow-skip=TRUE} - The [nycflights13](https://github.com/hadley/nycflights13) data contains information about all flights that departed from NYC (e.g. EWR, JFK and LGA) in 2013 (336,776 flights in total). ```{r ex-flights_explore-static, include=is_static} library(nycflights13) flights ``` ```{r ex-flights_explore, exercise= TRUE, exercise.eval= TRUE, include=!is_static} flights ```
#### Exercise 6 **Add a new variable to the flights dataset called `kmPerMinute` by dividing the `distance` variable by the `air_time` variable.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 6 Solution] ") ``` ```{r ex-flights, exercise= TRUE, exercise.eval= FALSE, include=!is_static} flights ``` ```{r ex-flights-solution, include=!is_static} flights <- mutate(flights, kmPerMinute = distance/air_time) flights ``` ```{r ex06-static, include=FALSE, echo=FALSE} flights <- mutate(flights, kmPerMinute = distance/air_time) flights ```

## Grouping {data-progressive=TRUE} ### `group_by()` - The `group_by()` function groups entries in a dataset by given variables. - This is particularly useful when used in conjunction with the `summarise()` function. - There are many useful summary functions which can be used inside the summarise. - `n()`, when used inside summarise this will count the number in each group. - `mean()` calculates the average of a variable across each group. - `min()`, `max()`, min and max values in the group. - Try running the following code which groups the players in the dataset by their college and then finds the average number of years players from different colleges are active. ```{r, include=FALSE} playerData <- mutate(playerData, years_active = year_end - year_start) ``` ```{r ex-player_groupby, exercise= TRUE, exercise.eval= FALSE} byCollege <- group_by(playerData, college) summarise(byCollege, averageYearsActive = mean(years_active)) ``` #### Exercise 7 **Group the dataset using the `year_start` variable and then find the maximum `year_end` associated with each starting year.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 7 Solution] ") ``` ```{r ex7-player, exercise= TRUE, exercise.eval= FALSE, include=!is_static} byStartYear <- group_by(playerData) summarise(byStartYear) ``` ```{r ex7-player-solution, include=!is_static} byStartYear <- group_by(playerData, year_start) summarise(byStartYear, maxYearEnd = max(year_end)) ``` ```{r ex07-static, include=FALSE, echo=FALSE} byStartYear <- group_by(playerData, year_start) summarise(byStartYear, maxYearEnd = max(year_end)) ``` #### Exercise 8 **Group the `flights` dataset by `dest` and `carrier` then find the average distance for each grouping.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 8 Solution] ") ``` ```{r ex8-player, exercise= TRUE, exercise.eval= FALSE, include=!is_static} byDestAndCarrier <- group_by(flights) summarise(byDestAndCarrier) ``` ```{r ex8-player-solution, include=!is_static} byDestAndCarrier <- group_by(flights, dest, carrier) summarise(byDestAndCarrier, averageDistance = mean(distance)) ``` ```{r ex08-static, include=FALSE, echo=FALSE} byDestAndCarrier <- group_by(flights, dest, carrier) summarise(byDestAndCarrier, averageDistance = mean(distance)) ```
### `ungroup()` - If you wish to remove a grouping, you can do so simply by using the `ungroup()` function as follows: ```{r , include=FALSE} byCollege <- group_by(playerData, college) ``` ```{r ex-player_ungroup, exercise= TRUE, exercise.eval= FALSE} byCollege <- group_by(playerData, college) ungroup(byCollege) ```

## Piping {data-progressive=TRUE} - The pipe operator is a powerful way to create readable code. ### Combining multiple operations with the pipe - We will look at the flight data again. - If we want to explore the relationship between distance and average delay for each location, we would write something similar to below. ```{r ex-flights_standard, exercise= TRUE, exercise.eval= FALSE} by_dest <- group_by(flights, dest) # group by destination delay <- summarise(by_dest, count = n(), # number of flights dist = mean(distance, na.rm = TRUE), # average distance delay = mean(arr_delay, na.rm = TRUE) # average delay ) delay <- filter(delay, count > 20, dest != "HNL") # filter locations with more than 20 flights # Plot the data ggplot(data = delay, mapping = aes(x = dist, y = delay)) + geom_point(aes(size = count), alpha = 1/3) ``` - There are three steps to prepare this data: - Group flights by destination. - Summarise to compute distance, average delay, and number of flights. - Filter to remove noisy points and Honolulu airport, which is almost twice as far away as the next closest airport - There’s another way to tackle the same problem with the pipe operator `%>%`. ### How `%>%` works ```{r ex_flights_pipe, exercise= TRUE, exercise.eval= FALSE} delay <- flights %>% group_by(dest) %>% summarise( count = n(), dist = mean(distance, na.rm = TRUE), delay = mean(arr_delay, na.rm = TRUE) ) %>% filter(count > 20, dest != "HNL") ggplot(data = delay, mapping = aes(x = dist, y = delay)) + geom_point(aes(size = count), alpha = 1/3) ``` - If we have a dataset `some_data` and a function `function_1(arg1, arg2)` - Normally we would use the function as follows: - `function_1(arg1 = some_data, arg2 = value)` - With the pipe operator we could call the function like so: - `some_data %>% function_1(arg2 = value)` - The pipe operator passes the input as the first argument to the next function. - Multiple functions can be used in sequence. - The results from a function will be passed on to the next function. - `some_data %>% function_1(f1_arg2 = value) %>% function_2(f2_arg2 = other_value)`
In the last exercise we grouped the `flights` dataset by `dest` and `carrier`, and then found the average distance for each grouping. #### Exercise 9 **Rewrite the code using the `%>%` operator.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 9 Solution] ") ``` ```{r ex10-player, exercise= TRUE, exercise.eval= FALSE, include=!is_static} byDestAndCarrier <- group_by(flights, dest, carrier) summarise(byDestAndCarrier, averageDistance = mean(distance)) ``` ```{r ex10-player-solution, include=!is_static} flights %>% group_by(dest, carrier) %>% summarise(averageDistance = mean(distance)) ``` ```{r ex09-static, include=FALSE, echo=FALSE} flights %>% group_by(dest, carrier) %>% summarise(averageDistance = mean(distance)) ```

## Data Visualisation - Before plotting a graph it is often useful to employ some data manipulation techniques on a dataframe. - This allows us to create plots that are more specific which can aid us in data analysis. - This section should help to consolidate what you have already learned while also incorporating the new techniques from this week. ### Example Look at the following code. Do you understand what the functions are doing and what the resulting graph is representing? ```{r ex_player_pipe, exercise= TRUE, exercise.eval= FALSE} averageYearsActive <- playerData %>% filter(year_start > 1990) %>% mutate(years_active = year_end - year_start) %>% group_by(year_start) %>% summarise(meanYearsActive = mean(years_active)) ggplot(data = averageYearsActive) + geom_point(mapping = aes(x = year_start, y = meanYearsActive)) + labs(x= "First year", y = "Average years active") ``` ```{r ex_player_pipe-solution} # The plot shows the average years active vs the first year a player played # For players who begun playing after 1990 only # As you might expect, the average years active drops for players who begun after 2010 ``` #### Exercise 10 **Using `playerData` create a boxplot comparing a players position and their weight. Note: In some cases players have switched positions and therefore their position values are equal to `G-F`, `F-C` etc. Do not alter the values, simply consider `G-F` as a seperate group to `G` and `F`.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 10 Solution] ") ``` ```{r ex10-boxplot, exercise= TRUE, exercise.eval= FALSE, include=!is_static} ``` ```{r ex10-boxplot-solution, include=!is_static} ggplot(data = playerData, mapping = aes(x = position, y = weight)) + geom_boxplot() ``` ```{r ex10-solution-static, include=FALSE, echo=FALSE} ggplot(data = playerData, mapping = aes(x = position, y = weight)) + geom_boxplot() ``` #### Exercise 11 **Create a bar plot using the positions variable but only for players of height of 6-8. Colour the bars based on the position.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 11 Solution] ") ``` ```{r ex11-barplot, exercise=TRUE, exercise.eval=FALSE, include=!is_static} ``` ```{r ex11-barplot-solution, include=!is_static} playerData %>% filter(height == "6-8") %>% ggplot() + geom_bar(mapping = aes(x = position, fill = position)) ``` ```{r ex11-solution-static, include=FALSE, echo=FALSE} playerData %>% filter(height == "6-8") %>% ggplot() + geom_bar(mapping = aes(x = position, fill = position)) ``` #### Exercise 12 **Group `playerData` by `college` and find the minimum `year_start` for each college. Create a bar plot of the number of colleges for each minimum start year up to and including 1955.** ```{r, include=is_static, results='asis', echo=FALSE} cat("[Exercise 12 Solution] ") ``` ```{r ex13-barplot, exercise=TRUE, exercise.eval=FALSE, include=!is_static} ``` ```{r ex13-barplot-solution, include=!is_static} playerData %>% group_by(college) %>% summarise(minYear = min(year_start)) %>% filter(minYear <= 1955) %>% ggplot() + geom_bar(mapping = aes(x = minYear)) ``` ```{r ex12-solution-static, include=FALSE, echo=FALSE} playerData %>% group_by(college) %>% summarise(minYear = min(year_start)) %>% filter(minYear <= 1955) %>% ggplot() + geom_bar(mapping = aes(x = minYear)) ``` For more information and examples on the functions used in this weeks tutorial and how to incorporate them in graphs, read the [data transformation](https://r4ds.had.co.nz/transform.html) and the [exploratory data analysis](https://r4ds.had.co.nz/exploratory-data-analysis.html) chapters from the [R for Data Science](http://r4ds.had.co.nz/index.html) book.

```{r, include=is_static, results='asis', echo=FALSE} cat("# Solutions") cat("\n### Exercise 1 Solution") ``` ```{r ex01-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 1][Exercise 1] \n") cat("\n### Exercise 2 Solution") ``` ```{r ex02-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 2][Exercise 2] \n") cat("\n### Exercise 3 Solution") ``` ```{r ex03-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 3][Exercise 3] \n") cat("\n### Exercise 4 Solution") ``` ```{r ex04-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 4][Exercise 4] \n") cat("\n### Exercise 5 Solution") ``` ```{r ex05-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 5][Exercise 5] \n") cat("\n### Exercise 6 Solution") ``` ```{r ex06-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 6][Exercise 6] \n") cat("\n### Exercise 7 Solution") ``` ```{r ex07-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 7][Exercise 7] \n") cat("\n### Exercise 8 Solution") ``` ```{r ex08-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 8][Exercise 8] \n") cat("\n### Exercise 9 Solution") ``` ```{r ex09-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 9][Exercise 9] \n") cat("\n### Exercise 10 Solution") ``` ```{r ex10-solution-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 10][Exercise 10] \n") cat("\n### Exercise 11 Solution") ``` ```{r ex11-solution-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 11][Exercise 11] \n") cat("\n### Exercise 12 Solution") ``` ```{r ex12-solution-static, include=is_static, eval=is_static} ``` ```{r, include=is_static, results='asis', echo=FALSE} cat("[Back to Exercise 12][Exercise 12] \n") ```