Using purrr or across with mutate to generate columns based on a condition

Viewed 51

There are many Q&As concerning the use of purrr package functions with mutate, but I haven't found one that I can apply to my particular situation, which involves a conditional function. Here's an example with a toy dataframe:

library(dplyr)

df <- tibble(year = c("2018", "2018", "2019", "2019"), 
             observed = c("YES", "NO", "NO", "YES"))

Here's the desired output:

df %>% mutate(observed_2018 = if_else(observed == "YES" & year == "2018", 1, 0), 
              observed_2019 = if_else(observed == "YES" & year == "2019", 1, 0))
#> # A tibble: 4 × 4
#>   year  observed observed_2018 observed_2019
#>   <chr> <chr>            <dbl>         <dbl>
#> 1 2018  YES                  1             0
#> 2 2018  NO                   0             0
#> 3 2019  NO                   0             0
#> 4 2019  YES                  0             1

How can I programmatically generate observed_2018 and observed_2019 using a purrr function (or, alternatively, across())?

3 Answers

You could use map_dfc() to column-bind the output of each year and unnest the list-column with tidyr::unnest().

library(dplyr)
library(purrr)

df %>%
  mutate(obs = map_dfc(set_names(2018:2019), ~ +(observed == "YES" & year == .x))) %>%
  tidyr::unnest(obs, names_sep = '_')

If you don't want to use an additional package (tidyr), you could just column-bind the output of map_dfc and the original data df.

bind_cols(
  df,
  map_dfc(2018:2019, ~ transmute(df, 'obs_{.x}' := +(observed == "YES" & year == .x)))
)

An alternative of map is reduce() from the same package.

reduce(2018:2019,
  ~ mutate(.x, 'obs_{.y}' := +(observed == "YES" & year == .y)), .init = df
)

Output
# # A tibble: 4 × 4
#   year  observed obs_2018 obs_2019
#   <chr> <chr>       <int>    <int>
# 1 2018  YES             1        0
# 2 2018  NO              0        0
# 3 2019  NO              0        0
# 4 2019  YES             0        1

You can also use data.table approaches

library(data.table)
setDT(df)
yrs = unique(df$year)

then, you can either use set() like this:

for(y in yrs) set(df,j=paste0("observed_",y),value = 1*(df$observed=="YES" & df$year==y))

or you can use lapply() in j like this:

df[, paste0("observed_",yrs):=lapply(yrs, \(y) 1*(observed=="YES" & year==y))]

both give the same output:

   year observed observed_2018 observed_2019
1: 2018      YES             1             0
2: 2018       NO             0             0
3: 2019       NO             0             0
4: 2019      YES             0             1

Using base R

cbind(df,  model.matrix(~ year - 1, df) * (df$observed == 'YES'))
  year observed year2018 year2019
1 2018      YES        1        0
2 2018       NO        0        0
3 2019       NO        0        0
4 2019      YES        0        1
Related