2020/05/28

R - 実践編 1 ファイルの読み込み、データフレームの加工

実際に R でデータ解析を行うために、CSV ファイル、TSV ファイルの読み込みを行ったときの知見をまとめました。

CSV ファイルの読み込みは read.csv()

CSV ファイルの読み込みは、read.csv() を用いて行います。read.csv() は、read.table() の引数の既定値を CSV ファイル向けに変更したものなので、動作は read.table() そのものです。read.table() についてはこちらの記事を見てください。引数の既定値は、以下のように変更されています。

  • header = TRUE (1行目は列数によらずヘッダ行と見做す)
  • sep = "," (列のセパレータはカンマ)
  • quote = "\"" (引用符はダブルクオートのみ)
  • fill = TRUE (列数が異なっていても、補完する)
  • comment.char = "" (コメントは使えない)
以下の説明は、read.csv() を使ったときの知見の紹介ですが、多くは read.table() にも当て嵌まります。

列のモードの指定

列のモードは read.table() の仕様に従って適宜自動的に判断されますが、それが望ましくない場合は colClasses で各列のクラスを指定することができます。
一旦は何も考えずに読み込んで、後から強制変換すれば良いだろうとも思っていたのですが、実際にやってみると、こんな問題がありました。

桁数の多い数値列

桁数が多い数字列は、double として読み込まれてしまうのですが、このとき倍精度実数の最大精度を越えていると、丸められてしまうということが分かりました。
多くの場合、桁数の多い数字列というのは、数値として扱いたいわけではなく、コード値のような扱いをするものだと思います。そういう場合は、colClasses で character や factor に指定しておけば、丸められずに読み込めます。本当に大きな数値を扱いたい場合は、いわゆる多倍長精度数値を扱うクラスがあるのではないかと想像しますが、今のところ知りません。すみません。

日付・日時列

日付、日時の列ならば colClasses に Date クラスが指定できます。ただフォーマットの指定は colClasses ではやりようが無いので、フォーマットが合わないときは、character で読み込んでおいて後から format.Date() で直せば良いでしょう。他の方法としては、既定のフォーマットを変更してから実行するとか、独自の Date クラスのサブクラスを作るといったことが考えられます。

ミリ秒まで持っている時刻データ列

ミリ秒精度だと、Date クラスでは保持できないので、POSIXct を指定します。ミリ秒も読み込まれます。表示上は見えませんが、options(digits.secs=3) とすればミリ秒まで見えてきます。ただ、精度の問題はあるようで、例えば 2020-05-05 00:00:00.123 を POSIXct に読み込ませて、options(digits.secs=3) の状態で表示させると、2020-05-05 00:00:00.122 となってしまいました。これはちょっと問題ですね。

colClasses に列名を指定する

colClasses に与えるベクトルを名前付きベクトルにすれば、列名で指定できます。

試行錯誤中は nrows で行数を限定する

read.csv() のパラメータの指定は割と試行錯誤になってしまうので、対象のファイルが大きいと、試す都度時間がかかって大変です。
と、思っていたら、nrows パラメータで最大行数が指定できるので、ここを少な目の行数に設定して試せば早いですね。もっと早く気付きたかったです。

TSV ファイルの読み込みは read.delim()

TSV ファイルの読み込みは、read.delim() を用います。read.csv() と同様、read.table() の引数の既定値を変更しただけのものです。

データフレームの加工

とりあえずデータフレームに読み込めたとして、実際に分析を開始するまでには、通常はある程度加工が必要となるでしょう。

列の抽出は添字ベクトル

解析に必要な行、あるいは必要な列だけを抽出したいということはよくあります。
列の抽出だけであれば添字ベクトルを使って、df <- df[select] とすることができます。select は添字ベクトルと呼ばれており、列名のベクトルです。
> df[c("x", "y")]
x y
1 1 4
2 2 5
3 3 6

行・列を一度に絞り込むなら subset()

必要な行、列を一度に絞り込むには subset(df, subset, select) が使えます。データフレーム df から、論理式 subset を満たす行を抽出し、文字列ベクトル select に含まれる列名の列を抽出する、という動作をします。
> df <- data.frame(x = c(1, 2, 3), y = c(4, 5, 6), z = c(7, 8, 9))
> subset(df, x %in% c(1, 3))
  x y z
1 1 4 7
3 3 6 9
> subset(df, select = c("x", "z"))
  x z
1 1 7
2 2 8
3 3 9
> subset(df, y %in% c(5, 6), c("y", "z"))
  y z
2 5 8
3 6 9

ファイル読み込み時に行や列を除外する

ファイルを全部読み込んでから列や行を削るのは効率が悪いので、読み込み時点である程度除外できると、効率が良いです。
行については、nrows (読み込む行数) と skip (指定した行数より前の行を読み飛ばす) を指定することで、ある程度対応できます。
列については、colClasses パラメータで NULL を指定した列はスキップされます。

列のクラスを変更する

読み込んだデータフレームの列のクラスを後から変更したいこともよくあります。この記事でも、一旦 character で読み込んでおいて、後でどうにかしましょう、という話をしていました。
実際には、例えば3列目を character から factor にしたければ以下のようにします。
> df[, 3] <- factor(df[, 3])

複数列まとめて変更したければ、lapply() を使うことができます。3列目と74列目を character から factor に変更するなら、こうです。
> df[, c(3, 74)] <- lapply(df[, c(3, 74)], factor)

データフレームの結合は rbind()

データが複数ファイルに分割されている場合など、複数のデータフレームに読み込まれたデータを1つのデータフレームに結合したいこともよくあります。こういったときは、rbind() を使うことができます。
例えば、filenames に読み込みたいファイル名のベクトルがあって、これらを全て読み込んで1つのデータフレームに結合したい場合は、次のようにすることができます。
> df <- data.frame()
> for (filename in filenames) {
+   dftmp <- read.delim(filename)
+   # 必要に応じて dftmp の subset() をとったりなんだり
+   df <- rbind(df, dftmp)
+ }

最初に空のデータフレーム df を用意しておき、そこにファイルから読み込んで、加工した上で df に結合していきます。
加工が必要なければ以下のように、よりシンプルにできます。
> df <- Reduce(rbind, Map(read.delim, filenames))

read.delim のかわりに加工も含めた関数を与えれば、前者と同じこともできますね。