シラバスのほかの部分では、性質の異なるさまざまなデータ型を見てきました。
このコンセプトでは、多次元で、異なる型が混在するデータを保存する方法を見ていきます。 実のところ、現実世界のデータは_ほとんど_がこうした形をしているので、ここからが、Rが実際に(おもに)どのように使われているかの核心に入ります。
何十年ものあいだに、Rには表形式のデータを扱うためのデータ型がいくつも追加されてきました。
このシラバスでは主にtibbleを扱いますが、ほかの選択肢についても知っておくと役立ちます。
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
文字列の列、数値の列、真偽値の列があります。 データが大きくなっても、現実世界のさまざまなデータを表す直感的な方法です。
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
デフォルトの表示形式に注目してください。次元を表すコメント行が自動的に表示され、列の型も示されます。
tibbleはTidyverseの中核なので、library(tibble)かlibrary(tidyverse)で読み込みます。
もっとも簡単なのは、data.frame()と同じように、tibble()関数で列のベクトルをまとめる方法です。
例は前のセクションで示しました。
行ごとに値を入力したほうが便利な場合は、tribble()関数を使います。
実際には、tibbleを作る方法は何十通りもあります。さまざまなTidyverseの関数が、tibbleをデフォルトの出力形式として返すからです。
関数型プログラミングのコンセプトでは、1次元のデータ構造であるベクトルやリストを操作するpurrrライブラリを扱いました。
データフレーム(従来のものかtibbleかを問わず)を操作するには、dplyrライブラリを使います。
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の関数のほうが一般に柔軟で便利なので、このコンセプトの残りではそちらを中心に見ていきます。
1つの列を取り出すには、pull()に名前か連番を渡します(負の数を使うと右から数えます)。
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
# 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)を使います。
言語だけの小さなデータフレームでは、こんな強力な機能は少し大げさに思えます。
幸いなことに、dplyrにはstarwarsというtibbleが含まれていて、練習に使える大きめのデータがあります。
# 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
条件に合う行を取り出すには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
まず注意点として、_copy-on-modify_がデフォルトなので、元のtibbleはたいてい変更されません。
ほとんどの変更は列単位で行います。
列名はrename(newname = oldname)で変更できます。関数を適用するならrename_with()を使います。
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()は強力で、ときに戸惑うこともありますが、copy-on-modifyのおかげでとても助かります。
スター・ウォーズの登場人物のBMIを気にする理由はとくにありませんが、念のため見てみましょう。
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
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
大きなサクランボ農園には、たいてい余剰の木が少しだけあり、それを木材として提供したいと考えています。そうすることで農園の健康を保ちながら、同時に追加の収入も得られます。そこで農園は、見込み客から電話があったときに木を選ぶ作業を手伝う機能をコードで書いてほしいと頼んできました。
R標準搭載のdatasetsライブラリには、たくさんのおもちゃのデータセットが含まれており、この演習ではtreesデータセットを使います。データセットを呼び出すには、treesという名前を使うだけです。
library(datasets) # loading is optional
trees |> head(3)
#> A data.frame: 3 × 3
#> Girth Height Volume
#> <dbl> <dbl> <dbl>
#> 1 8.3 70 10.3
#> 2 8.6 65 10.3
#> 3 8.8 63 10.2
Girth、Height、Volumeの3つの列に注目してください。
データセットは最初はdata.frameの形をしていますが、tibbleとして扱うことを選んでもよいでしょう。
あいにく、これまで修正されていない事務上のミスがあり、データセット内の1つの列はGirthという名前ですが、実際にはこれはDiameterです。
treesのGirth列の名前をDiameterに修正した、tree_dataというdata.frame/tibbleを定義してください。
head(tree_data, 3)
#> A tibble: 3 × 3
#> Diameter Height Volume
#> <dbl> <dbl> <dbl>
#> 8.3 70 10.3
#> 8.6 65 10.3
#> 8.8 63 10.2
周囲長(つまり円周)は依然として役立つ測定値で、木材の重量も同様です。周囲長はπDで簡単に計算できます。ここでDは直径です。木材全体の重量はρVで計算できます。ここでρ = 35はサクラ材の密度、Vは木材の体積です。
girth_n_weight(data, rnd_digits)関数を定義してください。これはデータフレームと数値の引数を取ります。この関数は、GirthとWeightという2つの追加列を持つ新しいデータフレームを返すようにしてください。値はrnd_digitsで指定された桁数に丸めます。
girth_n_weight(tree_data, 1) |> head(3)
#> A tibble: 3 × 5
#> Diameter Height Volume Girth Weight
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 8.3 70 10.3 26.1 360.5
#> 8.6 65 10.3 27.0 360.5
#> 8.8 63 10.2 27.6 357.0
見込み客ごとに、農園は販売を助けるためにデータセットの特別なコピーを用意します。このデータセットでは、最も関連性の高い列であるWeightとHeightが先頭に移動され、Weightで並べ替えられています。
orchard_copy(data, important_cols)関数を定義してください。これはデータフレームを入力として取ります。WeightとHeightを先頭に移動し、行をWeightで並べ替えた新しいデータフレームを返すようにしてください。
tree_data |> girth_n_weight() |> orchard_copy() |> head(3)
#> A tibble: 3 × 5
#> Weight Height Diameter Volume Girth
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 357 63 9 10 28
#> 360 70 8 10 26
#> 360 65 9 10 27
見込み客はそれぞれ、自分の好みに合わせたデータセットのバージョンを受け取ります。そのために、顧客は「最小の高さ、最大の高さ、1本の木の最大重量」を指定できます。
customer_copy(data, min_height, max_height, max_weight)関数を定義してください。Height、Weight、Diameter、Girthの列を持ち、制約の範囲内にある行だけを含む、顧客用のデータフレームのコピーを返してください。
tree_data |>
girth_n_weight(1) |>
orchard_copy() |>
customer_copy(65, 75, 1500) |>
head(3)
#> A tibble: 3 × 4
#> Height Weight Diameter Girth
#> <dbl> <dbl> <dbl> <dbl>
#> 70 360.5 8.3 26.1
#> 65 360.5 8.6 27.0
#> 66 546.0 11.0 34.6