web-dev-qa-db-ja.com

関数内のggplot2の遅延評価

私は主にggplot2を視覚化に使用します。通常、私はプロットをインタラクティブに設計します(つまり、NSEを使用する生のggplot2コード)が、最終的には、プロットするデータと変数を受け取る関数にそのコードをラップすることになります。そして、これは常に少し悪夢です。

したがって、典型的な状況は次のようになります。いくつかのデータがあり、そのプロットを作成します(この場合、ggplot2に付属するmpgデータセットを使用した非常に単純な例です)。

library(ggplot2)
data(mpg)

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


そして、プロットの設計が終了したら、通常、さまざまな変数やデータなどに使用したいと思います。そこで、プロットのデータと変数を引数として受け取る関数を作成します。ただし、NSEのため、関数ヘッダーを記述してから、関数引数の変数をコピー/貼り付けして置換するほど簡単ではありません。以下に示すように、それは機能しません。

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") # 


したがって、たとえば、NSEを使用するaesの代わりにaes_stringを使用して、コードに戻って修正する必要があります(この例では、かなり簡単ですが、より複雑ですたくさんの変換とレイヤーがあるプロットでは、これは悪夢になります)。

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


そして、非常に便利なNSEとlazyevalが見つかりました。だから私はこのようなことをするのが好きです。

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


これにより、プロット関数に多くの柔軟性がもたらされます。たとえば、引用符で囲まれた変数名または引用符で囲まれていない変数名、さらには変数名の代わりにデータを直接渡すことができます(遅延評価の悪用のようなもの)。

しかし、これには大きな問題があります。この機能はプログラムで使用することはできません。

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)

そのため、ループ(lapplyなど)を使用して、変数またはデータのさまざまな組み合わせに対して同じプロットを作成することはできません。

それで、私は怠惰な、標準的な、そして非標準的な評価をさらに乱用し、それらすべてを組み合わせて、上記の柔軟性とプログラムで機能を使用する能力の両方を手に入れようと考えました。基本的に、私が行うことは、tryCatchを使用して各変数の式を最初にlazy_evalし、失敗した場合は、解析された式を評価することです。

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) 


したがって、前述の柔軟性に加えて、ワンライナーなどを使用して、異なる変数(またはデータ)で同じプロットの多くを作成できるようになりました。

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

## [[1]]

## 
## [[2]]

## 
## [[3]]


非常に実用的ですが、これは良い習慣ではないと思います。しかし、それはどれほど悪い習慣ですか?それが私の重要な質問です。両方の長所を活かすために、他にどのような選択肢を使用できますか?

もちろん、このパターンが問題を引き起こす可能性があることがわかります。例えば。

# 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


そしていくつかの(多くの?)他の問題。しかし、構文の柔軟性に関する利点は、他の問題よりも重要であるように思われます。これについて何か考えはありますか?

7

明確にするために提案された関数を抽出します。

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")
}

このような関数は、文字列と裸の変数名を自由に組み合わせることができるため、一般的に非常に便利です。しかし、あなたが言うように、それは常に安全であるとは限りません。次の不自然な例を考えてみましょう。

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

あなたの関数は何を生成しますか?これらは同じですか(そうではありません)?結果がどうなるかは私にはよくわかりません。このような関数を使用してプログラミングすると、dataに存在する変数と環境に存在する変数によっては、予期しない結果が生じる可能性があります。多くの人がxxvarcountなどの変数名を使用しているため(おそらく使用すべきではありませんが)、事態が混乱する可能性があります。

また、classのいずれかの解釈を強制したい場合は、できません。

attachを使用するのと似ていると思いますが、ある時点であなたの後ろに噛み付く可能性があります。

したがって、NSEとSEのペアを使用します。

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")
}

これらの作成は、実際には関数よりも簡単だと思います。 lazy_dotsを使用してすべての引数を遅延キャプチャすることもできます。

安全なSEバージョンを使用すると、結果をより簡単に予測できるようになりました。

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

ただし、NSEバージョンは引き続き影響を受けます。

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

ggplot2::aes_も文字列を受け取らないのは少し面倒です。)

2
Axeman