Skip to content

Instantly share code, notes, and snippets.

@nozma
Last active August 6, 2018 08:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nozma/e937e64c8ea92ab9ac6119581bca8d95 to your computer and use it in GitHub Desktop.
Save nozma/e937e64c8ea92ab9ac6119581bca8d95 to your computer and use it in GitHub Desktop.
rlistのチュートリアル
---
title: "rlist Tutorial"
author: '@nozma'
date: "2018/8/4"
output:
prettydoc::html_pretty:
theme: leonids
highlight: github
keep_md: true
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
```
※この文章は[Introduction | rlist Tutorial](https://renkun-ken.github.io/rlist-tutorial/index.html)を翻訳したものです。
# Getting started
## Quick overview
近年、非リレーショナルデータにの重要性が高まってきています。大雑把にいえば、テーブル形式で表すことが難しいすべてのデータは非リレーショナルあるいは非テーブルデータと言えるでしょう。
Rはテーブル形式のデータを扱うことに長けています。組み込みクラスの`data.frame`は、長方形のテーブル形式に格納されうる各種のリレーショナルデータを表現するためには十分強力であるといえます。`data.table`や`dplyr`などのパッケージは、データフレームをより高速に扱ったり、さらなる機能を追加するために開発されました。例えば、テーブルデータは次のような形をしています。
| Name | Gender | Age | Major |
|------|--------|-----|-------|
| Ken | Male | 24 | Finance |
| Ashley | Female | 25 | Statistics |
| Jennifer | Female | 23 | Computer Science |
この種のデータは規則性があるので扱うのは簡単です。実際、各列に含まれる値の型は同じですし、長さも同じです。
データフレームに対する操作は非常に高速かつ簡単になってきていますが、依然としてRの基本機能では次のような非リレーショナルデータを扱うことはできません。
| Name | Age | Interests | Expertise |
|------|-----|-----------|-----------|
| Ken | 24 | reading, music, movies | R:2, C#:4, Python:3 |
| James | 25 | sports, music | R:3, Java:2, C++:5 |
| Penny | 24 | movies, reading | R:1, C++:4, Python:2 |
InterestsとExpertiseは複数の項目を含んでいますし、数も違います。
これを表形式にしようとすれば、複数の表が必要ですし、表の間の関係を表す必要も出てきます。このようなデータでもっと長いものがあったとします(例えば[こちら](https://renkun-ken.github.io/rlist-tutorial/data/people.json)を確認してください)。このとき、次のような質問にどのように答えれば良いでしょうか。
- Interestsの中で最も人気のある3項目の年齢分布はどのようになっているでしょうか。なお、集計対象はRとPythonの使用経験が1年以上の人とします。
これをSQLクエリとしてデータベースに送信しようとすれば多少の努力が必要です。しかし、`rlist`を使えばこの問いに簡単に答えることができます。
```{r}
library(rlist)
library(pipeR)
url <- "http://renkun-ken.github.io/rlist-tutorial/data/people.json"
people <- list.load(url)
people %>>%
list.filter(Expertise$R >= 1 & Expertise$Python >= 1) %>>%
list.class(Interests) %>>%
list.sort(-length(.)) %>>%
list.take(3) %>>%
list.map(. %>>% list.table(Age))
```
コード中ではスタイルを整えるために`pepeR`パッケージの`%>>%`演算子を利用しました。関数や演算子に慣れていなくても、コードが何をしているかは正しく推測できたでしょう。
それぞれのステップについて説明します。
1. RとPythonの使用経験が1年以上の人間に絞り込みました。
2. 人々をInterestsに基づいて分類します。すなわち、Interestsの取りうる値に基づいて、大きなリストを作成します。それぞれのリストは、Interestsの値ごとに人々を含むネストされたリストになります。
3. 人々の数をキーとして、Interestsのリストを降順ソートします。
4. 上位3件のリストを抽出します。
5. それぞれのクラスに対して年齢のテーブルを作成します。
この出力は当初の質問に完璧に答えられるものとなっています。
もうおわかりだと思いますが、rlistはリストオブジェクトを操作する一連の関数を定義しています。それぞれの関数は単純な仕事しかしませんが、組み合わせると非常に強力です。このチュートリアルは、ほとんどの機能について詳細に説明するとともに、データ処理の過程で一般的に遭遇する共通の問題について解決策の例を示します。
## Working in pipeline
`rlist`はパイプ演算子と親和性が高くなるように設計してあります。ほとんどの関数はデータを最初の引数として受け取ります。これにより、リストオブジェクトに格納された非テーブルデータに対して連続的な操作を簡単に施すことができるようになっています。
もし既に`dplyr`パッケージについて知っているのであれば、`magrittr`パッケージから流用された`%>%`演算子についても知っていることでしょう。`%>%`を使用しても`rlist`の関数はほとんどの場合で機能します。しかし、一部のケースでは`.`の解釈を巡って競合が発生し、予期せぬエラーにつながる場合があります。
`pipeR`は`rlist`と競合するような設計を含まないため、`rlist`を使用する場合は`pipeR`を使用することをおすすめします。もしパイプ演算子を使った操作に既に慣れているのであれば、`pipeR`のプロジェクトページ([renkun-ken/pipeR: Multi-Paradigm Pipeline Implementation](https://github.com/renkun-ken/pipeR))を確認することで概要をつかめるでしょう。もしパイプ処理やパッケージについてあまり慣れていないのであればチュートリアル([Getting started | pipeR Tutorial](https://renkun-ken.github.io/pipeR-tutorial/Getting-started/index.html))を読むことを強くおすすめします。
パイプ演算子を使用することで、確実にコードはエレガントになり、保守性と可読性が向上します。例えば、先ほどと同じ処理を`%>>%`を使用せずに伝統的なコードのみで記述することを考えてみましょう。例えば多数の中間的な変数を使って次のように書くことになるでしょう。
```r
people_filtered <- list.filter(people, Expertise$R >= 1 & Expertise$Python >= 1)
people_classes <- list.class(people_filtered, Interests)
people_classes_sorted <- list.sort(people_classes, -length(.)) %>>%
top_3_classes <- list.take(people_classes_sorted, 3)
top_3_table_age <- list.map(top_3_classes, . %>>% list.table(Age))
```
あるいは、一切の中間的な変数を使わないとすると次のようになります。
```r
list.map(list.take(list.sort(list.class(list.filter(people,
Expertise$R >= 1 & Expertise$Python >= 1),Interests),
-length(.)),3), . %>>% list.table(Age))
```
最初の方法では書くのが大変です(変数名を考えるのにも多くの時間が必要です)。二番目の方法では読むのが大変です(背後のロジックを見抜くのは至難の業でしょう)。いずれの方法にも共通して言えることとして、保守が大変です(例えば処理の中間に新しいフィルタリング処理を追加することを想像してみましょう)。
`%>>%`を使ったバージョンを改めて確認してみましょう。
```r
people %>>%
list.filter(Expertise$R >= 1 & Expertise$Python >= 1) %>>%
list.class(Interests) %>>%
list.sort(-length(.)) %>>%
list.take(3) %>>%
list.map(. %>>% list.table(Age))
```
これは書くのが簡単(変数名を考えるのに時間を費やす必要はありません)というだけでなく、読むのも簡単です(何をしているのかは一目瞭然でしょう)。そして保守も難しくありません(別のフィルタリング処理を追加するのは非常に簡単です)。
## File formats
ここまでに、`rlist`は表形式に適していないデータを扱えるようにデザインされているという話をしました。この点を強調するために、先程使用した例を再度使用します。
以下のテーブルは表形式のデータを表しています。
| Name | Gender | Age | Major |
|------|--------|-----|-------|
| Ken | Male | 24 | Finance |
| Ashley | Female | 25 | Statistics |
| Jennifer | Female | 23 | Computer Science |
テーブルは簡単にテキストベースのファイルやリレーショナルデータベースに保存することができます。テキストベースのファイル形式として最も一般的なのはCSVで、これは大抵の場合カンマ(`,`)を用いて値を区切ったものとなっています。CSVでは上記のデータは次のように表現されます。
```csv
Name,Gender,Age,Major
Ken,Male,24,Finance
Ashley,Female,25,Statistics
Jennifer,Female,23,Computer Science
```
1行が行を表しており、列はカンマによって区切られています。Rでは、`read.csv()`関数を使うことで簡単にCSVを読み込むことができます。
しかし、非テーブルデータは標準的なCSVと読み込み関数では扱うことができません。非テーブルデータの例を思い出してみましょう。
| Name | Age | Interests | Expertise |
|------+-----+-----------+-----------|
| Ken | 24 | reading, music, movies | R:2, C#:4, Python:3 |
| James | 25 | sports, music | R:3, Java:2, C++:5 |
| Penny | 24 | movies, reading | R:1, C++:4, Python:2 |
このデータを表現するためにCSVを使っても満足する結果は得られないでしょう。Interests列の値は数が不定ですし、Expertise列の値は複数の異なった名前を持っています。
データを格納するためにリレーショナルデータベースを構築することを考えても良いでしょう。しかし、データベースの構造は少し難しいものになります。複数のテーブルを作成する必要がありますし、それぞれのテーブルは構造の種類によって制限がかかります。データを柔軟に参照するためには複数のテーブルを結合する必要があります。
JSONはこのような柔軟なデータを表現するための強力なフォーマットです。これは多くの表記方法を持っていますが、かといって表現が複雑になりすぎることはありません。上記のテーブルのJSONによる表現は次のようになります。
```json
[
{
"Name" : "Ken",
"Age" : 24,
"Interests" : [
"reading",
"music",
"movies"
],
"Expertise" : {
"R": 2,
"CSharp": 4,
"Python" : 3
}
},
{
"Name" : "James",
"Age" : 25,
"Interests" : [
"sports",
"music"
],
"Expertise" : {
"R" : 3,
"Java" : 2,
"Cpp" : 5
}
},
{
"Name" : "Penny",
"Age" : 24,
"Interests" : [
"movies",
"reading"
],
"Expertise" : {
"R" : 1,
"Cpp" : 4,
"Python" : 2
}
}
]
```
JSONテキストは先程のテーブルに含まれる情報のすべてを表現できます。JSONの中には`[]`や`{}`、`"key": value`といった記法が出てきます。これらについて簡単に説明しておきます。
- `[]` 名前無しのノードの配列を作成します。
- `{}` 名前付きのノードのリストを作成します。
- `"key": value` キーバリューペアを作成します。`value`は数値、文字列、`[]`による配列、`{}`によるリストのいずれかです。
この記法は、Rのリストオブジェクトがそうであるのと同様に、ネストされたリストや配列を許容します。この類似性はJSONとRの使用方法にも繋がります。`rlist`パッケージはJSONの読み書きのために`jsonlite`パッケージをインポートします。
他に広く使われているファイル形式としてYAMLがあります。YAMLによる非テーブルデータの表現を次に示します。
```yaml
- Name: Ken
Age: 24
Interests:
- reading
- music
- movies
Expertise:
R: 2
CSharp: 4
Python: 3
- Name: James
Age: 25
Interests:
- sports
- music
Expertise:
R: 3
Java: 2
Cpp: 5
- Name: Penny
Age: 24
Interests:
- movies
- reading
Expertise:
R: 1
Cpp: 4
Python: 2
```
YAMLによる表現はJSONよりも遥かにクリーンです。`rlist`はYAMLの読み書きのために`yaml`パッケージもインポートします。
この先のチュートリアルでは、主にJSONを利用して`rlist`の機能を紹介します。
# Features
## Mapping
ここでは、次に示すようなデータを読み込むものとします。
| Name | Age | Interests | Expertise |
|------+-----+-----------+-----------|
| Ken | 24 | reading, music, movies | R:2, C#:4, Python:3 |
| James | 25 | sports, music | R:3, Java:2, C++:5 |
| Penny | 24 | movies, reading | R:1, C++:4, Python:2 |
`list.load()`は指定されたデータソースからデータを読み込むための関数です。データソースはローカルのものでもリモートのものでもどちらでも構いません。また、デフォルトでは拡張子から読み込み方法を自動的に決定します。
```{r}
people <- list.load("https://renkun-ken.github.io/rlist-tutorial/data/sample.json")
str(people)
```
注: `str()`はオブジェクトの構造を表示するための関数です。リストをそのまま表示すると表現が冗長なので、リストの構造確認のためにしばしばこの関数を利用します。
人の名前(リストの要素)を抽出したければ、Rの組み込み機能を使う場合は`lapply()`を使って次のように書きます。
```{r}
lapply(people, function(x){
x$Name
})
```
これと同様のことが`rlist`では`list.map()`関数を使って非常に簡単に記述できます。
```{r}
list.map(people, Name)
```
リストに対するマッピングでは、リストの要素それぞれに対して式を評価します。これは`rlist`における基本的な操作です。式とともに使う`rlist`のほとんどの関数は、大抵の場合マッピングを、それぞれ違うやりかたで利用しています。この後示す例では、マッピングのいくつかの種類についてもう少し詳しく説明します。
### list.map
マッピングのもっとも単純な例が`list.map()`によるものです。これは基本的には与えられた式をリストのそれぞれの要素に適用します。
この関数は、リストの各要素を式の評価環境にマッピングすることで、リストへの照会を簡単にします。言い換えれば、式は毎回リストの1つの要素に対して評価されるということです。
例えば、次のコードでは、`people`の各要素に対して`Age`を評価します。そして、`people`の各メンバーの値が式の評価結果になったリストが返ります。
```{r}
list.map(people, Age)
```
式は必ずしもリストのメンバーのフィールド名である必要はありません。リストのメンバーのコンテキストにおいて、望むものを評価することができます。
次に示すコードでは、リストのそれぞれのメンバーに対してプログラミング言語の経験年数の合計を求めています。
```{r}
list.map(people, sum(as.numeric(Expertise)))
```
もし各メンバーに対して複数の値を求める必要があれば、ベクトルを使ったり式のリストを使ったりすることができます。
次の例では、それぞれのメンバーに対して年齢とプログラミング言語の経験年数のレンジを計算しています。
```{r}
list.map(people, list(age = Age, range = range(as.numeric(Expertise))))
```
アイテム自体や、リスト中におけるインデックス、アイテムの名称を参照したいという場面に遭遇することもあるでしょう。式の中では、`.`はアイテムそれ自身を、`.i`はインデックスを、`.name`は名前を表します。
例を示しましょう。
```{r}
nums <- c(a = 3, b = 2, c = 1)
list.map(nums, . + 1)
```
```{r}
list.map(nums, .i)
```
```{r}
list.map(nums, paste0("name: ", .name))
```
もし標準のシンボルがデータと衝突する場合、ラムダ式を使用して他のシンボルを使うことができます。これについてはまた後で解説します。
注: `rlist`関数はベクトルに対しても十分スムーズに働きます。`list.map()`は`lapply()`と同じように機能するので、入力はリストに変換されて出力されます。
### list.mapv
もしマッピングの結果をリストではなくベクトルとして得たければ、`list.mapv()`を利用できます。これは基本的には`list.map()`の結果に`unlist()`を適用したのと同じ結果を返します。
```{r}
list.mapv(people, Age)
```
```{r}
list.mapv(people, sum(as.numeric(Expertise)))
```
### list.select
`list.map()`とは対象的に、`list.select()`は各リストのメンバーを新しいリストにマッピングする簡単な方法を提供します。基本的には、この関数はすべての与えられた式を評価して、結果をリストとして返します。
フィールド名がリストのメンバーとして選ばれた場合には、出力のリスト内でも名前は自動的に保存されます。それ以外の式の評価結果がリストの要素となる場合には、名前を明示的に与えたほうが良いですが、そうでなければ単にインデックスのみを与えます。
```{r}
list.select(people, Name, Age)
```
```{r}
list.select(people, Name, Age, nlanb = length(Expertise))
```
### list.iter
ときどき、マッピングの結果は不要で副作用のみがほしいという場合があります。例えばリストのメンバーについて何かをprintするというのが目的であれば、それ以外の出力は必要ありません。
`list.iter()`はリストの要素に対して繰り返し処理を行い、さらなる処理を続ける場合のために暗黙のうちに入力データを返します。
```{r}
list.iter(people, cat(Name, ":", Age, "\n"))
```
### list.maps
今まで説明したすべての関数は一つのリストを対象として動作するものでした。しかし、場合によっては複数のリストにまたがったマッピングを行いたくなる場合があります。`list.maps()`は、ユーザー定義のシンボルで表現された複数のリストに対して式を評価します。
```{r}
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)
```
`list.maps()`はこれまで説明した関数とは異なり、データを2番目以降の引数として受け取ります。また、複数のマッピングをサポートする都合上、曖昧さを避けるために暗黙のラムダ式のみが使用可能となっています。その後ろで、ユーザーは`...`引数を通じて各リストを表すシンボルを定義できます。
上記の例でいうと、`...`には`a = l1`と`b = l2`が該当します。`a`と`b`は式`a$x * b + a$y`の内部でのみ意味を持ちます。`a`と`b`は各リストの繰り返し対象となる要素に対応します。
なお、引数として与えるリストに必ずしも名前を設定する必要はありません。上記の例でいえば、代わりに`..1`と`..2`といった変数を利用可能です。これは最初の要素、二番目の要素に対応しています。この引数を使って書き換えた例を示します。
```{r}
list.maps(..1$x * ..2 + ..1$y, l1, l2)
```
## Filtering
ここで言うフィルタリングとは与えられた基準にしたがってリストの要素を選択する操作を意味します。`rlist`では、フィルタリングに関連する10以上の関数があります。基本的には、これらの関数は最初の要素として受け取ったリストに対してマッピングを行い、その結果を様々な方法で集約して返します。
サンプルデータは先程に引き続いて`people`を使用します。
### list.filter
`list.filter()`は`TRUE`または`FALSE`を返す式によってリストのフィルタリングを行います。結果として、式の値が`TRUE`となる要素のみが含まれるリストが返ります。
マッピングとの違いは、式によってリストの要素そのものを結果に含めるかどうかを決めるという点です。
```{r}
str(list.filter(people, Age >= 25))
```
`list.filter()`が指定された条件を満たす要素のみを返している点にのみ注目してください。上記で`str()`を使っているのは単に出力を短くするためだけに過ぎません。
パイプ処理を用いれば、フィルタリングの結果にマッピングを施すこともできます。例えば、25歳以上の年齢の人の名前を確認してみましょう。
```{r}
people %>>%
list.filter(Age >= 25) %>>%
list.mapv(Name)
```
古典的な記法を用いれば、次のようになります。
```r
list.mapv(list.filter(people, Age >= 25), Name)
```
あるいは中間変数を用いるならこうです。
```r
people_filtered <- list.filter(people, Age >= 25)
list.mapv(people_filtered, Name)
```
今示した2つの書き方は明らかに冗長です。したがって、データの処理をよりエレガントに行い、出力内の情報量を減らすために、これ以降のデモンストレーションでも頻繁にパイプ処理を使用します。
では、いくつかフィルタリングの例を示します。
`music`に興味がある人を抽出してみましょう。
```{r}
people %>>%
list.filter("music" %in% Interests) %>>%
list.mapv(Name)
```
プログラミング言語の平均利用年数が3年以上の人を抽出してみましょう。
```{r}
people %>>%
list.filter(mean(as.numeric(Expertise)) >= 3) %>>%
list.mapv(Name)
```
メタシンボルである`.`と`.i`はここでも利用可能です。偶数番目の要素の人の名前を取り出す例を見てみましょう。
```{r}
people %>>%
list.filter(.i %% 2 == 0) %>>%
list.mapv(Name)
```
### list.find
ときどき、基準を満たす要素の全てではなく一部だけがほしいという場合があります。というよりむしろ、ほんの少しだけとか、1つだけ欲しいということもあります。`list.find()`はすべての要素にわたって探索を行う代わりに、指定した数のアイテムが見つかった時点で探索を止めます。
```{r}
people %>>%
list.find(Age >= 25, 1) %>>%
list.mapv(Name)
```
### list.findi
`list.findi()`は`list.find()`に似ていますが、返り値は要素のインデックスです。
```{r}
list.findi(people, Age >= 23, 2)
```
### list.first, list.last
`list.first()`と`list.last()`は、それぞれ条件を満たす要素のうち最初、最後の要素だけを返す専用の関数です。
```{r}
str(list.first(people, Age >= 23))
```
```{r}
str(list.last(people, Age >= 23))
```
これらの関数は条件式を省略しても機能します。その場合、単に最初、最後の要素を返します。
```{r}
list.first(1:10)
```
```{r}
list.last(1:10)
```
### list.take
`list.take()`は指定された値の個数分の要素を返します。指定できる値は最大値です。つまり、指定された値がリストの要素数よりも大きければ、この関数はデフォルトではリストのすべての要素を返します。
```{r}
list.take(1:10, 3)
```
```{r}
list.take(1:5, 8)
```
### list.skip
`list.take()`とは逆に、`list.skip()`は無視する要素の最大数を指定します。リストの要素数よりも大きな値を指定した場合には、返り値は空になります。
```{r}
list.skip(1:10, 3)
```
```{r}
list.skip(1:5, 8)
```
### list.takeWhile
この関数は`list.take()`に似ていますが、`list.takeWhile()`は要素数の代わりに条件式を受け取ります。そして、条件式の値が真である間だけ要素を抽出します。
```{r}
people %>>%
list.takeWhile(Expertise$R >= 2) %>>%
list.map(list(Name = Name, R = Expertise$R)) %>>%
str
```
### list.skipWhile
条件式が真である間だけ要素をスキップする関数です。
```{r}
people %>>%
list.skipWhile(Expertise$R <= 2) %>>%
list.map(list(Name = Name, R = Expertise$R)) %>>%
str
```
### list.is
`list.is()`は、引数として与えた条件式が成立するかどうかを示す論理値のベクトルを返す関数です。
```{r}
list.is(people, "music" %in% Interests)
```
```{r}
list.is(people, "Java" %in% names(Expertise))
```
### list.which
`list.which()`は、引数として与えた条件式が成立する要素のインデックスを整数ベクトルとして返します。
```{r}
list.which(people, "music" %in% Interests)
```
```{r}
list.which(people, "Java" %in% names(Expertise))
```
### list.all
`list.all()`は指定した条件式がすべての要素で成立するなら`TRUE`を、そうでなければ`FALSE`を返します。
```{r}
list.all(people, mean(as.numeric(Expertise)) >= 3)
```
```{r}
list.all(people, "R" %in% names(Expertise))
```
### list.any
`list.any()`は指定した条件式の成立する要素が一つでもあれば`TRUE`を、一つも成立しなければFALSEを返します。
```{r}
list.any(people, mean(as.numeric(Expertise)) >= 3)
```
```{r}
list.any(people, "Python" %in% names(Expertise))
```
### list.count
`list.count()`は指定された条件式が成立する要素の個数を返します。
```{r}
list.count(people, mean(as.numeric(Expertise)) >= 3)
```
```{r}
list.count(people, "R" %in% names(Expertise))
```
### list.match
`list.match()`は、リストの要素の名前に対して正規表現を用いたパターンマッチを行い、マッチする要素を返します。
```{r}
data <- list(p1 = 1, p2 = 2, p3 = 3, p4 = 4)
list.match(data, "p[12]")
```
### list.remove
`list.remove()`は指定した要素を除外したリストを返します。要素の指定は名前もしくはインデックスで行います。
```{r}
list.remove(data, c("p1", "p2"))
```
```{r}
list.remove(data, c(2, 3))
```
### list.exclude
`list.exclude()`は、指定した条件を満たす要素を除外したリストを返します。
```{r}
people %>>%
list.exclude("sports" %in% Interests) %>>%
list.mapv(Name)
```
### list.clean
`list.clean()`はリストの中身を再帰的または非再帰的にチェックし、リストをクリーンアップします。この関数は組み込みの`is.null()`を使ってすべての`NULL`を削除したり、ユーザー定義の`function(x) length(x) == 0`のような関数を使って`NULL`や`character(0L)`のような空オブジェクトを削除できます。
```{r}
x <- list(a = 1, b = NULL, c = list(x = 1, y = NULL, z = logical(0L), w = c(NA, 1)))
str(x)
```
ここから`NULL`を再帰的に削除したければ次のようにします。
```{r}
str(list.clean(x, recursive = TRUE))
```
`NULL`を含むすべての長さ0のベクトルを削除したければ次のようにします。
```{r}
str(list.clean(x, function(x) length(x) == 0, recursive = TRUE))
```
この関数は欠損値に関連した操作もできます。例えば、少なくとも1つの`NA`を含むベクトルを除外したい場合は次のように行います。
```{r}
str(list.clean(x, function(x) length(x) == 0L || anyNA(x), recursive = TRUE))
```
### subset
`subset()`は`list.filter()`と`list.map()`を組み合わせたようなものです。この関数は基本的にはリストのフィルタリングとマッピングを同時に行います。
```{r}
people %>>%
subset(Age >= 24, Name)
```
```{r}
people %>>%
subset("reading" %in% Interests, sum(as.numeric(Expertise)))
```
## Updating
`list.update()`は式の結果の値でリストを部分的に変更します。
まず、特に変更をしていない状態のリストを確認してみましょう。
```{r}
people %>>%
list.select(Name, Age) %>>%
list.stack()
```
`list.stack()`はリストを同等の構造を維持したデータフレームに変換します。この関数についてはまた後で取り上げます。
例えばそれぞれの年齢が実際より1年少なく登録されていることが判明したとしましょう。それぞれの年齢を1ずつ大きくしなければなりません。そのためには次のように操作を行います。
```{r}
people %>>%
list.update(Age = Age + 1) %>>%
list.select(Name, Age) %>>%
list.stack()
```
`list.update()`は特定のフィールドを削除するためにも使えます。削除したいフィールドをNULLで更新すれば、そのフィールドは削除されます。
```{r}
people %>>%
list.update(Interests = NULL, Expertise = NULL, N = length(Expertise)) %>>%
list.stack()
```
## Sorting
`rlist`は一連の基準でリストをソートする関数を持っています。
### list.order
`list.order()`は与えられたラムダ式を評価し、その結果にしたがってデフォルトでは昇順で各要素を順位づけて順位を返します。ラムダ式は複数指定可能で、もし同順の要素が合った場合には、可能であれば次のラムダ式の評価結果に基づいて順位が決まります。
降順に順位付けしたければ、式を`()`で括るか、式の値が数値であれば式の前に`-`を付けます。
```{r}
list.order(people, Age)
```
Interestsの数で昇順の順位を得る場合を考えてみましょう。
```{r}
list.order(people, length(Interests))
```
次はRの利用年数で降順に順位づけしてみます。
```{r}
list.order(people, (Expertise$R))
```
プログラミング言語の経験年数の最大値で昇順に順位付けしてみましょう。
```{r}
list.order(people, max(unlist(Expertise)))
```
次はInterestsの数で降順ソートします。もし同順となった場合には、Rの経験年数が長い方を上の順位とします。
```{r}
list.order(people, (length(Interests)), (Expertise$R))
```
### list.sort
`list.sort()`はもともとのリストをソートしたリストを返します。使い方は`list.order()`と同じです。
```{r}
people %>>%
list.sort(Age) %>>%
list.select(Name, Age) %>>%
str
```
```{r}
people %>>%
list.sort(length(Interests)) %>>%
list.select(Name, nint = length(Interests)) %>>%
str
```
```{r}
people %>>%
list.sort((Expertise$R)) %>>%
list.select(Name, R = Expertise$R) %>>%
str
```
```{r}
people %>>%
list.sort(max(unlist(Expertise))) %>>%
list.mapv(Name)
```
```{r}
people %>>%
list.sort((length(Interests)), (Expertise$R)) %>>%
list.select(Name, nint = length(Interests), R = Expertise$R) %>>%
str
```
## Grouping
`rlist`は複数の方法によるグルーピングをサポートしています。
### list.group
`list.group()`は式の評価結果に従ってリストをサブグループに分割します。式としては論理値や文字列、数値などの単一値が生成されるものがよく使われます。式はリストの要素に対応して値を返しますが、グループはこの値に対応する形で生成されます。式の値が同じなら同じグループに分類されるため、グループが異なれば対応する値も異なります(つまり値は一意です)。また、リストの要素はただ一つのグループのみに所属します。
数字を奇数と偶数のグループに分割する例を見てみましょう。
```{r}
list.group(1:10, . %% 2 == 0)
```
`1:10`に対して`. %% 2 == 0`を評価した場合、2通りの結果が可能です。すなわち、`FALSE`は奇数を、`TRUE`は偶数を含むグループとなります。
この例は、`list.group()`の結果は常に式のすべての可能な結果を名前として持つサブリストを含むリストになることを示しています。それぞれのサブリストは、元のデータのサブセットであり、要素の値は同じになります。
同じルールを使って、人々を年齢のグループに分けることもできます。
```{r}
str(list.group(people, Age))
```
この結果から分かるように、グループ化のために使用した変数の要素が第一レベルの要素となります。したがって、各サブグループに含まれるグループ化のために使用した変数(上記の例ではAge)は常に第一要素の名前と同じ値となります。
グループ化をした結果もリストであることは変わりませんから、サブグループを対象として同じように`rlist`の関数を適用することができます。例えば、各グループに属する人の名前を取得するために、各グループのNameに対してマッピングをしてみましょう。
```{r}
people %>>%
list.group(Age) %>>%
list.map(. %>>% list.mapv(Name))
```
マッピングは第一レベル、すなわち各グループに対して実行されます。`. %>>% list.map(Name)`はグループに含まれるそれぞれの人をNameにマッピングすることを意味します。
同様の考え方に基づいて、例えばInterestsの数によるグルーピングというのも可能です。Interestsの数別に所属する人の名前を確認してみましょう。
```{r}
people %>>%
list.group(length(Interests)) %>>%
list.map(. %>>% list.mapv(Name))
```
### list.ungroup
`list.group()`は第一レベルがグループ化変数で、その下にオリジナルのデータの分割されたものが属するという形のネストされたリストを生成します。
`list.ungroup()`はこの操作をもとに戻します。つまり、グループ化のレベルをリストから削除するということです。
```{r}
ageGroups <- list.group(people, Age)
str(list.ungroup(ageGroups))
```
### list.cases
非リレーショナルなデータ構造においては、各フィールドは複数の値をベクトルとして持つことができます。`list.cases()`はこのベクトルの中でとりうる値の一覧を取得します。
```{r}
list.cases(people, Interests)
```
似たような例で、プログラミング言語のリストがほしければ次のように`Expertise`に`names()`を適用した結果を対象に関数を実行します。
```{r}
list.cases(people, names(Expertise))
```
### list.class
`list.class()`はリストをcaseに応じてグループ化します。つまり、リストの要素をある式で評価した結果に、特定のcaseが含まれるかどうかが分類基準になります。結果として、この関数は第一レベルがcaseを表し第二レベルが元のリストの部分になる長いリストを返します。
リストの要素は複数のcaseに所属する可能性があります。従ってグループは排他的ではなく、複数のグループに同一のリスト要素が所属する場合も有り得ます。
もし、グループの基準となる値を生成する式自体が排他的な単一値を返すようなものであれば、結果は`list.group()`と同じになります。
```{r}
1:10 %>>%
list.class(. %% 2 == 0)
```
式の値が単一値を返さない場合は、`list.class()`と`list.group()`の挙動は違ったものになります。`Interests`による分類の例を見てみましょう。
```{r}
str(list.class(people, Interests))
```
`Interests`の可能な値でグループ化されたリストが得られました。各グループに含まれる要素の`Interests`は、第一グループの名前の値を含むようになっているはずです。
`list.group()`のときに示したのと同様の考え方で、`Interests`の各クラスに含まれる人名を抽出してみましょう。
```{r}
people %>>%
list.class(Interests) %>>%
list.map(. %>>% list.mapv(Name))
```
経験のあるプログラミング言語で分類したい場合も同じような操作で可能です。
```{r}
people %>>%
list.class(names(Expertise)) %>>%
list.map(. %>>% list.mapv(Name))
```
### list.common
この関数はすべての場合で共通のcaseを返します。
共通の`Interests`を抽出してみましょう。
```{r}
list.common(people, Interests)
```
共通の項目は無かったようです。経験のあるプログラミング言語ではどうでしょうか。
```{r}
list.common(people, names(Expertise))
```
### list.table
`table()`関数は因子レベルの組み合わせのカウントを表した分割表を生成する。`list.table()`はこの関数のラッパーであり、式により分割された要素の個数を表すテーブルを生成する。
```{r}
list.table(1:1000, . %% 3)
```
`people`データセットを使って、InterestsとAgeで二次元の分割表を作成する例を見てみよう。
```{r}
list.table(people, Interests = length(Interests), Age)
```
## Joining
`list.join()`は2つのリストを特定の式で結合し、`list.merge()`は一連の名前付きリストをマージします。
```{r}
people_n <- people %>>% list.names(Name)
```
### list.join
`list.join()`は2つのリストをキーによって結合します。キーは2つのリストの要素に対して共通の式を適用するか、それぞれ別々の式を適用するかいずれかの方法で生成します。
以下に、2つのリストを共通の式`Name`で結合する例を示します。
```{r}
newinfo <- list(
list(Name = "Ken", Email = "ken@xyz.com"),
list(Name = "Penny", Email = "penny@xyz.com"),
list(Name = "James", Email = "james@xyz.com")
)
str(list.join(people_n, newinfo, Name))
```
### list.merge
`list.merge()`は2つのリストを引数にとり、後者のリストが前者のリストを再帰的に更新するように働きます。
マージは部分的な更新として働くので、追加や更新をしたい要素だけを指定したり、NULLを指定することでフィールドを削除したりすることもできます。
```{r}
rev1 <-
list(
Ken = list(Age=25),
James = list(Expertise = list(R=2, Cpp=4)),
Penny = list(Expertise = list(R=2, Python=NULL)))
str(list.merge(people_n,rev1))
```
複数のリストを受け取ることも可能です。この場合、2番目のリストによる更新が終わったら3番目のリストといった形で順番に変更が適用されます。
```{r}
rev2 <-
list(
James = list(Expertise=list(CSharp = 5)),
Penny = list(Age = 24,Expertise=list(R = 3)))
str(list.merge(people_n,rev1, rev2))
```
`list.merge()`は名前付きのリストに対してのみ機能するという点に注意して下さい。さもなければ、関数がマージするリスト間の対応関係を認識できないためです。
## Searching
`rlist`は検索機能も備えています。すなわち、リストを再帰的に検索して値を見つけられるということです。`list.search()`は様々な方法での検索に対応しています。
ここでは新しいデータ`friends`を説明に使用します。
```{r}
friends <- list.load("https://raw.githubusercontent.com/renkun-ken/rlist-tutorial/master/data/friends.json")
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment