シラバスのほかの部分では、さまざまな特徴をもつデータ型を見てきました。
このコンセプトでは、多次元で異なる型が混在するデータを保存する方法を見ていきます。 実際のところ、現実世界のデータの_ほとんど_はこの形をしているので、ここからRが実務で(主に)どのように使われているかの核心に入っていきます。
ここ数十年のあいだに、Rは表形式のデータを扱うためのデータ型をいくつも追加してきました。
このシラバスでは主にtibbleを扱いますが、いくつかの代替について知っておくと役に立ちます。
data.tabletibble(後述)とdata.tableはどちらも、Base Rの従来のdata.frameを改善しようとした、評価の高い試みです。
どちらが「優れているか」については、当然ながら多くの議論があります。
次の点については、(単純すぎるという批判はあるにせよ)ある程度の合意があるかもしれません。
tibbleは主に使いやすさと、Tidyverseエコシステムとの統合を重視して最適化されています。data.tableは主に、とりわけ非常に大きなデータセットを扱うときの生のパワーとスケーラビリティを重視して最適化されています。いずれにせよ、data.tableはExercismでは利用できないので、ここでは網羅性のために触れておくだけです。
data.frameBase Rでは、data.frameは同じ長さのvectorsのlistです。
これは、データの長方形の表と考えることができます。各列は均一ですが、各行には(たいていは)異なる型のデータが入ります。
これを示す例を見てみましょう。
# create the column vectors
languages <- c("Fortran", "R", "Python", "Julia")
created <- c(1957, 1993, 1991, 2012)
has.syllabus <- c(FALSE, TRUE, TRUE, TRUE)
# join columns to create the dataframe
df <- data.frame(languages, created, has.syllabus)
df
#> languages created has.syllabus
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
# look at the structure
str(df)
#> 'data.frame': 4 obs. of 3 variables:
#> $ languages : chr "Fortran" "R" "Python" "Julia"
#> $ created : num 1957 1993 1991 2012
#> $ has.syllabus: logi FALSE TRUE TRUE TRUE
文字列の列、数値の列、真偽値の列があります。 大規模になったときにも、現実世界の多くのデータの集まりを直感的に表せる方法です。
tibbledata.frameの設計は_古い_ものです。
数十年にわたる経験と、Rの使われ方の変化が再設計につながり、Tidyverseに近代化された選択肢としてtibbleが生まれました。
Base Rと比べて、tibbleには次の特徴があります。
要するに、tibbleは「やることが少なく、文句が多め」を目指しており、「怠惰で不機嫌」とも表現されます。
これらの型はたいてい互換性があります。data.frameを受け入れる関数はtibbleも受け入れ、_その逆も_同様です。
型の変換は簡単で、as_tibble(df)とas.data.frame(tbl)を使います。
新しい作業には、tibbleを使うとより堅牢なコードを書くのに役立つでしょう。
ただし、Rの世界にはレガシーなコードとデータが大量にあるため、data.frameはこれからも長く一般的であり続けるでしょう。
# column vectors are same as for data.frame
library(tibble)
tbl <- tibble(languages, created, has.syllabus)
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
str(tbl)
#> tibble [4 × 3] (S3: tbl_df/tbl/data.frame)
#> $ languages : chr [1:4] "Fortran" "R" "Python" "Julia"
#> $ created : num [1:4] 1957 1993 1991 2012
#> $ has.syllabus: logi [1:4] FALSE TRUE TRUE TRUE
デフォルトの表示形式に注目してください。次元を示すコメント行が自動的に表示され、列の型も表示されます。
tibbleはTidyverseの中核なので、library(tibble)かlibrary(tidyverse)で読み込みます。
ドキュメントはTidyverseらしいスタイルで、かなり充実しています。
もっとも簡単には、data.frame()と同じように、tibble()関数で列ベクトルをまとめることができます。
その例は前のセクションで示しました。
行単位で値を入力するほうが便利な場合は、tribble()関数を使います。
tbl_r <- tribble(
# column names marked with tilde prefix
~languages, ~created, ~has.syllabus,
"Fortran", 1957, FALSE,
"R", 1993, TRUE,
"Python", 1991, TRUE,
"Julia", 2012, TRUE
)
tbl_r
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
実際には、tibbleを作る方法は何十通りもあります。さまざまなTidyverse関数が、デフォルトの出力形式としてtibbleを返すからです。 これについては今後のコンセプトで改めて見ていきます。
関数型プログラミングのコンセプトでは、ベクトルと配列(1次元のデータ構造)を操作するpurrrライブラリを扱いました。
データフレーム(従来のものでもtibbleでも)に対しては、対応するライブラリとしてdplyrを使います。
dplyrは以前、Switchのコンセプトで紹介しました。
そこではいくつかのユーティリティ関数を使っただけですが、ここからこの大きなライブラリの残りの部分も見ていきましょう。
データフレームは、tibbleも含めて列ベクトルの配列とみなせるので、配列のインデックスで指定した列を取り出せます。
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl$created
#> [1] 1957 1993 1991 2012
データフレームは行列形式のインデックスでも参照できます。
tbl[c(2, 4), 1:2]
# A tibble: 2 × 2
#> languages created
#> <chr> <dbl>
#> 1 R 1993
#> 2 Julia 2012
Tidyverseエコシステムを備えた現代のRでは、dplyrの関数のほうが一般に柔軟で便利なので、このコンセプトの残りではそちらに焦点を当てます。
pull()で、名前か連番(負の数なら右から数えます)を指定して1つの列を取り出します。
tbl |> pull(created)
#> [1] 1957 1993 1991 2012
これはtbl$createdと同じ結果ですが、パイプラインで使いやすい関数を使っています。
複数の列を取り出すには、select()が適切です。これはとても柔軟です。
列の名前や型の性質に基づいて、列を取得(または除外)できます。
# Range with position and/or name
tbl |> select(1:created)
# A tibble: 4 × 2
#> languages created
#> <chr> <dbl>
#> 1 Fortran 1957
#> 2 R 1993
#> 3 Python 1991
#> 4 Julia 2012
# Exclude a column
tbl |> select(!created)
# A tibble: 4 × 2
#> languages has.syllabus
#> <chr> <lgl>
#> 1 Fortran FALSE
#> 2 R TRUE
#> 3 Python TRUE
#> 4 Julia TRUE
# Use type of column
tbl |> select(where(is.numeric))
# A tibble: 4 × 1
#> created
#> <dbl>
#> 1 1957
#> 2 1993
#> 3 1991
#> 4 2012
複数の条件は、真偽値演算子&、|、!(and、or、not)を使って指定できます。
_有効なRの識別子_である列名は、select()の中では引用符で囲む必要はありません。
無効な名前(スペースを含むものなど)はバッククォートで囲めますが、名前を変更したほうがよいかもしれません。
select()関数は、列名を選ぶためのさまざまなヘルパー関数と組み合わせて使えます。starts_with、contains、num_rangeなどです。
matchesでは正規表現による完全一致が使えます。
詳しくはドキュメントを参照してください。
言語だけのおもちゃのデータフレームでは、これほどの機能はばかげて思えます。
幸い、dplyrにはstarwarstibbleが含まれているので、練習にもっと大きなものを使えます。
# limit display to top 3 rows of non-list columns
starwars |>
select(!where(is.list)) |>
head(3)
# A tibble: 3 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
# pick a subset of columns
starwars |>
select(name | ends_with("color")) |>
head(5)
# A tibble: 5 × 4
#> name hair_color skin_color eye_color
#> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker blond fair blue
#> 2 C-3PO NA gold yellow
#> 3 R2-D2 NA white, blue red
#> 4 Darth Vader none white yellow
#> 5 Leia Organa brown light brown
明らかに、dplyrは名前で列を選ぶ強力な方法を提供しています。
行名でも同じようなことができるのでしょうか?
いいえ!
従来のRのデータフレームには行名を付けられますが、(バグと性能問題の歴史を経て)tibblesでは行名は_許されていません_。
名前を付けたい場合は、<chr>の列(通常は1列目)に入れて、ほかの列と同じように使います。
as_tibble()のようなインポート関数は、名前付きの行をもつデータを取り込むときに、これを自動的に作ります。
この行名の制限が妙に厳しく思えるなら、ほとんどの大規模データベースシステムも同じようにテーブルを扱うことを思い出してください。Oracle、SQL Server、PostgreSQL、MySQLなどです。
filter()で条件に一致する行を取り出し、filter_out()で除外します。
starwars |>
select(name:mass) |>
filter(between(height, 150, 165) & !is.na(mass))
# A tibble: 4 × 3
#> name height mass
#> <chr> <int> <dbl>
#> 1 Leia Organa 150 49
#> 2 Beru Whitesun Lars 165 75
#> 3 Nien Nunb 160 68
#> 4 Ben Quadinaros 163 65
フィルターの条件はいくらでも複雑にできますが、常に行の_中身_に基づきます。
行_番号_がわかっている場合は、さまざまなslice()関数が使えます。
starwars |>
select(name | homeworld) |>
slice(20:25)
# A tibble: 6 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Palpatine Naboo
#> 2 Boba Fett Kamino
#> 3 IG-88 NA
#> 4 Bossk Trandosha
#> 5 Lando Calrissian Socorro
#> 6 Lobot Bespin
# random sample of rows
starwars |>
select(name | homeworld) |>
slice_sample(n = 4)
# A tibble: 4 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Shaak Ti Shili
#> 2 Luminara Unduli Mirial
#> 3 Grievous Kalee
#> 4 Palpatine Naboo
重複した行を削除するには、distinct()を使います。
まず注意点として、変更時にコピーがデフォルトなので、元のtibbleはたいてい変更されません。
ほとんどの変更は列単位で行います。
列名はrename(newname = oldname)で変更できます。関数を適用するならrename_with()です。
典型的な使い方は、取り込んだ名前から空白を削除し、関連する名前の形式をそろえて、Rで扱いやすくする整理です。
rename()の中の構文に注目してください。
列oldnameの_中身_が名前newnameに_結び付けられる_ので、この順番になります。
列の順序はrelocate()で変更できます。
指定した列はデフォルトで左端に移動しますが、.beforeか.after引数を使えばより細かく位置を指定できます。
sw <- starwars |> select(name:species) |> slice(1:4)
sw
# A tibble: 4 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
#> 4 Darth Vader 202 136 none white yellow 41.9 male masculine Tatooine Human
sw |> relocate(c(species, homeworld), .after = name)
# A tibble: 4 × 11
#> name species homeworld height mass hair_color skin_color eye_color birth_year sex gender
#> <chr> <chr> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr>
#> 1 Luke Skywalker Human Tatooine 172 77 blond fair blue 19 male masculine
#> 2 C-3PO Droid Tatooine 167 75 NA gold yellow 112 none masculine
#> 3 R2-D2 Droid Naboo 96 32 NA white, blue red 33 none masculine
#> 4 Darth Vader Human Tatooine 202 136 none white yellow 41.9 male masculine
もっと大きな変更には、mutate()が使えます。
NULLに設定して、列を削除する。明らかに、mutate()は強力で、ときに混乱を招き、変更時にコピーしてくれることに感謝したくなる理由です。
スター・ウォーズの登場人物のボディマス指数を気にする理由は特にありませんが、念のため。
starwars |>
select(c(name, species, height, mass)) |>
mutate(BMI = mass / (height / 100)^2) |>
head(4)
# A tibble: 4 × 5
#> name species height mass BMI
#> <chr> <chr> <int> <dbl> <dbl>
#> 1 Luke Skywalker Human 172 77 26.0
#> 2 C-3PO Droid 167 75 26.9
#> 3 R2-D2 Droid 96 32 34.7
#> 4 Darth Vader Human 202 136 33.3
mutate()のような関数で一部の列だけを操作したいときは、上の例のselect() |> mutate()の順序が1つの選択肢です。
結果には選んだ列だけが残ります。
あるいは、mutate()の呼び出しの_中で_pick()を使うと便利なことがあります。
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
When you want to operate on a subset of the columns with functions such as `mutate()`, the `select() |> mutate()` sequence in the above example is one option.
Only the selected columns will be in the result.
Alternatively, it can be convenient to use `pick()` _within_ the `mutate()` call:
```R
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
# A tibble: 4 × 15
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species films vehicles
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <lis> <list>
#> 1 Luke Skywal… 172 77 blond fair blue 19 male mascu… Tatooine Human <chr> <chr>
#> 2 C-3PO 167 75 NA gold yellow 112 none mascu… Tatooine Droid <chr> <chr>
#> 3 R2-D2 96 32 NA white, bl… red 33 none mascu… Naboo Droid <chr> <chr>
#> 4 Darth Vader 202 136 none white yellow 41.9 male mascu… Tatooine Human <chr> <chr>
# ℹ 2 more variables: starships <list>, BMI <dbl>
pickした列だけが変更に使われますが、返ってくるのはすべての列です。
行単位の操作は、1つのtibbleを変更する用途ではあまり一般的ではありません(複数のtibbleの結合は後のコンセプトで扱います)。
1つ例外があります。arrange()は、1つ以上の列の値で行を並べ替えます。
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl |> arrange(languages)
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 Julia 2012 TRUE
#> 3 Python 1991 TRUE
#> 4 R 1993 TRUE
従来のものでもtibbleでも、データフレームは現代のRの典型的な使われ方の中心にあります。
Tidyverseの関数のほとんど(dplyrだけではありません)は、tibbleを入力として受け取ったり、出力として作ったりします。
このコンセプトでは簡単な紹介だけを行い、可能なことの表面をかすめたにすぎません。
今後のコンセプトでは、データフレームのさらにいくつかの側面を(Exercismの技術的な制約の範囲内で)扱います。