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のように表形式となったデータを扱います.この時,ただ漫然と表を作成するのではなく,一定のルールの下に作成,加工していくことで効率や安全性が増します.具体的なルールとは以下の三つのことです.
- 一つの変数は一つの列に(Each variable must have its own column.)
- 一つの観測は一つの行に(Each observation must have its own row.)
- 一つの値は一つのセルに(Each value must have its own cell.)
上記3つを満たした表形式のデータを tidy data(整列データ) といいます.
一つの変数は一つの列に(Each variable must have its own column.) #
この原則における変数とは,データの特徴を表すものです.人を対象にしたデータだとすると名前,身長,体重,年齢などが該当します.考え方として列では,単位が揃うようなデータにするようにします.例えば,一つの列でcmやkgが同時に登場しないようにします.
名前 | 年齢 | 身長 (cm) | 体重 (kg) |
---|---|---|---|
太郎 | 25 | 170 | 65 |
花子 | 22 | 160 | 50 |
名前 | 属性 | 値 |
---|---|---|
太郎 | 年齢 | 25 |
太郎 | 身長 | 170 |
太郎 | 体重 | 65 |
花子 | 年齢 | 22 |
花子 | 身長 | 160 |
花子 | 体重 | 50 |
一つの観測は一つの行に(Each observation must have its own row.) #
観測とは、一つのデータのまとまりのことを指します.人を対象にしたデータだとすると名前がAlice,身長160cm,体重50kg,年齢20歳という一人のデータのことを指します.これらをまとめて一つのデータとして,行単位で追加していきます.
名前 | 年齢 | 身長 (cm) | 体重 (kg) |
---|---|---|---|
太郎 | 25 | 170 | 65 |
花子 | 22 | 160 | 50 |
名前 | データ |
---|---|
太郎 | 25歳 |
太郎 | 170cm |
太郎 | 65kg |
花子 | 22歳 |
花子 | 160cm |
花子 | 50kg |
一つの値は一つのセルに(Each value must have its own cell.) #
各セルに入る値は,一つだけにするという原則です.名前がAliceというデータについて,身長が2つや3つもあるのは不自然ですよね. ただし,tidyverseで提供されているtibbleというデータフレーム(Excelの表の強化版みたいなもの)は一つのセルの中に,配列を入れることができたり,さらにtibbleを格納することもできるので,必ずしもこの原則が達成されているとは限らないので注意が必要です.
名前 | 好きな食べ物 |
---|---|
太郎 | 寿司 |
太郎 | ラーメン |
花子 | カレー |
花子 | パスタ |
名前 | 好きな食べ物 |
---|---|
太郎 | 寿司, ラーメン |
花子 | カレー, パスタ |
以上の内容を意識すると,統一的な記述方法でデータを解析,整形することができるので,tidy dataを意識してデータ解析すると良いでしょう.
余談
パイプライン演算子 |>
#
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
行の操作 #
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
データの要約 #
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
データの拡張 #
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
名前空間 #
上記でも説明のあったように,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のfilter
とlag
関数がstats
パッケージの関数と名前が被っているけど,dplyr
の方を優先して使うからなと書いてあります.
練習問題 #
以下は練習問題です.これまで学習してきたこと+αな内容ですので,適宜,chatGPTに聞いてみたり調べたりしながら解いてみてください.
問題1 #
starwars
データセットの構造を確認せよ。
💡 ヒント: glimpse(), head(), dim() などを使う
Answer
starwars |>
glimpse()
starwars |>
head()
starwars |>
dim()
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
関数の使い方を調べてみよう
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
関数の使い方を調べてみよう
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)))