dplyr::mutate() -- In a tibble nesting lists, how to ignore NULL nested lists?

Viewed 67

Occasionally, nested lists in my higher level tibble are NULL. I want to ignore those lists when using dplyr::mutate().

Example

Recoding values to lower case & underscore

Data

library(tibble)

df <-
  tibble(movies = c("The Shawshank Redemption", "The Godfather", "The Godfather: Part II", "The Dark Knight", "12 Angry Men"),
                continents = c("Asia", "Australia", "America", "Africa", "Europe"),
                michaels = c("Michael Jackson", "Michael Jordan", "Mike Tyson", "Michael Phelps", "Michael Schumacher"))

df <- add_column(df, ignore_me = list(NULL))

df

## # A tibble: 5 x 4
##   movies                   continents michaels           ignore_me
##   <chr>                    <chr>      <chr>              <list>   
## 1 The Shawshank Redemption Asia       Michael Jackson    <NULL>   
## 2 The Godfather            Australia  Michael Jordan     <NULL>   
## 3 The Godfather: Part II   America    Mike Tyson         <NULL>   
## 4 The Dark Knight          Africa     Michael Phelps     <NULL>   
## 5 12 Angry Men             Europe     Michael Schumacher <NULL> 

Trying to recode values

library(dplyr) # version 1.0.2
library(snakecase)

df %>%
  mutate(across(everything(), snakecase::to_any_case))

Error: Problem with mutate() input ..1.
x argument is not a character vector
i Input ..1 is across(everything(), snakecase::to_any_case).


Obviously, either of the following would work:

df %>% mutate(across(c(movies, continents, michaels), snakecase::to_any_case))
# or
df %>% mutate(across(-ignore_me, snakecase::to_any_case))

##   movies                   continents michaels           ignore_me
##   <chr>                    <chr>      <chr>              <list>   
## 1 the_shawshank_redemption asia       michael_jackson    <NULL>   
## 2 the_godfather            australia  michael_jordan     <NULL>   
## 3 the_godfather_part_ii    america    mike_tyson         <NULL>   
## 4 the_dark_knight          africa     michael_phelps     <NULL>   
## 5 12_angry_men             europe     michael_schumacher <NULL>  

But in reality I can't expect which column/nested list is going to be NULL and therefore I need my code to simply ignore such NULL but still apply on non-NULL columns.


EDIT


Original df above makes it simple to solve the problem by ignoring list altogether. But data can typically also be:

df_2 <-
  tibble(movies = c("The Shawshank Redemption", "The Godfather", "The Godfather: Part II", "The Dark Knight", "12 Angry Men"),
         continents = c("Asia", "Australia", "America", "Africa", "Europe"),
         michaels = c("Michael Jackson", "Michael Jordan", "Mike Tyson", "Michael Phelps", "Michael Schumacher"))

df_2 <- add_column(df_2, ignore_me = list(NULL))

set.seed(2021) ; df_2 <- mutate(df_2, across(sample(colnames(df_2), 1), as.list))

df_2

##   movies                   continents michaels  ignore_me
##   <chr>                    <chr>      <list>    <list>   
## 1 The Shawshank Redemption Asia       <chr [1]> <NULL>   
## 2 The Godfather            Australia  <chr [1]> <NULL>   
## 3 The Godfather: Part II   America    <chr [1]> <NULL>   
## 4 The Dark Knight          Africa     <chr [1]> <NULL>   
## 5 12 Angry Men             Europe     <chr [1]> <NULL>   
2 Answers

You can ignore all the list columns :

library(dplyr)
df %>% mutate(across(where(Negate(is.list)), snakecase::to_any_case))

Or if not all list column are going to be NULL, you can specifically find the columns that have NULL value by checking their length and ignore the columns that have length as 0.

df %>% mutate(across(where(~!all(lengths(.) == 0)), snakecase::to_any_case))


#  movies                   continents michaels           ignore_me
#  <chr>                    <chr>      <chr>              <list>   
#1 the_shawshank_redemption asia       michael_jackson    <NULL>   
#2 the_godfather            australia  michael_jordan     <NULL>   
#3 the_godfather_part_ii    america    mike_tyson         <NULL>   
#4 the_dark_knight          africa     michael_phelps     <NULL>   
#5 12_angry_men             europe     michael_schumacher <NULL>   

For modified df_2 we could use :

df_2$michaels[[3]] <- c(df_2$michaels[[3]], df_2$michaels[[4]]) 

df_2 %>% 
  mutate(across(where(~all(lengths(.) > 0)), 
                ~relist(to_any_case(unlist(.)), .)))


#  movies                   continents michaels  ignore_me
#  <chr>                    <chr>      <list>    <list>   
#1 the_shawshank_redemption asia       <chr [1]> <NULL>   
#2 the_godfather            australia  <chr [1]> <NULL>   
#3 the_godfather_part_ii    america    <chr [2]> <NULL>   
#4 the_dark_knight          africa     <chr [1]> <NULL>   
#5 12_angry_men             europe     <chr [1]> <NULL>   

One option with the addition of purrr could be:

df %>%
 mutate(across(where(~ !all(map_lgl(., is.null))), to_any_case))

  movies                   continents michaels           ignore_me
  <chr>                    <chr>      <chr>              <list>   
1 the_shawshank_redemption asia       michael_jackson    <NULL>   
2 the_godfather            australia  michael_jordan     <NULL>   
3 the_godfather_part_ii    america    mike_tyson         <NULL>   
4 the_dark_knight          africa     michael_phelps     <NULL>   
5 12_angry_men             europe     michael_schumacher <NULL>  

For the second dataset:

df_2 %>%
 mutate(across(where(~ !all(map_lgl(., is.null))), ~ to_any_case(unlist(.)))) 

  movies                   continents michaels           ignore_me
  <chr>                    <chr>      <chr>              <list>   
1 the_shawshank_redemption asia       michael_jackson    <NULL>   
2 the_godfather            australia  michael_jordan     <NULL>   
3 the_godfather_part_ii    america    mike_tyson         <NULL>   
4 the_dark_knight          africa     michael_phelps     <NULL>   
5 12_angry_men             europe     michael_schumacher <NULL>   
Related