見出し画像

【R言語】重たいファイルを効率的に処理する方法



背景

最近まわりがpython使いが多くなってしまい、pandasくらいは覚えておくかと思ったのですが、どうにもこうにも記述がややこしく感じてしまっていたところ、polarsというパッケージを見つけまして。(以下URLがわかりやすかったです)
これがpandasより(僕にとっては)覚えやすく、可読性も高いので少しずつ勉強始めたのですが、Rでもパッケージがあるのことで。しかも処理がdata.tableパッケージよりも高速と聞いたので、簡単な実験をしてみようと思った次第です。


手順

やったこと

そこそこ重たいファイルを生成し、ファイルの読み込み→簡単な処理を行って、処理速度を比較してみようという流れになります。

手順1 必要なパッケージと重たいファイルの生成

以下のコードを実行して、一時ディレクトリにlarge_data.csvファイルを掃き出します。凡そ2GB程度のファイルが出力されるかと思います。

library(pacman)
p_load(tidyverse, data.table)

# データを生成
set.seed(123)
n_rows = 5e6  # 行数
n_cols = 20   # 列数
data = as.data.table(
  matrix(runif(n_rows * n_cols), 
         nrow = n_rows, ncol = n_cols))

# tempdirにCSVで出力
temp_file = file.path(tempdir(), "large_data.csv")
write_csv(data, temp_file)

# ファイルサイズを測定
file.info(temp_file)$size / (1024^2)

手順2 readrパッケージを使った処理

まずは最も使われているであろうreadr::read_csv関数を使ってデータを読み込み、簡単な処理を実行し、tictocパッケージで処理速度を測ってみます。僕の環境下で14.5秒程度で処理が完了しました。

# readr version
p_load(tictoc)
tic()
readr::read_csv(temp_file) |> 
  filter(V1 > 0.5 & V2 < 0.5) |> 
  dplyr::select(V1, V2)
toc()

手順3 data.tableパッケージを使った処理

data.tableパッケージを使ってデータを読み込み、簡単な処理を実行し、tictocパッケージで処理速度を測ってみます。僕の環境下で4.5秒程度で処理が完了しました。さすがにdata.tableパッケージは早いですね。

# data.table version
p_load(tictoc)
tic()
data.table::fread(temp_file) |> 
  filter(V1 > 0.5 & V2 < 0.5) |> 
  dplyr::select(V1, V2)
toc()

手順4 polarsパッケージを使った処理

さて、最後にpolarsです。1.8秒程度で処理が完了しました。爆速でした。前者2つはデータを全て読み込んでから処理を行うのに対して、polarsは不要なデータは読み込まない(本ケースだとV1,V2以外は読み込まない)遅延処理が実行できるから処理速度が速いと言われているそうです。(そのほかの理由もあると思いますが・・)
こうなると、重たいデータの場合はとりあえずpolarsで読み込み、簡単な処理を行いある程度サイズを減らしてから、tidyverseで可読性高く後続処理を実行していく流れが好ましいかもしれませんね。

# polars version
# polarsを初めてインストールする人は以下の2行のコードをまず実行ください
# Sys.setenv(NOT_CRAN = "true") # ソースインストールの場合、この環境変数を設定することでビルド済バイナリをGitHubからダウンロードしてRustソースからのビルドを回避できます
# install.packages("polars", repos = "https://rpolars.r-universe.dev")
p_load(polars)
tic()
pl$scan_csv(temp_file)$
  filter(pl$col("V1") > 0.5 & pl$col("V2") < 0.5)$
  select("V1", "V2")$
  collect()
toc()

手順5 benchmarkで全比較

3つのパッケージで処理速度を比較してみましたが、メモリの消費量なども併せて比較してみたいと思います。benchというパッケージを使って、比較をしていきます。

# benchmarkで比較
p_load(bench)
bench::mark(
  # readr
  readcsv = {
    rr_result = readr::read_csv(temp_file) |> 
      filter(V1 > 0.5 & V2 < 0.5) |> 
      dplyr::select(V1, V2)
    as.data.frame(rr_result)
  },
  
  # data.table
  datatable = {
    dt_result = data.table::fread(temp_file) |> 
      filter(V1 > 0.5 & V2 < 0.5) |> 
      dplyr::select(V1, V2)
    as.data.frame(dt_result)  
  },
  
  # polars
  polars = {
    pl_result = pl$scan_csv(temp_file)$
      filter(pl$col("V1") > 0.5 & pl$col("V2") < 0.5)$
      select("V1", "V2")$
      collect()
    as.data.frame(pl_result)  
  },
  iterations = 3
)

以下は上記コードの出力になります。
mediaが処理速度の中央値、mem_allocがメモリの割り当て量になりますが、polarsは処理速度が速いだけでなく、遅延処理のおかげもあり効率的なメモリの活用が出来ているようです。(素晴らしいですね。。)

手順6 tempdirにあるファイルを削除

念のためtempdirに出力したファイルを削除しておきましょう。以上で完了です。

# tempdirにあるCSVファイルを削除
file.remove(temp_file)

全コード

以下に全コードを載せておきます。

library(pacman)
p_load(tidyverse, data.table)

# データを生成
set.seed(123)
n_rows = 5e6  # 行数
n_cols = 20   # 列数
data = as.data.table(
  matrix(runif(n_rows * n_cols), 
         nrow = n_rows, ncol = n_cols))

# tempdirにCSVで出力
temp_file = file.path(tempdir(), "large_data.csv")
write_csv(data, temp_file)

# ファイルサイズを測定
file.info(temp_file)$size / (1024^2)

# readr version
p_load(tictoc)
tic()
readr::read_csv(temp_file) |> 
  filter(V1 > 0.5 & V2 < 0.5) |> 
  dplyr::select(V1, V2)
toc()


# data.table version
p_load(tictoc)
tic()
data.table::fread(temp_file) |> 
  filter(V1 > 0.5 & V2 < 0.5) |> 
  dplyr::select(V1, V2)
toc()

# polars version
# polarsを初めてインストールする人は以下の2行のコードを参照ください
# Sys.setenv(NOT_CRAN = "true") # ソースインストールの場合、この環境変数を設定することでビルド済バイナリをGitHubからダウンロードしてRustソースからのビルドを回避できます
# install.packages("polars", repos = "https://rpolars.r-universe.dev")
p_load(polars)
tic()
pl$scan_csv(temp_file)$
  filter(pl$col("V1") > 0.5 & pl$col("V2") < 0.5)$
  select("V1", "V2")$
  collect()
toc()

# benchmarkで比較
p_load(bench)
bench::mark(
  # readr
  readcsv = {
    rr_result = readr::read_csv(temp_file) |> 
      filter(V1 > 0.5 & V2 < 0.5) |> 
      dplyr::select(V1, V2)
    as.data.frame(rr_result)
  },
  
  # data.table
  datatable = {
    dt_result = data.table::fread(temp_file) |> 
      filter(V1 > 0.5 & V2 < 0.5) |> 
      dplyr::select(V1, V2)
    as.data.frame(dt_result)  
  },
  
  # polars
  polars = {
    pl_result = pl$scan_csv(temp_file)$
      filter(pl$col("V1") > 0.5 & pl$col("V2") < 0.5)$
      select("V1", "V2")$
      collect()
    as.data.frame(pl_result)  
  },
  iterations = 3
)


# tempdirにあるCSVファイルを削除
file.remove(temp_file)

副業やってます

副業としてデータ分析やR言語を用いた業務効率化・自動化対応などをやっていますので、もしご興味ある方は以下のサイトからお問い合わせください。

拙い記事でしたが、ご一読ありがとうございました!

よろしければサポートお願いします。