Coalescing multiple chunks of columns with the same suffix in names (R)

Viewed 221

I have a dataset with various "chunks" of columns with different prefixes, but the same suffix:

ID A034 B034 C034 D034 A099 B099 A123 B123 ...
1 NA 1 NA NA NA 3 1 NA ...
2 2 NA NA NA 2 NA NA 2 ...
3 NA NA 2 NA NA 2 1 NA ...

The number of columns within each "chunk" also varies. Is there any way (other than manually, which is what I have been painstakingly doing with coalesce(!!! select(., contains("XXX")))) to automatically coalesce by chunk based on the shared suffix? That is, the result should resemble

ID 034 099 123 ...
1 1 3 1 ...
2 2 2 2 ...
3 2 2 1 ...

I'm not sure how to begin doing something like this, so any suggestions would be very helpful.

2 Answers

We reshape the data into 'long' format with pivot_longer, then we group by 'ID' and loop across the other columns, apply the na.omit to remove the NA elements (we assume that there is only one non-NA per each column by group)

library(dplyr)
library(tidyr)
df1 %>%
    pivot_longer(cols = -ID, names_to = ".value", 
           names_pattern = "[A-Z](\\d+)") %>% 
    group_by(ID) %>%
    summarise(across(everything(), na.omit), .groups = 'drop')

-output

# A tibble: 3 x 4
     ID `034` `099` `123`
  <int> <int> <int> <int>
1     1     1     3     1
2     2     2     2     2
3     3     2     2     1

Or to be safe, use complete.cases to create a logical vector for non-NA elements, and extract the first element (assuming we need only a single non-NA - if the non-NA lengths are different, we may need to return a list)

df1 %>%
     pivot_longer(cols = -ID, names_to = ".value",
          names_pattern = "[A-Z](\\d+)") %>%
     group_by(ID) %>% 
     summarise(across(everything(),  ~ .[complete.cases(.)][1]))

data

df1 <- structure(list(ID = 1:3, A034 = c(NA, 2L, NA), B034 = c(1L, NA, 
NA), C034 = c(NA, NA, 2L), D034 = c(NA, NA, NA), A099 = c(NA, 
2L, NA), B099 = c(3L, NA, 2L), A123 = c(1L, NA, 1L), B123 = c(NA, 
2L, NA)), class = "data.frame", row.names = c(NA, -3L))

one more approach

library(tidyverse)

split(names(df1)[-1], gsub('^\\D*(\\d+)$', '\\1', names(df1)[-1])) %>% map(~df1[c('ID', .x)]) %>%
  imap(~ .x %>% group_by(ID) %>% rowwise %>% transmute(!!.y := first(na.omit(c_across(everything())))) %>% ungroup) %>%
  reduce(left_join, by = 'ID')

#> # A tibble: 3 x 4
#>      ID `034` `099` `123`
#>   <int> <int> <int> <int>
#> 1     1     1     3     1
#> 2     2     2     2     2
#> 3     3     2     2     1

Created on 2021-06-20 by the reprex package (v2.0.0)

Related