rdplyrtidyversetidyrtidyselect

in dplyr::mutate, dplyr::starts_with works for .before but not .after?


Can someone explain why, in the below context, mutate .before starts_with works but not mutate .after starts_with?

dfr_before <- data.frame(old1=1, old2=1, prefix_old1=1, prefix_old2=1, old3=1)
dfr_after  <- dfr_before

newnames <- list("prefix_new1", "prefix_new2")

for (n in newnames) {
  dfr_before <- dfr_before |> dplyr::mutate(
    .before = dplyr::starts_with("prefix"),
    {{n}} := 2
  )
  dfr_after <- dfr_after |> dplyr::mutate(
    .after = dplyr::starts_with("prefix"),
    {{n}} := 2
  )
}

dfr_before
dfr_after

##   old1 old2 prefix_new2 prefix_new1 prefix_old1 prefix_old2 old3
## 1    1    1           2           2           1           1    1

##   old1 old2 prefix_old1 prefix_old2 old3 prefix_new1 prefix_new2
## 1    1    1           1           1    1           2           2

I thought it would insert the new variables after prefix_old2


Solution

  • That is a good question.

    I believe this is because your call to mutate() with the .after = starts_with("prefix") argument essentially resolves to a call to mutate() and then a call to relocate().

    The mutate() will place the new "prefix" column at the end of the frame, and then relocate() will "move" the newly created "prefix" column after the last "prefix" column. In this case, the last such column ends up being itself because the name of the column to be relocated matches your tidyselect specification.

    Here is a simplified reprex illustrating the behavior along with a third example removing the newly created column from relocation consideration:

    library(dplyr)
    
    data <- data.frame(a1 = "a1", b1 = "b1")
    
    # all together as per your example
    data %>% mutate(a2 = "a2", .after = starts_with("a"))
    #>   a1 b1 a2
    #> 1 a1 b1 a2
    
    # stepwise
    data %>%
      mutate(a2 = "a2") %>% 
      relocate(a2, .after = starts_with("a"))
    #>   a1 b1 a2
    #> 1 a1 b1 a2
    
    # remove new a3 from relocation consideration
    data %>% mutate(a2 = "a2", .after = c(starts_with("a"), -a2))
    #>   a1 a2 b1
    #> 1 a1 a2 b1
    

    Created on 2024-10-09 with reprex v2.1.1

    And for completeness, here is the same behavior but with the .before argument:

    library(dplyr)
    
    data.frame(a1 = "a1", b2 = "b2") %>% 
      # make b1 and place at beginning to illustrate identical behavior
      mutate(b1 = "b1", .before = a1) %>% 
      # notice b1 is "moved" before itself
      relocate(b1, .before = starts_with("b"))
    #>   b1 a1 b2
    #> 1 b1 a1 b2
    

    Created on 2024-10-09 with reprex v2.1.1