カスタムフィールドを展開する
Text Update: 11/10, 2018 (JST)

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”)

 

カスタムフィールドの展開

これで他のリスト型変数の項目(projecttrackerなど)と同様の形式になりましたので、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にリストの名前(namevalueというリスト変数内の項目名)や数値を指定していますのでインデックス参照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