## Learning goals {.unnumbered .smaller} 

After this lesson, you should be able to:


- Write a `for` loop in R to handle repeated tasks
    - Use `seq_along()` and `seq_len()` to iterate over vectors or rows of a data frame
    - Store output from a `for` loop in a list or other storage container
- Recognize that vectorized functions help us avoid `for` loops in many situations
- Use the `across()` function to wrangle multiple variables simultaneously

<br><br><br><br>

## Iteration {.unnumbered .smaller} 

When we want to reuse code, we can use:

- functions to save the steps of the task
- iteration to apply that function to new inputs

. . .

Let's talk about iteration in R.

- Iterating using a `for` loop & how to do it appropriately in R (today)
- Iterating `across` data frame columns (today)
- Iterating using `map` and `walk` for iteration, instead of a `for` loop (next class)

## Purposes of Iteration {.unnumbered .smaller} 

**Creating or saving a new object**

- Performing an action with a return output
- Ex. creating new variables/datasets, fitting models, etc.
- We'll need to consider storage (memory allocation).
- Next class, we'll use `map()` for this...

. . .

**Side effects**

- Performing an action without a R return output
- Ex: saving data using `write_csv()` or saving a plot with `png()`
- We don't need to worry about storage in R.
- We could use `walk()` for this...




## `for` loops {.unnumbered .smaller} 

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

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

. . .

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

- An **indexing** integer vector: `1:3` [Most common!]
- A character vector: `c("group1", "group2", "group3")`
- A vector of any other class

```{r for_examples}
groups <- c("group1", "group2", "group3") # a vector to iterate over
 
for (i in 1:3) { # i: index
    print(groups[i])
}

for (g in groups) { # iterating over the vector
    print(g)
}
```

. . .

To iterate over an **indexing** vector, it is useful to know about the following two functions:

- `seq_along(v)`: generates an integer sequence from 1 to the length of the vector `v` supplied
- `seq_len(n)`:  generates an integer sequence from 1 to `n`


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

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

. . .

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}
no_groups <- c()
seq_along(no_groups)
for (i in seq_along(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) { # each element is the column 
    print(col)
}

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

. . .

**Indexing** is particularly important when we want to create/save new objects as we need to store output created during a `for` loop. 

::: {.callout-tip appearance='simple' icon=false}
When using a for loop to create/save output, we should create storage containers ahead of time with the `vector()` function. This allocates memory for the output and is a more efficient use of memory. 
:::

```{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
```

### `for` loops and memory allocation {.unnumbered .smaller}   

If you don't create a storage container ahead of time, you will regret it. Let's bench mark our code to see the difference!

```{r}
# Example 1: No storage container; append new value to existing vector
x <- integer()
bench::mark(
  for (i in 1:1e5) {
    x <- c(x, i)
  }
)

# Example 2: Using a storage container!
#Notice median (time to run), mem_alloc (how much memory you are using), and gc (garbage collection)
x <- vector('integer', length = 1e5)
bench::mark(
  for (i in 1:1e5) {
    x[i] <- i
  }
)


# Example 3: Vectorized function
#Notice median (time to run), mem_alloc (how much memory you are using), and gc (garbage collection)
bench::mark(
  x <- seq(1, 1e5, by = 1)
)

```

## Exercises {.unnumbered .smaller} 

**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
    - Then, produce the same output with the `seq()` function
    
```{r}    
# for loop



# vectorized approach with `seq()`

```
    
2. Iterates over the `month.name` vector and stores a character vector of output containing strings like "Month 1: January", "Month 2: February".
    - Then, produce the same output with `str_c()` only.
    
```{r}    
# for loop



# vectorized approach with `str_c()`
```

3. On the `diamonds` dataset, fit stratified models of price vs. carat, model separately estimated for each value of `cut`, and store the fitted models in a list storage container.

```{r}    
# for loop




```



<br><br><br><br>


## Iteration `across` data frame columns {.unnumbered .smaller} 

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))
```

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


# Anonymous functions can be written in a more concise way
# function(x){ length(unique(x)) } OR ~ length(unique(.x))
# function(x,y){ length(unique(x + y)) } OR ~ length(unique(.x + .y))
```

. . .

<br><br>

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.

. . .

<br><br>

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)
```



## Exercises {.unnumbered .smaller} 

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").

```{r}

```

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.`

```{r}

```



<br><br><br><br>



## Case study: performing many different versions of an analysis {.unnumbered .smaller} 

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 iteration in our toobox, we can iterate the `fit_model()` function over the combinations in `df_arg_combos`.

## Exercises {.unnumbered .smaller} 

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

- 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`
- 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. As you work, make note of what is challenging and any helpful thought processes/strategies that arise from the collaboration.

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()`.)

```{r}

```

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)`.

```{r}

```

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

```{r eval=FALSE}
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()
}
```




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]`

```{r}

```
## Done!

- Render your notes.
- Check the solutions in the course website (Solution drop downs).
- If you finish all that during class, start your homework!


