以前、kaggle の Outbrain Click Prediction に挑戦しました。
このコンペはデータセットが巨大で、特に page_views.csv は 88 GB ありました。
とても一括では読み込めないので、分割して読み込む必要がありました。
そのときに R で分割して読み込む方法を調べたので、忘れないうちにメモ。
読み込み方
コネクションの作成
CSV ファイルへのコネクションを作成します。
今後は、コネクションを通して CSV ファイルにアクセスします。
コネクションを通すことで、指定した位置から指定した分だけ読み込めるようになります。
con <- file(description = "page_views.csv", open = "rt")
列名の読み込み
列名を先に読み込んでおきます。
読み込みには readLines
を使います。
col_names <- unlist(str_split(string = readLines(con = con, n = 1), pattern = ","))
なお、列名を(1行目を)読み込むと con
のファイルポインタが2行目に移動します。
そのため、con
に対して連続して readLines
を呼び出すことで、続きから読み込むことができます。
CSV ファイル本体の読み込み
以上で準備は整ったので、CSV ファイル本体を読み込みます。
手順は以下の通りです。
-
readLines
で CSV ファイルの一部を文字列として読み込む - 文字列からデータフレームを生成できる関数(※)を使い、データフレームに変換する
- データフレームに対して任意の処理を行う
- 読み込み終わるまで 1~3 を繰り返す
※ read.csv(text = *)
や readr::read_csv(file = *)
などがあります。
以下の例では readr::read_csv(file = *)
を使っています。
library(readr)
# 一度に読み込む行数
n <- 10000000
while(T)
{
#(続きから)n行だけ読み込む
lines <- readLines(con = con, n = n)
# 読み込み終わったら終了する
if(length(lines) == 0)
{
break
}
# read_csv は改行を含まない文字列をファイルパスと解釈するため、必ず複数行にする
if(length(lines) == 1)
{
lines <- c(lines, "")
}
# データフレームに変換する
string <- paste(lines, collapse = "\n")
page_views <- read_csv(file = string, col_names = col_names, col_types = "cccccc")
# 読み込んだデータに対する処理(ここでは分割してファイルに出力)
write_csv(x = page_views, path = sprintf("page_views_%d.csv", seek(con)))
}
コネクションのクローズ
最後に、コネクションを閉じます。
close(con = con)
ファイルポインタの明示的な移動
ちなみに、 seek
を使うことでファイルポインタを明示的に移動させることもできます。
# 現在のファイルポインタの位置(バイト数)を返す
seek(con)
# ファイルポインタの位置を移動させる。"start" を指定すると、先頭から指定バイト数だけ移動させる
seek(con, 123456, "start")
ダメな読み込み方
ところで、readr::read_csv
は skip
や n_max
を指定することで読み込む範囲を指定できます。
コネクションを陽に使わずとも、これらを駆使すれば分割読み込みできそうな気がします。
しかし、skip
は「読み飛ばす」のではなく「読み込んで破棄する」ので、無駄な読み込みが発生します。
そのため、CSV ファイル後半に行くほど読み込みが遅くなります。
また、n_max
も「指定行数だけ読み込む」のではなく「指定行数以上を読み込み、不要な行を破棄する」ようです。
そのため、n_max
に小さな値を設定してもメモリを大幅に消費する場合があります。
# 読み込み効率化のため、ある程度一括して読み込む実装になっていると、どこかで読んだ気が(うろ覚え)
追記(2017年5月2日)
readr::read_csv_chunked
を使えば分割読み込みできるっぽい。
メモリに乗らないデータを分割して読み込んでくれるパッケージ - 盆栽日記