Redmine では カスタムフィールド と呼ばれるユーザが自由に定義できる項目(フィールド)があります。カスタムフィールドはプロジェクト毎に異なる設定ができるため、redmineR
パッケージで取得できる情報ではcustom_fields
という項目にリスト型変数がネストされた形で格納されます。ネストされたリスト型関数はデータフレーム型変数のようには簡単には扱えませんのでpurrr
パッケージなどを用いて展開する必要があります。
Packages and Datasets
本ページではR version 3.4.4 (2018-03-15)の標準パッケージ以外に以下の追加パッケージを用いています。
Package | Version | Description |
---|---|---|
redmineR | 0.2.0 | R Client for Redmine API |
tidyverse | 1.2.1 | Easily Install and Load the ‘Tidyverse’ |
また、本ページでは以下のデータセットを用いています。
Dataset | Package | Version | Description |
---|---|---|---|
issues | N/A | N/A | Redmine Issues |
カスタムフィールドの展開
カスタムフィールドは前述のようにリスト型変数がネストしている構造(入れ子構造)ですので、データフレーム型変数のように直接、データの値にアクセス出来ません。最初はネスト構造を展開するところから始めます。
カスタムフィールドの確認
カスタムフィールドの中身を再確認しておきます(このようにデータフレーム型変数内にリスト型データを持っている場合はknitr::kable
関数を用いると分かりやすく表示shちえくれます)。
issues %>%
head() %>%
dplyr::select(id, custom_fields) %>%
knitr::kable()
id | custom_fields |
---|---|
29855 | list(list(id = 2, name = “Resolution”, value = “”), list(id = 4, name = “Affected version”, value = “138”)) |
29853 | list(list(id = 2, name = “Resolution”, value = “”), list(id = 4, name = “Affected version”, value = “141”)) |
29852 | list(list(id = 2, name = “Resolution”, value = “Duplicate”), list(id = 4, name = “Affected version”, value = “”)) |
29849 | list(list(id = 2, name = “Resolution”, value = “”), list(id = 4, name = “Affected version”, value = “122”)) |
29848 | list(list(id = 2, name = “Resolution”, value = “”), list(id = 4, name = “Affected version”, value = “141”)) |
29840 | list(list(id = 2, name = “Resolution”, value = “Invalid”), list(id = 4, name = “Affected version”, value = “134”)) |
このようにカスタムフィールドはリスト型変数内にリスト型変数を持っています。Rのコードで表現すると以下のような形になっています。
list(
list(id = n, name = "character", value = c("character")),
list(id = n, name = "character", value = c("character")),
...
)
リスト型変数内の各変数名は
項目名 | 内容 |
---|---|
id | カスタムフィールドの識別子(設定時にシステム側が自動付与) |
name | カスタムフィールド名称(「名称」欄に設定した値) |
value | カスタムフィールドの値1 |
1 プルダウン形式のカスタムフィールドの場合には選択肢IDが格納されているようです
ネスト構造を解消する
まずネスト構造を解消して単一のリスト型変数にすることから始めます。ネスト構造を解消するにはtidyr::unnest
関数を用います。
issues %>%
head() %>%
dplyr::select(id, custom_fields) %>%
tidyr::unnest() %>% knitr::kable()
id | custom_fields |
---|---|
29855 | list(id = 2, name = “Resolution”, value = “”) |
29855 | list(id = 4, name = “Affected version”, value = “138”) |
29853 | list(id = 2, name = “Resolution”, value = “”) |
29853 | list(id = 4, name = “Affected version”, value = “141”) |
29852 | list(id = 2, name = “Resolution”, value = “Duplicate”) |
29852 | list(id = 4, name = “Affected version”, value = “”) |
29849 | list(id = 2, name = “Resolution”, value = “”) |
29849 | list(id = 4, name = “Affected version”, value = “122”) |
29848 | list(id = 2, name = “Resolution”, value = “”) |
29848 | list(id = 4, name = “Affected version”, value = “141”) |
29840 | list(id = 2, name = “Resolution”, value = “Invalid”) |
29840 | list(id = 4, name = “Affected version”, value = “134”) |
tidyr::unnest
関数はこのようにネストされている変数を複数行に展開してくれます。したがって、展開後に識別できるような識別子(この場合はチケット番号であるid
)と共に適用する必要がある点に注意してください。
NAデータの処理
カスタムフィールドはプロジェクト単位で設定できますが、入力が必須でない場合には以下のようにデータとして記録されない(欠損値になる)ことがあるようです。
issues %>%
dplyr::select(id, custom_fields) %>%
dplyr::filter(is.na(custom_fields)) %>%
head() %>% knitr::kable()
id | custom_fields |
---|---|
29838 | NA |
29830 | NA |
29790 | NA |
29781 | NA |
29779 | NA |
29772 | NA |
このようなデータは展開処理に不要なので、tidyr::drop_na
関数を用いて削除しておきます。
issues %>%
head() %>%
dplyr::select(id, custom_fields) %>%
tidyr::unnest() %>%
tidyr::drop_na() %>% knitr::kable()
id | custom_fields |
---|---|
29855 | list(id = 2, name = “Resolution”, value = “”) |
29855 | list(id = 4, name = “Affected version”, value = “138”) |
29853 | list(id = 2, name = “Resolution”, value = “”) |
29853 | list(id = 4, name = “Affected version”, value = “141”) |
29852 | list(id = 2, name = “Resolution”, value = “Duplicate”) |
29852 | list(id = 4, name = “Affected version”, value = “”) |
29849 | list(id = 2, name = “Resolution”, value = “”) |
29849 | list(id = 4, name = “Affected version”, value = “122”) |
29848 | list(id = 2, name = “Resolution”, value = “”) |
29848 | list(id = 4, name = “Affected version”, value = “141”) |
29840 | list(id = 2, name = “Resolution”, value = “Invalid”) |
29840 | list(id = 4, name = “Affected version”, value = “134”) |
カスタムフィールドの展開
これで他のリスト型変数の項目(project
やtracker
など)と同様の形式になりましたので、purrr::map
関数群を用いてカスタムフィールドを展開します。
issues %>%
head() %>%
dplyr::select(id, custom_fields) %>%
tidyr::unnest() %>%
tidyr::drop_na() %>% knitr::kable()
id | custom_fields |
---|---|
29855 | list(id = 2, name = “Resolution”, value = “”) |
29855 | list(id = 4, name = “Affected version”, value = “138”) |
29853 | list(id = 2, name = “Resolution”, value = “”) |
29853 | list(id = 4, name = “Affected version”, value = “141”) |
29852 | list(id = 2, name = “Resolution”, value = “Duplicate”) |
29852 | list(id = 4, name = “Affected version”, value = “”) |
29849 | list(id = 2, name = “Resolution”, value = “”) |
29849 | list(id = 4, name = “Affected version”, value = “122”) |
29848 | list(id = 2, name = “Resolution”, value = “”) |
29848 | list(id = 4, name = “Affected version”, value = “141”) |
29840 | list(id = 2, name = “Resolution”, value = “Invalid”) |
29840 | list(id = 4, name = “Affected version”, value = “134”) |
カスタムフィールドの展開は リスト型変数の展開処理 で説明したように処理します。
map_if_chr <- function(.x, .f) {
purrr::map_if(.x, !is.na(.x), .f) %>%
purrr::map_chr(1L) %>%
return()
}
issues %>%
head() %>%
dplyr::select(id, custom_fields) %>%
tidyr::unnest() %>%
tidyr::drop_na() %>%
dplyr::mutate(name = map_if_chr(custom_fields, 'name'),
value = map_if_chr(custom_fields, 'value')) %>% knitr::kable()
id | custom_fields | name | value |
---|---|---|---|
29855 | list(id = 2, name = “Resolution”, value = “”) | Resolution | |
29855 | list(id = 4, name = “Affected version”, value = “138”) | Affected version | 138 |
29853 | list(id = 2, name = “Resolution”, value = “”) | Resolution | |
29853 | list(id = 4, name = “Affected version”, value = “141”) | Affected version | 141 |
29852 | list(id = 2, name = “Resolution”, value = “Duplicate”) | Resolution | Duplicate |
29852 | list(id = 4, name = “Affected version”, value = “”) | Affected version | |
29849 | list(id = 2, name = “Resolution”, value = “”) | Resolution | |
29849 | list(id = 4, name = “Affected version”, value = “122”) | Affected version | 122 |
29848 | list(id = 2, name = “Resolution”, value = “”) | Resolution | |
29848 | list(id = 4, name = “Affected version”, value = “141”) | Affected version | 141 |
29840 | list(id = 2, name = “Resolution”, value = “Invalid”) | Resolution | Invalid |
29840 | list(id = 4, name = “Affected version”, value = “134”) | Affected version | 134 |
purrr::map
関数群は引数(.x
)に対して.f
で指定した処理(関数や演算子など)を適用する関数です。この処理の場合、.f
にリストの名前(name
やvalue
というリスト変数内の項目名)や数値を指定していますのでインデックス参照1が行われています。すなわち、
custom_fields[[n]][[`name`]] # map_ifでは条件に一致した時のみ指定名で参照し
custom_fields[[n]][[1L]] # map_chrで文字列ベクトルとして取り出す
という参照を行っているのと等価です(n = 1, 2, ...
)。
1 ベクトルやリストの値を参照する時に利用する[
や[[
は、要素アクセス演算子と言える演算子の一種です
最後に不要な項目を削除してカスタムフィールド名が項目名になるように処理すれば、カスタムフィールド一覧の完了です。
issues %>%
head() %>%
dplyr::select(id, custom_fields) %>%
tidyr::unnest() %>%
tidyr::drop_na() %>%
dplyr::mutate(name = map_if_chr(custom_fields, 'name'),
value = map_if_chr(custom_fields, 'value')) %>%
dplyr::select(-custom_fields) %>%
tidyr::spread(key = name, value = value) %>% knitr::kable()
id | Affected version | Resolution |
---|---|---|
29840 | 134 | Invalid |
29848 | 141 | |
29849 | 122 | |
29852 | Duplicate | |
29853 | 141 | |
29855 | 138 |
あとはチケット一覧とマージすれば展開完了です。
issues %>%
head(100) %>%
dplyr::select(id, custom_fields) %>%
tidyr::unnest() %>%
tidyr::drop_na() %>%
dplyr::mutate(name = map_if_chr(custom_fields, 'name'),
value = map_if_chr(custom_fields, 'value')) %>%
dplyr::select(-custom_fields) %>%
tidyr::spread(key = name, value = value) %>%
dplyr::left_join(issues, ., by = "id") %>%
dplyr::select(id, subject, 'Affected version', 'Resolution') %>%
dplyr::arrange(dplyr::desc(id)) %>%
head(10) %>% knitr::kable()
id | subject | Affected version | Resolution |
---|---|---|---|
29855 | add_working_days returns wrong date | 138 | |
29853 | Default plugin value is not | 141 | |
29852 | Cannot use Wiki link with a different name in the table of markdown | Duplicate | |
29849 | Display order of Redmine parent/child tickets are not correct | 122 | |
29848 | Query to determine role ids is incorrect | 141 | |
29840 | Cant update status_id from existing tickets | 134 | Invalid |
29838 | time logging via commit message does not work when the configured activity has been overridden on the project level | NA | NA |
29830 | Issue CSV export options: checkboxes have no effect for columns activated in query | NA | NA |
29826 | Can’t use back_url in Documents | 141 | |
29824 | Add user initials as a Gravatar icon fallback | NA |