```{r echo=FALSE, message=FALSE}
library(tidyverse)
```

# Learning goals

After this lesson, you should be able to:

- Use the `across()` function to wrangle multiple variables simultaneously
    - Compare `across()` to an approach with `pivot_longer()` and `pivot_wider()`
- Write a `for` loop in R to handle repeated tasks
- Use the `map()` family of functions in the `purrr` package to handle repeated tasks




# Iteration across data frame columns with `across()`

Often we will have to perform the same data wrangling on many variables (e.g., rounding numbers)

```{r no_across}
diamonds %>%
    mutate(
        carat = round(carat, 1),
        x = round(x, 1),
        y = round(y, 1),
        z = round(z, 1)
    )
```

`dplyr` provides the `across()` function for performing these repeated function calls:

```{r with_across}
# Option 1: Create our own named function
round_to_one <- function(x) {
    round(x, digits = 1)
}
diamonds %>% 
    mutate(across(.cols = c(carat, x, y, z), .fns = round_to_one))

# Option 2: Use an "anonymous" or "lambda" function that isn't named
diamonds %>% 
    mutate(across(.cols = c(carat, x, y, z), .fns = function(x) {round(x, digits = 1)} ))
```

When we look at the documentation for `across()`, we see that the `.cols` argument specifies which variables we want to transform, and it has a `<tidy-select>` tag. This means that the syntax we use for `.cols` follows certain rules. Let's click this link to explore the possibilities for selecting variables.

- Read through the "Overview of selection features" section to get an overall sense of the many ways to select variables.
- Navigate back to the `across()` documentation and read through the Examples section at the bottom. Click the "Run examples" link to view the output for all of the examples.


## Exercises

(Not a pair programming exercise, but check in with each other as you work)

Using the `diamonds` dataset:

1. Transform the `x`, `y`, and `z` columns so that the units of millimeters are displayed (e.g., "4.0 mm").
2. Convert all numeric columns into character columns.
    - Hint: type `is.` and hit Tab in the Console. Scroll through the function options. Do the same with `as.`


----------


What if we wanted to perform multiple transformations on each of many variables? 

Within the different values of diamond `cut`, let's summarize the mean, median, and standard deviation of the numeric variables. When we look at the `.fns` argument in the `across()` documentation, we see that we can provide a **list** of functions:

```{r across_many_fns}
diamonds %>%
    group_by(cut) %>% 
    summarize(across(.cols = where(is.numeric), .fns = list(mean = mean, med = median, sd = sd)))
```

What does the list of functions look like? What is the structure of this list object?

```{r list_str}
list_of_fcts <- list(mean = mean, med = median, sd = sd)
list_of_fcts
str(list_of_fcts)
```

We'll be working more with lists soon.




# `for` loops

In R, `for` loops have the following general structure:

```{r for_syntax, eval=FALSE}
for (i in some_vector) {
    # Code to do stuff with i
}
```

`some_vector` can be **any** vector, including:

- An indexing vector: `1:3`
- A character vector: `c("group1", "group2", "group3")`
- A vector of any other class

```{r for_examples}
groups <- c("group1", "group2", "group3")

for (i in 1:3) {
    print(groups[i])
}

for (g in groups) {
    print(g)
}
```

The `seq_along()` function generates an integer sequence from 1 to the length of the vector supplied. A nice feature of `seq_along()` is that it generates an empty iteration vector if the vector you're iterating over itself has length 0.

```{r for_seq_along}
seq_along(groups)
no_groups <- c()
seq_along(no_groups)

for (i in seq_along(groups)) {
    print(groups[i])
}

for (i in seq_along(no_groups)) {
    print(no_groups[i])
}
```

Closely related to `seq_along()` is `seq_len()`. While `seq_along(x)` generates an integer sequence from 1 to `length(x)`, `seq_len(x)` takes `x` itself to be a length:

```{r for_seq_len}
seq_len(3)
seq_len(0)

for (i in seq_len(length(groups))) {
    print(groups[i])
}

for (i in seq_len(length(no_groups))) {
    print(no_groups[i])
}
```

`seq_len()` is useful for iterating over the rows of a data frame because `seq_along()` would iterate over columns:

```{r seq_len_df}
small_data <- tibble(a = 1:2, b = 2:3, c = 4:5)
for (col in small_data) {
    print(col)
}

for (r in seq_len(nrow(small_data))) {
    print(r)
}
```

Often we'll want to store output created during a `for` loop. We can create storage containers with the `vector()` function:

```{r store_results}
char_storage <- vector("character", length = 3)
num_storage <- vector("numeric", length = 3)
list_storage <- vector("list", length = 3)

char_storage
num_storage
list_storage

for (i in seq_len(3)) {
    char_storage[i] <- str_c("Number: ", i)
    num_storage[i] <- 2*i
    list_storage[[i]] <- i # Note the [[ for subsetting here
}

char_storage
num_storage
list_storage
```


## Exercises

**Pair programming exercises:** Whoever has most recently eaten dessert (broadly interpreted) should be the driver first. Switch after Exercise 2.

Write `for`-loops that do each of the following:

1. Prints the even numbers from 2:20
    - Produce the same output with the `seq()` function
2. Iterates over the `month.name` vector and stores a character vector of output containing strings like "Month 1: January", "Month 2: February".
    - Produce the same output with `str_c()` only.
3. On the `diamonds` dataset, fit models of price vs. carat separately for each value of `cut`, and store the fitted models in a list storage container.




# Case study: performing many different versions of an analysis

One of my most common use cases for writing functions and iteration/looping is to perform some exploration or modeling repeatedly for several different "settings". For example, our broad goal might be to fit a linear regression model to our data. However, there are often multiple choices that we have to make in practice:

- Keep missing values or fill them in (imputation)?
- Fit the model only on a certain group of cases?
- Filter out outliers in one or more variables?
- Transform certain variables? (e.g., log transformation)

We can map any number of these choices to **arguments** in a custom model-fitting function:

- `impute`: TRUE or FALSE
- `filter_to`: This could be a string description like "All cases", "Group 1", or "Groups 1 and 2"

```{r fit_model_example}
fit_model <- function(data, impute, filter_to) {
    if (impute) {
        data <- some_imputation_function(data)
    }
    
    if (filter_to=="Group 1") {
        data <- data %>% 
            filter(group==1)
    } else if (filter_to=="Groups 1 and 2") {
        data <- data %>% 
            filter(group %in% c(1,2))
    }
    
    lm(y ~ x1 + x2, data = data)
}
```

The `tidyr` package has a useful function called `crossing()` that is useful for generating argument combinations. For each argument, we specify all possible values for that argument. `crossing()` generates all combinations:

```{r}
df_arg_combos <- crossing(
    impute = c(TRUE, FALSE),
    filter_to = c("All cases", "Group 1", "Groups 1 and 2")
)
df_arg_combos

# Another example
crossing(
    option1 = 1:3,
    option2 = c(TRUE, FALSE),
    option3 = c("low", "medium", "high")
)
```

With loops in our toobox, we can iterate the `fit_model()` function over the combinations in `df_arg_combos`.

## Exercises

**Goal:** In the `diamonds` dataset, we want to understand the relationship between `price` and size (`carat`). We want to explore variation along two choices:

1. The variables included in the model. We'll explore 3 sets of variables:
    - No further variables (just `price` and `carat`)
    - Adjusting for `cut`
    - Adjusting for `cut` and `clarity`
    - Adjusting for `cut`, `clarity`, and `color`
2. Whether or not to remove outliers in the `carat` variable. We'll define outliers as cases whose `carat` is over 3 SDs away from the mean.

Work with your partner on the following exercises (not in a pair-programming fashion). As you work, make note of what is challenging and any helpful thought processes/strategies that arise from the collaboration.

**Exercise 1:** Use `crossing()` to create the data frame of argument combinations for our analyses. Call it `df_arg_combos`. Note that you can create a list of **formula** objects in R with `c(y ~ x1, y ~ x1 + x2)`. (Something like this will be the right hand side of an argument to `crossing()`.)



**Exercise 2:** Write a function called `remove_outliers` that removes outliers in a dataset. The user should be able to supply the dataset (`data`), the variable to remove outliers in (`what_var`), and a threshold on the number of SDs away from the mean used to define outliers (`sd_thresh`). Write your function so that it runs as follows: `remove_outliers(diamonds, what_var = carat, sd_thresh = 3)`.



**Exercise 3:** Write a function called `fit_model` that implements the different settings for our `diamonds` analysis.

```{r}
fit_model <- function(data, mod_formula, remove_outliers) {
    # remove_outliers is a TRUE/FALSE flag of whether or not to remove outliers
    # This function implements our specific use case: outliers are cases that are 3 SDs away from the mean for the carat variable
    
    # Use mod_formula as the first argument of lm()
}

fit_model(data = diamonds, mod_formula = price ~ carat, outlier_var = carat, )
```



**Exercise 4:** Write a `for` loop that stores the fitted linear models from all versions of the analysis.

Recall that you can pull out the contents of a single data frame column in many ways. For a data frame `df` with a variable named `x`:

- `df$x`
- `df %>% pull(x)`
- `df[["x"]]`

Note that in `df_arg_combos`:

- `mod_formula`: this column is a **list** and you can extract the 1st element with `[[1]]`
- `remove_outliers`: this column is a **vector** and you can extract the 1st element with `[1]`


