※この文章はrlist Tutorialを翻訳したら長くなったのでちょっと短めに要約したものです。まだ結構長いんですが、それでも結構省略したので原文もあわせて確認することをお勧めします。
前置き
rlistパッケージ
表の形で表現できるデータの扱いはRの得意とするところで、dplyr
やdata.table
などの優れたパッケージもあります。
表の形で表すことができない非リレーショナルなデータは、Rではリストを使って扱います。しかし、組込み関数だけでリストを扱うことは少々骨の折れる作業です。
rlist
はリストを扱うためのパッケージです。rlist
を使うことで、リストをよりエレガントに扱えるようになるはずです。
pipeRパッケージ
rlist
はパイプを使った操作のチェインと親和性が高い設計となっています。
rlist
のほとんどの関数は、dplyr
で使われているmagrittr
の%>%
演算子を使っても問題なく機能します。しかし、一部の関数ではメタシンボル.
の解釈を巡って競合が発生し、予期せぬエラーの原因となることがあります。
pipeR
パッケージは、パイプ演算子%>>%
を提供するパッケージです。演算子の使用方法はmagrittr
のそれに良く似ていますが、pipeR
はrlist
と併用しても競合が発生しないような設計になっています。したがって、rlist
を使用する場合はpipeR
を使用することを推奨します。
ファイル形式とデータの読み込み
JSON
非表形式のデータ構造を表現するフォーマットの代表的なものにJSONとYAMLがあります。JSONは次のような構造をしています。
[
{
"Name" : "Ken",
"Age" : 24,
"Interests" : [
"reading",
"music",
"movies"
],
"Expertise" : {
"R": 2,
"CSharp": 4,
"Python" : 3
}
},
...(以下省略)
YAML
YAMLはJSONと同様にネストしたデータ構造を表現できるフォーマットですが、その表現方法はJSONよりもクリーンです。
- Name: Ken
Age: 24
Interests:
- reading
- music
- movies
Expertise:
R: 2
CSharp: 4
...(以下省略)
JSONとYAMLの読み込み
rlist
はJSONとYAMLのいずれもlist.load()
という関数でリストとして読み込むことができます。データソースはファイルとurlのどちらでもかまいません。拡張子があれば読み込み方法は自動で判断されます。
試しにこれからの説明で使うデータを読み込んでみましょう。
library(rlist)
people <- list.load("https://renkun-ken.github.io/rlist-tutorial/data/sample.json")
str(people)
#> List of 3
#> $ :List of 4
#> ..$ Name : chr "Ken"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:3] "reading" "music" "movies"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 2
#> .. ..$ CSharp: int 4
#> .. ..$ Python: int 3
#> $ :List of 4
#> ..$ Name : chr "James"
#> ..$ Age : int 25
#> ..$ Interests: chr [1:2] "sports" "music"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 3
#> .. ..$ Java: int 2
#> .. ..$ Cpp : int 5
#> $ :List of 4
#> ..$ Name : chr "Penny"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:2] "movies" "reading"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 1
#> .. ..$ Cpp : int 4
#> .. ..$ Python: int 2
ちなみに、リストは上記のようにstr()
を使って表示すると確認しやすくなります。これから示す例でも、結果がリストである場合には基本的にstr()
を最後に実行して表示を簡略化します。
rlistの関数
マッピング
リストの要素それぞれに何らかの式を評価する操作をマッピングと呼びます。
list.map
list.map()
は与えられた式を要素のそれぞれに対して評価します。そして戻り値は評価結果のリストになります。
リストの要素のフィールドを評価すると、そのフィールドを抽出できます。
library(pipeR)
people %>>%
list.map(Age) %>>%
str
#> List of 3
#> $ : int 24
#> $ : int 25
#> $ : int 24
リストの要素のコンテキストで評価できる式であれば、どのような式でも与えることができます。
people %>>%
list.map(sum(as.numeric(Expertise))) %>>%
str
#> List of 3
#> $ : num 9
#> $ : num 10
#> $ : num 7
ここでメタ変数について説明しておきます。この式の中では次の3つの変数を使うことができます。
-
.
...要素それ自身を表す。 -
.i
...要素のインデックスを表す。 -
.name
...要素の名前を表す。
簡単な例を見てみましょう。
nums <- c(a = 10, b = 20, c = 30)
nums %>>%
list.map(list(val = ., idx = .i, name = .name)) %>>%
str
#> List of 3
#> $ a:List of 3
#> ..$ val : num 10
#> ..$ idx : int 1
#> ..$ name: chr "a"
#> $ b:List of 3
#> ..$ val : num 20
#> ..$ idx : int 2
#> ..$ name: chr "b"
#> $ c:List of 3
#> ..$ val : num 30
#> ..$ idx : int 3
#> ..$ name: chr "c"
今示したようにrlist
の関数はベクトルでも問題なく機能します。ただし、戻り値はリストとなる点に注意してください。
list.mapv
この関数はlist.map()
とほぼ同じですが、結果がリストではなくベクトルになります。
people %>>%
list.mapv(Name)
#> [1] "Ken" "James" "Penny"
list.select
list.select()
は複数の式を受け取り、リストの要素単位ですべての式を評価し、結果をリストにして返します。
people %>>%
list.select(Name, Age, length(Expertise)) %>>%
str
#> List of 3
#> $ :List of 3
#> ..$ Name: chr "Ken"
#> ..$ Age : int 24
#> ..$ : int 3
#> $ :List of 3
#> ..$ Name: chr "James"
#> ..$ Age : int 25
#> ..$ : int 3
#> $ :List of 3
#> ..$ Name: chr "Penny"
#> ..$ Age : int 24
#> ..$ : int 3
これは次の式と同じ意味になります。
people %>>%
list.map(list(Name = Name, Age = Age, length(Expertise))) %>>%
str
#> List of 3
#> $ :List of 3
#> ..$ Name: chr "Ken"
#> ..$ Age : int 24
#> ..$ : int 3
#> $ :List of 3
#> ..$ Name: chr "James"
#> ..$ Age : int 25
#> ..$ : int 3
#> $ :List of 3
#> ..$ Name: chr "Penny"
#> ..$ Age : int 24
#> ..$ : int 3
list.select()
では一部の式に自動的に名前が設定されていることに注目してください。式が単にリストのフィールドを示すものである場合は、それがそのまま名前として使用されます。それ以外の場合はなるべく明示的に名前を設定したほうがよいでしょう。
list.iter
list.iter()
はprintを行いません。次のように副作用だけが欲しい場合に使用します。
people %>>%
list.iter(cat(Name, "\n"))
#> Ken
#> James
#> Penny
list.maps
list.maps()
は任意の数のリストを受け取り、それらにまたがったマッピングを行う関数です。
l1 <- list(
p1 = list(x = 1, y = 2),
p2 = list(x = 3, y = 4),
p3 = list(x = 1, y = 3)
)
l2 <- list(2, 3, 5)
list.maps(a$x * b + a$y, a = l1, b = l2) %>>% str
#> List of 3
#> $ p1: num 4
#> $ p2: num 13
#> $ p3: num 8
上記の例から分かるように、この関数は第一引数が式です。その後にリストを並べていきます。リストは明示的に名前を指定する必要はなく、一つ目のリストは..1
、二つ目のリストは..2
といった具合に、専用のメタ変数を通じてアクセス可能です。上記の例を書き換えれば次のようになります。
list.maps(..1$x * ..2 + ..1$y, l1, l2) %>>% str
#> List of 3
#> $ p1: num 4
#> $ p2: num 13
#> $ p3: num 8
フィルタリング
何らかの基準を与えてリストの要素を選択する操作をフィルタリングと呼びます。
list.filter
list.filter()
はTRUE
かFALSE
を返す式をリストの要素ごとに評価し、結果がTRUE
の要素のみ返します。
people %>>%
list.filter(Age >= 25) %>>%
str
#> List of 1
#> $ :List of 4
#> ..$ Name : chr "James"
#> ..$ Age : int 25
#> ..$ Interests: chr [1:2] "sports" "music"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 3
#> .. ..$ Java: int 2
#> .. ..$ Cpp : int 5
list.find
list.find()
は、ほとんどlist.filter()
と同じですが、返すアイテムの数に上限を設定します。
people %>>%
list.find(Expertise$R > 1, n = 1) %>>%
str
#> List of 1
#> $ :List of 4
#> ..$ Name : chr "Ken"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:3] "reading" "music" "movies"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 2
#> .. ..$ CSharp: int 4
#> .. ..$ Python: int 3
デフォルトでは、要素が1つ見つかった時点で探索を止めます(したがって、本当は上記の例のように1
を指定する必要はありません)。
list.findi
list.findi()
はlist.find()
とほぼ同じように機能しますが、返り値はインデックスのベクトルです。
people %>>%
list.findi(Expertise$R > 1)
#> [1] 1
list.first, list.last
これもlist.find()
によく似た関数ですが、式がTRUE
を返す要素のうちlist.first()
は最初のものを、list.last()
は最後のものを返します。
この関数は式を省略しても機能します。その場合は単に最初の要素、最後の要素が返ります。
list.first(1:10)
#> [1] 1
list.last(1:10)
#> [1] 10
list.take
list.take()
は指定した数だけの要素を返す関数です。要素数の方が小さければ、単にすべての要素が返ります。
list.take(1:10, 3)
#> [1] 1 2 3
list.take(1:5, 8)
#> [1] 1 2 3 4 5
list.skip
指定した数だけの要素をスキップし、残りの要素を返します。要素数の方が小さいときは、戻り値は空になります。
list.skip(1:10, 3)
#> [1] 4 5 6 7 8 9 10
list.skip(1:5, 8)
#> integer(0)
list.takeWhile
list.take()
に似た関数ですが、要素数ではなく式を受け取ります。そして、式が真である間だけ要素を返します。式の結果がFALSE
になったら、その後の要素は評価せずスキップします。
people %>>%
list.takeWhile(Expertise$R <= 2) %>>%
str
#> List of 1
#> $ :List of 4
#> ..$ Name : chr "Ken"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:3] "reading" "music" "movies"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 2
#> .. ..$ CSharp: int 4
#> .. ..$ Python: int 3
list.is
各要素に対して式を評価し、結果を論理値ベクトルとして返します。
people %>>%
list.is("music" %in% Interests)
#> [1] TRUE TRUE FALSE
list.which
各要素に対して式を評価し、TRUE
となる要素のインデックスをベクトルとして返します。
people %>>%
list.which("music" %in% Interests)
#> [1] 1 2
list.all
各要素に対して式を評価し、すべての結果がTRUE
であればTRUE
を、そうでなければFALSE
を返します。
people %>>%
list.all("R" %in% names(Expertise))
#> [1] TRUE
list.any
各要素に対して式を評価し、少なくとも一つの結果がTRUE
であればTRUE
を、そうでなければFALSE
を返します。
people %>>%
list.any("Lisp" %in% names(Expertise))
#> [1] FALSE
list.count
各要素に対して式を評価し、TRUE
となる要素の個数を返します。
people %>>%
list.count(mean(as.numeric(Expertise)) >= 3)
#> [1] 2
list.match
リストの要素の名前に対して正規表現によるパターンマッチングを行い、マッチした要素を返します。
d1 <- list(p1 = 1, p2 = 2, p3 = 3)
d1 %>>%
list.match("p[12]") %>>%
str
#> List of 2
#> $ p1: num 1
#> $ p2: num 2
list.remove
指定した要素を除外したリストを返します。要素は名前またはインデックスで指定します。
d1 %>>%
list.remove(2)
#> $p1
#> [1] 1
#>
#> $p3
#> [1] 3
list.exclude
指定した条件を満たす要素を除外したリストを返します。
people %>>%
list.exclude(Name == "Ken") %>>%
list.mapv(Name)
#> [1] "James" "Penny"
list.clean
この関数はデフォルトではNULL
である要素を除外します。また、引数recursive =
にTRUE
を指定すると、再帰的に子要素まで探索します。
d2 <- list(
a = 1,
b = NULL,
c = list(
x = 1,
y = NULL,
z = logical(0L),
w = c(NA, 1)
)
)
str(d2)
#> List of 3
#> $ a: num 1
#> $ b: NULL
#> $ c:List of 4
#> ..$ x: num 1
#> ..$ y: NULL
#> ..$ z: logi(0)
#> ..$ w: num [1:2] NA 1
d2 %>>%
list.clean(recursive = TRUE) %>>%
str
#> List of 2
#> $ a: num 1
#> $ c:List of 3
#> ..$ x: num 1
#> ..$ z: logi(0)
#> ..$ w: num [1:2] NA 1
削除される要素は、デフォルトではis.null()
がTRUEになる要素です。削除の判定に使う関数を指定することもできます。長さ0のベクトル(これにはNULL
も含まれます)とNA
を1つでも含むベクトルを削除したい場合の例を示します。
d2 %>>%
list.clean(function(x) length(x) == 0 || anyNA(x), recursive = TRUE) %>>%
str
#> List of 2
#> $ a: num 1
#> $ c:List of 1
#> ..$ x: num 1
subset
subset()
は組込み関数ですが、これを使うとlist.filter()
とlist.map()
を同時に使用したような結果を得られます。
people %>>%
subset(Age >= 24, Name) %>>%
str
#> List of 3
#> $ : chr "Ken"
#> $ : chr "James"
#> $ : chr "Penny"
更新
list.update
式の結果でリストを更新します。
people %>>%
list.update(Age = Age + 10) %>>%
list.mapv(Age)
#> [1] 34 35 34
式は複数指定可能です。また、NULL
で更新することでフィールドを削除できます。
people %>>%
list.update(Age = NULL, Expertise = NULL) %>>%
str
#> List of 3
#> $ :List of 2
#> ..$ Name : chr "Ken"
#> ..$ Interests: chr [1:3] "reading" "music" "movies"
#> $ :List of 2
#> ..$ Name : chr "James"
#> ..$ Interests: chr [1:2] "sports" "music"
#> $ :List of 2
#> ..$ Name : chr "Penny"
#> ..$ Interests: chr [1:2] "movies" "reading"
ソート
list.order
リストの各要素に対して式を評価し、評価結果に対して昇順で順位をつけ、順位のベクトルを返します。
people %>>%
list.order(Age)
#> [1] 1 3 2
降順にソートしたければ、式を()
でくくります。式の値が数値の場合は、-
を式の前に付けて値を反転させてもかまいません。
people %>>%
list.order((Age))
#> [1] 2 1 3
式は複数指定でき、複数指定すると先の式でタイだった場合に次の式が使われます。
list.sort
list.order()
と似た関数ですが、順位ではなく、ソートされたリストそのものを返します。
people %>>%
list.sort(Age) %>>%
list.select(Name, Age) %>>%
str
#> List of 3
#> $ :List of 2
#> ..$ Name: chr "Ken"
#> ..$ Age : int 24
#> $ :List of 2
#> ..$ Name: chr "Penny"
#> ..$ Age : int 24
#> $ :List of 2
#> ..$ Name: chr "James"
#> ..$ Age : int 25
グルーピング
list.group
リストの各要素に対して式を評価し、その評価結果でリストをグループに分割します。式は、評価結果が単一の値となるようなものを使うのが一般的です。リストの要素はただひとつのグループのみに所属し、複数のグループに同じ要素が所属することはありません。
people %>>%
list.group(Age) %>>%
str
#> List of 2
#> $ 24:List of 2
#> ..$ :List of 4
#> .. ..$ Name : chr "Ken"
#> .. ..$ Age : int 24
#> .. ..$ Interests: chr [1:3] "reading" "music" "movies"
#> .. ..$ Expertise:List of 3
#> .. .. ..$ R : int 2
#> .. .. ..$ CSharp: int 4
#> .. .. ..$ Python: int 3
#> ..$ :List of 4
#> .. ..$ Name : chr "Penny"
#> .. ..$ Age : int 24
#> .. ..$ Interests: chr [1:2] "movies" "reading"
#> .. ..$ Expertise:List of 3
#> .. .. ..$ R : int 1
#> .. .. ..$ Cpp : int 4
#> .. .. ..$ Python: int 2
#> $ 25:List of 1
#> ..$ :List of 4
#> .. ..$ Name : chr "James"
#> .. ..$ Age : int 25
#> .. ..$ Interests: chr [1:2] "sports" "music"
#> .. ..$ Expertise:List of 3
#> .. .. ..$ R : int 3
#> .. .. ..$ Java: int 2
#> .. .. ..$ Cpp : int 5
分割されたサブリストは、上記のように式の結果を名前として持ちます。もし式の結果が複数の値を返すようなものだった場合は、c(TRUE, FALSE, FALSE)
のような結果そのものが名前として扱われます。一つの要素は一つのグループにしか属さないという関係は絶対なのです。
上記の結果からさらにName
だけを取り出すことを考えてみましょう。グルーピングの結果はリストになっていますから、それぞれのリストの要素に対して再度マッピングをする必要があります。これは以下のように行います。
people %>>%
list.group(Age) %>>%
list.map(. %>>% list.mapv(Name))
#> $`24`
#> [1] "Ken" "Penny"
#>
#> $`25`
#> [1] "James"
この操作を行うためにはmagrittr
ではなくpipeR
が必要です。
list.ungroup
list.ungroup()
はlist.group()
と逆の操作を行います。すなわち、最上位のグルーピング変数を削除します。
people %>>%
list.group(Age) %>>%
list.ungroup() %>>%
str
#> List of 3
#> $ :List of 4
#> ..$ Name : chr "Ken"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:3] "reading" "music" "movies"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 2
#> .. ..$ CSharp: int 4
#> .. ..$ Python: int 3
#> $ :List of 4
#> ..$ Name : chr "Penny"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:2] "movies" "reading"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 1
#> .. ..$ Cpp : int 4
#> .. ..$ Python: int 2
#> $ :List of 4
#> ..$ Name : chr "James"
#> ..$ Age : int 25
#> ..$ Interests: chr [1:2] "sports" "music"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 3
#> .. ..$ Java: int 2
#> .. ..$ Cpp : int 5
list.cases
people
データのInterests
の中に出現しうる値のケースが欲しい時、list.cases()
が役立ちます。この関数は特定のフィールドに現れうる値のケースをベクトルとして返します。
people %>>%
list.cases(Name)
#> [1] "James" "Ken" "Penny"
people %>>%
list.cases(Interests)
#> [1] "movies" "music" "reading" "sports"
実際にはフィールドではなく任意の式が指定でき、式の評価結果としてありうる値が返ります。したがって、例えばExpertise
の名前としてありうるケースを列挙したければ、次のようにできます。
people %>>%
list.cases(names(Expertise))
#> [1] "Cpp" "CSharp" "Java" "Python" "R"
list.class
この関数もグループを作る関数ですが、分類基準がケースになります。つまり、式の評価結果がc("A", "B")
という2つの値を返す場合には、対応するリスト要素は"A"
と"B"
という2つのグループに所属することになります。
式の評価結果が1つの値しか返さない場合にはlist.group()
と結果は等しくなりますが、そうでない場合にはグループ間で重複したリスト要素を含む長いリストとなります。
例えば、Interestsのケース別に所属する人物を抽出したい、というような場合にこの関数が利用できます。
people %>>%
list.class(Interests) %>>%
list.map(. %>>% list.mapv(Name)) %>>%
str
#> List of 4
#> $ movies : chr [1:2] "Ken" "Penny"
#> $ music : chr [1:2] "Ken" "James"
#> $ reading: chr [1:2] "Ken" "Penny"
#> $ sports : chr "James"
list.common
list.case()
はいずれか1つの式の評価結果に出現する要素を列挙する関数でしたが、list.common()
はすべての式の評価結果に出現する共通の要素を抽出する関数です。
people %>>%
list.common(names(Expertise))
#> [1] "R"
list.table
この関数は組み込みのtable()
のラッパーです。データと式の2つの引数をとり、式により分割した要素の個数をテーブルとして表示します。式を複数指定すれば多元表が得られます。
people %>>%
list.table(Interests = length(Interests), Age)
#> Age
#> Interests 24 25
#> 2 1 1
#> 3 1 0
結合
list.join
list.join()
は2つのリストを式の評価結果をキーとして結合します。キーにする式は2つのリストに共通のものを指定することも、別々のものを指定することもできます。
Name
をキーとしてpeople
に新しいリストを結合する例を示します。
newinfo <- list(
list(Name = "Ken", Email = "ken@xyz.com"),
list(Name = "Penny", Email = "penny@xyz.com"),
list(Name = "James", Email = "james@xyz.com")
)
people %>>%
list.join(newinfo, Name) %>>%
str
#> List of 3
#> $ :List of 5
#> ..$ Name : chr "Ken"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:3] "reading" "music" "movies"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 2
#> .. ..$ CSharp: int 4
#> .. ..$ Python: int 3
#> ..$ Email : chr "ken@xyz.com"
#> $ :List of 5
#> ..$ Name : chr "James"
#> ..$ Age : int 25
#> ..$ Interests: chr [1:2] "sports" "music"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 3
#> .. ..$ Java: int 2
#> .. ..$ Cpp : int 5
#> ..$ Email : chr "james@xyz.com"
#> $ :List of 5
#> ..$ Name : chr "Penny"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:2] "movies" "reading"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 1
#> .. ..$ Cpp : int 4
#> .. ..$ Python: int 2
#> ..$ Email : chr "penny@xyz.com"
list.merge
この関数は2つのリストを受け取り、2つめのリストが1つめのリストを再帰的に更新します。
people_n <- people %>>% list.names(Name)
rev1 <- list(
Ken = list(Age=25),
James = list(Expertise = list(R=2L, Cpp=4L)),
Penny = list(Expertise = list(R=2L, Python=NULL))
)
people_n %>>%
list.merge(rev1) %>>%
str
#> List of 3
#> $ Ken :List of 4
#> ..$ Name : chr "Ken"
#> ..$ Age : num 25
#> ..$ Interests: chr [1:3] "reading" "music" "movies"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 2
#> .. ..$ CSharp: int 4
#> .. ..$ Python: int 3
#> $ James:List of 4
#> ..$ Name : chr "James"
#> ..$ Age : int 25
#> ..$ Interests: chr [1:2] "sports" "music"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 2
#> .. ..$ Java: int 2
#> .. ..$ Cpp : int 4
#> $ Penny:List of 4
#> ..$ Name : chr "Penny"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:2] "movies" "reading"
#> ..$ Expertise:List of 2
#> .. ..$ R : int 2
#> .. ..$ Cpp: int 4
この関数はリストの要素のうちどれを更新すべきなのかを名前で判断します。したがって、引数に与えるリストは相互に対応する名前を持ったリストでなければならない、という点に注意してください。
検索
list.search
list.search()
は式の評価結果がTRUE
になる要素を返す関数です。説明だけではlist.filter()
に似ていますが、2点の大きな違いがあります。
- リストに対して再帰的に式が評価される。
- 式の結果が単一の
TRUE
となる場合のみ対応する要素をそのまま返す。複数の要素からなる結果の場合は、その結果のうちNA
ではない要素がベクトルとしてすべて返る。
people %>>%
list.search(. == 2L)
#> $Interests
#> [1] FALSE FALSE FALSE
#>
#> $Expertise.R
#> [1] 2
#>
#> $Interests
#> [1] FALSE FALSE
#>
#> $Expertise.Java
#> [1] 2
#>
#> $Interests
#> [1] FALSE FALSE
#>
#> $Expertise.Python
#> [1] 2
探索対象のクラスをclass =
引数で指定できます。一般的には指定しておいたほうが安全であり、パフォーマンスも向上します。クラスはベクトルで複数指定することもできます。
people %>>%
list.search(. == 2L, class = "integer")
#> $Expertise.R
#> [1] 2
#>
#> $Expertise.Java
#> [1] 2
#>
#> $Expertise.Python
#> [1] 2
次のようにすると正規表現と組み合わせた探索もできます。
people %>>%
list.search(.[grepl("en", .)])
#> $Name
#> [1] "Ken"
#>
#> $Name
#> [1] "Penny"
比較
比較はlist.filter()
やlist.search()
でよく使う操作ですが、これにはいくつかの方法があります。大きく分けて正確な比較とあいまい検索があります。
正確な比較
正確な比較では2つの変数の値が等しいかどうかが問題となります。
identical()
による比較
この関数による比較は非常に厳しい部類に入り、2つのオブジェクトの型、値、属性が等しいかどうかが確認され、すべてが等しいときにTRUE
が返ります。
次の例では型が違います(numericとint)。
identical(c(1, 2), 1:2)
#> [1] FALSE
次の例では属性が違います(名前付きと名前なし)。
identical(c(a = 1), 1)
#> [1] FALSE
では、数値24をpeople_n
の中から検索してみましょう。
people_n %>>%
list.search(identical(., 24))
#> named list()
年齢が24の人がいたはずですが、何も出てきません。すでにお気付きかもしれませんが、24
はnumericです。データ中の年齢はintでしたから、明示的に24L
としてint型にする必要があったのです。
people_n %>>%
list.search(identical(., 24L))
#> $Ken.Age
#> [1] 24
#>
#> $Penny.Age
#> [1] 24
==
による比較
==
は2つのアトミックベクトルを比較しますが、比較の前に2つのオブジェクトは同一の形に変換されます。したがって、次のように文字列で整数型を検索するようなこともできます。
people_n %>>%
list.search(. == "24", class = "integer")
#> $Ken.Age
#> [1] 24
#>
#> $Penny.Age
#> [1] 24
あいまい比較
あいまいな比較では、ターゲットを特定の値と比較するのではなく、ターゲットが特定の値の範囲に属するかどうかが問題となります。
正規表現
文字列に対するあいまいな検索の代表として正規表現があります。正規表現はメタシンボルを利用して文字列の範囲を指定します。すでに例を挙げましたが、grepl()
は正規表現を使う組込み数のひとつで、パターンにマッチすればTRUE
を、しなければFALSE
を返します。したがって、list.filter()
などと組み合わせて利用できます。
people %>>%
list.filter(grepl("en", Name)) %>>%
list.mapv(Name)
#> [1] "Ken" "Penny"
正規表現そのものについてはここで説明するには広すぎる話題なので省略しますが、文字列操作を行うための非常に強力なツールです。次のような優れたサイトがあるので、ぜひとも習得を目指してください。
- RegexOne - Learn Regular Expressions...インタラクティブに正規表現を学ぶことができます。
- RegExr: Learn, Build, & Test RegEx...パターンをテストできます。
文字列距離
stringdist
パッケージは文字列間の距離を計算できるパッケージです。パッケージのstringdist()
関数は、デフォルトでrestricted Damerau-Levenshtein距離を計算します。この距離は片方の文字列をもう片方へ変換するために必要な置換などの操作の手数から計算されます。
次のようなデータを例に使用例を確認してみましょう。
people1 <- list(
p1 = list(name = "Ken", age = 24),
p2 = list(name = "Kent", age = 26),
p3 = list(name = "Sam", age = 24),
p4 = list(name = "Keynes", age = 30),
p5 = list(name = "Kwen", age = 31)
)
名前が"Ken"から距離1以内のものを抽出してみましょう。
library(stringdist)
people1 %>>%
list.filter(stringdist(name, "Ken") <= 1) %>>%
list.mapv(name)
#> p1 p2 p5
#> "Ken" "Kent" "Kwen"
文字列距離にもとづ比較は、スペルミスのような微妙な表記ゆれに柔軟に対応できる可能性を持っています。
データ変換と入出力
list.parse
与えられたデータをリストに変換します。データフレームや配列のほか、JSONやYAMLの文字列も与えることができます。
list.stack
この関数はlist.parse()
と逆の操作をします。つまり、リストからデータフレームを生成します。操作の結果がデータフレームへ変換するのに適した結果となることが分かっている場合は役立つでしょう。
people %>>%
list.select(Name, Age) %>>%
list.stack()
#> Name Age
#> 1 Ken 24
#> 2 James 25
#> 3 Penny 24
list.load, list.save
このチュートリアルの一番始めにlist.load()
を使ってJSONを読み込みましたが、この関数は組込み関数のload()
やloadRDS()
と同様にRDataやRDSも扱うことができます。
また、その逆の関数としてlist.save()
があります。操作の結果をJSON等で保存したい場合に役立つでしょう。
list.serialize, list.unserialize
シリアル化というのはオブジェクトを完全に復元可能な形式で保存するプロセスを指します。これらの関数はRネイティブのシリアライザと、jsonlite
パッケージのシリアライザをサポートしています。これはデフォルトでは拡張子に応じて自動で使い分けられます。
上記以外の関数
list.append, list.prepend
list.append()
はリストの最後に要素を追加します。
1:3 %>>%
list.append(4)
#> [1] 1 2 3 4
list.prepend()
はリストの最初に要素を追加します。
1:3 %>>%
list.prepend(0)
#> [1] 0 1 2 3
list.reverse
リストを逆順にします。
1:3 %>>%
list.reverse()
#> [1] 3 2 1
list.zip
2つのリストを要素単位で結合して新しいリストを作ります。
1:3 %>>%
list.zip(4:6) %>>%
str
#> List of 3
#> $ :List of 2
#> ..$ .: int 1
#> ..$ : int 4
#> $ :List of 2
#> ..$ .: int 2
#> ..$ : int 5
#> $ :List of 2
#> ..$ .: int 3
#> ..$ : int 6
list.rbind, list.cbind
リストの要素を行単位または列単位で結合します。
l <- list(
1:3,
4:6,
7:9
)
list.rbind(l)
#> [,1] [,2] [,3]
#> [1,] 1 2 3
#> [2,] 4 5 6
#> [3,] 7 8 9
list.cbind(l)
#> [,1] [,2] [,3]
#> [1,] 1 4 7
#> [2,] 2 5 8
#> [3,] 3 6 9
この関数は最終的にrbind()
かcbind()
を呼び出すので、結果はデータフレームか行列になります。
リストのリストをこの関数に渡すと、リストの行列が生成されます。
l2 <- list(
ll1 = list(1, 2, 3),
ll2 = list(4, 5, 6),
ll3 = list(7, 8, 9)
)
l3 <- list.rbind(l2)
l3
#> [,1] [,2] [,3]
#> ll1 1 2 3
#> ll2 4 5 6
#> ll3 7 8 9
これは予期せぬ間違いにつながる可能性があります。ベクトルを抽出したつもりでリストを抽出していたという場合があり得るためです。
l3[,1]
#> $ll1
#> [1] 1
#>
#> $ll2
#> [1] 4
#>
#> $ll3
#> [1] 7
一般的にはリストのリストにこの関数を適用することはお勧めできません。
list.flatten
すべてのリストのレベルを均一にします。
data <- list(list(a = 1, b = 2), list(c = 1, d = list(x = 1, y = 2)))
str(data)
#> List of 2
#> $ :List of 2
#> ..$ a: num 1
#> ..$ b: num 2
#> $ :List of 2
#> ..$ c: num 1
#> ..$ d:List of 2
#> .. ..$ x: num 1
#> .. ..$ y: num 2
list.flatten(data)
#> $a
#> [1] 1
#>
#> $b
#> [1] 2
#>
#> $c
#> [1] 1
#>
#> $d.x
#> [1] 1
#>
#> $d.y
#> [1] 2
list.names
既に説明の中でも出てきましたが、この関数は式の評価結果をリストの要素の名前として割り当てます。
people %>>%
list.names(Name) %>>%
str
#> List of 3
#> $ Ken :List of 4
#> ..$ Name : chr "Ken"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:3] "reading" "music" "movies"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 2
#> .. ..$ CSharp: int 4
#> .. ..$ Python: int 3
#> $ James:List of 4
#> ..$ Name : chr "James"
#> ..$ Age : int 25
#> ..$ Interests: chr [1:2] "sports" "music"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 3
#> .. ..$ Java: int 2
#> .. ..$ Cpp : int 5
#> $ Penny:List of 4
#> ..$ Name : chr "Penny"
#> ..$ Age : int 24
#> ..$ Interests: chr [1:2] "movies" "reading"
#> ..$ Expertise:List of 3
#> .. ..$ R : int 1
#> .. ..$ Cpp : int 4
#> .. ..$ Python: int 2
名前付きリストの操作結果には基本的には名前が残るので、名前を設定しておいたほうが良い場面は多くあります。
people %>>%
list.names(Name) %>>%
list.mapv(Age)
#> Ken James Penny
#> 24 25 24
list.sample
リストからサンプリングを行います。この関数は式を使って重み付けをすることもできます。
set.seed(0)
list.sample(1:10, size = 3, weight = .^2)
#> [1] 5 10 8
ラムダ式
これまでの説明で何気なく式と呼んできましたが、rlist
における式はラムダ式です。ラムダ式の中ではリスト要素のフィールドに直接アクセスできることはこれまでに確認したとおりです。
そして、ラムダ式には暗黙のラムダ式と明示的なラムダ式があります。
暗黙のラムダ式
すべてのrlist
の関数は暗黙のラムダ式をサポートします。これは特別な構文を持たない普通の式ですが、初めの方で説明したように特別なシンボルをいくつか扱えます。すなわち、要素を表す.
、インデックスを表す.i
、名前を表す.name
です。
明示的なラムダ式
.
、.i
、.name
に対して他の名前を使いたい場合は明示的なラムダ式を使います。明示的なラムダ式はlist.select()
以外のすべてのrlist
の関数がサポートしています。
明示的なラムダ式には、.
だけを指定する一変量ラムダ式と、.
以外のシンボルも指定する多変量ラムダ式があります。
- 一変量ラムダ式: 次の形で要素を指定するシンボルを指定します。
x
がシンボルとして扱われます。x ~ expression
f(x) ~ expression
- 多変量ラムダ式: シンボル、インデックス、名前の3つのシンボル名をこの順で指定します。
f(x, i) ~ expression
f(x, i, name) ~ expression
リスト環境
関数List()
を使うと、リストをラップしてrlist
のほとんどの関数を含む環境オブジェクトを生成します。これを使うと、%>>%
演算子を使わずに操作のチェインをすることもできます。
まずはリスト環境を作成してみます。
m <- List(people)
メソッドを呼び出してみましょう。
m$mapv(Name)
#> $data : character
#> ------
#> [1] "Ken" "James" "Penny"
リスト環境の操作結果は相変わらずリスト環境なので、操作を$
で連続させられます。
m$filter(Expertise$R > 1)$mapv(Name)
#> $data : character
#> ------
#> [1] "Ken" "James"
結果をリスト環境から取り出したければ、$data
を使います。
m$filter(Expertise$R > 1)$mapv(Name)$data
#> [1] "Ken" "James"
あるいは[]
を付けても構いません。
m$filter(Expertise$R > 1)$mapv(Name)[]
#> [1] "Ken" "James"