2011-12-27 5 views
7

Come posso estrarre in modo efficiente colonne costanti per gruppo da un frame di dati? Di seguito ho incluso un'implementazione plyr per rendere preciso ciò che sto cercando di fare, ma è lento. Come posso farlo nel modo più efficiente possibile? (Idealmente senza dividere il frame di dati).Individuare in modo efficiente le colonne della costante di gruppo in un data.frame

base <- data.frame(group = 1:1000, a = sample(1000), b = sample(1000)) 
df <- data.frame(
    base[rep(seq_len(nrow(base)), length = 1e6), ], 
    c = runif(1e6), 
    d = runif(1e6) 
) 


is.constant <- function(x) length(unique(x)) == 1 
constant_cols <- function(x) head(Filter(is.constant, x), 1) 
system.time(constant <- ddply(df, "group", constant_cols)) 
# user system elapsed 
# 20.531 1.670 22.378 
stopifnot(identical(names(constant), c("group", "a", "b"))) 
stopifnot(nrow(constant) == 1000) 

Nel mio caso reale utilizzo (profondità ggplot2) vi può essere un numero arbitrario di colonne costanti e non costanti. La dimensione dei dati nell'esempio riguarda il giusto ordine di grandezza.

+0

si sta già facendo meglio di qualsiasi implementazione pure-R utilizzando plyr. IMHO si può fare solo meglio ordinando il df per gruppo (abbastanza velocemente) e poi scansionando per le interruzioni nel codice C. –

+0

@Simon Sto facendo meglio di qualsiasi soluzione basata su riga con plyr - Mi sento come se ci dovesse essere una soluzione basata su astuzia basata su colonne. – hadley

risposta

3

Ispirato @ risposta di Joran, ecco strategia simile che è un po 'più veloce (1 s vs 1,5 s sulla mia macchina)

changed <- function(x) c(TRUE, x[-1] != x[-n]) 

constant_cols2 <- function(df,grp){ 
    df <- df[order(df[,grp]),] 
    n <- nrow(df) 
    changes <- lapply(df, changed) 

    vapply(changes[-1], identical, changes[[1]], FUN.VALUE = logical(1)) 
} 
system.time(cols <- constant_cols2(df, "group")) # about 1 s 

system.time(constant <- df[changed(df$group), cols]) 
# user system elapsed 
# 1.057 0.230 1.314 

stopifnot(identical(names(constant), c("group", "a", "b"))) 
stopifnot(nrow(constant) == 1000) 

Ha gli stessi difetti, però, in quanto non in grado di rilevare le colonne sono avere gli stessi valori per i gruppi adiacenti (per esempio df$f <- 1)

Con un altro pensiero bit più @ idee di David:

constant_cols3 <- function(df, grp) { 
    # If col == TRUE and group == FALSE, not constant 
    matching_breaks <- function(group, col) { 
    !any(col & !group) 
    } 

    n <- nrow(df) 
    changed <- function(x) c(TRUE, x[-1] != x[-n]) 

    df <- df[order(df[,grp]),] 
    changes <- lapply(df, changed) 
    vapply(changes[-1], matching_breaks, group = changes[[1]], 
    FUN.VALUE = logical(1)) 
} 

system.time(x <- constant_cols3(df, "group")) 
# user system elapsed 
# 1.086 0.221 1.413 

e che dà il risultato corretto .

+0

Mi è appena venuto in mente che potresti essere in grado di risolvere gli stessi problemi nei gruppi adiacenti aggiungendo un vettore '0: 1' a ciascuna colonna che si ripete insieme a 'group' prima di eseguire' rle'. – joran

+0

Hmmm, sembra essere ancora più veloce se invece di ordinare il frame dei dati, ho ordinato le singole colonne mentre calcolo le modifiche. – hadley

3

(edit: risposta migliore)

Che dire qualcosa di simile

is.constant<-function(x) length(which(x==x[1])) == length(x)

Questo sembra essere un bel miglioramento. Confronta il seguente

> a<-rnorm(5000000) 

> system.time(is.constant(a)) 
    user system elapsed 
    0.039 0.010 0.048 
> 
> system.time(is.constantOld(a)) 
    user system elapsed 
    1.049 0.084 1.125 
+0

Ah, ma inserendolo nel vecchio codice, is.constant non sembra essere il collo di bottiglia. Hrm ... Comunque, ogni cosa aiuta, eh? – jebyrnes

+0

Avrei pensato che 'is.constant <- function (x)! Any (x [1]! = X)' sarebbe ancora meglio. Ma hai ragione che questo non è il collo di bottiglia - è la divisione e la combinazione dei frame di dati che è lento. – hadley

4

(A cura di affrontare eventualmente la questione dei gruppi consecutivi con lo stesso valore)

sto timidamente presentazione di questa risposta, ma non ho del tutto me stesso convinto che possa identificare correttamente all'interno raggruppa colonne costanti in tutti i casi. Ma è sicuramente più veloce (e probabilmente può essere migliorata):

constant_cols1 <- function(df,grp){ 
    df <- df[order(df[,grp]),] 

    #Adjust values based on max diff in data 
    rle_group <- rle(df[,grp]) 
    vec <- rep(rep(c(0,ceiling(diff(range(df)))), 
       length.out = length(rle_group$lengths)), 
       times = rle_group$lengths) 
    m <- matrix(vec,nrow = length(vec),ncol = ncol(df)-1) 
    df_new <- df 
    df_new[,-1] <- df[,-1] + m 

    rles <- lapply(df_new,FUN = rle) 
    nms <- names(rles) 
    tmp <- sapply(rles[nms != grp], 
        FUN = function(x){identical(x$lengths,rles[[grp]]$lengths)}) 
    return(tmp) 
} 

La mia idea di base era quella di utilizzare rle, ovviamente.

+0

Hmmm, penso che non funzionerà se il valore è lo stesso su più gruppi (quindi ad esempio la lunghezza sarebbe 2000). Approccio davvero interessante anche se – hadley

+0

@hadley Drat, hai ragione. – joran

+0

Penso che dovrebbe essere più facile risolvere il mio approccio che funziona in modo simile al tuo ma usa i vettori logici – hadley

4

Non sono sicuro se questo è esattamente quello che stai cercando, ma identifica le colonne aeb.

require(data.table) 
is.constant <- function(x) identical(var(x), 0) 
dtOne <- data.table(df) 
system.time({dtTwo <- dtOne[, lapply(.SD, is.constant), by=group] 
result <- apply(X=dtTwo[, list(a, b, c, d)], 2, all) 
result <- result[result == TRUE] }) 
stopifnot(identical(names(result), c("a", "b"))) 
result 
+0

Sfortunatamente sto provando a farlo con il minor numero possibile di dipendenze esterne, ma questo dà un tempo per mirare a: 0,5 s sul mio computer. – hadley

+0

Ho provato a fare la stessa cosa con aggregato e da e sono stati circa 10 e 18 secondi in modo ricettivo invece dei dati di 0,3 secondi presi da. – Jared

+0

sì, perché un grande collo di bottiglia sta subendo i frame di dati - è lento perché crea una copia. Le tabelle di dati non lo fanno, quindi è veloce. – hadley

1

Quanto velocemente si fa is.unsorted(x) fallire per non costante x? Purtroppo non ho accesso a R al momento. Sembra anche che non sia il collo di bottiglia però.

3

leggermente più lento di quello hadley suggerita, ma penso che dovrebbe gestire il caso di gruppi adiacenti uguali

findBreaks <- function(x) cumsum(rle(x)$lengths) 

constantGroups <- function(d, groupColIndex=1) { 
    d <- d[order(d[, groupColIndex]), ] 
    breaks <- lapply(d, findBreaks) 
    groupBreaks <- breaks[[groupColIndex]] 
    numBreaks <- length(groupBreaks) 
    isSubset <- function(x) length(x) <= numBreaks && length(setdiff(x, groupBreaks)) == 0 
    unlist(lapply(breaks[-groupColIndex], isSubset)) 
} 

L'intuizione è che se una colonna è costante groupwise allora le discontinuità tra i valori delle colonne (ordinato in base al valore del gruppo) sarà un sottoinsieme delle interruzioni nel valore di gruppo.

Ora, si confronti con Hadley di (con piccole modifiche per garantire n è definito)

# df defined as in the question 

n <- nrow(df) 
changed <- function(x) c(TRUE, x[-1] != x[-n]) 

constant_cols2 <- function(df,grp){ 
    df <- df[order(df[,grp]),] 
    changes <- lapply(df, changed) 
    vapply(changes[-1], identical, changes[[1]], FUN.VALUE = logical(1)) 
} 

> system.time(constant_cols2(df, 1)) 
    user system elapsed 
    1.779 0.075 1.869 
> system.time(constantGroups(df)) 
    user system elapsed 
    2.503 0.126 2.614 
> df$f <- 1 
> constant_cols2(df, 1) 
    a  b  c  d  f 
TRUE TRUE FALSE FALSE FALSE 
> constantGroups(df) 
    a  b  c  d  f 
TRUE TRUE FALSE FALSE TRUE 
+0

Bello! Penso che sia possibile adattare la mia versione per usare la stessa strategia della tua, quindi può rimanere un po 'più veloce. – hadley

+0

Semplicemente adattato dalla risposta per usare la stessa linea di pensiero della tua, ma con vettori logici. Grazie! – hadley