この vignette は、2つの異なる、しかし関連する目的を持っています。
パッケージで tidyr を使用するための一般的なベストプラクティスを文書化します。 パッケージでのggplot2の使用を参考にしています。
tidyr v0.8.3 から v1.0.0 への移行のための移行パターンを説明しています。 v1.0.0への移行パターンを説明しています。 これは tidyr 内および tidyverse の残りの部分との一貫性を高めるためです。
先に進む前に、使用しているパッケージをアタッチし、tidyr のバージョンを確認し、例題で使用するための小さなデータセットを作成します。
library(tidyr)
library(dplyr, warn.conflicts = FALSE)
library(purrr)
packageVersion("tidyr")
#> [1] '1.2.0'
<- as_tibble(iris)[c(1, 2, 51, 52, 101, 102), ]
mini_iris
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
ここでは、vignette("programming.Rmd"
) で説明されているように、関数の中で tidyr を使用することにすでに慣れていると仮定します。 パッケージで tidyr を使用する際には、2つの重要な考慮事項があります。
R CMD CHECK
の注記を避ける方法。列名がわかっていれば、このコードはパッケージの中でも外でも同じように動作します。
%>% nest(
mini_iris 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 ヘルパーで、文字列として保存された列名を受け取ります。
%>% nest(
mini_iris 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 と密接に結合している場合は、これを常に残すことを検討してください。 これにより、tidyrの変更があなたのパッケージに影響するかどうかを知ることができます。
tidyr のリリースの直前?それ以外の人には、tidyr を追加(または 既存の)tidyr-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レポをいつでも利用することができます。
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で異なるコードを実行する方法を簡単に説明し、その後、回避策が必要になるかもしれない主な変更点を説明します。
nest()
と unnest()
に新しいインターフェースが追加されました。nest()
はグループを保持します。nest_()
と unnest_()
は廃止されました。ここに記載されていない問題でお困りの場合は、githubまたはemailまでご連絡いただければ、お手伝いいたします。
v0.8.3 と v1.0.0 の両方で動作するコードを書けることがあります。 しかし、この場合、どちらのバージョンでも特に問題のないコードが必要になることが多いので、(一時的に) 別のコードパスを用意して、それぞれに制約のないコードを記述した方が良いでしょう。 「古い」ブランチでは既存のコードを再利用し、「新しい」ブランチではクリーンで前向きなコードを書くことができます。
基本的なアプローチは次のようなものです。 まず、tidyr の新バージョンに対してTRUE
を返す関数を定義します。
<- function() {
tidyr_new_interface packageVersion("tidyr") > "0.8.99"
}
この機能を維持することを強くお勧めします。 なぜなら、パッケージの移行に関するメモを書き留めるための明白な場所を提供し、移行コードを後から簡単に削除できるからです。 もう一つの利点は、tidyr のバージョンは、ビルド時ではなく、実行時に決定されるので、ユーザーの現在の tidyr のバージョンを検出することができます。
そして、関数の中で、if
文を使って、バージョンごとに異なるコードを呼び出します。
<- function(...)
my_function_inside_a_package # my code here
if (tidyr_new_interface()) {
# Freshly written code for v1.0.0
<- tidyr::nest(df, data = any_of(c("x", "y", "z")))
out else {
} # Existing code for v0.8.3
<- tidyr::nest(df, x, y, z)
out
}
# more code here
}
新しいコードが tidyr 1.0.0 にしか存在しない関数を使用している場合、R CMD check
から NOTE
を受け取ることになります。 これは、CRAN 投稿コメントで説明できる数少ないノートの一つです。 tidyr 1.0.0 との互換性のためであることを述べれば、CRAN はあなたのパッケージを通してくれます。
nest()
の新しい構文変更点
.key
引数で指定しなくなりました。new_col = <something about existing cols>
.なぜ変更になったかというと
メタデータに ...
を使用することは、問題のあるパターンであり、私たちはこれを避けようとしています。 https://design.tidyverse.org/dots-data.html
new_col = <something about existing cols>
の構成により、一度に複数のネストしたリストカラムを作成することができます。 ネストした複数のリストカラムを一度に作成することができます(“multi-nest”)。
%>%
mini_iris nest(petal = matches("Petal"), sepal = matches("Sepal"))
#> # 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]>
変更前と変更後の例:
# 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()) {
<- tidyr::nest_legacy(df, x, y, z)
out else {
} <- tidyr::nest(df, x, y, z)
out }
unnest()
の新しい構文変更点は以下の通りです。
アンネストされるカラムは、デフォルトではすべてのリストカラムではなく、明示的に指定する必要があります。 デフォルトではすべてのリストカラムになります。これにより、 .drop
や .preserve
も非推奨となります。
.sep
は非推奨となり、names_sep
に置き換えられました。
unnest()
は[新興のTidyverse標準][名前の修復]を使用します。 重複した名前を曖昧にするために。以前の方法を要求するには names_repair = tidyr_legacy
を使用してください。 以前のアプローチを要求します。
.id
は、unnest
の前に名前のカラムを作成することで簡単に置き換えられるため、非推奨となりました。 なぜなら、.id
は、unnest()
の前に名前のカラムを作成することで簡単に置き換えられるからです。
# v0.8.3
%>% unnest(x, .id = "id")
df
# v1.0.0
%>% mutate(id = names(x)) %>% unnest(x)) df
なぜ変更したのか?
メタデータに ...
を使用することは、問題のあるパターンであり、私たちはこれを避けようとしています。 https://design.tidyverse.org/dots-data.html
詳細引数への変更は、tidyverse の複数のパッケージにわたって展開される機能に関連しています。 新しい vctrs パッケージ からのプロトタイプサポートを公開します。 names_repair
は、重複した名前や非シンタクティックな名前をどうするかを指定します。 これは tibble や readxl と一致しています。
変更前後:
<- mini_iris %>%
nested nest(my_data = c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width))
# v0.8.3 automatically unnests list-cols
%>% unnest()
nested
# v1.0.0 must be told which columns to unnest
%>% unnest(any_of("my_data")) nested
何も考えずに手っ取り早く修正したい場合は、unnest()
の代わりに unnest_legacy()
を呼べばいいのです。 これは v0.8.3 の unnest()
と同じです。
if (tidyr_new_interface()) {
<- tidyr::unnest_legacy(df)
out else {
} <- tidyr::unnest(df)
out }
nest()
はグループを保持します。変更点は以下の通りです。
nest()
は入力に含まれるグループを保持するようになりました。変更点は以下の通りです。
dplyr::group_modify()
, group_map()
などを参照してください。もし、nest()
がグループを保持するようになったことが下流で問題になる場合、いくつかの選択肢があります。
結果に ungroup()
を適用する。このレベルのプラグマティズムは示唆しています。 しかし、少なくとも次の2つの選択肢を考慮すべきだと考えています。
そもそもグループ化するべきではない。そもそもグループ化するべきではなかった。 group_by()
を廃止して、入れ子にする列と入れ子にしない列を nest()
で直接指定する。
下流のコードをグループ化に対応させる。
例えば、mini_iris
に対して、group_by()
と nest()
を使用した後、データフレームの外にあるリストカラムで計算したとします。
<- mini_iris %>%
(df 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]>
<- map_int(df$data, nrow))
(external_variable #> [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 ::add_column(n_rows = external_variable)
tibble#> # 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_()
は廃止されました。変更点
nest_()
と unnest_()
は動作しなくなりました。なぜ変わったのか?
foo()
の補完としての foo_()
など。 例えば,foo_()
は foo()
の補完物です.変更前と変更後。
# v0.8.3
%>%
mini_iris nest_(
key_col = "my_data",
nest_cols = c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")
)
%>% unnest_(~ my_data)
nested
# v1.0.0
%>%
mini_iris nest(my_data = any_of(c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")))
%>% unnest(any_of("my_data")) nested