Expandir automáticamente un factor R en una colección de variables indicadoras 1/0 para cada nivel de factor

108

Tengo un marco de datos R que contiene un factor que quiero "expandir" para que para cada nivel de factor, haya una columna asociada en un nuevo marco de datos, que contiene un indicador 1/0. Por ejemplo, supongamos que tengo:

df.original <-data.frame(eggs = c("foo", "foo", "bar", "bar"), ham = c(1,2,3,4))

Quiero:

df.desired  <- data.frame(foo = c(1,1,0,0), bar=c(0,0,1,1), ham=c(1,2,3,4))

Porque para ciertos análisis para los que necesita tener un marco de datos completamente numérico (por ejemplo, análisis de componentes principales), pensé que esta característica podría estar incorporada. Escribir una función para hacer esto no debería ser demasiado difícil, pero puedo prever algunos desafíos relacionados con los nombres de las columnas y, si ya existe algo, prefiero usarlo.

John Horton
fuente

Respuestas:

131

Usa la model.matrixfunción:

model.matrix( ~ Species - 1, data=iris )
Greg Snow
fuente
1
Puedo agregar que este método fue mucho más rápido que el que usé castpara mí.
Matt Weller
3
@GregSnow revisé el segundo párrafo del ?formula, así como ?model.matrix, pero no estaba claro (podría ser mi falta de profundidad de los conocimientos en álgebra matricial y la formulación del modelo). Después de investigar más, he podido deducir que el -1 solo especifica no incluir la columna "interceptar". Si omite el -1, verá una columna de intercepción de 1 en la salida con una columna binaria omitida. Puede ver qué valores de la columna omitida son 1 según las filas donde los valores de las otras columnas son 0. La documentación parece críptica, ¿hay otro buen recurso?
Ryan Chase
1
@RyanChase, hay muchos tutoriales en línea y libros sobre R / S (varios con breves descripciones en la página web r-project.org). Mi propio aprendizaje de S y R ha sido bastante ecléctico (y largo), por lo que no soy el mejor para dar una opinión sobre cómo los libros / tutoriales actuales atraen a los principiantes. Sin embargo, soy un fanático de la experimentación. Probar algo en una nueva sesión de R puede ser muy esclarecedor y no peligroso (lo peor que me ha pasado es que se cuelgue R, y eso rara vez, lo que lleva a mejoras en R). Stackoverflow es un buen recurso para comprender lo que sucedió.
Greg Snow
7
Y si desea convertir todas las columnas de factores, puede usar:model.matrix(~., data=iris)[,-1]
user890739
1
@colin, no es completamente automático, pero puede usar naresidpara volver a colocar los valores faltantes después de usar na.exclude. Un ejemplo rápido:tmp <- data.frame(x=factor(c('a','b','c',NA,'a'))); tmp2 <- na.exclude(tmp); tmp3 <- model.matrix( ~x-1, tmp2); tmp4 <- naresid(attr(tmp2,'na.action'), tmp3)
Greg Snow
17

Si su marco de datos solo está formado por factores (o está trabajando en un subconjunto de variables que son todos factores), también puede usar la acm.disjonctiffunción del ade4paquete:

R> library(ade4)
R> df <-data.frame(eggs = c("foo", "foo", "bar", "bar"), ham = c("red","blue","green","red"))
R> acm.disjonctif(df)
  eggs.bar eggs.foo ham.blue ham.green ham.red
1        0        1        0         0       1
2        0        1        1         0       0
3        1        0        0         1       0
4        1        0        0         0       1

No es exactamente el caso que está describiendo, pero también puede ser útil ...

juba
fuente
Gracias, esto me ayudó mucho ya que usa menos memoria que model.matrix!
Serhiy
Me gusta la forma en que se nombran las variables; No me gusta que se devuelvan como números que consumen mucho almacenamiento cuando deberían (en mi humilde opinión) ser simplemente lógicos.
dsz
9

Una forma rápida de usar el reshape2paquete:

require(reshape2)

> dcast(df.original, ham ~ eggs, length)

Using ham as value column: use value_var to override.
  ham bar foo
1   1   0   1
2   2   0   1
3   3   1   0
4   4   1   0

Tenga en cuenta que esto produce precisamente los nombres de columna que desea.

Prasad Chalasani
fuente
Bueno. Pero cuidado con el duplicado de jamón. digamos, d <- data.frame (huevos = c ("foo", "bar", "foo"), jamón = c (1,2,1)); dcast (d, jamón ~ huevos, longitud) hace foo = 2.
kohske
@Kohske, cierto, pero suponía que era hamuna identificación de fila única. Si hamno es un ID único, entonces uno debe usar otro ID único (o crear uno ficticio) y usarlo en lugar de ham. Convertir una etiqueta categórica en un indicador binario solo tendría sentido para ID únicos.
Prasad Chalasani
6

probablemente la variable ficticia sea similar a lo que desea. Entonces, model.matrix es útil:

> with(df.original, data.frame(model.matrix(~eggs+0), ham))
  eggsbar eggsfoo ham
1       0       1   1
2       0       1   2
3       1       0   3
4       1       0   4
Kohske
fuente
6

Una entrada tardía class.inddel nnetpaquete.

library(nnet)
 with(df.original, data.frame(class.ind(eggs), ham))
  bar foo ham
1   0   1   1
2   0   1   2
3   1   0   3
4   1   0   4
mnel
fuente
4

Me encontré con este viejo hilo y pensé en agregar una función que utiliza ade4 para tomar un marco de datos que consta de factores y / o datos numéricos y devuelve un marco de datos con factores como códigos ficticios.

dummy <- function(df) {  

    NUM <- function(dataframe)dataframe[,sapply(dataframe,is.numeric)]
    FAC <- function(dataframe)dataframe[,sapply(dataframe,is.factor)]

    require(ade4)
    if (is.null(ncol(NUM(df)))) {
        DF <- data.frame(NUM(df), acm.disjonctif(FAC(df)))
        names(DF)[1] <- colnames(df)[which(sapply(df, is.numeric))]
    } else {
        DF <- data.frame(NUM(df), acm.disjonctif(FAC(df)))
    }
    return(DF)
} 

Vamos a intentarlo.

df <-data.frame(eggs = c("foo", "foo", "bar", "bar"), 
            ham = c("red","blue","green","red"), x=rnorm(4))     
dummy(df)

df2 <-data.frame(eggs = c("foo", "foo", "bar", "bar"), 
            ham = c("red","blue","green","red"))  
dummy(df2)
Tyler Rinker
fuente
3

Aquí tienes una forma más clara de hacerlo. Utilizo model.matrix para crear las variables booleanas ficticias y luego las fusiono de nuevo en el marco de datos original.

df.original <-data.frame(eggs = c("foo", "foo", "bar", "bar"), ham = c(1,2,3,4))
df.original
#   eggs ham
# 1  foo   1
# 2  foo   2
# 3  bar   3
# 4  bar   4

# Create the dummy boolean variables using the model.matrix() function.
> mm <- model.matrix(~eggs-1, df.original)
> mm
#   eggsbar eggsfoo
# 1       0       1
# 2       0       1
# 3       1       0
# 4       1       0
# attr(,"assign")
# [1] 1 1
# attr(,"contrasts")
# attr(,"contrasts")$eggs
# [1] "contr.treatment"

# Remove the "eggs" prefix from the column names as the OP desired.
colnames(mm) <- gsub("eggs","",colnames(mm))
mm
#   bar foo
# 1   0   1
# 2   0   1
# 3   1   0
# 4   1   0
# attr(,"assign")
# [1] 1 1
# attr(,"contrasts")
# attr(,"contrasts")$eggs
# [1] "contr.treatment"

# Combine the matrix back with the original dataframe.
result <- cbind(df.original, mm)
result
#   eggs ham bar foo
# 1  foo   1   0   1
# 2  foo   2   0   1
# 3  bar   3   1   0
# 4  bar   4   1   0

# At this point, you can select out the columns that you want.
stackoverflowuser2010
fuente
0

Necesitaba una función para 'explotar' factores que sea un poco más flexible, e hice una basada en la función acm.disjonctif del paquete ade4. Esto le permite elegir los valores explotados, que son 0 y 1 en acm.disjonctif. Solo explota los factores que tienen "pocos" niveles. Se conservan las columnas numéricas.

# Function to explode factors that are considered to be categorical,
# i.e., they do not have too many levels.
# - data: The data.frame in which categorical variables will be exploded.
# - values: The exploded values for the value being unequal and equal to a level.
# - max_factor_level_fraction: Maximum number of levels as a fraction of column length. Set to 1 to explode all factors.
# Inspired by the acm.disjonctif function in the ade4 package.
explode_factors <- function(data, values = c(-0.8, 0.8), max_factor_level_fraction = 0.2) {
  exploders <- colnames(data)[sapply(data, function(col){
      is.factor(col) && nlevels(col) <= max_factor_level_fraction * length(col)
    })]
  if (length(exploders) > 0) {
    exploded <- lapply(exploders, function(exp){
        col <- data[, exp]
        n <- length(col)
        dummies <- matrix(values[1], n, length(levels(col)))
        dummies[(1:n) + n * (unclass(col) - 1)] <- values[2]
        colnames(dummies) <- paste(exp, levels(col), sep = '_')
        dummies
      })
    # Only keep numeric data.
    data <- data[sapply(data, is.numeric)]
    # Add exploded values.
    data <- cbind(data, exploded)
  }
  return(data)
}
rakensi
fuente