Reduce a function on a grouped column in mutate

Viewed 93

I have a function which finds a sort of intersection of two strings of prose:

# Function to get intersection of words
str_intersect_by_word_list <- function(string1, string2){
  map2_chr(str_split(string1, '\\s'), str_split(string2, '\\s'), 
           ~str_c(intersect(.x, .y), collapse = " "))
} 

and a table with strings to match on:

# Sample data
my_df <- tibble(
  grp = rep(LETTERS[1:3], each = 3),
  strng = c(
    "Hi I'm Abe",
    "Hi I'm Beau",
    "Hi I'm Cat",
    "Hi there I'm Doug",
    "Hi there I'm Emily",
    "Hi there I'm Finn",
    "Hi it's nice to be here",
    "Hi it's nice to meet you",
    "Hi it's nice outside"
  )
)

If I want to create a column with the common string, I can do so like this:

# This works as expected
my_df %>% 
  mutate(
    common_string = my_df %>% 
      pull(strng) %>% 
      reduce(str_intersect_by_word_list)
  )

which gives

# A tibble: 9 x 3
  grp   strng                    common_string
  <chr> <chr>                    <chr>        
1 A     Hi I'm Abe               Hi           
2 A     Hi I'm Beau              Hi           
3 A     Hi I'm Cat               Hi           
4 B     Hi there I'm Doug        Hi           
5 B     Hi there I'm Emily       Hi           
6 B     Hi there I'm Finn        Hi           
7 C     Hi it's nice to be here  Hi           
8 C     Hi it's nice to meet you Hi           
9 C     Hi it's nice outside     Hi           

I would like to create a column with the string that is common to each group. However, inside the grouping, I can only access either the entire columns worth of strng, which gives the same output as above, or the current value of strng, which causes an error since my function str_intersect_by_word_list expects two inputs.

I tried referencing cur_data_all, but I don't think this is how that function was intended to be used, and it gives me the same results as above anyway.

# This fails.
my_df %>% 
  group_by(grp) %>% 
  mutate(
    grp_string = cur_data_all() %>% 
      pull(strng) %>% 
      reduce(str_intersect_by_word_list)
  )

The expected output would be

# A tibble: 9 x 3
  grp   strng                    grp_string
  <chr> <chr>                    <chr>        
1 A     Hi I'm Abe               Hi I'm           
2 A     Hi I'm Beau              Hi I'm          
3 A     Hi I'm Cat               Hi I'm          
4 B     Hi there I'm Doug        Hi there I'm         
5 B     Hi there I'm Emily       Hi there I'm         
6 B     Hi there I'm Finn        Hi there I'm         
7 C     Hi it's nice to be here  Hi it's nice          
8 C     Hi it's nice to meet you Hi it's nice          
9 C     Hi it's nice outside     Hi it's nice          

How can I get the common words from each group?

3 Answers

Please consider doing:

my_df %>%
   group_by(grp) %>%
   mutate(common_string = str_c(reduce(str_split(strng, "\\s+"), intersect),collapse = ' '))

# A tibble: 9 x 3
# Groups:   grp [3]
  grp   strng                    common_string
  <chr> <chr>                    <chr>        
1 A     Hi I'm Abe               Hi I'm       
2 A     Hi I'm Beau              Hi I'm       
3 A     Hi I'm Cat               Hi I'm       
4 B     Hi there I'm Doug        Hi there I'm 
5 B     Hi there I'm Emily       Hi there I'm 
6 B     Hi there I'm Finn        Hi there I'm 
7 C     Hi it's nice to be here  Hi it's nice 
8 C     Hi it's nice to meet you Hi it's nice 
9 C     Hi it's nice outside     Hi it's nice 

I mostly just rearranged code from the original question and borrowed a suggestion from @Martin Gal regarding rep.

library(dplyr, warn.conflicts = FALSE)

# Function to get intersection of words
str_intersect_by_word_list <- function(string1, string2){
  purrr::map2_chr(stringr::str_split(string1, '\\s'), 
                  stringr::str_split(string2, '\\s'), 
           ~stringr::str_c(intersect(.x, .y), collapse = " "))
} 

# Sample data
my_df <- tibble(
    grp = rep(LETTERS[1:3], each = 3),
    strng = c(
      "Hi I'm Abe",
      "Hi I'm Beau",
      "Hi I'm Cat",
      "Hi there I'm Doug",
      "Hi there I'm Emily",
      "Hi there I'm Finn",
      "Hi it's nice to be here",
      "Hi it's nice to meet you",
      "Hi it's nice outside"
    )
  )

my_df %>% 
  group_by(grp) %>% 
  mutate(grp_string = purrr::reduce(strng, str_intersect_by_word_list))
#> # A tibble: 9 x 3
#> # Groups:   grp [3]
#>   grp   strng                    grp_string  
#>   <chr> <chr>                    <chr>       
#> 1 A     Hi I'm Abe               Hi I'm      
#> 2 A     Hi I'm Beau              Hi I'm      
#> 3 A     Hi I'm Cat               Hi I'm      
#> 4 B     Hi there I'm Doug        Hi there I'm
#> 5 B     Hi there I'm Emily       Hi there I'm
#> 6 B     Hi there I'm Finn        Hi there I'm
#> 7 C     Hi it's nice to be here  Hi it's nice
#> 8 C     Hi it's nice to meet you Hi it's nice
#> 9 C     Hi it's nice outside     Hi it's nice

Created on 2021-07-12 by the reprex package (v2.0.0)

And here's a variant that puts all the details in a single function:

library(dplyr, warn.conflicts = FALSE)

# Function to get intersection of words
str_intersect <- function(str_vec) {
  
  str_intersect_by_word_list <- function(string1, string2){
    purrr::map2_chr(stringr::str_split(string1, '\\s'), 
                    stringr::str_split(string2, '\\s'), 
                    ~stringr::str_c(intersect(.x, .y), collapse = " "))
  } 
  purrr::reduce(str_vec, str_intersect_by_word_list)
}

# Sample data
my_df <- tibble(
    grp = rep(LETTERS[1:3], each = 3),
    strng = c(
      "Hi I'm Abe",
      "Hi I'm Beau",
      "Hi I'm Cat",
      "Hi there I'm Doug",
      "Hi there I'm Emily",
      "Hi there I'm Finn",
      "Hi it's nice to be here",
      "Hi it's nice to meet you",
      "Hi it's nice outside"
    )
  )

my_df %>% 
  group_by(grp) %>% 
  mutate(grp_string = str_intersect(strng))
#> # A tibble: 9 x 3
#> # Groups:   grp [3]
#>   grp   strng                    grp_string  
#>   <chr> <chr>                    <chr>       
#> 1 A     Hi I'm Abe               Hi I'm      
#> 2 A     Hi I'm Beau              Hi I'm      
#> 3 A     Hi I'm Cat               Hi I'm      
#> 4 B     Hi there I'm Doug        Hi there I'm
#> 5 B     Hi there I'm Emily       Hi there I'm
#> 6 B     Hi there I'm Finn        Hi there I'm
#> 7 C     Hi it's nice to be here  Hi it's nice
#> 8 C     Hi it's nice to meet you Hi it's nice
#> 9 C     Hi it's nice outside     Hi it's nice

Created on 2021-07-12 by the reprex package (v2.0.0)

TLDR

Your workflow seems fine! Your sample data is responsible for the bad results. Use my original suggestion (approved here) to generate the correct sample data via grp = rep(LETTERS[1:3], each = 3).

What Went Wrong

When generating your sample data

# Sample data
my_df <- tibble(
  grp = rep(LETTERS[1:3], 3),
  strng = c(
    "Hi I'm Abe",
    "Hi I'm Beau",
    "Hi I'm Cat",
    "Hi there I'm Doug",
    "Hi there I'm Emily",
    "Hi there I'm Finn",
    "Hi it's nice to be here",
    "Hi it's nice to meet you",
    "Hi it's nice outside"
  )
)

you incorrectly used rep(LETTERS[1:3], 3), which takes the entire vector c("A", "B", "C") and repeats it 3 times:

#   1     2     3
# |---| |---| |---|
  A B C A B C A B C

The result is an incorrect grp column in my_df

# A tibble: 9 x 2
  grp   strng                   
  <chr> <chr>                   
1 A     Hi I'm Abe              
2 B     Hi I'm Beau             
3 C     Hi I'm Cat              
4 A     Hi there I'm Doug       
5 B     Hi there I'm Emily      
6 C     Hi there I'm Finn       
7 A     Hi it's nice to be here 
8 B     Hi it's nice to meet you
9 C     Hi it's nice outside    

Now, even a perfect workflow will give you undesired results. When grouping by grp, here is group "A":

  grp   strng                  
  <chr> <chr>                  
1 A     Hi I'm Abe             
4 A     Hi there I'm Doug      
7 A     Hi it's nice to be here

Naturally, the only thing these strngs have in common — their only "intersection" — is the prefix "Hi".

  grp   strng                   grp_string
  <chr> <chr>                   <chr>     
1 A     Hi I'm Abe              Hi        
4 A     Hi there I'm Doug       Hi        
7 A     Hi it's nice to be here Hi        

Since the same goes for the other groups "B" and "C", then — by sheer coincidence — each group happens to independently have "Hi" as its "intersection". The result is a grp_string column populated exclusively by "Hi", whose (grouped) results

# A tibble: 9 x 3
# Groups:   grp [3]
  grp   strng                    grp_string
  <chr> <chr>                    <chr>     
1 A     Hi I'm Abe               Hi        
2 B     Hi I'm Beau              Hi        
3 C     Hi I'm Cat               Hi        
4 A     Hi there I'm Doug        Hi        
5 B     Hi there I'm Emily       Hi        
6 C     Hi there I'm Finn        Hi        
7 A     Hi it's nice to be here  Hi        
8 B     Hi it's nice to meet you Hi        
9 C     Hi it's nice outside     Hi        

look identical to the (ungrouped) results of your ungrouped attempt:

# A tibble: 9 x 3
  grp   strng                    grp_string
  <chr> <chr>                    <chr>     
1 A     Hi I'm Abe               Hi        
2 B     Hi I'm Beau              Hi        
3 C     Hi I'm Cat               Hi        
4 A     Hi there I'm Doug        Hi        
5 B     Hi there I'm Emily       Hi        
6 C     Hi there I'm Finn        Hi        
7 A     Hi it's nice to be here  Hi        
8 B     Hi it's nice to meet you Hi        
9 C     Hi it's nice outside     Hi        

How to Fix It

Instead of using rep(LETTERS[1:3], 3), you should use rep(LETTERS[1:3], each = 3), which repeats each element in the vector 3 times:

# 1 2 3 1 2 3 1 2 3
  A A A B B B C C C

By correcting this one oversight when generating my_df

# Sample data
my_df <- tibble(
  grp = rep(LETTERS[1:3], each = 3),
  #                       ^^^^^^^^
  strng = c(
    "Hi I'm Abe",
    "Hi I'm Beau",
    "Hi I'm Cat",
    "Hi there I'm Doug",
    "Hi there I'm Emily",
    "Hi there I'm Finn",
    "Hi it's nice to be here",
    "Hi it's nice to meet you",
    "Hi it's nice outside"
  )
)

you will produce the correct grp column in my_df:

# A tibble: 9 x 2
  grp   strng                   
  <chr> <chr>                   
1 A     Hi I'm Abe              
2 A     Hi I'm Beau             
3 A     Hi I'm Cat              
4 B     Hi there I'm Doug       
5 B     Hi there I'm Emily      
6 B     Hi there I'm Finn       
7 C     Hi it's nice to be here 
8 C     Hi it's nice to meet you
9 C     Hi it's nice outside            

and your original code

# Function to get intersection of words
str_intersect_by_word_list <- function(string1, string2){
  map2_chr(str_split(string1, '\\s'), str_split(string2, '\\s'), 
           ~str_c(intersect(.x, .y), collapse = " "))
} 

# ...

my_df %>% 
  group_by(grp) %>% 
  mutate(
    grp_string = cur_data_all() %>% 
      pull(strng) %>% 
      reduce(str_intersect_by_word_list)
  )

will produce your intended results:

# A tibble: 9 x 3
# Groups:   grp [3]
  grp   strng                    grp_string  
  <chr> <chr>                    <chr>       
1 A     Hi I'm Abe               Hi I'm      
2 A     Hi I'm Beau              Hi I'm      
3 A     Hi I'm Cat               Hi I'm      
4 B     Hi there I'm Doug        Hi there I'm
5 B     Hi there I'm Emily       Hi there I'm
6 B     Hi there I'm Finn        Hi there I'm
7 C     Hi it's nice to be here  Hi it's nice
8 C     Hi it's nice to meet you Hi it's nice
9 C     Hi it's nice outside     Hi it's nice

Note

While your workflow does work here, it's certainly worth exploring the alternatives by Onyambu or Ian Gow, which might prove cleaner, more efficient, or more extensible.

Related