---
title: "Conditionals, Loops, and Batch Processing"
subtitle: "Basics"
author: "Yue Hu"
institute: "Learning R with Dr. Hu and His Friends Workshop Series"

webr:
  packages:
    - drhur
    - drhutools
    - dplyr
    - tidyr
    - purrr
    - ggplot2
---

{{< include _extensions/r-wasm/live/_knitr.qmd >}}

# Conditionals, Loops, and Batch Processing {#sec-loop .smaller}

::: {.callout-note title="Chapter Highlights"}

1. Conditional judgment and assignment
2. Explicit and implicit loops

*Research question*: Do people from different income strata differ in their views on equality?

:::

---

- Repetitive tasks (cleaning 500 surveys, computing by province, organizing dozens of references) are time-consuming and error-prone when done by hand
- Letting R do repetitive dirty work requires two skills:
    - **Teach R to choose**: conditional statements
    - **Teach R to repeat**: loops and batch processing
- Learning order: conditionals → loops → batch processing

::: {.callout-important title="Loops vs. Batch Processing"}

- **Loop**: a control structure that repeatedly executes the same block of code, handling one object at a time with full involvement throughout (like filling out delivery slips one by one)
- **Batch processing**: an execution mode that packages many tasks for the computer to complete automatically, without per-item intervention (like handing all the parcels to the courier company)

:::

## Conditional Statements

- We constantly "make choices" in everyday life: bring an umbrella when it rains, decide whether 89 counts as a good grade
- Conditional judgment = distinguishing situations and making choices
- It is the foundation of loops, determining when a loop begins and when it ends
- And the foundation of conditional judgment is logical judgment

## Logical Judgment

- Logical judgment: expressions that produce TRUE/FALSE, on which R decides which block to execute
- Introduced in the algebra chapter; reviewed here

```{webr}
#| label: logicalJudge

# Load data
data("wvs7", package = "drhur")

1 + 1 != 2

# Check whether each element of the vector is a missing value NA
is.na(c(1, 2, NA, 3))

# Check whether the `female` variable in the wvs7 dataset is numeric
is.numeric(wvs7$female)
```

- `1 + 1 != 2` → FALSE; `is.na()` checks missingness element-wise; `is.numeric()` checks type

## Single-condition Judgment

- Only one condition → use `if`, meaning "if... then... else..."
- Single-line commands can be shortened: `if (<logical>) <command>`

```r
if (<logical condition>) {
  <command executed when condition is met>
} else {
  <command executed otherwise>
}
```

---

- Researchers often group income before analysis (to reduce measurement error and improve validity)
- Same objective income can receive different subjective ratings (measurement noise); grouping merges adjacent brackets
- First, dichotomize into "low/high income" by the median; try it on the first respondent:

```{webr}
#| label: incomeBinary

# Compute the median income; remember to exclude missing values
median_income <- median(wvs7$incomeLevel, na.rm = TRUE)

# Judge for the first respondent
if (wvs7$incomeLevel[1] <= median_income) {
  wvs7$income_binary[1] <- "Low income"
} else {
  wvs7$income_binary[1] <- "High income"
}

# Inspect the assignment
wvs7$income_binary[1]
```

---

::: {.callout-important title="One-at-a-time judgment is a key feature of conditional statements"}

- The logical condition of `if()` accepts only a **single** TRUE/FALSE
- Feed in a whole vector → R uses only the first element and issues a warning/error
- Fix: use a loop (later) or vectorized functions `ifelse()`/`case_when()`

:::

```{webr}
#| label: ifBinaryError

if (wvs7$incomeLevel <= median_income) {
  wvs7$income_binary <- "Low income"
} else {
  wvs7$income_binary <- "High income"
}
```

## Multi-condition Judgment

- Binary is too coarse: when the median = 5, values 1 and 5 land in the same group despite the gap being far larger than between 5 and 6
- Solution: split into more groups (high/medium/low) by chaining `if - else if - else`

```r
if (<condition 1>) {
  <command 1>
} else if (<condition 2>) {
  <command 2>
} else if (<condition 3>) {
  <command 3>
} else {
  <command when none holds>
}
```

---

- Groups: low (≤3) / medium (>3 and ≤7) / high (>7); try it on the first respondent:

```{webr}
#| label: incomeTriple

# Check the first respondent:
if (wvs7$incomeLevel[1] <= 3) {
  # If income <= 3, classify as low income
  wvs7$incomeCat3[1] <- "Low income"
} else if (wvs7$incomeLevel[1] <= 7) {
  # If income <= 7, classify as medium income
  wvs7$incomeCat3[1] <- "Medium income"
} else {
  # Otherwise, classify as high income
  wvs7$incomeCat3[1] <- "High income"
}

# Inspect the result
wvs7$incomeCat3[1]
```

## Vectorized Conditional Operations

- `if-else` judges one value at a time; research often requires checking every element of a vector (e.g., 1264 observations)
- Vectorization: apply the same operation to each element of a vector → `ifelse()`

```r
ifelse(<condition>, <value if TRUE>, <value if FALSE>)
```

```{webr}
#| label: ifelseIncome

# Use ifelse to judge the entire income vector
wvs7$income_high <- ifelse(wvs7$incomeLevel > median_income,
                            "High income",
                            "Low income")

# Inspect classification results, also showing potential missing values
table(wvs7$income_high, useNA = "ifany")
```

---

::: {.callout-warning title="`NA` in conditional statements"}

- Any comparison involving `NA` (`>`, `<`, `==`) returns `NA`, not TRUE/FALSE
- `if()` will error if the condition is `NA`; use `is.na()` to test for missingness
- `ifelse()` automatically sets the output to `NA` when the condition is `NA` (vectorized approach is recommended)

:::

```{webr}
#| label: naInConditions

# Logical comparisons involving NA always return NA
NA > 5
NA <= 3
NA == NA

# To test whether a value is NA, use is.na()
is.na(NA)
```

---

- `if()` errors out directly when the condition is `NA`:

```{webr}
#| label: naInIf

if (NA > 5) {
  print("Greater than 5")
}
```

---

- Multiple conditions can be handled by nesting `ifelse()`; logic is the same as chained `else if()`, but deep nesting becomes hard to read

```{webr}
#| label: ifelseNested

# Nested ifelse for a three-way classification
wvs7$incomeCat3_ifelse <- ifelse(
  wvs7$incomeLevel <= 3,
  "Low income",
  ifelse(
    wvs7$incomeLevel > 7,
    "High income",
    ifelse(is.na(wvs7$incomeLevel), NA, "Medium income")
  )
)

# Inspect the result
table(wvs7$incomeCat3_ifelse, useNA = "ifany")
```

---

- `dplyr::case_when()` is clearer: use `~` to link condition and assigned value
- Advantage: correspondences are easy to see; adding or removing a condition is just adding or removing a line, no parenthesis counting

```r
case_when(
  <condition 1> ~ <value 1>,
  <condition 2> ~ <value 2>,
  ...)
```

```{webr}
#| label: caseWhenIncome

wvs7$incomeCat3 <- case_when(
  wvs7$incomeLevel <= 3 ~ "Low income",      # low-income group
  wvs7$incomeLevel <= 7 ~ "Medium income",   # medium-income group
  wvs7$incomeLevel > 7 ~ "High income"       # high-income group
)

table(wvs7$incomeCat3, useNA = "ifany")
```

---

- With income groups in hand, we can preliminarily compare their views on equality
- Combine with `group_by()` to compute group means:

```{webr}
#| label: incomeEqualityTable

# Compute mean equality attitudes by income group
wvs7 |>
  group_by(incomeCat3) |>
  summarise(
    mean_equalIncentive = mean(equalIncentive, na.rm = TRUE)
  )
```

- Equality attitudes differ across income groups, echoing the visualization trends from the previous chapter

## Practice {.unnumbered}

**Question 1**: Multi-way group analysis

Equality attitudes may be jointly affected by income, gender, education, etc. Use `group_by()` with multiple grouping variables to complete two tasks, and try to interpret the sociological meaning:

- Task A: Group jointly by `incomeCat3` and `female`; compute the mean equality attitude and sample size for each group
- Task B: First create a binary "higher education / non-higher education" variable using a conditional statement (cutoff = 6), then examine how it combines with `incomeCat3` to shape equality attitudes

::: { .hint exercise="conditionalGroupExercise" }

- `group_by(incomeCat3, female)` enables multi-way grouping
- Use `mean(..., na.rm = TRUE)` and `n()` inside `summarise()`
- Binary variable: `ifelse(education > 6, "Higher education", "Non-higher education")`
- Use `drop_na()` to remove rows missing on the grouping variables before grouping

:::

```{webr}
#| label: conditionalGroupExercise
#| exercise: conditionalGroupExercise

library(dplyr)
data("wvs7", package = "drhur")

# Create the three-way income grouping incomeCat3 first (preparation)
wvs7$incomeCat3 <- case_when(
  wvs7$incomeLevel <= 3 ~ "Low income",
  wvs7$incomeLevel <= 7 ~ "Medium income",
  wvs7$incomeLevel > 7 ~ "High income"
)

# Complete Task A and Task B here

```

::: { .solution exercise="conditionalGroupExercise" }

```r
# Examine the combined effect of income and gender
wvs7 |>
  drop_na(incomeCat3, female) |>
  group_by(incomeCat3, female) |>
  summarise(
    mean_eq = mean(equalIncentive, na.rm = TRUE),
    n = n()
  ) |>
  arrange(incomeCat3, female)

# Examine the combined effect of income and education
wvs7$edu_binary <- ifelse(wvs7$education > 6, "Higher education", "Non-higher education")

wvs7 |>
  drop_na(incomeCat3, edu_binary) |>
  group_by(incomeCat3, edu_binary) |>
  summarise(
    mean_eq = mean(equalIncentive, na.rm = TRUE),
    n = n()
  ) |>
  arrange(incomeCat3, edu_binary)

# Interpretation:
# - Gender: within the same stratum, women have slightly lower mean_eq than men
#   (more inclined toward equality of outcome)
# - Education: within each stratum, those with higher education lean more toward
#   equality of opportunity; the pattern is especially strong among low-income +
#   highly educated respondents (the "highly educated, low income" group's strong
#   desire for fair competitive opportunities)
```

:::

## Loops {#sec-loop-basics}

- Core idea of a loop: have R repeatedly execute the same block of code until a stopping condition is met
- Example: drawing a plot separately for 45 countries — repeating by hand 45 times is tedious and error-prone
- Two main kinds: `for` loops and `while` loops

## for loop: a known number of iterations

- Use when "I know how many times to repeat"
- Hand R a to-do list; it takes items in order, executes each, and moves to the next

```r
for (<index variable> in <sequence>) {
  <code to execute>
}
```

- The index variable (conventionally `i`, `j`) takes one value of the sequence each iteration; the code inside `{}` is repeated

---

- Demo: "dancing in chains" — using only single-value `if()` together with `for` to split income into three groups

```{webr}
#| label: forIncomeCat

# Check the data size
length(wvs7$incomeLevel)

# Use a for loop to process each observation
for (i in 1:nrow(wvs7)) {  # i goes from 1 to 1264, one value at a time
  if (is.na(wvs7$incomeLevel[i])) {
    wvs7$incomeCat3_loop[i] <- NA
  } else if (wvs7$incomeLevel[i] <= 3) {
    wvs7$incomeCat3_loop[i] <- "Low income"
  } else if (wvs7$incomeLevel[i] <= 7) {
    wvs7$incomeCat3_loop[i] <- "Medium income"
  } else {
    wvs7$incomeCat3_loop[i] <- "High income"
  }
}

# Inspect the loop result
table(wvs7$incomeCat3_loop, useNA = "ifany")

# Verify that the loop result matches the earlier case_when result exactly
identical(wvs7$incomeCat3_loop, wvs7$incomeCat3)
```

- `identical()` confirms the loop matches `case_when()`; but `case_when()` is more concise
- Loops are better suited to non-vectorizable tasks (reading files, plotting, saving results)

## while loop: an unknown number of iterations

- Use when "I don't know how many times to repeat, but I know when to stop"
- Check the condition first; if TRUE, execute; then check again... until the condition becomes FALSE

```r
while (<condition>) {
  <code to execute>
}
```

---

- Demo: random sampling until we collect 10 high-income respondents

```{webr}
#| label: whileSample

# Initialize an empty vector to store sampling results
set.seed(313)
high_income_sample <- numeric()
attempts <- 0

# Keep sampling as long as we have fewer than 10
while (length(high_income_sample) < 10) {
  idx <- sample(1:nrow(wvs7), 1)
  attempts <- attempts + 1

# Check whether this respondent belongs to the "High income" group and is non-missing
  if (!is.na(wvs7$incomeCat3[idx]) && wvs7$incomeCat3[idx] == "High income") {
    high_income_sample <- c(high_income_sample, wvs7$equalIncentive[idx])
  }
}

# Inspect the final sample
high_income_sample
```

---

- Every `for` loop can be rewritten as an equivalent `while` loop

```{webr}
#| label: whileIncomeCat

# Initialize the counter i
i <- 1

# Continue as long as i is no greater than the total number of rows
while (i <= nrow(wvs7)) {
  if (is.na(wvs7$incomeLevel[i])) {
    wvs7$incomeCat3_while[i] <- NA
  } else if (wvs7$incomeLevel[i] <= 3) {
    wvs7$incomeCat3_while[i] <- "Low income"
  } else if (wvs7$incomeLevel[i] <= 7) {
    wvs7$incomeCat3_while[i] <- "Medium income"
  } else {
    wvs7$incomeCat3_while[i] <- "High income"
  }

# Manually update the counter, otherwise the loop runs forever!
  i <- i + 1
}

# Verify the result
table(wvs7$incomeCat3_while, useNA = "ifany")
identical(wvs7$incomeCat3_while, wvs7$incomeCat3)
```

---

:::{.callout-warning title="while-loop pitfalls"}

- Most common mistake: forgetting to update the condition variable → infinite loop!
- If you find yourself in an infinite loop: press `Esc` to interrupt
- A "safety valve" can be added; `&&` ensures a single logical value and short-circuits when the left side is FALSE

:::

```r
maxIter <- 1000
iter <- 0

while (<condition> && iter < maxIter) {
  <code to execute>
  iter <- iter + 1
}

if (iter == maxIter) warning("Maximum number of iterations reached; loop forcibly terminated")
```

## repeat loop: execute first, then judge

- `repeat`: keep executing unconditionally until a `break` is hit
- Difference from `while`: execute once first, then decide whether to continue

```r
repeat {
  <code to execute>
  if (some condition) break  # exit when met
}
```

---

- Rewrite the earlier `while` sampling example with `repeat`:

```{webr}
#| label: repeatSample

set.seed(313)

# Initialize the container
high_income_sample2 <- numeric()
attempts2 <- 0

# Start the repeat loop
repeat {
  # Random sampling
  idx <- sample(1:nrow(wvs7), 1)
  attempts2 <- attempts2 + 1

  # Check the condition
  if (!is.na(wvs7$incomeCat3[idx]) && wvs7$incomeCat3[idx] == "High income") {
    high_income_sample2 <- c(high_income_sample2, wvs7$equalIncentive[idx])
  }

  # Once we have 10, break out of the loop
  if (length(high_income_sample2) >= 10) break
}

high_income_sample2

identical(high_income_sample2, high_income_sample)
```

## `break` and `next`: loop control

- `break`: terminates the entire loop
- `next`: skips the current iteration and proceeds to the next
- Both work in all loop types

- `break` example: stop and alert the researcher when 5 consecutive missing values appear

```{webr}
#| label: breakDemo

missing_count <- 0
wvs7$data_check <- NA

for (i in 1:nrow(wvs7)) {
  # Check the current row: are income and equality attitudes both missing?
  if (is.na(wvs7$incomeLevel[i]) && is.na(wvs7$equalIncentive[i])) {
    missing_count <- missing_count + 1

    # If 5 consecutive missing values are reached, trigger break
    if (missing_count >= 5) {
      cat("Warning: at observation", i, "we found", missing_count,
          "consecutive missing values. Data quality may be problematic; stopping!\n")
      break  # immediately terminate the entire loop
    }
  } else {
    missing_count <- 0  # reset the counter

    # Mark observations with complete data
    wvs7$data_check[i] <- "Complete"
  }
}

wvs7$data_check |>
  table(useNA = "ifany")
```

---

- `next` example: find the first 10 high-income respondents with complete data (like a filtering funnel)

```{webr}
#| label: nextDemo

# Counter for successfully found samples
count <- 0

for (i in 1:nrow(wvs7)) {
  # Gate 1: if income is missing, skip this iteration (next)
  if (is.na(wvs7$incomeLevel[i])) {
    next
  }
  # Gate 2: if equality attitude is missing, skip
  if (is.na(wvs7$equalIncentive[i])) {
    next
  }
  # Gate 3: if income is not high (<=7), skip
  if (wvs7$incomeLevel[i] <= 7) {
    next
  }
  # Passed all gates: increment counter and print info
  count <- count + 1
  cat("No.", count, ": income level =", wvs7$incomeLevel[i],
      ", equality attitude =", wvs7$equalIncentive[i], "\n")

  # Stop once we have 10
  if (count >= 10) break
}
```

## Batch Processing

- Loops are powerful but cumbersome to write: initialization, indexing, and value extraction are all manual and error-prone
- Nested loops turn code into a maze that is hard to read
- R offers batch-processing functions: the native `*apply()` family and the `purrr` package
- Just tell R "apply this to every element of the list" and it loops and returns tidy results
- We focus on common methods in `dplyr` and `purrr`

## Batch processing in `dplyr`

- `across()`: batch process **columns**, often used with `mutate()`

```r
mutate(<data>, across(<selected variables>, <function or code>))
```

- Example: standardize variables (z-score) to remove differences in scale
- Standardized variable = (raw variable − mean) / standard deviation

---

```{webr}
#| label: acrossScale

# Standardize all numeric variables
wvs7_std <- mutate(wvs7, across(where(is.numeric),
                \(x) (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE),
                .names = "{.col}_scaled"))

# View part of the standardized result
select(wvs7_std, incomeLevel, incomeLevel_scaled, equalIncentive, equalIncentive_scaled, age, age_scaled) |>
  head()
```

- `where(is.numeric)` selects only numeric columns; `.names = "{.col}_scaled"` auto-names the new variables

---

- `if_any()`/`if_all()`: filter **rows** based on multiple columns, typically used with `filter()`
- `if_any`: "OR" logic — keep rows where any column meets the condition; `if_all`: "AND" logic — keep rows where all columns meet it

```r
filter(<data>, if_any(<selected variables>, <condition function>))
filter(<data>, if_all(<selected variables>, <condition function>))
```

```{webr}
#| label: ifAnyAll

# Find respondents with extreme values on income or equality attitudes
wvs7 |>
  filter(if_any(c(incomeLevel, equalIncentive), \(x) x == 10)) |>
  select(country, incomeLevel, equalIncentive) |>
  head()

# Find respondents whose income and equality attitudes are both 10
wvs7 |>
  filter(if_all(c(incomeLevel, equalIncentive), \(x) x == 10)) |>
  select(country, incomeLevel, equalIncentive) |>
  head()
```

## Batch Processing with `purrr`

- `dplyr` functions act only on data frames/lists; for general tasks (reading files, fitting models, plotting), use `purrr`'s `map` family
- `map(<target>, {<code>})`: apply code to each element and return a list of results
- "map" means mapping — each element gets handled and a new object is produced

```r
map(<target>, {<code>})
```

---

- With a `for` background, learning `map` is easy; first compute mean equality attitudes per income group using `for`

```{webr}
#| label: forBasic

# Get all distinct income-group names
incomeGroups <- unique(wvs7$incomeCat3) |> na.omit()
df_equalMean_for <- vector("list", length = length(incomeGroups))

# Loop: compute group by group
for (i in seq_along(incomeGroups)) {
  group_data <-  filter(wvs7, incomeCat3 == incomeGroups[i])
  df_equalMean_for[[i]] <- mean(group_data$equalIncentive, na.rm = TRUE)
}

# Name the list elements for easier inspection
names(df_equalMean_for) <- incomeGroups
df_equalMean_for
```

---

- Same task with `map()`: `split()` to partition + `map()` to apply

```{webr}
#| label: mapBasic

# Split: partition the data by income group into a list
df_equalMean_map <- split(wvs7, ~ incomeCat3) |>
  # Map: compute the mean for each sub data frame (aGroup) in the list
  map(\(aGroup) mean(aGroup$equalIncentive, na.rm = TRUE))

df_equalMean_map
```

- Compare: `for` requires a container, indexing, and manual naming; `map` does it in one line

---

::: {.callout-tip title="Output types"}

- `map()` returns a list by default
- For other types use variants: `map_dbl()` / `map_chr()` / `map_lgl()` / `map_dfr()`
- To simplify an existing list, use `list_c()`
- `walk()`: like `map` but returns `NULL`; use for side effects like saving files, plotting, or printing

:::

```{webr}
#| label: mapList

# Check the class of the object returned by map
class(df_equalMean_map)
```

---

```{webr}
#| label: mapVec

# Convert the list into a numeric vector
df_equalMean_map_vec <- df_equalMean_map |>
  list_c()

df_equalMean_map_vec
class(df_equalMean_map_vec)
```

- Advanced `map`: iterating over multiple objects simultaneously
- `map2()` iterates over two equal-length objects ("dual track", like two parallel rails)

```r
map2(<object 1>, <object 2>, {<code>})
```

---

- Example: draw a scatter + fitted line per income group, each with a different color

```{webr}
#| label: map2Demo

# Define three colors, one for each income group
colors <- drhutools::gb_cols("gold", "black", "red")

# Iterate over the data list and the color list together
split(wvs7, ~ incomeCat3) |>
  map2(colors, \(data, col) {
  # Use the current data (data) and the corresponding color (col) to plot
  ggplot(data, aes(x = incomeLevel, y = equalIncentive)) +
    geom_point(color = col, alpha = 0.5) +
    geom_smooth(method = "lm", color = col) +
    labs(title = ,
    x = paste0(unique(data$incomeCat3), " group income level"),
    y = "Equality attitude")
})
```

- Note: `title` must be placed inside `labs()`; the shortcut above triggers a warning and the title is not displayed

---

- Three or more objects use `pmap()` (p = parallel)

```r
pmap(<list of objects>, {<code>})
```

```{webr}
#| label: pmapDemo

# Build a parameter list: data, colors, and titles as three parallel vectors
params <- list(
  data = split(wvs7, ~ incomeCat3),
  color = colors,
  title = paste(na.omit(unique(wvs7$incomeCat3)), "group: income-equality relationship")
)

# Use pmap to process the three parameters in parallel
pmap(params, \(data, color, title) {
  ggplot(data, aes(x = incomeLevel, y = equalIncentive)) +
    geom_point(color = color, alpha = 0.5) +
    geom_smooth(method = "lm", color = color) +
    labs(title = title, x = "Income level", y = "Equality attitude")
})
```

## Debugging in Batch Processing

- In complex batch pipelines, a single bad observation (missing, wrong type) can halt the entire process
- Loops have an index variable (`i`, `j`) to locate errors; batch processing does not
- `purrr`'s "adverb" functions `safely()`/`possibly()`: wrap an action to produce a safer new action
- `safely()`: returns a list with `result` + `error` (errors do not interrupt; check `error` afterward)
- `possibly()`: returns a preset default on error (e.g., NA); more convenient when only the result matters

---

- First construct an error-throwing function for demonstration:

```{webr}
#| label: safelyError

# Define a function: error if any value is non-positive, otherwise take the square root
calc_sqrt <- function(x) {
  if (any(x <= 0)) {
    stop("Input must be all positive!")
  }
  sqrt(x)
}

# Construct data: the second element b contains negative numbers
num_list <- list(a = c(1, 4, 9), b = c(-1, 2, 3), c = c(16, 25, 36))

# Calling map directly raises an error and aborts, returning nothing
map(num_list, calc_sqrt)
```

---

- After wrapping with `safely()`, `map` no longer aborts and returns result/error per element

```{webr}
#| label: safelyDemo

# Wrap the original function with safely
safe_sqrt <- safely(calc_sqrt)

# Run map again — this time it won't abort
results <- map(num_list, safe_sqrt)

results
```

---

- `possibly()`: fills NA on error and keeps going

```{webr}
#| label: possiblyDemo

# Use possibly: on error, return NA_real_
possible_sqrt <- possibly(calc_sqrt, otherwise = NA_real_)

map(num_list, possible_sqrt)
```

---

:::: {style="font-size: 0.7em;"}

::: {.callout-tip title="The `apply` function family"}

- R ships with its own batch-processing `apply` family (apply / lapply / sapply / vapply / tapply / mapply)
- The structure is less uniform than `map`'s, but no extra package is needed; the family is common in textbooks and online
- One option: write `map` first, then ask an LLM to rewrite it as `apply`

:::

| Function | Basic syntax | Purpose | purrr counterpart |
| --- | --- | --- | --- |
| `apply()` | `apply(<data>, <row/col>, <code>)` | Apply over rows or columns of a matrix/data frame | Columns: `map()`; rows: `pmap()` |
| `lapply()` | `lapply(<data>, <code>)` | Apply per element; returns a list | `map()` |
| `sapply()` | `sapply(<data>, <code>)` | Like lapply but auto-simplifies the result | `map()` + `list_simplify()` |
| `vapply()` | `vapply(<data>, <code>, FUN.VALUE)` | Like lapply but with a pre-specified return type | `map()` + type spec |
| `tapply()` | `tapply(<data>, <group>, <code>)` | Apply over a vector by group | `map()` + `split()` |
| `mapply()` | `mapply(<code>, ...)` | Multi-input element-wise application | `map2()` / `pmap()` |

::::

## Practice {.unnumbered}

**Question 2**: Cross-country differences in the income–equality relationship (batch processing)

Does the relationship between income level and equality attitudes vary across countries (`country`)? Use the batch-processing tools introduced in this chapter to:

- Use a batch-processing function to compute one or two descriptive statistics **by country** and inspect the results
- Use a batch-processing function to generate scatter or linear-fit plots of the income–equality relationship for each country

::: { .hint exercise="batchExercise" }

- `split(wvs7, ~ country)` partitions the data into a list by country
- For statistics, use `map_dfr()` (which row-binds into a data frame; `.id = "country"` keeps the country name)
- For plots, use `map()` (plot objects can only live inside a list)

:::

```{webr}
#| label: batchExercise
#| exercise: batchExercise

library(dplyr)
library(purrr)
library(ggplot2)
data("wvs7", package = "drhur")

# Complete the by-country batch processing here

```

::: { .solution exercise="batchExercise" }

```r
# Step 1: compute mean income level and mean equality attitudes by country
wvs7_split <- wvs7 |> split(~country)

country_means <- map_dfr(wvs7_split, \(df) {
  tibble(
    mean_income = mean(df$incomeLevel, na.rm = TRUE),
    mean_equality = mean(df$equalIncentive, na.rm = TRUE)
  )
}, .id = "country")

country_means

# Step 2: compute the ratio of the two (income / equality)
country_means <- country_means |>
  mutate(ratio = mean_income / mean_equality)

country_means

# Step 3: draw a scatter plot of income vs equality attitudes for each country
country_plots <- map(wvs7_split, \(df) {
  country_name <- unique(df$country)

  ggplot(df, aes(x = incomeLevel, y = equalIncentive)) +
    geom_jitter(alpha = 0.3, width = 0.2, height = 0.2) +
    geom_smooth(method = "lm", se = TRUE, color = "steelblue") +
    labs(
      title = paste(country_name, ": income level vs equality attitudes"),
      x = "Income level (1-10)",
      y = "Equality attitude (1 = equality of outcome, 10 = equality of opportunity)"
    ) +
    theme_minimal()
})

# Show plots for the first four countries
country_plots[1:4]

# Interpretation:
# - split(~country) splits by country; map_dfr binds the single-row tibbles
#   into a summary table
# - .id = "country" turns the list name into a column, preventing loss of the
#   grouping information
# - Plotting goes back to map (plot objects cannot live inside a data frame,
#   so a list is required)
# - Look at the slope of the fitted line: upward = higher-income respondents
#   lean toward equality of opportunity; flat/downward = the relationship is
#   weak or reversed
```

:::

## Chapter Recap {.smaller}

- This chapter covered using conditional statements to classify data, and loops and batch processing to handle repetitive tasks efficiently
- Findings: low-income respondents lean slightly toward equality of outcome (≈5.85), high-income respondents lean slightly toward equality of opportunity (≈6.22), echoing the visualizations of the previous two chapters

- Conditional statements
    - Single judgment: `if()`, `else`
    - Multi-way judgment: `else if()`
    - Vectorized judgment: `ifelse()`, `case_when()`
- Loop structures
    - Loop statements: `for()`, `while()`, `repeat`
    - Loop control: `next`, `break`

---

- Batch processing
    - dplyr column-wise batching: `across()`
    - dplyr row-filtering batching: `if_any()`, `if_all()`
    - purrr list batching: `map()`, `map2()`, `pmap()`
    - Exception handling: `safely()`, `possibly()`
