packages | 説明 |
---|---|
tidyverse | tidyr,dplyr,ggplot2,lubridateとか |
flexdashboard | ダッシュボード作成用パッケージ |
devtools | githubからパッケージをインストールするためのツール |
psych | 表をうまく表示するときに使う |
knitr | Rmarkdownコンパイルに使う |
rmarkdown | Rmarkdownコンパイルに使う |
install.packages(c("tidyverse", "flexdashboard","devtools","psych","knitr","rmarkdown"))
require(psych)
require(lubridate)
require(tidyverse) # tidyverseは最後に呼び出す方がコンフリクトの影響小
redmineR
パッケージはGitHubからインストールするためにdevtools
やremotes
というパッケージを事前にインストールしておく必要があります。第1回勉強会にて環境構築した方はdevtools
パッケージがインストールされているハズですので、これを使います。
devtools::install_github("openanalytics/redmineR")
redmineR
を読み込めればOK
library(redmineR)
あとはtidyverse
を読み込む。tidyverse
のうち使うのはこの3つ。
パッケージ | 説明 |
---|---|
dplyr |
データフレーム操作 |
lubridate |
日付操作 |
ggplot2 |
可視化 |
lubridate
パッケージはtidyverse
ファミリーですがtidyverse
パッケージを読み込んだだけでは読み込まれないないので、別途、明示的に読み込んでください。
require(psych)
require(lubridate)
library(tidyverse) # tidyverseは最後に呼び出す方がコンフリクトの影響小
なお、追加パッケージを読み込んだ場合は、tidyverse::tidyverse_conflicts
関数でコンフリクト状態を把握しておくと意図しない動きの場合の原因追及に使えるので便利です。
tidyverse::tidyverse_conflicts()
## -- Conflicts ------------------------------------- tidyverse_conflicts() --
## x ggplot2::%+%() masks psych::%+%()
## x ggplot2::alpha() masks psych::alpha()
## x lubridate::as.difftime() masks base::as.difftime()
## x lubridate::date() masks base::date()
## x dplyr::filter() masks stats::filter()
## x lubridate::intersect() masks base::intersect()
## x dplyr::lag() masks stats::lag()
## x lubridate::setdiff() masks base::setdiff()
## x lubridate::union() masks base::union()
URLとAPIアクセストークンを環境変数に指定します。
APIアクセストークンはRedmineにログインして、個人設定ページから入手してください。
Sys.setenv("REDMINE_URL" = "http://xxx.xxx.xxx/xxx")
Sys.setenv("REDMINE_TOKEN" = "APIACCESSTOKEN")
プロジェクトの一覧取得とかが軽めのAPIでおすすめ。
redmineR::redmine_projects() %>% as.data.frame()
redmine_issues
でチケット一覧を取得します。
デフォルトの引数だとクローズしたチケットを取得して来れません。
全てのステータスのチケットを取得するためにはstatus_id = "*"
を指定します。
issues <- redmineR::redmine_issues(status_id = "*")
皆様はサンプルデータをダウンロードして、読込んでお使いください。
load("../data/redmine_issues_all.RData")
issues %>% head(20) %>% as.data.frame()
<list [2]>
やら、NA
やら、結構汚いデータなので、きれいにするところから始めます。
issues %>% head(100) %>%
purrr::map_df(class)
projectやtrackerなど、リストから選ぶ系のフィールドがlist
型になってしまっています。 list(id="ID", name="NAME")
からname="NAME"
を取り出したい。
issues %>% head() %>% dplyr::as_data_frame() %>%
dplyr::select(id, project, status) %>% knitr::kable()
id | project | status |
---|---|---|
29767 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
29764 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
29763 | list(id = 1, name = “Redmine”) | list(id = 5, name = “Closed”) |
29758 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
29757 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
29756 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
日付、日時も文字列型になってしまっています。日付はdate
に、日時はPOSIXct
型に変換したい。
issues %>% head(20) %>%
dplyr::select(id, created_on, closed_on, updated_on, start_date, due_date) %>%
as_data_frame()
NA
有無の確認厄介なのは、リスト型のカラムの中にあるNA
です。(category
とかassigned_to
とか)
# NAの有無確認
issues %>% head(100) %>%
purrr::map_df(anyNA)
リスト型変数は任意の型で任意の数のデータを任意な数だけ持てる柔軟性のあるデータ構造です。リスト型の中にリスト型をネストさせることも可能です。
list(list(id = c(1, 2)), list(name = c("Defects")))
## [[1]]
## [[1]]$id
## [1] 1 2
##
##
## [[2]]
## [[2]]$name
## [1] "Defects"
project
の場合、こんな感じ。ここからname="Redmine"
を取り出します。
issues[[1, "project"]]
## $id
## [1] 1
##
## $name
## [1] "Redmine"
後で、purrr
を使った処理も説明しますが、割と難しいので、 今は構造を理解するために、とりあえずBase Rで説明します。
NA
を考慮しなければ、これでOKです。
issues[[1, "project"]]$name
## [1] "Redmine"
たとえば、category
とかだとデータにNA
が入っていることがあります。 その場合、for文の途中で以下のようなエラーになってしまいます。
tmp_list <- NA
tmp_list$name
## Error in tmp_list$name: $ operator is invalid for atomic vectors
NA
じゃないときだけリストの要素(name
)にアクセスするように、 ifelse
を使って分岐させます。
ifelse(条件, TRUEだったときの値, FALSEだったときの値)
条件には長さが1より大きいかどうかを入れます。
リスト型のパターン | 長さ |
---|---|
NA じゃないとき |
length(list(id=12, name="Translations")) = 2 |
NA のとき |
lenght(NA) = 1 |
具体的には、こんな感じです。
category <- list(id=12, name="Translations")
ifelse(length(category) < 2, NA, issues[1, "category"][[1]]$name)
## [1] "Translations"
category <- NA
ifelse(length(category) < 2, NA, issues[1, "category"][[1]]$name)
## [1] NA
ちなみに、is.na
を使うと、データフレームがreturnされてしまいます。
is.na(issues[[1, "category"]])
## id name
## FALSE FALSE
このように、created_on
が文字列型になってしまっています。
issues[1,"created_on"]
## [1] "2018-10-15T01:43:12Z"
class(issues[1,"created_on"])
## [1] "character"
Redmineのフィールドで日付型は以下の5つ
フィールド | フィールドの情報 | 例 |
---|---|---|
created_on | 日時 | 2018-11-17T10:00:00 |
closed_on | 日時 | 2018-11-17T12:00:00 |
updated_on | 日時 | 2018-11-17T15:00:00 |
start_date | 日付 | 2018-11-17 |
due_date | 日付 | 2018-11-17 |
文字列 -> 日時型、日付型への変換にはlubridate
を使うと便利です。
型 | 変換関数 | 変換後の型 |
---|---|---|
日付 | lubridate::ymd |
Date |
時間 | lubridate::hms |
Period |
日時 | lubridate::ymd_hms |
POSIXct |
日時
date <- lubridate::ymd("2018/11/17")
class(date)
## [1] "Date"
date
## [1] "2018-11-17"
時間
time <- lubridate::hms("01:43:12")
class(time)
## [1] "Period"
## attr(,"package")
## [1] "lubridate"
time
## [1] "1H 43M 12S"
日付
datetime <- lubridate::ymd_hms("2018-10-15T01:43:12Z")
class(datetime)
## [1] "POSIXct" "POSIXt"
datetime
## [1] "2018-10-15 01:43:12 UTC"
tmp_custom_fields <- issues %>%
dplyr::filter(id == 29753) %>% .[[1, "custom_fields"]]
tmp_custom_fields
## [[1]]
## [[1]]$id
## [1] 2
##
## [[1]]$name
## [1] "Resolution"
##
## [[1]]$value
## [1] "Invalid"
##
##
## [[2]]
## [[2]]$id
## [1] 4
##
## [[2]]$name
## [1] "Affected version"
##
## [[2]]$value
## [1] "133"
とっても複雑な構造。。。
要するにこういうこと
list(
list(
list(id=2, name="Resolution", value="Invalid"),
list(id=4, name="Affected version", value=133)
)
)
リスト内の各フィールドの説明
フィールド | フィールドの説明 |
---|---|
id | カスタムフィールドID |
name | カスタムフィールド名 |
value | カスタムフィールドの値 |
例えば、Resolutionならこのように取得します。
tmp_custom_fields[[1]]$value
## [1] "Invalid"
Affected versionなら、このように取得します。
tmp_custom_fields[[2]]$value
## [1] "133"
Affected versionのvalueは実際のバージョン名ではなく、各選択肢へのIDになっている。
プルダウンやチェックボックスで選ぶ形式のカスタムフィールドの場合、 valueは各選択肢へのIDになっています。 実際の値を得るためには、バージョンやカスタムフィールドの情報を 別途APIを叩いて入手するしかありません。
以下が、カスタムフィールドの値取得に必要なAPIですが、 残念ながらredmineR
では取れません。 そのため、value
に相当する値を取得するためには、 REST Client(RCurl
or httr
がメジャーらしい)を使って、 直接REST APIを叩くことになります。
jsonの中身を分析するだけなので、頑張ればできるとは思います。
今回は割愛します。
フィールドの種類 | API | 必要な権限 |
---|---|---|
version型 | /projects/ |
普通でOK |
list型 | /custom_fields | 管理者権限 |
dplyr使ってできるとことはdplyrで処理します。
df <- issues %>%
dplyr::mutate(
created_on = lubridate::ymd_hms(created_on),
closed_on = lubridate::ymd_hms(closed_on),
updated_on = lubridate::ymd_hms(updated_on),
due_date = lubridate::ymd(due_date),
start_date = lubridate::ymd(start_date)
) %>%
dplyr::select(id, created_on, closed_on, updated_on, due_date, start_date)
df
リスト型はfor文で処理します。(あとでpurrr
を使った処理も説明します。)
is_na <- function(.x){
return(length(.x) < 2)
}
for (i in 1:nrow(df)){
# リストの処理
df[i, "project"] <- issues[[i, "project"]]$name
df[i, "tracker"] <- issues[[i, "tracker"]]$name
df[i, "status"] <- issues[[i, "status"]]$name
df[i, "priority"] <- issues[[i, "priority"]]$name
df[i, "author"] <- issues[[i, "author"]]$name
# NAがあるリストの処理
df[i, "category"] <- ifelse(is_na(issues[[i, "category"]]), NA, issues[[i, "category"]]$name)
df[i, "fixed_version"] <- ifelse(is_na(issues[i, "fixed_version"]), NA, issues[i, "fixed_version"][[1]]$name)
df[i, "assigned_to"] <- ifelse(is_na(issues[[i, "assigned_to"]]), NA, issues[[i, "assigned_to"]]$name)
# カスタムフィールドの処理
if(!is_na(issues[[i,"custom_fields"]])){
for(custom_field in issues[[i,"custom_fields"]]){
if(!is.null(custom_field$value)){
df[i, custom_field$name] <- custom_field$value
}
}
}
}
df
for(i in 1:100){
if(is.na(issues[i, "category"])){
if(is.na(issues[[i, "category"]])){
print(i)
}
}
}
## [1] 4
## [1] 9
## [1] 39
## [1] 40
## [1] 50
## [1] 68
## [1] 79
## [1] 87
## [1] 88
#windowsFonts("MEI"=windowsFont("Meiryo"))
df %>% dplyr::select(id, status) %>%
ggplot(aes(x = status)) + geom_bar()
df %>% ggplot(aes(x="", fill=tracker)) +
geom_bar(width = 1) + coord_polar("y") + theme_bw(base_family = "HiraKakuProN-W3")
open <- df %>%
# created_on(日時)を日付になおす。
mutate(
date = lubridate::as_date(created_on)
) %>%
# dateでグループ化して、dateごとにチケット数を数える。
dplyr::group_by(date) %>%
dplyr::summarise(open = n())
open %>% psych::headTail()
closed_on
が入っているチケットは一度クローズされています。 しかし、チケットを再オープンするとclosed_on
が入っているのに、クローズされてない状態になります。 そのため、dplyr::filter
を使ってクローズチケットのみに絞る必要があります。
close <- df %>%
# クローズチケットだけをカウントする。
dplyr::filter(status=="Closed") %>%
dplyr::mutate(date = lubridate::as_date(closed_on)) %>%
dplyr::group_by(date) %>%
dplyr::summarise(close = n())
close %>% psych::headTail()
最初に起票された日から、 バグが最後に起票された日 or クローズされた日まで、 一日おきの日付を持ったデータフレームを作成します。
min_date <- min(open$date, close$date)
max_date <- max(open$date, close$date)
data.frame(date=seq(min_date, max_date, by = "day")) %>% psych::headTail()
先作った、date
だけを持ったデータフレームにopen
とclose
を結合します。 2006-06-29〜2018-10-15の日付情報をすべて残しながら、結合するのでleft_join
を使います。 (このデータならfull_join
でも同じです。)
openclose <- data.frame(date=seq(min_date, max_date, by = "day")) %>%
dplyr::left_join(open, by = "date") %>%
dplyr::left_join(close, by = "date")
openclose
NA
が入っているので、0
に置き換えます。
openclose <- openclose %>%
replace_na(list(open=0, close=0))
openclose
累積和を求めるためにはcumsum
を使います。
openclose$cumsum_open <- cumsum(openclose$open)
openclose$cumsum_close <- cumsum(openclose$close)
openclose %>% head(100)
openclose %>%
ggplot(aes(x = date)) +
geom_line(aes(y = cumsum_close, color = "cumsum_close")) +
geom_line(aes(y = cumsum_open, color = "cumsum_open"))