トラック
/
R
R
/
演習
/
Trees & Tibble-ations
Trees & Tibble-ations

Trees & Tibble-ations

学習演習

はじめに

シラバスのほかの部分では、性質の異なるさまざまなデータ型を見てきました。

  • アトミックベクターは1次元で、型が均一です。
  • リストは1次元で、要素に異なる型を混在させられます。
  • 行列と配列は多次元で、型は均一です。

このコンセプトでは、多次元で、異なる型が混在するデータを保存する方法を見ていきます。 実のところ、現実世界のデータは_ほとんど_がこうした形をしているので、ここからが、Rが実際に(おもに)どのように使われているかの核心に入ります。

データフレームの種類

何十年ものあいだに、Rには表形式のデータを扱うためのデータ型がいくつも追加されてきました。

このシラバスでは主にtibbleを扱いますが、ほかの選択肢についても知っておくと役立ちます。

data.frame

Base 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

文字列の列、数値の列、真偽値の列があります。 データが大きくなっても、現実世界のさまざまなデータを表す直感的な方法です。

tibble

data.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を扱う

tibbleはTidyverseの中核なので、library(tibble)かlibrary(tidyverse)で読み込みます。

tibbleを作成する

もっとも簡単なのは、data.frame()と同じように、tibble()関数で列のベクトルをまとめる方法です。 例は前のセクションで示しました。

行ごとに値を入力したほうが便利な場合は、tribble()関数を使います。

実際には、tibbleを作る方法は何十通りもあります。さまざまなTidyverseの関数が、tibbleをデフォルトの出力形式として返すからです。

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   

tibbleを変更する

まず注意点として、_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     

説明

大きなサクランボ農園には、たいてい余剰の木が少しだけあり、それを木材として提供したいと考えています。そうすることで農園の健康を保ちながら、同時に追加の収入も得られます。そこで農園は、見込み客から電話があったときに木を選ぶ作業を手伝う機能をコードで書いてほしいと頼んできました。

Note

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. 列の名前を変更する

あいにく、これまで修正されていない事務上のミスがあり、データセット内の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

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

3. 農園用のデータセットのコピー

見込み客ごとに、農園は販売を助けるためにデータセットの特別なコピーを用意します。このデータセットでは、最も関連性の高い列である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

4. 顧客用のデータセットのコピー

見込み客はそれぞれ、自分の好みに合わせたデータセットのバージョンを受け取ります。そのために、顧客は「最小の高さ、最大の高さ、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
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
R Exercism

Trees & Tibble-ationsを始める準備はできましたか?

Exercismに登録すれば、21個のコンセプト111個の演習、そして本物の人間によるメンタリングとともに、Rを学んでマスターできます。すべて無料です。