複数のファイルをインポートする
Text Update: 11/10, 2018 (JST)

環境やセキュリティポリシーによっては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レコードしかダウンロードできませんので、ダウンロードした複数のファイルを結合する必要があります。
 

結合のための基礎知識

複数のデータファイルを結合するには基本的に以下の手順にしたがいます。

  1. 整然データ化
  2. ファイルリストの取得
  3. ファイルの読み込み
  4. ファイルの結合
  5. 重複行の削除

 

ファイルリストの取得

ファイルやディレクトリを扱うための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系の関数を使います。出力分だけ行方向に結合する関数(rbinddplyr::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>