環境やセキュリティポリシーによってはBTS(Bug Tracking System)などのデータベースに直接アクセスできずCSV形式ファイルなどでデータをインポートしなければならない場合があります。このような場合、一つのファイルで全てのデータを入手できずに複数のファイルになる場合があります。そこで本ページではRedmineのチケットデータを利用し複数のファイルを一括処理してデータフレームに読み込む方法を紹介します。
ただし、複数のファイルを一括処理する場合は全てのファイルで項目(列)が同一である必要があります。個々のファイルで項目(列)が異なる場合は、一括読み込みは可能ですが、ファイルの結合処理等は本ページの方法ではできませんので注意してください。
Packages and Datasets
本ページではR version 3.4.4 (2018-03-15)の標準パッケージ以外に以下の追加パッケージを用いています。
Package | Version | Description |
---|---|---|
tidyverse | 1.2.1 | Easily Install and Load the ‘Tidyverse’ |
また、本ページでは以下のデータセットを用いています。
Dataset | Package | Version | Description |
---|---|---|---|
redmine | N/A | N/A | Redmine Issues |
Redmine
RedmineはGPL v2ライセンスの下で提供されているオープンソースのプロジェクト管理ソフトウェアです。BTS機能を備えておりRedmine自身のチケットを Redmineを用いて公開 しています。ただし、一度に50レコードまでしかダウンロードできないため長期間に渡るレコードを入手しようとすると必然的に複数のCSVファイルをダウンロードすることになります。
チケット項目
複数のファイルのインポートを説明する前にRedmineで標準的に用意されている項目を簡単に説明しておきます。
これはファイルの読み込みに利用するreadr::read_csv
関数が読み込んだ最初の100行のデータを元に列(カラム、変数)のデータ型を自動的に判別するために、あまり使わていない(データが入力されない)カラムの場合、ファイル毎にデータ型の判定結果が異なってしまう可能性があるため事前にカラム(変数)タイプを指定しておく必要があるためです。
項目 | 概要 | データ型 |
---|---|---|
# | 識別番号(Primary Key) | 整数型 |
プロジェクト | 属するプロジェクト | 文字型(因子型) |
トラッカー | 大分類 | 文字型(因子型) |
親チケット | 親子関係を定義したい場合に用いる | 文字型 |
ステータス | 対応状況 | 文字型(因子型) |
優先度 | 対応優先度 | 文字型(因子型) |
題名 | タイトル | 文字型 |
作成者 | 作成者 | 文字型(因子型) |
担当者 | 対応担当者 | 文字型(因子型) |
更新日 | 更新日時 | 日時型(POSIXct) |
カテゴリ | 分類(任意に利用設定できる) | 文字型(因子型) |
対象バージョン | チケット対処したバージョン | 文字型 |
開始日 | 対応を開始した日 | 日付型 |
期日 | 対応予定期間 | 日付型 |
予定工数 | 対応予定工数 | 数値型 |
進捗率 | 対応の進捗率 | 数値型(%表記) |
作成日 | 作成日時 | 日時型(POSIXct) |
終了日 | 対応完了日時 | 日時型(POSIXct) |
関連するチケット | 関係するチケット番号 | 文字型 |
Resolution | 解決結果(非標準) | 文字型(因子型) |
Affected version | 影響のあるバージョン | 文字型 |
説明 | 詳細 | 文字型 |
このようにカラム(変数)タイプを事前に把握しておくことで読み込み後の結合時に絡む形式不一致のエラー発生を抑制することが可能になります。
処理の概要
前述の通り、 Redmine からは一度に50レコードしかダウンロードできませんので、ダウンロードした複数のファイルを結合する必要があります。
結合のための基礎知識
複数のデータファイルを結合するには基本的に以下の手順にしたがいます。
- 整然データ化
- ファイルリストの取得
- ファイルの読み込み
- ファイルの結合
- 重複行の削除
ファイルリストの取得
ファイルやディレクトリを扱うためのlist.*
関数群、file.*
関数群が用意されています。
list.files(path = 'ファイルリストを取得したいパス',
pattern = '正規表現によるファイル名の指定' # 省略可
full.names = TRUE)
Windows環境ではchoose.dir
関数を用いて’ファイルリストを取得したいパス’をインタラクティブに指定することも可能です。
file_path %>%
list.files(path = ., pattern = "(issues_)", full.names = TRUE) %>%
tibble::as_data_frame()
## # A tibble: 80 x 1
## value
## <chr>
## 1 ../../static/data/redmine//issues_001_CP932.csv
## 2 ../../static/data/redmine//issues_002_CP932.csv
## 3 ../../static/data/redmine//issues_003_CP932.csv
## 4 ../../static/data/redmine//issues_004_CP932.csv
## 5 ../../static/data/redmine//issues_005_CP932.csv
## 6 ../../static/data/redmine//issues_006_CP932.csv
## 7 ../../static/data/redmine//issues_007_CP932.csv
## 8 ../../static/data/redmine//issues_008_CP932.csv
## 9 ../../static/data/redmine//issues_009_CP932.csv
## 10 ../../static/data/redmine//issues_010_CP932.csv
## # ... with 70 more rows
この場合は80個のファイルがあることが分かります。
ファイルの読み込み
Redmineチケットのように自由記述欄があるようなデータはCSV形式で書きだしていてもExcelで読みこんだ際に正しく処理されない場合があります。このような場合は標準のread.csv
関数でなくreadr
パッケージを用いる方が正しく読みこめる可能性が高くなります。
readr::read_csv
関数は読み込んだ最初の100行のデータを元に列(カラム、変数)のデータ型を自動的に判別します。ただし、稀にしか使われていない列がある場合、読み込んだファイルごとに列のデータ型が異なってしまう場合があります。このような場合は前述のようにカラムタイプを明示的に指定しておきます。
ファイルの結合処理を考慮してlapply
関数でファイルのリストから一括で読み込み処理します。
lapply(X = 'ベクトル型のファイルリスト',
FUN = readr::read_csv,
locale = readr::locale(encoding = "cp932"),
col_types = 'カラムタイプ')
file_path %>%
list.files(path = ., pattern = "(issues_)", full.names = TRUE) %>%
lapply(X = ., FUN = readr::read_csv,
locale = readr::locale(encoding = "cp932"), col_types = col_type)
実行結果が冗長なので省略します。
ファイルの結合
行方向の結合にはbind
系の関数を使います。出力分だけ行方向に結合する関数(rbind
やdplyr::bind_rows
)関数を記述するのは手間がかかりますのでdo.call
関数を用いて一括で処理します。
do.call(what = dplyr::bind_rows, args = 'リスト形式の引数')
file_path %>%
list.files(path = ., pattern = "(issues_)", full.names = TRUE) %>%
lapply(X = ., FUN = readr::read_csv,
locale = readr::locale(encoding = "cp932"), col_types = col_type) %>%
do.call(what = dplyr::bind_rows, args = .)
## # A tibble: 4,000 x 22
## `#` プロジェクト トラッカー 親チケット ステータス 優先度 題名 作成者
## <int> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
## 1 28967 Redmine Defect <NA> New Normal coul… jiang…
## 2 28953 Redmine Defect <NA> New Normal Issu… Andr?…
## 3 28951 Redmine Defect <NA> New Normal Cann… L?szl…
## 4 28946 Redmine Defect <NA> New Normal If a… Mariu…
## 5 28943 Redmine Patch <NA> New Low Remo… Sho H…
## 6 28940 Redmine Patch <NA> New Normal redu… Pavel…
## 7 28939 Redmine Patch <NA> Closed Normal repl… Pavel…
## 8 28934 Redmine Patch <NA> New Normal [Rai… Pavel…
## 9 28933 Redmine Patch <NA> New Normal Migr… Pavel…
## 10 28932 Redmine Patch <NA> Closed Normal [Rai… Pavel…
## # ... with 3,990 more rows, and 14 more variables: 担当者 <chr>,
## # 更新日 <dttm>, カテゴリ <chr>, 対象バージョン <chr>, 開始日 <date>,
## # 期日 <date>, 予定工数 <dbl>, 進捗率 <dbl>, 作成日 <dttm>,
## # 終了日 <dttm>, 関連するチケット <chr>, Resolution <chr>, `Affected
## # version` <chr>, 説明 <chr>
purrrパッケージによる結合
lapply
関数とdo.call
関数の処理はpurrr::map_df
関数を用いることでよりシンプルに記述することができます。purrr
パッケージの詳細についてはpurrr
タグで関連記事を検索してください。
file_path %>%
list.files(path = ., pattern = "(issues_)", full.names = TRUE) %>%
purrr::map_df(.x = ., .f = readr::read_csv,
locale = readr::locale(encoding = "cp932"), col_types = col_type)
## # A tibble: 4,000 x 22
## `#` プロジェクト トラッカー 親チケット ステータス 優先度 題名 作成者
## <int> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
## 1 28967 Redmine Defect <NA> New Normal coul… jiang…
## 2 28953 Redmine Defect <NA> New Normal Issu… Andr?…
## 3 28951 Redmine Defect <NA> New Normal Cann… L?szl…
## 4 28946 Redmine Defect <NA> New Normal If a… Mariu…
## 5 28943 Redmine Patch <NA> New Low Remo… Sho H…
## 6 28940 Redmine Patch <NA> New Normal redu… Pavel…
## 7 28939 Redmine Patch <NA> Closed Normal repl… Pavel…
## 8 28934 Redmine Patch <NA> New Normal [Rai… Pavel…
## 9 28933 Redmine Patch <NA> New Normal Migr… Pavel…
## 10 28932 Redmine Patch <NA> Closed Normal [Rai… Pavel…
## # ... with 3,990 more rows, and 14 more variables: 担当者 <chr>,
## # 更新日 <dttm>, カテゴリ <chr>, 対象バージョン <chr>, 開始日 <date>,
## # 期日 <date>, 予定工数 <dbl>, 進捗率 <dbl>, 作成日 <dttm>,
## # 終了日 <dttm>, 関連するチケット <chr>, Resolution <chr>, `Affected
## # version` <chr>, 説明 <chr>
重複行の削除
複数のCSVファイルを結合処理しましたのでレコード(行)が重複している可能性があります。このような場合はdplyr::distinct
関数を用いて重複行を削除しておきます。
dplyr::distinct(.data = 'データ')
file_path %>%
list.files(path = ., pattern = "(issues_)", full.names = TRUE) %>%
lapply(X = ., FUN = readr::read_csv,
locale = readr::locale(encoding = "cp932"), col_types = col_type) %>%
do.call(what = dplyr::bind_rows, args = .) %>%
dplyr::distinct()
## # A tibble: 3,826 x 22
## `#` プロジェクト トラッカー 親チケット ステータス 優先度 題名 作成者
## <int> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
## 1 28967 Redmine Defect <NA> New Normal coul… jiang…
## 2 28953 Redmine Defect <NA> New Normal Issu… Andr?…
## 3 28951 Redmine Defect <NA> New Normal Cann… L?szl…
## 4 28946 Redmine Defect <NA> New Normal If a… Mariu…
## 5 28943 Redmine Patch <NA> New Low Remo… Sho H…
## 6 28940 Redmine Patch <NA> New Normal redu… Pavel…
## 7 28939 Redmine Patch <NA> Closed Normal repl… Pavel…
## 8 28934 Redmine Patch <NA> New Normal [Rai… Pavel…
## 9 28933 Redmine Patch <NA> New Normal Migr… Pavel…
## 10 28932 Redmine Patch <NA> Closed Normal [Rai… Pavel…
## # ... with 3,816 more rows, and 14 more variables: 担当者 <chr>,
## # 更新日 <dttm>, カテゴリ <chr>, 対象バージョン <chr>, 開始日 <date>,
## # 期日 <date>, 予定工数 <dbl>, 進捗率 <dbl>, 作成日 <dttm>,
## # 終了日 <dttm>, 関連するチケット <chr>, Resolution <chr>, `Affected
## # version` <chr>, 説明 <chr>
カラム(変数)タイプの設定
カラム(変数)タイプは前述の表にしたがってリスト型で指定します。本来であれば因子型で指定すべき項目は文字型として指定します。因子型は指定時に水準も含めて指定する必要があるため水準を把握していない(できない)場合には読み込めなくなります。
col_type <- list(readr::col_integer(), readr::col_character(),
readr::col_character(), readr::col_character(),
readr::col_character(), readr::col_character(),
readr::col_character(), readr::col_character(),
readr::col_character(), readr::col_datetime(format = ""),
readr::col_character(), readr::col_character(),
readr::col_date(), readr::col_date(), readr::col_double(),
readr::col_double(), readr::col_datetime(format = ""),
readr::col_datetime(format = ""), readr::col_character(),
readr::col_character(), readr::col_character(),
readr::col_character())
データの確認
変数名が日本語のままだと何かと扱い難いので変更しておきます。また、チケットオープン、クローズは時間まで必要ないので年月日データに変換しておきます。
(x <- tmp %>%
dplyr::select(id = `#`, tracker = `トラッカー`, status = `ステータス`,
priority = `優先度`, category = `カテゴリ`,
version = `対象バージョン`, affected = `Affected version`,
open = `作成日`, close = `終了日`) %>%
dplyr::mutate(open = lubridate::date(open), close = lubridate::date(close)))
## # A tibble: 3,826 x 9
## id tracker status priority category version affected open
## <int> <chr> <chr> <chr> <chr> <chr> <chr> <date>
## 1 28967 Defect New Normal REST API <NA> <NA> 2018-06-06
## 2 28953 Defect New Normal Issues <NA> 3.4.5 2018-06-05
## 3 28951 Defect New Normal Issues <NA> 3.4.5 2018-06-05
## 4 28946 Defect New Normal Issues Candid… 3.4.5 2018-06-04
## 5 28943 Patch New Low Documen… 4.1.0 <NA> 2018-06-04
## 6 28940 Patch New Normal Perform… Candid… <NA> 2018-06-04
## 7 28939 Patch Closed Normal Perform… <NA> <NA> 2018-06-04
## 8 28934 Patch New Normal Perform… <NA> <NA> 2018-06-02
## 9 28933 Patch New Normal Gems su… Candid… <NA> 2018-06-01
## 10 28932 Patch Closed Normal Code cl… <NA> <NA> 2018-06-01
## # ... with 3,816 more rows, and 1 more variable: close <date>