はじめに
Kaggleとかに参加すると、大量のデータファイルを与えられる事が多い。例えばGoogle Cloud & NCAA® ML Competition 2018-Men'sを例に取ると、DataFilesフォルダ以下は下記のようになる。
下記のように一つ一つデータを読み込もうとすると面倒くさいので、どうにかしたいというのがこの記事のモチベーションです。なので、「もっといい方法あるよ!」というツッコミをいただけると幸いです。
library(readr)
Cities <- read_csv("./inputs/DataFiles/Cities.csv")
Conference <- read_csv("./inputs/DataFiles/Conference.csv")
︙
list.files関数とread_csv関数を使って一括で読み込む
list.files関数を使って、ファイルのリストを作り、lapply関数を使うことで、一気にデータを読み込むことができる。
dataframe_paths <- list.files(path = "./inputs/DataFiles", full.names = T)
dataframe_list <- lapply(dataframe_paths, read_csv)
こうするとそれぞれのファイルから読み込まれたデータがリストdataframe_listの要素として格納される。
このままだとname属性がついていないので、下記のようにすることで名前をつけることもできる。
dataframe_names <- list.files(path = "./inputs/DataFiles", full.names = F) %>%
gsub(".csv", "", .) # .csvという文字列を一括で削除する
names(dataframe_list) <- dataframe_names
おそらくこれが一番簡単な方法だと思う。
各データファイルを独立したオブジェクトとして読込みたい
上記の例だと、一つのリストにまとめてデータが格納される。しかし、同じ性格をもったデータファイル(例えばFY2017.csv, FY2018.csv等)であれば一つのリストにまとめて格納したほうが都合がいいが、全然違う性格のデータファイルであれば、それぞれ独立したオブジェクトとして格納したくなる。
assign関数を使う
assign関数を使うことで、データファイルに任意の名前をつけて読み込むことができる。assign関数はassign(objectname, value)という文法で、これは"objectname <- value"に等しい。valueにはベクトルだろうと、データフレームだろうとなんでも入れることができる。というわけで下記のような関数を作ってみた。
assign_data <- function(path, type = "csv"){
names <- list.files(path = path,
full.names = F,
pattern = paste0("\\.", type, "$")) %>%
gsub(paste0(".",type), "",.)
paths <- list.files(path = path,
full.names = T,
pattern = paste0(".", type, "$"))
for(i in 1:length(names)){
assign(names[i], read_csv(paths[i]), envir = .GlobalEnv)
}
}
assign_data("./inputs/DataFiles")
これは引数のpathに任意のフォルダパスを与えると、type(デフォルトはcsv)に引っかかるデータファイルを見つけて、読み込む関数。
namesでオブジェクトにつける名前のリストを作り、pathsで読み込む対象となるファイルのパスのリストを作る。あとはfor文でnamesの(= pathsの)数だけread_csvで読み込んでいく。assign関数のenvir = .GlobalEnvオプションをつける必要があることに注意(関数の中で呼び出しているため)。
一応、他のフォーマット(tsvとか)に対応できるように、第二引数にtypeを取れるようにしたが、ほとんどcsvで提供されることが多いので、csv決め打ちでも良かったかもしれない。
自作関数の応用
例えば上記の./inputs配下にDataFiles以外にもフォルダがある場合に下記のようにすれば一括で読み込むこともできる。
dir_list <- list.dirs(path = "./inputs", recursive = T)
lapply(dir_list, assign_data)
lapplyはリストを返すが、assign_dataは何も返さないので、下記の様にNull値が返ってくる形になるが問題はない。
[[1]]
NULL
︙
[[10]]
NULL
さいごに
他にもeval(parse(text = s))を使った方法とかも試してみたけれども、これが一番簡単だったかなと思う。ただ、自作関数のところでfor文を使わざるをえなかったり、なんかもっと簡単な方法がある気がして仕方がなかったので、ツッコミをいただけると幸いです。
追記
for文を使わない自作関数として下記のようなものを作ってみた
assign_data <- function(path, type = "csv"){
names <- list.files(path = path,
full.names = F,
pattern = paste0("\\.", type, "$")) %>%
gsub(paste0(".",type), "",.)
paths <- list.files(path = path,
full.names = T,
pattern = paste0("\\.", type, "$"))
lapply(names, function(x){
assign(x, read_csv(paths[grep(paste0("/",x), paths)]), envir = .GlobalEnv)
}
ところがsystem.timeで処理時間を計測してみたら、for文使ったほうが早かった orz。多分grepとかやってる分だけ余計に時間がかかっているんだろうなぁ… map()とか使ったらうまくいくんだろうか。
パターン | ユーザ | システム | 経過 |
---|---|---|---|
for文パターン | 14.600 | 1.659 | 16.546 |
lapplyパターン | 15.236 | 1.788 | 17.521 |
追記2
というわけでmap関数を使ったバージョンを作ってみた、namesとpathsの2つのベクトルを使うので、map2()を使ってみた、が、やっぱり遅い。なぜだorz。あと関係ないけどreadr::read_csvの代わりにdata.table::fread使ったら半分以下の時間で終わった。freadすげぇ。
library(purrr)
assign_data <- function(path, type = "csv"){
names <- list.files(path = path,
full.names = F,
pattern = paste0("\\.", type, "$")) %>%
gsub(paste0(".",type), "",.)
paths <- list.files(path = path,
full.names = T,
pattern = paste0("\\.", type, "$"))
map2(names, paths, ~assign(.x, read_csv(.y), envir = .GlobalEnv))
}
パターン | ユーザ | システム | 経過 |
---|---|---|---|
for文パターン | 14.600 | 1.659 | 16.546 |
lapplyパターン | 15.236 | 1.788 | 17.521 |
mapパターン | 15.143 | 1.824 | 17.450 |
参考:freadパターン | 6.953 | 0.797 | 8.502 |