この記事ではdplyr
のfilter()
関数について解説します!filter()
関数は簡単でありながら、応用次第では複雑なフィルターをあてがうことができる、奥が深い関数です。
今回は「困ったときに辞書代わりに引く」いつものスタイルではなく、「filter()
関数の原理原則を本質的に理解する」スタイルで読んでもらえればと思います♪
filter()内に記述した条件を満たす行が抽出されます。
pacman::p_load(tidyverse)
starwars %>%
dplyr::filter(height > 100)
r$> starwars %>%
dplyr::filter(height > 100)
# A tibble: 74 × 14
name height mass hair_color skin_color eye_color birth_year sex
<chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr>
1 Luke Sky… 172 77 blond fair blue 19 male
2 C-3PO 167 75 NA gold yellow 112 none
3 Darth Va… 202 136 none white yellow 41.9 male
4 Leia Org… 150 49 brown light brown 19 fema…
5 Owen Lars 178 120 brown, gr… light blue 52 male
6 Beru Whi… 165 75 brown light blue 47 fema…
7 Biggs Da… 183 84 black light brown 24 male
8 Obi-Wan … 182 77 auburn, w… fair blue-gray 57 male
9 Anakin S… 188 84 blond fair blue 41.9 male
10 Wilhuff … 180 NA auburn, g… fair blue 64 male
# … with 64 more rows, and 6 more variables: gender <chr>,
# homeworld <chr>, species <chr>, films <list>, vehicles <list>,
# starships <list>
Info
以降filter()
をdplyr::filter()
としていますが、filter()
関数が意図せずほかのパッケージのfilter()
と衝突することを回避する目的で使用しています。 意識する必要がない場合がほとんどですが、filter()
やselect()は同じ名前の関数が存在するケースが非常に多いため、私はバグ回避のためこうしています。
使い方の例を挙げるとキリがないのですが、いくつかよくあるパターンの例を挙げてみます。 一つ目は数式での評価です。
例えば、starwarsデータセットの中から、mass(重量)が50以上の行をフィルターするには以下のようにします。
starwars %>%
dplyr::filter(mass > 50)
数式は以下のように複雑なものになっても問題なくfilterされます。
starwars %>%
filter(log10(mass * height / 100) * pi > 7)
注意しなければならないポイントとしては、numeric型ではない列に対して>,>=, \<=, \<演算子を適用してしまうと、おそらくあなたの意図しない挙動が起きます。
starwars %>%
mutate(name > 1)
name列はcharacter型なので、\character \> 1\
は一般的なプログラミング言語だと明らかにエラーが出ます。しかしながら、R言語ではエラーにならぬのです。
"mojiretsu" > 1
# TRUE
除算・乗算であればきちんとエラーが出てくれます。
#| error=TRUE
"mojiretsu" / 1
# "mojiretsu"/1 でエラー: 二項演算子の引数が数値ではありません
完全一致するかどうかで判断する、という使い方もよくあるパターンの一つですね。
starwars %>%
filter(species == "Droid")
逆に、不一致を評価したいときは!=
演算子を使用します。
starwars %>%
filter(species != "Droid")
Info
論理演算(TRUEかFALSEを判断する処理)では、「!」が否定を表します🖐️
たとえば、starwarsデータセットの中から、“Skywalker”を含む行(部分一致)を抽出したいとします。 \ 列名であればcontains("Skywalker")
のような部分一致選択が可能ですが、これはtidyselectという特殊なメソッドで実現されています。 starts_with()
やcontains()
は行に対して実行することはできません。
もし行に対して文字列フィルターをしたい場合には、stringr
というパッケージを使用します。
starwars %>%
filter(str_detect(name, "Skywalker"))
どういう理屈でこう書くのかはいったん置いといて、今はfilter(str_detect(列名, "文字列"))
という文法をイディオムとして覚えておけばよいでしょう。
また、str_detect()
は正規表現にも対応しているので、複雑なフィルターも可能です!
starwars %>%
filter(str_detect(name, "^S")) # 「"S"から始まる」パターン一致
データ分析でよくある事例として、NA(欠損値)がデータに含まれているケースがあります。 組み込み関数is.na()
を使用することで特定の列のNA判定をすることができます。
たとえばこのようなデータフレームがあったとします。
tibble(var1 = LETTERS[1:10], var2 = c(1, 2, 3, NA, 5, 6, NA, NA, 9, 10))
# # a tibble: 10 × 2
# # var1 var2
# # <chr> <dbl>
# # 1 a 1
# # 2 b 2
# # 3 c 3
# # 4 d na
# # 5 e 5
# # 6 f 6
# # 7 g na
# # 8 h na
# # 9 i 9
# # 10 j 10
この中からvar2の列がNAではないものをフィルターしたしたい場合、filter()
を使ってこのようにします。
tibble(var1 = LETTERS[1:10], var2 = c(1, 2, 3, NA, 5, 6, NA, NA, 9, 10)) %>%
dplyr::filter(!is.na(var2))
今回のように「NAを含む行を消す」だけの用途であれば、以下のように
na.omit()
するだけの方が簡単ですが、na.omit()
はNAを一つでも含む行をすべて消してしまう点には注意が必要です⚠️na.omit()
tibble(var1 = LETTERS[1:10], var2 = c(1, 2, 3, NA, 5, 6, NA, NA, 9, 10)) %>% mutate(var3 = c(NA, NA, NA, NA, NA, NA, NA, NA, NA, 1)) %>% na.omit() # # A tibble: 1 × 3 # var1 var2 var3 # # 1 J 10 1
記事をアップした後に「そういえばこれもよく使うな💡」と思ったので追記です。
文字列が含まれる列を基準に行を抽出したいとき、その列が多様なデータで構成されているとフィルターが大変になってしまいます。もしも「~~のいずれかに合致する行」を抽出したいのであれば、%in%
演算子を使うといいと思います。
read_csv("https://github.com/eggplants/nijisanji-v23d-status/raw/master/result.csv") %>%
dplyr::filter(name %in% c("葛葉", "社築", "剣持刀也", "でびでび・でびる"))
# ℹ Use `spec()` to retrieve the full column specification for this data.
# ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# # A tibble: 4 × 5
# name popularity `2dv2` `2dv3` `3d`
# <chr> <dbl> <chr> <chr> <chr>
# 1 葛葉 125 o o o
# 2 剣持刀也 60 o x o
# 3 社築 60 o x o
# 4 でびでび・でびる 45 o o o
データ出典: nijisanji-v23d-status https://github.com/eggplants/nijisanji-v23d-status/blob/master/result.csv
このように手打ちで照合リストを作ることもありますが、別のデータフレームのcolnames()
から作るとか、paste()
関数を使って作るなどして半手動で作る方がスマートでしょうね🤖
between()
と組み合わせるせっかくだからとことんdplyr::filter()
の使い方を調べておこう!と思って調べたら私も知らなかったやつがでてきました😏 思いっきり公式のドキュメントにも書いてあったんですけどね・・・💧
https://dplyr.tidyverse.org/reference/between.html
between()と組み合わせると、数値型の列に対して「○以上○以下」のフィルターが可能になるそうです💡
starwars %>%
dplyr::filter(between(height, 100, 130))
# # A tibble: 2 × 14
# name height mass hair_color skin_color eye_color birth_year
# <chr> <int> <dbl> <chr> <chr> <chr> <dbl>
# 1 Sebulba 112 40 none grey, red orange NA
# 2 Gasgano 122 NA none white, bl… black NA
# # … with 7 more variables: sex <chr>, gender <chr>,
# # homeworld <chr>, species <chr>, films <list>,
# # vehicles <list>, starships <list>
私はあんまり使わないかなあ・・・。
group_by()
と組み合わせる初心者の方にはgroup_by()
がまず難しいかもしれませんが、とりあえず今は**group_by()
はグループごとの処理を可能にする関数である**ということだけ押さえておきましょう。
たとえば、starwarsデータについて、「各種族(species)の中で平均よりも身長が高いデータ」を計算したい場合には以下のようにします。
starwars %>%
group_by(species) %>%
dplyr::filter(height > mean(height)) %>%
# わかりやすくするためにグループ変数のspeciesを先頭に移動
relocate(species, .before = name)
# # A tibble: 6 × 14
# # Groups: species [6]
# species name height mass hair_color skin_color eye_color
# <chr> <chr> <int> <dbl> <chr> <chr> <chr>
# 1 Gungan Roos Tar… 224 82 none grey orange
# 2 Zabrak Darth Ma… 175 80 none red yellow
# 3 Twi'lek Bib Fort… 180 NA none pale pink
# 4 Mirialan Luminara… 170 56.2 black yellow blue
# 5 Kaminoan Lama Su 229 88 none grey black
# 6 Wookiee Tarfful 234 136 brown brown blue
# # … with 7 more variables: birth_year <dbl>, sex <chr>,
# # gender <chr>, homeworld <chr>, films <list>,
# # vehicles <list>, starships <list>
こんな感じでmean()
, max()
などと組み合わせる例が多いでしょうか。
他には、グループのデータ数で区切るためにn()
を使うというのもありますね。n()
は与えれたデータフレーム(tibble)の行数を得るシンプルな関数ですが、割と地味に使います。
以下の例はspeciesをグループ変数として、データが二つ以上ある場合のみを抽出しています。カテゴリー変数(グループ変数)が非常に雑多で多いけどもざっくり平均をとってみたい、なんて時に使いますね。
Info
バイオインフォマティクスでは、腸内細菌叢構成の解析で属レベル解析をしたい場合などに使います。属情報をグループにすると、データ数が1つしかない菌が数多く存在してしまい、雑多な情報に支配されてしまいます。n()>1
を使用すればこれらの不要な情報を排除することができます。
starwars %>%
group_by(species) %>%
dplyr::filter(n() > 1)
# # A tibble: 58 × 14
# # Groups: species [9]
# name height mass hair_color skin_color eye_color birth_year
# <chr> <int> <dbl> <chr> <chr> <chr> <dbl>
# 1 Luke … 172 77 blond fair blue 19
# 2 C-3PO 167 75 NA gold yellow 112
# 3 R2-D2 96 32 NA white, bl… red 33
# 4 Darth… 202 136 none white yellow 41.9
# 5 Leia … 150 49 brown light brown 19
# 6 Owen … 178 120 brown, gr… light blue 52
# 7 Beru … 165 75 brown light blue 47
# 8 R5-D4 97 32 NA white, red red NA
# 9 Biggs… 183 84 black light brown 24
# 10 Obi-W… 182 77 auburn, w… fair blue-gray 57
# # … with 48 more rows, and 7 more variables: sex <chr>,
# # gender <chr>, homeworld <chr>, species <chr>, films <list>,
# # vehicles <list>, starships <list>
ここまで来ると結構マニアックな内容かもなので、忘れたらまたこの記事を見直す程度でいいでしょう。
dplyr::filter()
関数をちゃんと理解するここまではfilter()
関数の典型的な使い方を紹介してきました。一見すると多種多様な使い方に見えるかもしれませんが、filter()関数は一貫した原理で動作しています。その原理とは、****論理値ベクトルを引数にとって、TRUEの行だけを残しているということです。
たとえば、一番最初の例で考えてみます。
starwars %>%
dplyr::filter(height > 100)
この例ではheight
が100よりも大きい行をフィルターしました。実はheight > 100
という表現だけで論理値ベクトルが返ってくる処理となっています。
わかりやすいように、height > 100
の結果をmutate()
関数で新しい列に取り出してみましょう。
starwars %>%
mutate(height_greater_than_100 = height > 100, .before = name)
# # A tibble: 87 × 15
# height_greater_th… name height mass hair_color skin_color
# <lgl> <chr> <int> <dbl> <chr> <chr>
# 1 TRUE Luke… 172 77 blond fair
# 2 TRUE C-3PO 167 75 NA gold
# 3 FALSE R2-D2 96 32 NA white, bl…
# 4 TRUE Dart… 202 136 none white
# 5 TRUE Leia… 150 49 brown light
# 6 TRUE Owen… 178 120 brown, gr… light
# 7 TRUE Beru… 165 75 brown light
# 8 FALSE R5-D4 97 32 NA white, red
# 9 TRUE Bigg… 183 84 black light
# 10 TRUE Obi-… 182 77 auburn, w… fair
# # … with 77 more rows, and 9 more variables: eye_color <chr>,
# # birth_year <dbl>, sex <chr>, gender <chr>,
# # homeworld <chr>, species <chr>, films <list>,
# # vehicles <list>, starships <list>
今足した列が、論理値のベクトル(TRUE, TRUE, FALSE)になっていますよね? この列の中でTRUEになっている行が抽出されていたというのが種明かしです。 論理値のベクトルならなんだっていいわけですから、直接論理値ベクトルを引数に与えることだってできるわけですね。試しにはじめの二行分のTRUEと85行分のFALSEを持つ論理値ベクトルをfilterの引数に与えてみましょう。
starwars %>%
dplyr::filter(c(TRUE, TRUE, rep(FALSE, 85)))
# # A tibble: 2 × 14
# name height mass hair_color skin_color eye_color
# <chr> <int> <dbl> <chr> <chr> <chr>
# 1 Luke Skywalker 172 77 blond fair blue
# 2 C-3PO 167 75 NA gold yellow
# # … with 8 more variables: birth_year <dbl>, sex <chr>,
# # gender <chr>, homeworld <chr>, species <chr>,
# # films <list>, vehicles <list>, starships <list>
このとおり、最初の二行だけが抽出されました。 というわけで、filter()
の条件は一見多種多様に見えますが、TRUE/FALSEの論理値ベクトルを与える方法であれば何をしたっていいわけです。これがfilter()
関数が柔軟に操作できる所以です。
次に、私がバグを防ぐために使うテクニックをご紹介します👮♂️
論理演算(かつ/または/~でない.. といった計算)を使えば複雑な条件でfilter関数を使うことができますが、正直私はちょっと苦手です😓というのも、論理演算子がいくつも組み合わさると以下のように式が複雑になってしまい、後々見返したときにどういった条件でフィルターしているのかがわかりにくくなるためです。
starwars %>%
dplyr::filter(height > 100 & mass < 60 & species == "Human")
こういったコードはバグのもとになりそうで少し危なっかしい感じがします。複雑な条件でフィルターしたい場合には以下のように条件そのものを列として用意することで、よりわかりやすいコーディングにすることができます😮
starwars %>%
mutate(
is_tall = height > 100,
is_light = mass < 60,
is_human = species == "Human"
) %>%
dplyr::filter(is_tall & is_light & is_human)
dplyr::if_any()
とdplyr::if_all()
は2021年に追加されたばかりの新しい関数です🌞 正直私もまだ実務で使いこなしているわけではないので、今回はdplyr::filter()
と組み合わせた使い方をさらっと紹介しておきます。
dplyr::if_all()
を使うと、複数列に対して同じ条件のフィルターを一度に課すことができるようになります。 たとえば、「50以上である」という条件を「すべての数値データ列」に課すには、以下のようにします。
starwars %>%
dplyr::filter(
if_all(where(is.numeric), function(x) x > 50)
)
Info
x > 50
の部分には無名関数を使用しています。
ここで使用したif_all()
は全てがTRUEである場合に使用します。一方、if_any()
は一つでもTRUEがある場合に使用します。割とわかりやすいネーミングで非常にいい感じです😄
注意
昔(二年以上前)ではfilter_at()
やfilter_all()
を使用して上記の操作を行っていましたが、今ではdeprecated(=非推奨)となっています。今後使用できなくなる可能性が高いので、使用はやめておきましょう。
ということで、今回はdplyr::filter()
関数の紹介でした! tidyverseを使ったデータ整形において、filter()
関数は使用率トップ3に入る重要な関数です。 この記事で皆さんが今以上にfilter()
関数を便利に使いこなせることができるよう願っています♪
それではまた!⛄