2020/05/28

R - 実践編 2 データフレームにおける計算、集計

分析対象データをデータフレームに読み込んだあと、本格的な分析に入る前に、計算や集計などを行う必要があることが多いかと思います。R での代表的なやり方を説明します。

既存列に基づく計算結果を新しい列に持たせる

データフレーム df の列 a, b を元に、何らかの計算 f() を行ってその結果を列 c に持たせるのであれば、基本的には以下のようにします。
df$c <- f(a, b)

この方法では、f が同じ長さのベクトル2つを引数にとり、同じ長さのベクトルを結果として返すように定義されていることを想定しています。R では多くの算術演算子や算術関数がそのように振舞いますので、こういった簡単な書き方が可能なのです。

1 ステップで書きづらいものや、無理に 1 ステップで書こうと思うと効率が悪くなるものは、次のような方法もあります。例えば、データフレーム df の列 a の値をキーとして、別のデータフレーム df2 から行を選択し、その行から列 b の値に応じて df2 の列 X か列 Y かのいずれかの値を取って列 c に格納する、といった例を考えてみましょう。
df$c <- local({
  tmp <- df2[df2$a == df$a,]
  ifelse(df$b == "X", tmp$X, tmp$Y)
})

df2 から選択した行は一度 tmp に付値しておくことで、df2 への添字ベクトルの適用を一度で済ませています。そうしないと、列 X や列 Y を取得するときにも添字ベクトルによる行選択が必要となり、非効率です。local() は、識別子 tmp が名前空間を汚さないようにするためのものです。

二分探索法

別の例を見てみましょう。「df2\$a == df\$a」という同値条件に代えて、「df\$a の値を越えない最大の df2\$a の値を持つ行」を抽出してみましょう。
単純に考えると、以下のようになります。
df$c <- local({
  fun <- function(x) df2[which(df2$a == max(df2$a[df2$a <= x])),]
  tmp <- lapply(df$a, fun)
  ifelse(df$b == "X", tmp$X, tmp$Y)
})

ですがこの方法は、df2 からの行抽出が何度も行われ、とても効率が悪いです。
こういった場合は、df2 を列 a をキーに整列しておき、二分探索法を使います。まず以下のように二分探索を行う関数 bsearch() を作りましょう。標準で合ってもよさそうなものなのですが、見当たらないので……。
bsearch <- function(val, tab, L=1L, H=length(tab)) {
  b <- cbind(L=rep(L, length(val)), H=rep(H, length(val)))
  i0 <- seq_along(val)
  repeat {
    updt <- M <- b[i0, "L"] + (b[i0, "H"] - b[i0, "L"]) %/% 2L
    tabM <- tab[M]
    val0 <- val[i0]
    i <- tabM < val0
    updt[i] <- M[i] + 1L
    i <- tabM > val0
    updt[i] <- M[i] - 1L
    b[i0 + i * length(val)] <- updt
    i0 <- which(b[i0, "H"] >= b[i0, "L"])
    if (!length(i0)) break;
  }
  b[,"L"] - 1L
}

この bsearch() を用いて、次のようにすると、とても速いです。
df$c <- local({
  tmp <- df2[bsearch(df$a, df2$a),]
  ifelse(df$b == "X", tmp$X, tmp$Y)
})

データフレームの集計

データフレームを集計するには by() を使います。例えば df\$d に何らかの因子が入っているとして、その水準ごとに df\$c の平均、標準偏差、個数を集計するとしましょう。
summ <- by(df$c, df$d, function(x) { x <- x[[1]]; c(mean(x), sd(x), length(x)) })

by() の戻り値は by クラスのオブジェクトで、df\$d の水準名が要素名となり、function(x) ... の戻り値が値となったリストの形をしています。