Desanidar múltiples tibbles dentro de un tibble para dividir las preguntas de la encuesta Mutli-Select

Aug 21 2020

Estoy tratando de generar una solución programática para expandir las preguntas de 'Respuesta múltiple' de las encuestas en columnas separadas. La configuración incluye datos de la encuesta (df1) y un archivo auxiliar que relaciona las variables con la información sobre la variable. Con los datos de muestra a continuación, el objetivo es expandir las respuestas en DVar y EVar en columnas separadas, por ejemplo, DVar.A, DVar.b, etc., con 1,0 binario si esa ID marcó la casilla correspondiente.

df1 <- tibble(ID = rep(1:8), AVar = sample(1:10, 8), BVar = rnorm(8), 
              CVar = c("Got", "Some", "Stuff", "In", "Here", "Got", "Others", "Too"),
              DVar = c("A,B", NA , "C", "A,C", "B,D", "C", "D", "B,D"), 
              EVar = c("Banana,Apple", "Orange,Raspberry", "Apple", NA, "Orange", "Banana", "Banana", "Raspberry"))

Helper <- tibble(VariableName = c("ID", "AVar", "BVar", "CVar", "DVar", "EVar"), 
                 QuestionType = c("ID", "Numeric", "Numeric", "Single Response", "Multiple Response", "Multiple Response"))

La función de trabajo actual toma un ID y la columna para difundir. Para mis propósitos actuales, esta función funciona muy bien. Excepto cuando una columna tiene una ausencia de NA (lo cual es inusual), lo que arroja un error sobre 'Ninguno' que no está en el conjunto de datos en la declaración de selección final.


MultiToCol <- function(ID, toSpread) {
  X <- tibble(ID, toSpread)
  
  X %>% mutate(varLong = strsplit(as.character(replace_na(toSpread, "None")),split=",")) %>% 
    unnest(varLong) %>% mutate(tmpValue = 1) %>% spread(varLong, tmpValue, fill = 0) %>% select(-None, -ID, -toSpread, None)
  
}

Usando mutate (al otro lado) puedo recuperar los datos necesarios, que luego se unen al conjunto de datos completo (o podría estar en el ejemplo).

getCols <- Helper %>% filter(QuestionType == "Multiple Response") %>% select(VariableName)

spreadCols <- df1 %>% select_if(names(.) %in% c('ID', getCols$VariableName)) %>% 
  mutate(across(.cols = !ID, .fns = ~MultiToCol1(ID,.))) 

Cuando veo los datos, rstudio me devuelve lo que quiero.

ID  DVar.A  DVar.B  DVar.C  DVar.D  DVar.None   EVar.Apple  EVar.Banana EVar.Orange EVar.Raspberry  Evar.None
1   1   1   0   0   0   1   1   0   0   0
2   0   0   0   0   1   0   0   1   1   0
3   0   0   1   0   0   1   0   0   0   0
⋮

Sin embargo, al escribir los datos, recibo un error sobre dimensiones no coincidentes. Esto se debe a que la estructura de datos resultante es un tibble de 8x3 con columnas que son (Int, Tibble, Tibble). Y los Tibbles internos parecen estar transpuestos.

tibble [8 x 3] (S3: tbl_df/tbl/data.frame)
 $ ID  : int [1:8] 1 2 3 4 5 6 7 8
 $ DVar: tibble [8 x 5] (S3: tbl_df/tbl/data.frame) ..$ A   : num [1:8] 1 0 0 1 0 0 0 0
  ..$ B : num [1:8] 1 0 0 0 1 0 0 1 ..$ C   : num [1:8] 0 0 1 1 0 1 0 0
  ..$ D : num [1:8] 0 0 0 0 1 0 1 1 ..$ None: num [1:8] 0 1 0 0 0 0 0 0
 $ EVar: tibble [8 x 5] (S3: tbl_df/tbl/data.frame) ..$ Apple    : num [1:8] 1 0 1 0 0 0 0 0
  ..$ Banana : num [1:8] 1 0 0 0 0 1 1 0 ..$ Orange   : num [1:8] 0 1 0 0 1 0 0 0
  ..$ Raspberry: num [1:8] 0 1 0 0 0 0 0 1 ..$ None     : num [1:8] 0 0 0 1 0 0 0 0

El uso de la función unnest produce el mismo error que las funciones write_ sobre dimensiones no coincidentes.

También intenté usar unnest_wider , pero tengo problemas con varias columnas tibble ya que la función unnest_wider solo toma una sola columna como argumento.

Intenté usar pivot_wider pero no puedo averiguar cómo pasarle los nombres de columna correctamente de getCols $ VariableName.

Tengo algunos de mis intentos fallidos que puedo agregar, pero siento que esta es una solución simple con el mapa, y simplemente no estoy acertando.

¿Hay alguna solución simple para desanidar múltiples tibbles desde el interior de un tibble? Feliz de escuchar cualquier otro comentario para crear una solución más ordenada y elegante para el problema más grande también.

Respuestas

1 akrun Aug 21 2020 at 02:27

Podríamos usar cSplit_e

library(splitstackshape)
library(dplyr)
df1 %>% 
    select_if(names(.) %in% c('ID', getCols$VariableName)) %>%
    cSplit_e("DVar", type = "character", fill = 0, sep=",") %>% 
    cSplit_e("EVar", type = "character", fill = 0, sep=",")

O si quisiéramos usar para múltiples columnas, una opción es map

library(purrr)
tmp <- df1 %>%  
           select_if(names(.) %in% c('ID', getCols$VariableName))
map_dfc(setdiff(names(tmp), "ID"), ~
     tmp %>%
      select(.x) %>% 
      cSplit_e( .x, type = "character", fill = 0, sep=",") %>% 
      select(-.x)) %>% 
 bind_cols(tmp, .)

Usando la función del OP, se puede aplanar fácilmente con as.data.frame

out <- df1 %>%
    select_if(names(.) %in% c('ID', getCols$VariableName)) %>% mutate(across(.cols = !ID, .fns = ~MultiToCol(ID,.))) %>% do.call(data.frame, .) out ID DVar.A DVar.B DVar.C DVar.D DVar.None EVar.Apple EVar.Banana EVar.Orange EVar.Raspberry EVar.None 1 1 1 1 0 0 0 1 1 0 0 0 2 2 0 0 0 0 1 0 0 1 1 0 3 3 0 0 1 0 0 1 0 0 0 0 4 4 1 0 1 0 0 0 0 0 0 1 5 5 0 1 0 1 0 0 0 1 0 0 6 6 0 0 1 0 0 0 1 0 0 0 7 7 0 0 0 1 0 0 1 0 0 0 8 8 0 1 0 1 0 0 0 0 1 0 str(out) #'data.frame': 8 obs. of 11 variables: # $ ID            : int  1 2 3 4 5 6 7 8
# $ DVar.A : num 1 0 0 1 0 0 0 0 # $ DVar.B        : num  1 0 0 0 1 0 0 1
# $ DVar.C : num 0 0 1 1 0 1 0 0 # $ DVar.D        : num  0 0 0 0 1 0 1 1
# $ DVar.None : num 0 1 0 0 0 0 0 0 # $ EVar.Apple    : num  1 0 1 0 0 0 0 0
# $ EVar.Banana : num 1 0 0 0 0 1 1 0 # $ EVar.Orange   : num  0 1 0 0 1 0 0 0
# $ EVar.Raspberry: num 0 1 0 0 0 0 0 1 # $ EVar.None     : num  0 0 0 1 0 0 0 0

O puede usar invoke

 ....
   %>% invoke(data.frame, .)