LoginSignup
12
18

More than 3 years have passed since last update.

Rでリストを扱うrlistパッケージのチュートリアル

Last updated at Posted at 2018-08-09

※この文章はrlist Tutorialを翻訳したら長くなったのでちょっと短めに要約したものです。まだ結構長いんですが、それでも結構省略したので原文もあわせて確認することをお勧めします。

前置き

rlistパッケージ

表の形で表現できるデータの扱いはRの得意とするところで、dplyrdata.tableなどの優れたパッケージもあります。

表の形で表すことができない非リレーショナルなデータは、Rではリストを使って扱います。しかし、組込み関数だけでリストを扱うことは少々骨の折れる作業です。

rlistはリストを扱うためのパッケージです。rlistを使うことで、リストをよりエレガントに扱えるようになるはずです。

pipeRパッケージ

rlistはパイプを使った操作のチェインと親和性が高い設計となっています。

rlistのほとんどの関数は、dplyrで使われているmagrittr%>%演算子を使っても問題なく機能します。しかし、一部の関数ではメタシンボル.の解釈を巡って競合が発生し、予期せぬエラーの原因となることがあります。

pipeRパッケージは、パイプ演算子%>>%を提供するパッケージです。演算子の使用方法はmagrittrのそれに良く似ていますが、pipeRrlistと併用しても競合が発生しないような設計になっています。したがって、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()TRUEFALSEを返す式をリストの要素ごとに評価し、結果が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点の大きな違いがあります。

  1. リストに対して再帰的に式が評価される。
  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"

正規表現そのものについてはここで説明するには広すぎる話題なので省略しますが、文字列操作を行うための非常に強力なツールです。次のような優れたサイトがあるので、ぜひとも習得を目指してください。

  1. RegexOne - Learn Regular Expressions...インタラクティブに正規表現を学ぶことができます。
  2. 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"
12
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
18