tidyverse

tidyverse #

tidyverseってなんぞや #

tidyverseは,データ分析と可視化を効率的に行うために設計されたr言語のパッケージ群です. このパッケージの思想上の特徴としてtidy dataの原則のもと,一貫性のある操作体系により,データ処理の生産性を保つことを特徴としています.

とはいえ含まれる内容や思想も膨大かつ難解なので,この章では,よくする使い方をメインに書いていきます. より詳しく知りたい方は,それぞれ詳細のページを見てください.(現在製作中です)

tidyverseのパッケージ群
パッケージ名役割主な機能
ggplot2データ可視化グラフィックスの文法に基づいて、グラフを宣言的に作成できる
dplyrデータ操作フィルタリング,要約,並べ替え,結合といったデータ操作
tidyrデータ整理tidy data作成
readrデータ読み込み矩形データ(csv、tsv、fwfなど)の高速読み込み
purrr関数型プログラミングベクトルやリストの操作(map関数など)
tibbleデータフレームの現代的な再構築モダンなデータフレーム構造
stringr文字列操作パターンマッチングや文字列の処理
forcats因子型操作因子データのレベルの順序や値の変更など
lubridate日付・時刻データ操作日付や時間の解析と処理

Tidy Data #

基本的にデータはExcelのように表形式となったデータを扱います.この時,ただ漫然と表を作成するのではなく,一定のルールの下に作成,加工していくことで効率や安全性が増します.具体的なルールとは以下の三つのことです.

  1. 一つの変数は一つの列に(Each variable must have its own column.)
  2. 一つの観測は一つの行に(Each observation must have its own row.)
  3. 一つの値は一つのセルに(Each value must have its own cell.)

上記3つを満たした表形式のデータを tidy data(整列データ) といいます.

一つの変数は一つの列に(Each variable must have its own column.) #

この原則における変数とは,データの特徴を表すものです.人を対象にしたデータだとすると名前,身長,体重,年齢などが該当します.考え方として列では,単位が揃うようなデータにするようにします.例えば,一つの列でcmやkgが同時に登場しないようにします.

名前年齢身長 (cm)体重 (kg)
太郎2517065
花子2216050
名前属性
太郎年齢25
太郎身長170
太郎体重65
花子年齢22
花子身長160
花子体重50

一つの観測は一つの行に(Each observation must have its own row.) #

観測とは、一つのデータのまとまりのことを指します.人を対象にしたデータだとすると名前がAlice,身長160cm,体重50kg,年齢20歳という一人のデータのことを指します.これらをまとめて一つのデータとして,行単位で追加していきます.

名前年齢身長 (cm)体重 (kg)
太郎2517065
花子2216050
名前データ
太郎25歳
太郎170cm
太郎65kg
花子22歳
花子160cm
花子50kg

一つの値は一つのセルに(Each value must have its own cell.) #

各セルに入る値は,一つだけにするという原則です.名前がAliceというデータについて,身長が2つや3つもあるのは不自然ですよね. ただし,tidyverseで提供されているtibbleというデータフレーム(Excelの表の強化版みたいなもの)は一つのセルの中に,配列を入れることができたり,さらにtibbleを格納することもできるので,必ずしもこの原則が達成されているとは限らないので注意が必要です.

名前好きな食べ物
太郎寿司
太郎ラーメン
花子カレー
花子パスタ
名前好きな食べ物
太郎寿司, ラーメン
花子カレー, パスタ

以上の内容を意識すると,統一的な記述方法でデータを解析,整形することができるので,tidy dataを意識してデータ解析すると良いでしょう.

余談
tidy dataが解析しやすいデータだとすれば,それ以外のデータは解析しずらい,めんどくさいデータです.例え人にとって見やすかったり,扱いやすかったりしてもtidyでないならコンピュータには扱いづらいです.そのため,データを提供する側と解析する側では,往々にしてこの部分ですれ違いがあったりするので,何かデータを扱うときはtidyであるかどうかを気にしてみるといいかもしれません.

パイプライン演算子 |> #

tidyverseを使用していく上でパイプライン演算子|> を活用することで,処理を簡潔かつ解釈しやすく記述することができます.

関数 #

まず,パイプライン演算子を使う前に,プログラミングにおける関数について説明します.プログラミングにおける関数は,ある処理のまとまりを集めて,いつでも呼び出せるようにしたものです.これは数学における,ある集合からある集合への対応を示す関数とは違い,サブルーチンといった方がより適切です.そのため,処理をまとめただけの関数もあれば,ある入力に対してある出力を返す関数があったりします.

ここで入力に対応するものを引数,出力に対応するものを返り値(戻り値)といいます.

純粋関数という概念があります.純粋関数とは所謂,数学における関数と等価なもので,ある入力(引数)に対して,いつも同じ出力(返り値)を返す関数です.この純粋関数を組み合わせることでコーディングをしていくのがtidyverseとパイプラインを組み合わせた方法です.

具体的には純粋関数は,同じ引数に対して,特定の返り値を返すので,ある純粋関数の返り値をそのまま,別の純粋関数の引数に入力することで,処理を記述していきます.これまでのプログラミングといえばやりたいことを逐一丁寧に記述していく手続き型プログラミングといったものを行ってきました.しかし,それとは考え方,アプローチが違ってくるので戸惑う点があるかもしれません.とはいえ,基本的にプログラミングとは何をするかというと,与えられた入力(データ)を加工していく処理,作業に他なりません.その大前提を意識しておけば,どちらもやることがたいして変わりません.以下で具体例を見てみましょう.

余談

純粋関数とは,ある引数に対して特定の返り値を返す—つまりは引数が同じ時,常に同じ返り値を返すことと,副作用が発生しない関数のことを指します.副作用とは引数以外の入力,返り値以外の出力を伴うことです.具体的には関数の外の変数を参照,変更することであったり,I/O画面やストレージと相互作用をすることを指します.

例えばprint関数であったり(文字を外部に出力するため), random関数(同じ引数を入力しても出力がランダム)といった関数が非純粋関数となります.

しかし,Rにおいてそのような関数は大多数を占めますし,本当の意味で純粋関数は少ないのですが,今回は簡単のために上記ような解説となりました.

パイプライン演算子を使ってみよう #

初めに手続きプログラミング的にコードを書いてみます.以下のコードは,データフレームdfを宣言し,value列の値が20より大きいものを選別し,groupごとに集計し,groupごとの平均値を算出したものを出力するコードです.

library(dplyr)

df <- data.frame(
    group = c("A", "B", "A", "B", "A"),
    value = c(10, 20, 30, 40, 50)
)

temp1 <- filter(df, value > 20)  # 20より大きい値をフィルタ
temp2 <- group_by(temp1, group)  # "group" でグループ化
result <- summarise(temp2, mean_value = mean(value))  # "group"ごとの平均値を計算

print(result)

このようなコードでは,変数temp1, temp2のように演算結果を一時的に保存するためだけの変数を宣言したりと無駄が多いです.無駄が多くなると,論理を追いづらくなったり,コードを手直しするにも大変です.

次の例では一時的な変数を作成しないようにコードを書き直してみます.

result <- summarise(
    group_by(filter(df, value > 20), group),
    mean_value = mean(value)
)

print(result)

この書き方では,余計な変数はありませんが,関数の中に関数を書き込んだ形になってしまい,非常に可読性が悪いです.私たちは左から右,外から内に文字を読むのに,処理の流れは右から左に,内から外の順番になってしまっています. ここでパイプライン演算子の出番です.Rのパイプライン演算子|>ある関数の返り値をそのまま,次の関数の第一引数にしてしまいます.

library(dplyr)

df |>
    filter(value > 20) |>
    group_by(group) |>
    summarise(mean_value = mean(value)) |>
    print()

こうすることによって,処理の流れと記述の流れが同じになりました.純粋関数を用いると同じ引数に対し,同じ返り値が帰ってくるので,このような記述が可能になります.

Advanced

やや発展的な内容になりますが,パイプライン演算子にはRで定義されたnative pipelineと呼ばれる|>とtidyverseのmagrittrパッケージによって提供されている%>%の2種類があります.

これらの使い分けとしては,返り値を第一引数ではない場所に用いたい時に,書き方が異なります.%>%では第二引数などに値を渡す時.を使うことで可能になります(place holderといいます).|>では_を使用しますが,x |> f(100, y=_)のように名前付き引数の時だけ使用できます.

# place holderは`.`。引数名を指定しなくても良い。
c("apple", "pineapple", "banana") %>% grepl("apple", .)
#> TRUE  TRUE FALSE

# place holderは`_`。引数名を指定しなければいけない。
c("apple", "pineapple", "banana") |> grepl("apple", x = _)
#> TRUE  TRUE FALSE

# 引数名を指定しないとエラーになる。
c("apple", "pineapple", "banana") |> grepl("apple", _)
#> Error: pipe placeholder can only be used as a named argument

また複数箇所にplace holderを使用したい場合,%>%しか選択肢はありません. とはいえ,基本的には|>の方が高速に動作し,何よりパッケージではなく,R言語そのもので定義されているので,|>の使用を個人的には推奨します.

tidyverseで遊んでみよう #

お待たせしました.ここからは実際にcsvデータを加工して,tidyverseの凄さを思い知りましょう.tidyverseには多くのパッケージが含まれますが,ここではよく使う,dplyrを中心に使っていきます.

ここで加工していくデータは,dplyr内に練習用として定義されているstarwarsのデータを用います.スターウォーズのキャラクターについての情報が詰まれたデータフレームです. glimpse関数を使用することで,データフレームの概観を知ることができます.(そのままstarwarsと入力するだけでもいいです)

starwars |> glimpse()

#> Rows: 87
#> Columns: 14
#> $ name       <chr> "Luke Skywalker", "C-3PO", "R2-D2", "Darth Vader", "L…
#> $ height     <int> 172, 167, 96, 202, 150, 178, 165, 97, 183, 182, 188, …
#> $ mass       <dbl> 77.0, 75.0, 32.0, 136.0, 49.0, 120.0, 75.0, 32.0, 84.…
#> $ hair_color <chr> "blond", NA, NA, "none", "brown", "brown, grey", "bro…
#> $ skin_color <chr> "fair", "gold", "white, blue", "white", "light", "lig…
#> $ eye_color  <chr> "blue", "yellow", "red", "yellow", "brown", "blue", "…
#> $ birth_year <dbl> 19.0, 112.0, 33.0, 41.9, 19.0, 52.0, 47.0, NA, 24.0, …
#> $ sex        <chr> "male", "none", "none", "male", "female", "male", "fe…
#> $ gender     <chr> "masculine", "masculine", "masculine", "masculine", "…
#> $ homeworld  <chr> "Tatooine", "Tatooine", "Naboo", "Tatooine", "Alderaa…
#> $ species    <chr> "Human", "Droid", "Droid", "Human", "Human", "Human",…
#> $ films      <list> <"A New Hope", "The Empire Strikes Back", "Return of…
#> $ vehicles   <list> <"Snowspeeder", "Imperial Speeder Bike">, <>, <>, <>…
#> $ starships  <list> <"X-wing", "Imperial shuttle">, <>, <>, "TIE Advance…

glimpse()を適用すると,行と列がひっくり返ってしまうので注意が必要です.しかし,これでstarwarsが87×14のtibbleで,それぞれの列がどのようなデータを保持しているがわかりました.例えばname列はcharacter(文字列)を保持している列ですね.

このようなデータが与えられたときに概観をざっくり知るのは重要です.具体的には行数(レコード数),列数(特徴量数),それぞれの列は何を内蔵しているデータなのか,列名と列のデータ型(数値型,文字列型など)に矛盾はないか,データに欠損値はないか,解析の目的にあったデータの内容かといったことを確認するのが重要になります.

問題1へ

列の操作 #

まず初めに列に対する操作です.パイプ演算子|>を使ってselect関数を適用することで,列を抽出できます.rename関数では列名を新しく付け替えることができます.

starwars |>
    select(name, height, homeworld) #name, height, homeworld列だけを抽出
#> # A tibble: 87 × 3
#>    name               height homeworld
#>    <chr>               <int> <chr>    
#>  1 Luke Skywalker        172 Tatooine 
#>  2 C-3PO                 167 Tatooine 
#>  3 R2-D2                  96 Naboo    
#>  4 Darth Vader           202 Tatooine 

starwars |>
    select(!starships) # starships以外の列を抽出
#> # A tibble: 87 × 13
#>    name     height  mass hair_color skin_color eye_color birth_year sex   gender
#>    <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
#>  1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
#>  2 C-3PO       167    75 NA         gold       yellow         112   none  mascu…
#>  3 R2-D2        96    32 NA         white, bl… red             33   none  mascu…
#>  4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…

starwars |>
    select(ends_with("color")) #末尾がcolorで終わる列(hair_color, skin_color, eye_color)を抽出
#> # A tibble: 87 × 3
#>    hair_color    skin_color  eye_color
#>    <chr>         <chr>       <chr>    
#>  1 blond         fair        blue     
#>  2 NA            gold        yellow   
#>  3 NA            white, blue red      
#>  4 none          white       yellow   

starwars |>
    select(where(is.character)) #値が文字列(chr)である列を抽出
#> # A tibble: 87 × 8
#>    name           hair_color skin_color eye_color sex   gender homeworld species
#>    <chr>          <chr>      <chr>      <chr>     <chr> <chr>  <chr>     <chr>  
#>  1 Luke Skywalker blond      fair       blue      male  mascu… Tatooine  Human  
#>  2 C-3PO          NA         gold       yellow    none  mascu… Tatooine  Droid  
#>  3 R2-D2          NA         white, bl… red       none  mascu… Naboo     Droid  
#>  4 Darth Vader    none       white      yellow    male  mascu… Tatooine  Human 

starwars |>
    select(homeworld) |> # homeworld列を抽出
    rename(home_planet = homeworld) # 列名homeworldをhome_planetに変更
#> # A tibble: 87 × 1
#>    home_planet
#>    <chr>      
#>  1 Tatooine   
#>  2 Tatooine   
#>  3 Naboo      
#>  4 Tatooine   
#>  5 Alderaan   

問題2へ 問題4へ

行の操作 #

slice_*系の関数は行の操作に使います.slice関数は指定した行番号の列を,slice_head,slice_tailはそれぞれ最初のn行と最後のn行を,slice_min, slice_maxは指定して列の値が小さい順ないし大きい順にn行抽出します.

filter関数は何かの条件と合致する(あるいは合致しない)行のみ抽出する関数です. arrange関数はソート関数です.

starwars |>
    select(name, homeworld) |> # name, homeworld列だけ抽出
    filter(homeworld == "Tatooine") |> # homeworldがTatooineの行のみを抽出
    slice_head(n = 4) # そのうち先頭4行だけ抽出
#> # A tibble: 4 × 2
#>   name               homeworld
#>   <chr>              <chr>    
#> 1 Luke Skywalker     Tatooine 
#> 2 C-3PO              Tatooine 
#> 3 Darth Vader        Tatooine 
#> 4 Owen Lars          Tatooine 

starwars |>
    select(name, height, homeworld) |> # name,height,homeworld列だけ抽出
    filter(homeworld != "Tatooine") |> # homeworldがTatooineでない行を抽出
    slice_max(height, n = 3) # そのうちheightが大きい順に3行抽出
#> # A tibble: 3 × 3
#>   name        height homeworld
#>   <chr>        <int> <chr>    
#> 1 Yarael Poof    264 Quermia  
#> 2 Tarfful        234 Kashyyyk 
#> 3 Lama Su        229 Kamino   

starwars |>
    select(name, hair_color, height, mass) |> # name, height, sex, homeworld列を抽出
    filter(!is.na(mass), height >= 100, hair_color == "blond") |> # massが欠損値がなくて,尚且つheightが100以上かつ,hair_colorがblondの行を抽出
    arrange(mass) # massが昇順になるようにソート
    #> # A tibble: 2 × 4
    #>   name             hair_color height  mass
#>   <chr>            <chr>       <int> <dbl>
#> 1 Luke Skywalker   blond         172    77
#> 2 Anakin Skywalker blond         188    84
問題3へ

データの要約 #

summarise関数を使うと変数の平均値や標準偏差などの記述統計量(要約統計量)を計算できます. group_by関数と組み合わせることで値ごとの記述統計量を出すことができます.

starwars |>
    group_by(homeworld) |> # homeworldごとに集計
    summarise(
        height_mean = mean(height, na.rm = TRUE), #欠損値を除外してheightの平均を計算
        mass_mean = mean(mass, na.rm = TRUE), #欠損値を除外してmassの平均を計算
    )
#> # A tibble: 49 × 3
#>    homeworld      height_mean mass_mean
#>    <chr>                <dbl>     <dbl>
#>  1 Alderaan              176.      64  
#>  2 Aleen Minor            79       15  
#>  3 Bespin                175       79  
#>  4 Bestine IV            180      110  
問題7へ

データの拡張 #

mutate関数はtibble内の変数を用いて計算を行い,その結果を新しい列として追加する関数です.

starwars |>
    select(name, height) |>
    mutate(height_M = height / 100) # heightを100でわりm換算したものをheight_in_mとした
#> # A tibble: 87 × 3
#>    name               height       height_M
#>    <chr>               <int>       <dbl>
#>  1 Luke Skywalker        172        1.72
#>  2 C-3PO                 167        1.67
#>  3 R2-D2                  96        0.96
#>  4 Darth Vader           202        2.02

starwars |>
    select(name, height, mass) |>
    mutate(height_M = height / 100, BMI = mass / height_M / height_M) |> # heightを100でわりm換算したものをheight_in_mとした
    mutate(is_obesity = if_else(BMI >= 25, true="obesity", false="not_obesity")) # if_else関数を用いると,条件を指定して値を指定できる
#> # A tibble: 87 × 6
#>    name               height  mass height_M   BMI is_obesity 
#>    <chr>               <int> <dbl>    <dbl> <dbl> <chr>      
#>  1 Luke Skywalker        172    77     1.72  26.0 obesity    
#>  2 C-3PO                 167    75     1.67  26.9 obesity    
#>  3 R2-D2                  96    32     0.96  34.7 obesity    
#>  4 Darth Vader           202   136     2.02  33.3 obesity    
#>  5 Leia Organa           150    49     1.5   21.8 not_obesity
問題5へ

名前空間 #

上記でも説明のあったように,tidyverseとは幾つかの便利なパッケージの総称です.tibble, ggplot2など,それぞれのパッケージごとに便利な関数が用意されています.例えばselect関数はtidyverseの中でもdplyrパッケージで提供されている関数です.

上記の例では,この関数を使用するとき,ただ単にselect(...)という書き方をしてきましたがdplyr::select(...)と言ったように明示的のパッケージ名::関数と言った形式で書くこともできます.このパッケージ名::の部分を名前空間といいます.

何故わざわざ記述量を増やすような真似をするのかというと,語彙が限られているためです.それぞれ別々のパッケージで同じ名前の関数が提供されていることがあります.この時,Rを実行しようとした際に,どのパッケージの関数を実行すればいいのか分からずバグの温床になったりします.例えば,Rの標準パッケージであるstatsと,tidyverseの一角であるdplyrにはどちらもfilter関数が定義されています.この時,ただ漫然とfilter関数を使ってしまうと,どちらのパッケージの関数が実行されたのが判然としません. そのため明示的のどのパッケージの関数なのか伝える必要があるのです.また,どのパッケージでどの関数を使えるのか勉強する意味でも,暇な時があれば,名前空間を用いてコーディングしてみるのもいいかもしれません.

ちなみに

── Attaching core tidyverse packages ───────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ─────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()

ℹ Use the conflicted package to force all conflicts to become errors

library(tidyverse)を実行すると以上のような表示が出力されます.上のAttaching core tidyverse packagesと書いてある段にはtidyverseに収録されており今回ロードされたライブラリの一覧が,下のConflictsの段にはdplyrのfilterlag関数がstatsパッケージの関数と名前が被っているけど,dplyrの方を優先して使うからなと書いてあります.

練習問題 #

以下は練習問題です.これまで学習してきたこと+αな内容ですので,適宜,chatGPTに聞いてみたり調べたりしながら解いてみてください.

問題1 #

starwars データセットの構造を確認せよ。

💡 ヒント: glimpse(), head(), dim() などを使う

参考

Answer
starwars |>
    glimpse()

starwars |>
    head()

starwars |>
    dim()
### 問題2

name, height, mass, homeworld の4列のみを選択し,キャラクターを mass の降順で並べ替えよ.

💡 ヒント: arrange関数と,desc関数について調べてみよう

列の操作

Answer
starwars |>
    select(name, heightm, mass, homeworld) |>
    arrange(desc(mass))

問題3 #

starwarsデータセットのうち,gender が “masculine” かつ mass (体重) が 80kg 以上のキャラクターを抽出せよ

💡 ヒント: filter関数について調べてみよう

行の操作

Answer
starwars |>
    filter(gender == "masculine", mass >= 80)

問題4 #

nameと列名にアンダーバー _を含む列のみを選択せよ

💡 ヒント: select, contains関数の使い方を調べてみよう

列の操作

Answer
starwars |>
    select(name, contains("_"))

問題5 #

birth_year が 100 より大きい場合は “Old”、それ以外は “Young” とする age_category という新しい列を作成せよ

💡 ヒント: if_else関数の使い方を調べてみよう

データの拡張

if_else関数

Answer
starwars |>
    mutate(age_category = if_else(birth_year > 100, "Old", "Young"))

問題6 #

以下のルールに基づいて weight_category という新しい列を作成せよ。

  • mass が 100 kg 以上なら “Heavy”
  • mass が 50 以上 100 未満なら “Medium”
  • mass が 50 未満なら “Light”
  • mass が NA の場合は “Unknown”

💡 ヒント: case_when関数の使い方を調べてみよう

case_when関数

Answer
starwars |>
    mutate(weight_category = case_when(
        is.na(mass) ~ "Unknown",
        mass >= 100 ~ "Heavy",
        mass >= 50 ~ "Medium",
        TRUE ~ "Lignt"
    ))

問題7 #

species ごとに、

  • キャラクター数 (n())
  • height の平均 (mean(height, na.rm = TRUE))
  • mass の平均 (mean(mass, na.rm = TRUE)) を求めよ。

ただし、キャラクター数が 3 人未満の種族は除外せよ。

💡 ヒント: group_by, summarise, filter関数の使い方を調べてみよう

データの要約

Answer
starwars |>
    group_by(species) |>
    summarise(
        N = n(),
        height_mean = mean(height, na.rm = TRUE),
        mass_mean = mean(mass, na.rm = TRUE)
    ) |>
    filter(N > 3)

問題8 #

NA を含む数値型の列をすべて選択し、それらの NA を 0 に置き換えよ。

💡 ヒント: mutate, across, where関数の使い方を調べてみよう. Rにおける無名関数について調べてみよう

Answer
starwars |>
    mutate(across(where(is.numeric), ~ replace(., is.na(.), 0)))