{purrr} でリストデータを操作する <1>
R で関数型プログラミングを行うためのパッケージである {purrr}
、すこし使い方がわかってきたので整理をしたい。RStudio のブログの記載をみると、とくにデータ処理フローを関数型のように記述することが目的のようだ。
The core of purrr is a set of functions for manipulating vectors (atomic vectors, lists, and data frames). The goal is similar to dplyr: help you tackle the most common 90% of data manipulation challenges.
ここでいう"関数型プログラミング言語"とは Haskell のような静的型の言語を想定しており、型チェック、ガード、リフトなど断片的に影響を受けたと思われる関数群をもつ。ただし、同時に Haskell を目指すものではない、とも明言されている。
そもそも R は Scheme の影響をうけているためか、関数型らしい言語仕様やビルトイン関数群 Map
, Reduce
, Filter
などをすでに持っている。ただ、これらの関数群は引数の順序からパイプ演算子と組み合わせて使いにくい。{purrr}
でこのあたりが使いやすくなっているとうれしい。
R: Common Higher-Order Functions in Functional Programming...
※ 上記の Map
など、S のリファレンスに見当たらなかったため R 独自だと思っていますが、間違っていたら教えてください。
{purrr}
によるリストの操作
{purrr}
を使うとリストやベクトルの処理が書けるのかをまとめたい。リスト操作のためのパッケージである {rlist}
とできること、記法をくらべてみる。
{purrr}
を利用する目的は 処理を簡単に、見やすく書くことであるため、{rlist}
と比べて可読性が悪いもの、複雑になるものは "できない" 処理として扱う。サンプルデータとして、R の著名パッケージに関連した情報のリストを用意した。
packages <- list( list(name = 'dplyr', star = 979L, maintainer = 'hadley' , authors = c('hadley', 'romain')), list(name = 'ggplot2', star = 1546L, maintainer = 'hadley' , authors = c('hadley')), list(name = 'knitr', star = 1047L, maintainer = 'yihui' , authors = c('yihui', 'hadley', '...and all')) ) packages ## x = list 3 (2632 bytes) ## . [[1]] = list 4 ## . . name = character 1= dplyr ## . . star = integer 1= 979 ## . . maintainer = character 1= hadley ## . . authors = character 2= hadley romain ## . [[2]] = list 4 ## . . name = character 1= ggplot2 ## . . star = integer 1= 1546 ## . . maintainer = character 1= hadley ## . . authors = character 1= hadley ## . [[3]] = list 4 ## . . name = character 1= knitr ## . . star = integer 1= 1047 ## . . maintainer = character 1= yihui ## . . authors = character 3= yihui hadley ...
1レベル目がレコード、以降のレベルが各レコードの要素となっている。この記事では主に以下二つの操作に関連した機能について記載する。
- 要素に対する操作: 特定の要素を選択する。特定の要素を元に新しい要素をつくる (マッピングする)。
- レコードに対する操作: 特定のレコードを選択する。
準備: リストの表示
既定の print
ではリストの階層構造がわかりにくく、出力も長い。わかりやすくするため Hmisc::list.tree
を利用した出力を記載する。また、出力が同一のものは省略する。
library(Hmisc) l1 <- list(a = 1, b = c(3L, 4L), c = list(x = c(5, 6), y = 'AAA')) # 既定での表示 l1 ## $a ## [1] 1 ## ## $b ## [1] 3 4 ## ## $c ## $c$x ## [1] 5 6 ## ## $c$y ## [1] "AAA" # list.tree での表示 (名前 = 型 要素数 = 値 と読む) Hmisc::list.tree(l1) ## l1 = list 3 (968 bytes) ## . a = double 1= 1 ## . b = integer 2= 3 4 ## . c = list 2 ## . . x = double 2= 5 6 ## . . y = character 1= AAA
パッケージのロード
library(rlist) library(pipeR) library(purrr) library(dplyr)
要素の選択とマッピング
rlist Tutorial: Mapping に相当する内容。
要素の選択
R 標準の lapply
他 apply
系の関数に対応するのは rlist::list.map
と purrr::map
。purrr::map
では第二引数に文字列を渡すとその要素の抽出、ラムダ式 ( ~ .$name
)を渡すとその式の適用になる。
lapply(packages, function(x) { x$name }) ## x = list 3 (360 bytes) ## . [[1]] = character 1= dplyr ## . [[2]] = character 1= ggplot2 ## . [[3]] = character 1= knitr rlist::list.map(packages, name) # 略 purrr::map(packages, 'name') # 略 purrr::map(packages, ~ .$name) # 略
複数の要素を一度に選択したい場合、{rlist}
には rlist::list.select
という選択専用の関数がある。{purrr}
では渡せる式が一つのため、ラムダ式を利用する。
rlist::list.select(packages, maintainer, star) ## x = list 3 (1488 bytes) ## . [[1]] = list 2 ## . . maintainer = character 1= hadley ## . . star = integer 1= 979 ## . [[2]] = list 2 ## . . maintainer = character 1= hadley ## . . star = integer 1= 1546 ## . [[3]] = list 2 ## . . maintainer = character 1= yihui ## . . star = integer 1= 1047 purrr::map(packages, ~ .[c('maintainer', 'star')]) # 略
補足 第二引数にベクトルを渡した場合の処理は、以下のように階層的な選択になる ( l[[c('a', 'b')]]
と一緒)。
l2 <- list(list(a=list(b=1)), list(a=list(b=2))) l2 ## x = list 2 (1176 bytes) ## . [[1]] = list 1 ## . . a = list 1 ## . . . b = double 1= 1 ## . [[2]] = list 1 ## . . a = list 1 ## . . . b = double 1= 2 purrr::map(l2, c('a', 'b')) ## x = list 2 (152 bytes) ## . [[1]] = double 1= 1 ## . [[2]] = double 1= 2
{rlist}
, {purrr}
でのラムダ式
f <- function(.) {. + 1}
に対応する無名関数を {rlist}
, {purrr}
それぞれの記法で書く。形式はチルダの有無以外は同じ。ドットが引数に対応する。
nums <- c(a = 3, b = 2, c = 1) rlist::list.map(nums, . + 1) ## x = list 3 (544 bytes) ## . a = double 1= 4 ## . b = double 1= 3 ## . c = double 1= 2 purrr::map(nums, ~ . + 1) # 略
{rilst}
では ラムダ式中の .i
で元のリストの位置 (インデックス) を、.name
で名前 (names
) をそれぞれ参照できる。purrr
のラムダ式には対応する記法がないため、近いことをやるためには直接 map
の引数として渡す必要がある。
rlist::list.map(nums, .i) ## x = list 3 (544 bytes) ## . a = integer 1= 1 ## . b = integer 1= 2 ## . c = integer 1= 3 purrr::map(seq_along(nums), ~ .) # namesは変わってしまう ## x = list 3 (216 bytes) ## . [[1]] = integer 1= 1 ## . [[2]] = integer 1= 2 ## . [[3]] = integer 1= 3
rlist::list.map(nums, paste0("Name: ", .name)) ## x = list 3 (688 bytes) ## . a = character 1= Name: a ## . b = character 1= Name: b ## . c = character 1= Name: c purrr::map(names(nums), ~ paste0("Name: ", .)) # namesは変わってしまう ## x = list 3 (360 bytes) ## . [[1]] = character 1= Name: a ## . [[2]] = character 1= Name: b ## . [[3]] = character 1= Name: c
また、内部的にはラムダ式を eval
で評価をしているため、変数として扱われない位置にあるドットは評価されない。
purrr::map(nums, ~ list(. = 1)) ## x = list 3 (1312 bytes) ## . a = list 1 ## . . . = double 1= 1 ## . b = list 1 ## . . . = double 1= 1 ## . c = list 1 ## . . . = double 1= 1
要素の追加、変更
元となるリストの値から新しいリストを作りたい場合はラムダ式でリストを返す。
rlist::list.map(packages, list(star = star, had = 'hadley' %in% authors)) ## x = list 3 (1320 bytes) ## . [[1]] = list 2 ## . . star = integer 1= 979 ## . . had = logical 1= TRUE ## . [[2]] = list 2 ## . . star = integer 1= 1546 ## . . had = logical 1= TRUE ## . [[3]] = list 2 ## . . star = integer 1= 1047 ## . . had = logical 1= TRUE purrr::map(packages, ~ list(star = .$star, had = 'hadley' %in% .$authors)) # 略
結果の型の変更
結果をベクトルで取得したい場合、{rilst}
では list.mapv
。{purrr}
では flatmap
もしくは map_int
( map_xxx
のように結果の型を指定できる関数群がある )。
rlist::list.mapv(packages, star) ## [1] 979 1546 1047 purrr::flatmap(packages, 'star') # 略 purrr::map_int(packages, 'star') # 略
data.frame
としたい場合は rlist::list.stack
。{purrr}
では dplyr::bind_rows
に渡す。
packages %>>% rlist::list.select(name, star) %>>% rlist::list.stack() ## name star ## 1 dplyr 979 ## 2 ggplot2 1546 ## 3 knitr 1047 packages %>% purrr::map(~ .[c('name', 'star')]) %>% dplyr::bind_rows()) # 略
関数の適用
返り値を変更せずに関数を適用するには rlist::list.iter
と purrr::walk
。パイプ処理の間に出力やプロットなど意図する返り値を持たない処理を挟み込む場合に利用する。
r <- rlist::list.iter(packages, cat(name, ":", star, "\n")) ## dplyr : 979 ## ggplot2 : 1546 ## knitr : 1047 r <- purrr::walk(packages, ~ cat(.$name, ":", .$star, "\n")) # 略
レコードの選択
rlist Tutorial: Filtering 前半の単純な条件での選択。
{rilst}
では list.filter
。{purrr}
では keep
。ラムダ式が使えるのは map
などと同じ。
packages %>>% list.filter(star >= 1500) ## x = list 1 (840 bytes) ## . [[1]] = list 4 ## . . name = character 1= ggplot2 ## . . star = integer 1= 1546 ## . . maintainer = character 1= hadley ## . . authors = character 1= hadley packages %>% purrr::keep(~ .$star >= 1500) # 略
packages %>>% list.filter("yihui" %in% authors) ## x = list 1 (968 bytes) ## . [[1]] = list 4 ## . . name = character 1= knitr ## . . star = integer 1= 1047 ## . . maintainer = character 1= yihui ## . . authors = character 3= yihui hadley ... packages %>% purrr::keep(~ "yihui" %in% .$authors) # 略
まとめ
{purrr}
によるリストデータの属性、レコードに対する操作を記載した。purrr::map
と purrr::keep
だけでもパイプ演算子 + ラムダ式と組み合わせて幅広い処理ができそうだ。
11/28追記 続きです。
- 作者: Hadley Wickham,石田基広,市川太祐,高柳慎一,福島真太朗
- 出版社/メーカー: 共立出版
- 発売日: 2016/01/23
- メディア: 単行本
- この商品を含むブログ (25件) を見る