2016-04-17 32 views
6

Uso principalmente ggplot2 per visualizzazioni. In genere, disegno la trama in modo interattivo (ovvero codice ggplot2 grezzo che utilizza NSE) ma alla fine, I frequentemente finiscono per avvolgere quel codice in una funzione che riceve i dati e le variabili da tracciare. E questo è sempre un po 'un incubo di .Valutazione pigro per ggplot2 all'interno di una funzione

Quindi, le situazioni tipiche si presentano così. Ho alcuni dati e I crea un grafico per questo (in questo caso, un esempio molto molto semplice, utilizzando il set di dati mpg fornito con ggplot2).

library(ggplot2) 
data(mpg) 

ggplot(data = mpg, 
     mapping = aes(x = class, y = hwy)) + 
    geom_boxplot() + 
    geom_jitter(alpha = 0.1, color = "blue") 


E quando ho finito di disegnare la trama, che in genere voglio usarlo per diverse variabili o dati, ecc Quindi creo una funzione che riceve i dati e le variabili per la trama come argomenti . Ma a causa di NSE, è non facile come scrivere l'intestazione della funzione e quindi copiare/incollare e sostituire le variabili per gli argomenti delle funzioni. Non funzionerebbe, come mostrato di seguito.

mpg <- mpg 
plotfn <- function(data, xvar, yvar){ 
    ggplot(data = data, 
      mapping = aes(x = xvar, y = yvar)) + 
    geom_boxplot() + 
    geom_jitter(alpha = 0.1, color = "blue") 
} 
plotfn(mpg, class, hwy) # Can't find object 

## Don't know how to automatically pick scale for object of type function. Defaulting to continuous. 

## Warning: restarting interrupted promise evaluation 

## Error in eval(expr, envir, enclos): object 'hwy' not found 

plotfn(mpg, "class", "hwy") # 


così devo tornare indietro e correggere il codice, ad esempio, utilizzando aes_string intead del aes che utilizza NSE (in questo esempio è piuttosto semplice, ma per trame più complesse, con molte trasformazioni e livelli, diventa un incubo).

plotfn <- function(data, xvar, yvar){ 
    ggplot(data = data, 
      mapping = aes_string(x = xvar, y = yvar)) + 
    geom_boxplot() + 
    geom_jitter(alpha = 0.1, color = "blue") 
} 
plotfn(mpg, "class", "hwy") # Now this works 


E la cosa è che trovo molto comodo NSE e anche lazyeval. Quindi Mi piace fare qualcosa del genere.

mpg <- mpg 
plotfn <- function(data, xvar, yvar){ 
    data_gd <- data.frame(
     xvar = lazyeval::lazy_eval(substitute(xvar), data = data), 
     yvar = lazyeval::lazy_eval(substitute(yvar), data = data)) 

    ggplot(data = data_gd, 
      mapping = aes(x = xvar, y = yvar)) + 
    geom_boxplot() + 
    geom_jitter(alpha = 0.1, color = "blue") 
} 
plotfn(mpg, class, hwy) # Now this works 

plotfn(mpg, "class", "hwy") # This still works 

plotfn(NULL, rep(letters[1:4], 250), 1:100) # And even this crazyness works 


Questo dà la mia funzione plot un sacco di flessibilità. Ad esempio, è possibile passare nomi di variabili quotati o non quotati e anche i dati direttamente anziché un nome di variabile (tipo di abuso di valutazione lazy).

Ma questo ha un grosso problema. La funzione non può essere utilizzata a livello di programmazione .

dynamically_changing_xvar <- "class" 
plotfn(mpg, dynamically_changing_xvar, hwy) 

## Error in eval(expr, envir, enclos): object 'dynamically_changing_xvar' not found 

# This does not work, because it never finds the object 
# dynamically_changing_xvar in the data, and it does not get evaluated to 
# obtain the variable name (class) 

Quindi non posso usare i loop (ad esempio lapply) per produrre la stessa trama di diverse combinazioni di variabili, o dati.

Così ho pensato di abusare ancora di più pigri, standard e non standard di valutazione, e cercare di combinarli tutti così ho sia, la flessibilità sopra indicato e la possibilità di utilizzare la funzione di programmazione. Fondamentalmente, quello che faccio è usare tryCatch per la prima lazy_eval l'espressione per ogni variabile e, se fallisce, per valutare l'espressione analizzata .

plotfn <- function(data, xvar, yvar){ 
    data_gd <- NULL 
    data_gd$xvar <- tryCatch(
     expr = lazyeval::lazy_eval(substitute(xvar), data = data), 
     error = function(e) eval(envir = data, expr = parse(text=xvar)) 
    ) 
    data_gd$yvar <- tryCatch(
     expr = lazyeval::lazy_eval(substitute(yvar), data = data), 
     error = function(e) eval(envir = data, expr = parse(text=yvar)) 
    ) 


    ggplot(data = as.data.frame(data_gd), 
      mapping = aes(x = xvar, y = yvar)) + 
    geom_boxplot() + 
    geom_jitter(alpha = 0.1, color = "blue") 
} 

plotfn(mpg, class, hwy) # Now this works, again 

plotfn(mpg, "class", "hwy") # This still works, again 

plotfn(NULL, rep(letters[1:4], 250), 1:100) # And this crazyness still works 

# And now, I can also pass a local variable to the function, that contains 
# the name of the variable that I want to plot 
dynamically_changing_xvar <- "class" 
plotfn(mpg, dynamically_changing_xvar, hwy) 


Quindi, oltre alla flessibilità menzionata sopra, ora posso usare lo one-liner o così, per produrre molti dello stesso grafico, con diverse variabili (o dati) .

lapply(c("class", "fl", "drv"), FUN = plotfn, yvar = hwy, data = mpg) 

## [[1]] 

## 
## [[2]] 

## 
## [[3]] 


Anche se è molto pratico, ho il sospetto che questo non è una buona pratica. Ma quanto male pratica è? Questa è la mia domanda chiave. Quali altre alternative posso utilizzare per avere il meglio di entrambi i mondi?

Naturalmente, posso vedere questo modello può creare problemi. Per esempio.

# If I have a variable in the global environment that contains the variable 
# I want to plot, but whose name is in the data passed to the function, 
# then it will use the name of the variable and not its content 
drv <- "class" 
plotfn(mpg, drv, hwy) # Here xvar on the plot is drv and not class 


E alcuni (molti?) Altri problemi. Ma mi sembra che i vantaggi in termini di flessibilità della sintassi superino questi altri problemi. Qualche idea su questo?

+1

La pratica migliore è quella di produrre una coppia di funzioni. Uno è NSE, l'altro SE. Questo è descritto in 'vignette ('nse')'. Questo significa usare 'aes_' invece di' aes'. – Axeman

+0

Grazie, ..., sì, temevo che sarebbe stata la risposta. Anche se vedo i benefici di dplyr & co. "schema di denominazione coerente: SE è il nome NSE con _ alla fine", mi dà sempre fastidio dover utilizzare una funzione diversa per programmare e lavorare in modo interattivo. – elikesprogramming

risposta

2

Estrazione la funzione proposto per la chiarezza:

library(ggplot2) 
data(mpg) 

plotfn <- function(data, xvar, yvar){ 
    data_gd <- NULL 
    data_gd$xvar <- tryCatch(
    expr = lazyeval::lazy_eval(substitute(xvar), data = data), 
    error = function(e) eval(envir = data, expr = parse(text=xvar)) 
) 
    data_gd$yvar <- tryCatch(
    expr = lazyeval::lazy_eval(substitute(yvar), data = data), 
    error = function(e) eval(envir = data, expr = parse(text=yvar)) 
) 

    ggplot(data = as.data.frame(data_gd), 
     mapping = aes(x = xvar, y = yvar)) + 
    geom_boxplot() + 
    geom_jitter(alpha = 0.1, color = "blue") 
} 

Tale funzione è generalmente molto utile, dato che è possibile mescolare liberamente stringhe, e nomi di variabili nude. Ma come dici tu, potrebbe non essere sempre al sicuro. Considera il seguente esempio inventato:

class <- "drv" 
Class <- "drv" 
plotfn(mpg, class, hwy) 
plotfn(mpg, Class, hwy) 

Che cosa genererà la tua funzione? Saranno gli stessi (non lo sono)? Non è davvero chiaro per me quale sarà il risultato. La programmazione con tale funzione può dare risultati inaspettati, a seconda delle variabili esistenti in data e che esistono nell'ambiente. Dal momento che molte persone usano nomi variabili come x, xvar o count (anche se forse non dovrebbero), le cose possono diventare disordinate.

Inoltre, se volevo forzare l'una o l'altra interpretazione di class, non posso.

Direi che è un po 'come usare attach: conveniente, ma a un certo punto potrebbe morderti dietro.

Pertanto, mi piacerebbe utilizzare un NSE e SE coppia:

plotfn <- function(data, xvar, yvar) { 
    plotfn_(data, 
      lazyeval::lazy_eval(xvar, data = data), 
      lazyeval::lazy_eval(yvar, data = data)) 
) 
} 

plotfn_ <- function(data, xvar, yvar){ 
    ggplot(data = data, 
     mapping = aes_(x = xvar, y = yvar)) + 
    geom_boxplot() + 
    geom_jitter(alpha = 0.1, color = "blue") 
} 

creazione di questi è in realtà più facile che la vostra funzione, credo. Puoi scegliere di acquisire tutti gli argomenti pigramente anche con lazy_dots.

Ora arriviamo più facile prevedere i risultati quando si utilizza la cassaforte versione SE:

class <- "drv" 
Class <- "drv" 
plotfn_(mpg, class, 'hwy') 
plotfn_(mpg, Class, 'hwy') 

La versione NSE risente ancora però:

plotfn(mpg, class, hwy) 
plotfn(mpg, Class, hwy) 

(Trovo leggermente fastidioso che ggplot2::aes_ doesn 't anche prendere stringhe.)

+1

sì, sono d'accordo al 100% con "La programmazione con tale funzione può dare risultati inaspettati, a seconda di quali variabili esistono nei dati e che esistono nell'ambiente.", ..., solo che a volte penso che la praticità supera il rischio di essere morso nel mio dietro. – elikesprogramming

+0

Le ultime due righe di codice non funzionavano quando ho provato a eseguirle. – student

+0

NSE nel tidyverse è cambiato, quindi è molto possibile. – Axeman