In packages

はじめに

この vignette は、2つの異なる、しかし関連する目的を持っています。

先に進む前に、使用しているパッケージをアタッチし、tidyr のバージョンを確認し、例題で使用するための小さなデータセットを作成します。

library(tidyr)
library(dplyr, warn.conflicts = FALSE)
library(purrr)

packageVersion("tidyr")
#> [1] '1.2.0'

mini_iris <- as_tibble(iris)[c(1, 2, 51, 52, 101, 102), ]
mini_iris
#> # A tibble: 6 × 5
#>   Sepal.Length Sepal.Width Petal.Length Petal.Width Species   
#>          <dbl>       <dbl>        <dbl>       <dbl> <fct>     
#> 1          5.1         3.5          1.4         0.2 setosa    
#> 2          4.9         3            1.4         0.2 setosa    
#> 3          7           3.2          4.7         1.4 versicolor
#> 4          6.4         3.2          4.5         1.5 versicolor
#> 5          6.3         3.3          6           2.5 virginica 
#> 6          5.8         2.7          5.1         1.9 virginica

パッケージでの tidyr の使用

ここでは、vignette("programming.Rmd") で説明されているように、関数の中で tidyr を使用することにすでに慣れていると仮定します。 パッケージで tidyr を使用する際には、2つの重要な考慮事項があります。

固定の列名

列名がわかっていれば、このコードはパッケージの中でも外でも同じように動作します。

mini_iris %>% nest(
  petal = c(Petal.Length, Petal.Width), 
  sepal = c(Sepal.Length, Sepal.Width)
)
#> # A tibble: 3 × 3
#>   Species    petal            sepal           
#>   <fct>      <list>           <list>          
#> 1 setosa     <tibble [2 × 2]> <tibble [2 × 2]>
#> 2 versicolor <tibble [2 × 2]> <tibble [2 × 2]>
#> 3 virginica  <tibble [2 × 2]> <tibble [2 × 2]>

しかし、R CMD check は未定義のグローバル変数 (Petal.Length, Petal.Width, Sepal.Length, Sepal.Width) について警告を出します。これは、nest()mini_iris の中の変数を探していることを知らないからです (つまり、Petal.Length とその仲間はデータ変数であり、env 変数ではありません)。

この注意を払拭する最も簡単な方法は、all_of()を使うことです。 all_of()は(starts_with()ends_with()などのような)tidyselect ヘルパーで、文字列として保存された列名を受け取ります。

mini_iris %>% nest(
  petal = all_of(c("Petal.Length", "Petal.Width")), 
  sepal = all_of(c("Sepal.Length", "Sepal.Width"))
)
#> # A tibble: 3 × 3
#>   Species    petal            sepal           
#>   <fct>      <list>           <list>          
#> 1 setosa     <tibble [2 × 2]> <tibble [2 × 2]>
#> 2 versicolor <tibble [2 × 2]> <tibble [2 × 2]>
#> 3 virginica  <tibble [2 × 2]> <tibble [2 × 2]>

あるいは、指定された変数のいくつかが入力データの中で見つからないことが OK であれば、any_of() を使いたいかもしれません。

tidyselect パッケージは、selectヘルパーの全ファミリーを提供します。 おそらく、dplyr::select() を使うことで、これらにはすでに慣れていることでしょう。

継続的インテグレーション

願わくば、あなたのパッケージにはすでに継続的インテグレーションを採用していて、R CMD check(あなた自身のテストを含む)を定期的に、例えば GitHub などでパッケージのソースに変更をプッシュするたびに実行しています。 tidyverse チームは現在、GitHub Actions に最も大きく依存しているので、それが私たちの例となります。 usethis::use_github_action() を使えば、すぐに始められます。

tidyyr の devel バージョンを対象としたワークフローを追加することをお勧めします。 どのような場合にこれを行うべきでしょうか?

tidyr の開発版に対して自作パッケージをテストする GitHub Actions ワークフローの例。:

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

name: R-CMD-check-tidyr-devel

jobs:
  R-CMD-check:
    runs-on: macOS-latest
    steps:
      - uses: actions/checkout@v2
      - uses: r-lib/actions/setup-r@master
      - name: Install dependencies
        run: |
          install.packages(c("remotes", "rcmdcheck"))
          remotes::install_deps(dependencies = TRUE)
          remotes::install_github("tidyverse/tidyr")
        shell: Rscript {0}
      - name: Check
        run: rcmdcheck::rcmdcheck(args = "--no-manual", error_on = "error")
        shell: Rscript {0}

GitHub アクションは進化し続けているので、アイデアを得るために、tidyr 自体のワークフロー(tidyverse/tidyr/.github/workflows)や、メインのr-lib/actionsレポをいつでも利用することができます。

tidyr v0.8.3 -> v1.0.0

v1.0.0では、nest()unnest() のインターフェイスを、新しい tidyverse の規約に沿ったものにするために、かなりの変更を加えています。 可能な限り下位互換性を持たせ、有益な警告メッセージを表示するようにしましたが、100%のユースケースをカバーすることはできませんでしたので、あなたのパッケージコードを変更する必要があるかもしれません。 このガイドでは、そのような場合に最小限の負担で済むようにしています。

理想的には、あなたのパッケージを tidyr 0.8.3 と tidyr 1.0.0 の両方で動作するように調整してください。 これにより、CRANへの投稿を調整する必要がなくなり、生活が非常に楽になります。 このセクションでは、https://principles.tidyverse.org/changes-multivers.htmlで説明されている一般的な原則に基づいて、そうするための推奨される方法を説明します。

すでに継続的インテグレーションを使用している場合は、tidyr の開発バージョンでテストするビルドを追加することを強くお勧めします。詳細は上記を参照してください。

このセクションでは、異なるバージョンのtidyrで異なるコードを実行する方法を簡単に説明し、その後、回避策が必要になるかもしれない主な変更点を説明します。

ここに記載されていない問題でお困りの場合は、githubまたはemailまでご連絡いただければ、お手伝いいたします。

条件付きのコード

v0.8.3 v1.0.0 の両方で動作するコードを書けることがあります。 しかし、この場合、どちらのバージョンでも特に問題のないコードが必要になることが多いので、(一時的に) 別のコードパスを用意して、それぞれに制約のないコードを記述した方が良いでしょう。 「古い」ブランチでは既存のコードを再利用し、「新しい」ブランチではクリーンで前向きなコードを書くことができます。

基本的なアプローチは次のようなものです。 まず、tidyr の新バージョンに対してTRUEを返す関数を定義します。

tidyr_new_interface <- function() {
  packageVersion("tidyr") > "0.8.99"
}

この機能を維持することを強くお勧めします。 なぜなら、パッケージの移行に関するメモを書き留めるための明白な場所を提供し、移行コードを後から簡単に削除できるからです。 もう一つの利点は、tidyr のバージョンは、ビルド時ではなく、実行時に決定されるので、ユーザーの現在の tidyr のバージョンを検出することができます。

そして、関数の中で、if 文を使って、バージョンごとに異なるコードを呼び出します。

my_function_inside_a_package <- function(...)
  # my code here

  if (tidyr_new_interface()) {
    # Freshly written code for v1.0.0
    out <- tidyr::nest(df, data = any_of(c("x", "y", "z")))
  } else {
    # Existing code for v0.8.3
    out <- tidyr::nest(df, x, y, z)
  }

  # more code here
}

新しいコードが tidyr 1.0.0 にしか存在しない関数を使用している場合、R CMD check から NOTE を受け取ることになります。 これは、CRAN 投稿コメントで説明できる数少ないノートの一つです。 tidyr 1.0.0 との互換性のためであることを述べれば、CRAN はあなたのパッケージを通してくれます。

nest() の新しい構文

変更点

なぜ変更になったかというと

変更前と変更後の例:

# v0.8.3
mini_iris %>% 
  nest(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width, .key = "my_data")

# v1.0.0
mini_iris %>% 
  nest(my_data = c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width))

# v1.0.0 avoiding R CMD check NOTE
mini_iris %>% 
  nest(my_data = any_of(c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")))

# or equivalently:
mini_iris %>% 
  nest(my_data = !any_of("Species"))

何も考えずに手っ取り早く修正したい場合は、nest() の代わりに nest_legacy() を呼び出してください。これは v0.8.3 の nest() と同じです。

if (tidyr_new_interface()) {
  out <- tidyr::nest_legacy(df, x, y, z)
} else {
  out <- tidyr::nest(df, x, y, z)
}

unnest() の新しい構文

変更点は以下の通りです。

なぜ変更したのか?

変更前後:

nested <- mini_iris %>% 
  nest(my_data = c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width))

# v0.8.3 automatically unnests list-cols
nested %>% unnest()

# v1.0.0 must be told which columns to unnest
nested %>% unnest(any_of("my_data"))

何も考えずに手っ取り早く修正したい場合は、unnest() の代わりに unnest_legacy() を呼べばいいのです。 これは v0.8.3 の unnest() と同じです。

if (tidyr_new_interface()) {
  out <- tidyr::unnest_legacy(df)
} else {
  out <- tidyr::unnest(df)
}

nest() はグループを保持します。

変更点は以下の通りです。

変更点は以下の通りです。

もし、nest() がグループを保持するようになったことが下流で問題になる場合、いくつかの選択肢があります。

例えば、mini_iris に対して、group_by()nest() を使用した後、データフレームの外にあるリストカラムで計算したとします。

(df <- mini_iris %>% 
   group_by(Species) %>% 
   nest())
#> # A tibble: 3 × 2
#> # Groups:   Species [3]
#>   Species    data            
#>   <fct>      <list>          
#> 1 setosa     <tibble [2 × 4]>
#> 2 versicolor <tibble [2 × 4]>
#> 3 virginica  <tibble [2 × 4]>
(external_variable <- map_int(df$data, nrow))
#> [1] 2 2 2

そして今度は、post hoc で、データに追加しようとします。

df %>% 
  mutate(n_rows = external_variable)
#> Error in `mutate()`:
#> ! Problem while computing `n_rows = external_variable`.
#> ✖ `n_rows` must be size 1, not 3.
#> ℹ The error occurred in group 1: Species = setosa.

これは、df がグループ化されていて、mutate() がグループを認識しているので、完全に外部の変数を追加するのは難しいからです。現実的に ungroup() する以外に、何ができるでしょうか? 一つの方法は、データフレームの中で作業することです。 つまり、map()mutate() の中に持ってきて、問題を取り除くように設計するのです。:

df %>% 
  mutate(n_rows = map_int(data, nrow))
#> # A tibble: 3 × 3
#> # Groups:   Species [3]
#>   Species    data             n_rows
#>   <fct>      <list>            <int>
#> 1 setosa     <tibble [2 × 4]>      2
#> 2 versicolor <tibble [2 × 4]>      2
#> 3 virginica  <tibble [2 × 4]>      2

何らかの理由でグループ化が適切であり、データフレームの中で作業することができない場合、tibble::add_column() はグループを意識しません。 これにより、グループ化されたデータフレームに外部データを追加することができます。

df %>% 
  tibble::add_column(n_rows = external_variable)
#> # A tibble: 3 × 3
#> # Groups:   Species [3]
#>   Species    data             n_rows
#>   <fct>      <list>            <int>
#> 1 setosa     <tibble [2 × 4]>      2
#> 2 versicolor <tibble [2 × 4]>      2
#> 3 virginica  <tibble [2 × 4]>      2

nest_() および unnest_() は廃止されました。

変更点

なぜ変わったのか?

変更前と変更後。

# v0.8.3
mini_iris %>% 
  nest_(
    key_col = "my_data",
    nest_cols = c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")
  )

nested %>% unnest_(~ my_data)

# v1.0.0
mini_iris %>% 
  nest(my_data = any_of(c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")))

nested %>% unnest(any_of("my_data"))