[tidyverse関数辞書] 条件に合う行をデータフレームから抽出するdplyr::filter()

tidyverse辞書

はじめに

この記事ではdplyrfilter()関数について解説します!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()は同じ名前の関数が存在するケースが非常に多いため、私はバグ回避のためこうしています。

よくある使い方1 numericに対して数式で評価する

使い方の例を挙げるとキリがないのですが、いくつかよくあるパターンの例を挙げてみます。
一つ目は数式での評価です。

例えば、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 でエラー:  二項演算子の引数が数値ではありません

よくある使い方2 一致/不一致

完全一致するかどうかで判断する、という使い方もよくあるパターンの一つですね。

starwars %>% 
    filter(species == "Droid")

逆に、不一致を評価したいときは!= 演算子を使用します。

starwars %>% 
    filter(species != "Droid")
Info

論理演算(TRUEかFALSEを判断する処理)では、「!」が否定を表します🖐️

よくある使い方3 文字列を含む行の抽出

たとえば、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"から始まる」パターン一致

よくある使い方4 NA判定をする

データ分析でよくある事例として、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

よくある使い方5: 文字列が~に含まれる

記事をアップした後に「そういえばこれもよく使うな💡」と思ったので追記です。

文字列が含まれる列を基準に行を抽出したいとき、その列が多様なデータで構成されているとフィルターが大変になってしまいます。もしも「~~のいずれかに合致する行」を抽出したいのであれば、%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()の使い方を調べておこう!と思って調べたら私も知らなかったやつがでてきました😏 思いっきり公式のドキュメントにも書いてあったんですけどね・・・💧

Do values in a numeric vector fall in specified range? — between
This is a shortcut for x >= left & x <= right, implemented efficiently in C++ for local values, and translated to the appropriate SQL for remote table...

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)

最新情報: if_any() if_all() と組み合わせる

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()関数を便利に使いこなせることができるよう願っています♪

それではまた!⛄

コメント

タイトルとURLをコピーしました