Reformar data.frame de formato ancho a largo


164

Tengo algunos problemas para convertir mi data.framede una mesa ancha a una larga. Por el momento se ve así:

Code Country        1950    1951    1952    1953    1954
AFG  Afghanistan    20,249  21,352  22,532  23,557  24,555
ALB  Albania        8,097   8,986   10,058  11,123  12,246

Ahora me gustaría transformar esto data.frameen un largo data.frame. Algo como esto:

Code Country        Year    Value
AFG  Afghanistan    1950    20,249
AFG  Afghanistan    1951    21,352
AFG  Afghanistan    1952    22,532
AFG  Afghanistan    1953    23,557
AFG  Afghanistan    1954    24,555
ALB  Albania        1950    8,097
ALB  Albania        1951    8,986
ALB  Albania        1952    10,058
ALB  Albania        1953    11,123
ALB  Albania        1954    12,246

He mirado y ya he intentado usar melt()las reshape()funciones y como algunas personas sugirieron en preguntas similares. Sin embargo, hasta ahora solo obtengo resultados desordenados.

Si es posible, me gustaría hacerlo con la reshape()función, ya que parece un poco más agradable de manejar.


2
No sé si ese fue el problema, pero las funciones en el paquete de remodelación son fundir y fundir (y refundir)
Eduardo Leoni

1
Y el paquete de remodelación ha sido reemplazado por reshape2.
IRTFM

55
Y ahora reshape2 ha sido reemplazado por tidyr.
drhagen

Respuestas:


93

reshape()toma un tiempo acostumbrarse, igual que melt/ cast. Aquí hay una solución con remodelación, asumiendo que su marco de datos se llama d:

reshape(d, 
        direction = "long",
        varying = list(names(d)[3:7]),
        v.names = "Value",
        idvar = c("Code", "Country"),
        timevar = "Year",
        times = 1950:1954)

153

Tres soluciones alternativas:

1) con :

Puede usar la misma meltfunción que en el reshape2paquete (que es una implementación extendida y mejorada). meltfrom data.tabletambién tiene más parámetros que la meltfunción-from reshape2. Por ejemplo, también puede especificar el nombre de la columna variable:

library(data.table)
long <- melt(setDT(wide), id.vars = c("Code","Country"), variable.name = "year")

lo que da:

> long
    Code     Country year  value
 1:  AFG Afghanistan 1950 20,249
 2:  ALB     Albania 1950  8,097
 3:  AFG Afghanistan 1951 21,352
 4:  ALB     Albania 1951  8,986
 5:  AFG Afghanistan 1952 22,532
 6:  ALB     Albania 1952 10,058
 7:  AFG Afghanistan 1953 23,557
 8:  ALB     Albania 1953 11,123
 9:  AFG Afghanistan 1954 24,555
10:  ALB     Albania 1954 12,246

Algunas anotaciones alternativas:

melt(setDT(wide), id.vars = 1:2, variable.name = "year")
melt(setDT(wide), measure.vars = 3:7, variable.name = "year")
melt(setDT(wide), measure.vars = as.character(1950:1954), variable.name = "year")

2) con :

library(tidyr)
long <- wide %>% gather(year, value, -c(Code, Country))

Algunas anotaciones alternativas:

wide %>% gather(year, value, -Code, -Country)
wide %>% gather(year, value, -1:-2)
wide %>% gather(year, value, -(1:2))
wide %>% gather(year, value, -1, -2)
wide %>% gather(year, value, 3:7)
wide %>% gather(year, value, `1950`:`1954`)

3) con :

library(reshape2)
long <- melt(wide, id.vars = c("Code", "Country"))

Algunas notaciones alternativas que dan el mismo resultado:

# you can also define the id-variables by column number
melt(wide, id.vars = 1:2)

# as an alternative you can also specify the measure-variables
# all other variables will then be used as id-variables
melt(wide, measure.vars = 3:7)
melt(wide, measure.vars = as.character(1950:1954))

NOTAS

  • Es retirado. Solo se realizarán los cambios necesarios para mantenerlo en CRAN. ( fuente )
  • Si desea excluir NAvalores, puede agregar tanto na.rm = TRUEa las meltfunciones como a las gatherfunciones.

Otro problema con los datos es que los valores serán leídos por R como valores de caracteres (como resultado de ,los números). Puede reparar eso con gsuby as.numeric:

long$value <- as.numeric(gsub(",", "", long$value))

O directamente con data.tableo dplyr:

# data.table
long <- melt(setDT(wide),
             id.vars = c("Code","Country"),
             variable.name = "year")[, value := as.numeric(gsub(",", "", value))]

# tidyr and dplyr
long <- wide %>% gather(year, value, -c(Code,Country)) %>% 
  mutate(value = as.numeric(gsub(",", "", value)))

Datos:

wide <- read.table(text="Code Country        1950    1951    1952    1953    1954
AFG  Afghanistan    20,249  21,352  22,532  23,557  24,555
ALB  Albania        8,097   8,986   10,058  11,123  12,246", header=TRUE, check.names=FALSE)

gran respuesta, solo un pequeño recordatorio más: no coloque ninguna variable que no sea idy timeen su marco de datos, meltno podría decir qué quiere hacer en este caso.
Jason Goal

1
@JasonGoal ¿Podrías dar más detalles al respecto? Como estoy interpretando tu comentario, no debería ser un problema. Solo especifique tanto el id.varscomo el measure.vars.
Jaap

, eso es bueno para mí, no lo sé id.varsy measure.varsse puede especificar en la primera alternativa, perdón por el desastre, es mi culpa.
Jason Goal

Perdón por necro esta publicación, ¿alguien podría explicarme por qué 3 funciona? Lo he probado y funciona, pero no entiendo qué está haciendo dplyr cuando ve -c(var1, var2)...

1
@ReputableMisnomer Cuando tidyr ve -c(var1, var2)que omite estas variables al transformar los datos de formato ancho a largo.
Jaap

35

Usando el paquete de remodelación :

#data
x <- read.table(textConnection(
"Code Country        1950    1951    1952    1953    1954
AFG  Afghanistan    20,249  21,352  22,532  23,557  24,555
ALB  Albania        8,097   8,986   10,058  11,123  12,246"), header=TRUE)

library(reshape)

x2 <- melt(x, id = c("Code", "Country"), variable_name = "Year")
x2[,"Year"] <- as.numeric(gsub("X", "" , x2[,"Year"]))

18

Con tidyr_1.0.0, otra opción espivot_longer

library(tidyr)
pivot_longer(df1, -c(Code, Country), values_to = "Value", names_to = "Year")
# A tibble: 10 x 4
#   Code  Country     Year  Value 
#   <fct> <fct>       <chr> <fct> 
# 1 AFG   Afghanistan 1950  20,249
# 2 AFG   Afghanistan 1951  21,352
# 3 AFG   Afghanistan 1952  22,532
# 4 AFG   Afghanistan 1953  23,557
# 5 AFG   Afghanistan 1954  24,555
# 6 ALB   Albania     1950  8,097 
# 7 ALB   Albania     1951  8,986 
# 8 ALB   Albania     1952  10,058
# 9 ALB   Albania     1953  11,123
#10 ALB   Albania     1954  12,246

datos

df1 <- structure(list(Code = structure(1:2, .Label = c("AFG", "ALB"), class = "factor"), 
    Country = structure(1:2, .Label = c("Afghanistan", "Albania"
    ), class = "factor"), `1950` = structure(1:2, .Label = c("20,249", 
    "8,097"), class = "factor"), `1951` = structure(1:2, .Label = c("21,352", 
    "8,986"), class = "factor"), `1952` = structure(2:1, .Label = c("10,058", 
    "22,532"), class = "factor"), `1953` = structure(2:1, .Label = c("11,123", 
    "23,557"), class = "factor"), `1954` = structure(2:1, .Label = c("12,246", 
    "24,555"), class = "factor")), class = "data.frame", row.names = c(NA, 
-2L))

1
Esto necesita más votos a favor. Según el blog de Tidyverse, gather se está retirando y pivot_longerahora es la forma correcta de lograrlo.
Evan Rosica

16

Como esta respuesta está etiquetada con , Sentí que sería útil compartir otra alternativa de la base R: stack.

Sin embargo, tenga en cuenta que eso stackno funciona con factors, solo funciona si is.vectores así TRUE, y de la documentación para is.vector, encontramos que:

is.vectordevuelve TRUEsi x es un vector del modo especificado que no tiene atributos distintos a los nombres . Vuelve de FALSEotra manera.

Estoy usando los datos de muestra de la respuesta de @ Jaap , donde los valores en las columnas del año son factors.

Aquí está el stackenfoque:

cbind(wide[1:2], stack(lapply(wide[-c(1, 2)], as.character)))
##    Code     Country values  ind
## 1   AFG Afghanistan 20,249 1950
## 2   ALB     Albania  8,097 1950
## 3   AFG Afghanistan 21,352 1951
## 4   ALB     Albania  8,986 1951
## 5   AFG Afghanistan 22,532 1952
## 6   ALB     Albania 10,058 1952
## 7   AFG Afghanistan 23,557 1953
## 8   ALB     Albania 11,123 1953
## 9   AFG Afghanistan 24,555 1954
## 10  ALB     Albania 12,246 1954

11

Aquí hay otro ejemplo que muestra el uso de gatherfrom tidyr. Puede seleccionar las columnas gatherquitándolas individualmente (como hago aquí) o incluyendo los años que desea explícitamente.

Tenga en cuenta que, para manejar las comas (y las X agregadas si check.names = FALSEno están configuradas), también estoy usando dplyr'mutate with parse_numberfrom' readrpara convertir los valores de texto nuevamente en números. Todos estos son parte de tidyversey, por lo tanto, se pueden cargar junto conlibrary(tidyverse)

wide %>%
  gather(Year, Value, -Code, -Country) %>%
  mutate(Year = parse_number(Year)
         , Value = parse_number(Value))

Devoluciones:

   Code     Country Year Value
1   AFG Afghanistan 1950 20249
2   ALB     Albania 1950  8097
3   AFG Afghanistan 1951 21352
4   ALB     Albania 1951  8986
5   AFG Afghanistan 1952 22532
6   ALB     Albania 1952 10058
7   AFG Afghanistan 1953 23557
8   ALB     Albania 1953 11123
9   AFG Afghanistan 1954 24555
10  ALB     Albania 1954 12246

4

Aquí está un solución:

sqldf("Select Code, Country, '1950' As Year, `1950` As Value From wide
        Union All
       Select Code, Country, '1951' As Year, `1951` As Value From wide
        Union All
       Select Code, Country, '1952' As Year, `1952` As Value From wide
        Union All
       Select Code, Country, '1953' As Year, `1953` As Value From wide
        Union All
       Select Code, Country, '1954' As Year, `1954` As Value From wide;")

Para realizar la consulta sin escribir todo, puede usar lo siguiente:

Gracias a G. Grothendieck por implementarlo.

ValCol <- tail(names(wide), -2)

s <- sprintf("Select Code, Country, '%s' As Year, `%s` As Value from wide", ValCol, ValCol)
mquery <- paste(s, collapse = "\n Union All\n")

cat(mquery) #just to show the query
 #> Select Code, Country, '1950' As Year, `1950` As Value from wide
 #>  Union All
 #> Select Code, Country, '1951' As Year, `1951` As Value from wide
 #>  Union All
 #> Select Code, Country, '1952' As Year, `1952` As Value from wide
 #>  Union All
 #> Select Code, Country, '1953' As Year, `1953` As Value from wide
 #>  Union All
 #> Select Code, Country, '1954' As Year, `1954` As Value from wide

sqldf(mquery)
 #>    Code     Country Year  Value
 #> 1   AFG Afghanistan 1950 20,249
 #> 2   ALB     Albania 1950  8,097
 #> 3   AFG Afghanistan 1951 21,352
 #> 4   ALB     Albania 1951  8,986
 #> 5   AFG Afghanistan 1952 22,532
 #> 6   ALB     Albania 1952 10,058
 #> 7   AFG Afghanistan 1953 23,557
 #> 8   ALB     Albania 1953 11,123
 #> 9   AFG Afghanistan 1954 24,555
 #> 10  ALB     Albania 1954 12,246

Desafortunadamente, no creo eso PIVOTy UNPIVOTfuncionaría R SQLite. Si desea escribir su consulta de una manera más sofisticada, también puede echar un vistazo a estas publicaciones:

Usando la sprintfescritura de consultas sql    o    Pase variables asqldf


0

También puede usar el cdatapaquete, que usa el concepto de tabla de control (transformación):

# data
wide <- read.table(text="Code Country        1950    1951    1952    1953    1954
AFG  Afghanistan    20,249  21,352  22,532  23,557  24,555
ALB  Albania        8,097   8,986   10,058  11,123  12,246", header=TRUE, check.names=FALSE)

library(cdata)
# build control table
drec <- data.frame(
    Year=as.character(1950:1954),
    Value=as.character(1950:1954),
    stringsAsFactors=FALSE
)
drec <- cdata::rowrecs_to_blocks_spec(drec, recordKeys=c("Code", "Country"))

# apply control table
cdata::layout_by(drec, wide)

Actualmente estoy explorando ese paquete y lo encuentro bastante accesible. Está diseñado para transformaciones mucho más complicadas e incluye la transformación inversa. Hay un tutorial disponible.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.