大量で巨大なcsvをRで読み込む時の最適解【vroom,readr,data.table】



拝啓Rユーザーの皆様。
巨大なデータ、大量のデータを扱うときにどのようにお読み込みなされていますでしょうか。

単純なcsvデータであればdplyr::read_csv()か、もしくはread_delim()かと思います。
データを分析する中で「**ファイル数が大量**」であり「**合計すると総サイズが巨大**」になるようなデータと対面することもあるかと思います。
このような大量のファイルを読み込む方法と、読み込み速度およびその後の処理速度を検証した結果から最適な組み合わせを提案したいと思います。




# はじめに結論から

今回の検証では「**メモリサイズを無視して実行速度の面から考えると**」

```{r}
purrr::map_dfr() + data.table::fread
```

が最も最適でした。

検証コードは最後にまとめています。


![スライド9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275572/9eb19923-ce60-38b3-09f7-a7b367502b16.png)




# 読み込み方法の紹介

読込方法には、本記事の初めに紹介したdplyrパッケージのread_csv関数か、もしくはdata.tableパッケージのfread関数が利用されているように思います。
しかし、どちらも単一のファイルを読み込むことを目的に設計された関数であるため、大量のファイルを同時に読み込むには少しテクニックが必要です。

そのテクニックとしてはpurrrパッケージのmap_dfr関数を使います。
これは渡されたリスト(またはベクトル)に対して特定の関数を適応してから、結果をデータフレーム型で返すものです。

```{r}
map_dfr(csv_list, fread)
```

もっと簡単な読み込みパッケージとして、2019年にvroomパッケージが登場しました。


https://www.tidyverse.org/blog/2019/05/vroom-1-0-0/


vroomパッケージのvroom関数はファイルのパスを渡すだけですべてを読み込み、データフレームの形で返してくれます。
purrrはapplyファミリーの使い方を知っていないと挙動を理解しにくいパッケージですが、vroomは初心者にもやさしい設計となっています。

そして何より早いです。データを効率的に扱えるとうたっているdata.tableよりも読み込みが早いです。(後述)


```{r}
vroom(csv_list)
```


当時、2019年時点ではバージョン1.0.0でしたが、現在2022-02-26時点では1.5.7まで成長しております。
登場当時は大量の入力ファイルを扱うことのできる設計にも関わらず、**大量すぎると処理が正常に動かない**という現象が頻発していました。
数十個程度ならまだしも数百数千のファイルには弱い状態でした。
しかしバージョンアップを繰り返す中でいつの間にか**安定して動く**ようになっていました。


そこで~~vroomを虐めてみよう~~大量のデータを扱う最適な方法は何か考えてみようと思い検証してみたので結果を記録しておきます。

ちなみに検証してから気づいたのですが、vroom公式も読み込みや読み込み後の処理についてのベンチマーク測定を行っていましたのでそちらもご参照ください。

https://vroom.r-lib.org/articles/benchmarks.html


vroomの公式な検証では1.5 Gbのcsv読み込みでしたが、本記事ではさらに大量かつサイズが巨大なデータを取り扱います。


# vroomはなぜ早い？

簡単に説明しますと、「**インデックスだけを読み込み必要に応じて処理をする**」という方式をとっているためです。
このメリットが特に効果を発揮するのは文字データに対してであり、数値データにはあまりメリットがありません(インデックス化していない実体化とデータサイズが変わらないです)。

技術的にどうやって実現しているのかについてはALTREPと呼ばれるオブジェクト形式で扱っているので、こちらを学習していただくとヒントになるかと思います。

https://svn.r-project.org/R/branches/ALTREP/ALTREP.html

ちなみにこんなニッチな内容を2020年にQiitaに投稿してくださっている方がいらっしゃいます。


https://qiita.com/hkzm/items/5ec868bb5e4ec8e3fdb0


※注：RStudio 1.2.1335+ 以前のRstudioではALTREPの扱いにバグが存在しているので注意です。




# 検証開始

検証対象として、「8列3万行 × 4000ファイル = 合計 11.2 Gb」のすべて数値データを生成しました。
今回は「読み込み」と「その後の処理」の時間を検証します。

その後の処理として、groupでグルーピングしてグループ毎の標準偏差を求めるようなコードを作ってみました。


```
データの例：
   group_1 group_2 group_3  data_1 data_2 data_3 data_4 data_5
     <dbl>   <dbl>   <dbl>   <dbl>  <dbl>  <dbl>  <dbl>  <dbl>
 1      10      10       1  1.19    23.7    270.   943.  2881.
 2      10      10       1  0.950   14.9    520.   910.  6055.
 3      10      10       1  0.483   25.9    448.   520.  4251.
```


# 結果


読込結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275572/521fa021-1c83-021a-dac4-c812b3942289.png)



全て数値で出来ているということもありread_csvで読み込んだ時のような実体化データとオブジェクトサイズは変わりがありません。
しかし読み込み速度は格段に速くなっています。


その後の処理はどうでしょうか？
読み込んだオブジェクトから標準偏差を計算してみた結果は



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275572/0b17f016-5a58-fb3c-0a89-e94674273f06.png)




このようになりました。
vroomは読み込み後の処理が劇的に遅く、freadは読み込みに多少時間がかかっても、その後の処理が早いことが分かりました。

データの読み込みが早くともその後の処理が遅くては使い物にならない場面が出てきます。
vroomが遅いのはおそらくですがファイルの実体化を行っているためだと考えています。



# もしかしてvroomで読んだ後に実体化を工夫したら爆速なのでは？


と思ったのでtibbleとdata.table形式に実体化してから処理しました。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275572/0ed7c1cf-3ab0-42f9-66ab-adb0a7a94532.png)


結果、tibbleもdata.tableも仲介するメリットがありませんでした。

実体化についてはvroomにaltrep引数があり、これをFalseにすることで実体化しながら読み込むことができます。



# altrep=False　も含めた検証全体

![スライド9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275572/aa2c04d6-7944-a7b3-c566-1af158ed8011.png)



数値データだけを読み込む場合は、後々の操作を考えるとfreadで読み込んでおくのが良さそうです。



# 文字データの読み込みも試す

念のため文字データだとどれだけ速度が出るのか試したところ、vroomが圧勝でした。


![スライド10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275572/aa6f94ad-17d6-d369-c283-7ce4c6e6b009.png)



# 数値データの生成と検証コード

```r
library(tidyverse)

set.seed(1112)

for(i in 1:20){
  for(j in 1:50){
    for(k in 1:4){
      df <- tibble(group_1=i,
               group_2=j,
               group_3=k,
               data_1=rnorm(30000,0,1),
               data_2=rnorm(30000,20,10),
               data_3=rnorm(30000,400,100),
               data_4=rnorm(30000,1000,300),
               data_5=rnorm(30000,2000,5000)
               )  
      write_csv(df,str_c("data_",i,"_",j,"_",k,".csv"))
    }
  }
}


# reset session every time

library(tidyverse)
library(tictoc)
library(fs)
library(vroom)
library(data.table)

memory.limit(24000)

csv_list <- dir_ls()
tic()
all_df <- map_dfr(csv_list,read_csv)
toc()

object.size(all_df)

# I don't use across(), but there is no particular reason to.

tic()
sd_df <- all_df %>% 
  group_by(group_1,group_2,group_3) %>% 
  summarise(sd_1 = sd(data_1),
            sd_2 = sd(data_2),
            sd_3 = sd(data_3),
            sd_4 = sd(data_4),
            sd_5 = sd(data_5))
toc()

##### Replacement Parts
all_df <- vroom::vroom(csv_list)
all_df <- vroom::vroom(csv_list,altrep = F)
all_df <- map_dfr(csv_list,fread)
all_df <- map_dfr(csv_list,read_csv)
#####

```

# 文字データの生成

```r
library(tidyverse)
library(randomNames)

for(i in 1:20){
  for(j in 1:50){
    for(k in 1:4){
      df <- tibble(group_1=LETTERS[i],
                   group_2=LETTERS[j],
                   group_3=LETTERS[k],
                   data_1=randomNames(30000),
                   data_2=randomNames(30000),
                   data_3=randomNames(30000),
                   data_4=randomNames(30000),
                   data_5=randomNames(30000))  
      write_csv(df,str_c("data_",i,"_",j,"_",k,".csv"))
    }
  }
}
```


# 以上

何かお気づきの点があれば教えてください。

追記：
read_csvは内部vroomになり複数ファイル読み込むことができるようになっているようです。
https://www.tidyverse.org/blog/2021/07/readr-2-0-0/#reading-multiple-files-at-once
@eitsupi 様よりご指摘。