RでFizzBuzz問題を解いてみる~ifelse関数のベンチマーク~
FizzBuzz問題とは、
入力:数字ベクトル (長さ1以上)
出力:各要素に対して、3の倍数のときは”Fizz”、5の倍数のときは”Buzz”、3と5両方の倍数の場合には”FizzBuzz”、その他の場合は数字を文字列化する
1:100を入力とすると、 (“1”, “2”, “Fizz”, “4”, “Buzz”, “Fizz”, …という出力になる)
という問題です。
FizzBuzz問題を、Rで高速に計算する実験をしてみます。
高速化のカギとなるのはif文をR特有のベクトル演算処理で行う、ifelse関数です。ifelse関数とは文字通り、if文の分岐処理をベクトルの要素ごとに行う関数で、Rでは基本パッケージのifelse関数以外にも、dplyrパッケージのif_else、data.tableパッケージの高速化版fifelseがあります。どれも機能は同じです。
今回はifelseを使わない場合と、上記3つのifelseを使って実行速度の比較をしてみます。
方法① 要素ごとにforループを回す。
Rに詳しくない人がまず思いつく方法です。
func.naive <- function(x)
{
out <- rep("", length(x))
for (i in seq_along(x)) {
if (x[i] %% 3 == 0 & x[i] %% 5 == 0)
out[i] <- "FizzBuzz"
else if (x[i] %% 3 == 0)
out[i] <- "Fizz"
else if (x[i] %% 5 == 0)
out[i] <- "Buzz"
else
out[i] <- as.character(x[i])
}
out[out == ""] <- as.character(x[out==""])
out
}
入力データを作ります。1~1000万までの自然数からランダムに100万個を抽出したベクトルを作ります。
> set.seed(1234) # 再現性確保のためにシードを固定します。
> x <- sample(1e+09, 1e+06)
実行結果①
> time <- system.time(func.naive(x))
> system.time(out <- func.naive(x))
ユーザ システム 経過
3.48 0.01 3.52
約3.5秒。遅いです。
方法② ifelseを使う
if文の各要素のループをベクトル演算で一気にやってしまうのが、ifelse関数です。
func0 <- function(x)
{
#
# base:ifelseを使う場合
#
is3 <- x %% 3 == 0 # 3の倍数かどうかのboolean
is5 <- x %% 5 == 0 # 5の倍数かどうかのboolean
out <- ifelse(is3 & is5, "FizzBuzz",
ifelse(is3, "Fizz",
ifelse(is5, "Buzz", "")))
out[out == ""] <- as.character(x[out==""])
out
}
実行結果②
> system.time(out <- func0(x))
ユーザ システム 経過
1.93 0.11 2.04
方法①から約1秒速くなりました。
方法③ dplyrパッケージのif_elseを使う場合
方法②のifelseの部分をdplyr::if_elseで置き換えます。
func1 <- function(x)
{
#
# dplyr:if_elseを使う
#
is3 <- x %% 3 == 0
is5 <- x %% 5 == 0
out <- dplyr::if_else(is3 & is5, "FizzBuzz",
dplyr::if_else(is3, "Fizz",
dplyr::if_else(is5, "Buzz", "")))
out[out == ""] <- as.character(x[out==""])
out
}
実行結果③
> system.time(out <- func1(x))
ユーザ システム 経過
0.76 0.07 0.84
1秒を切りました。
方法④ data.tableパッケージのfifelseを使う。
func2 <- function(x)
{
#
# data.table::fifelseを使う
#
is3 <- x %% 3 == 0
is5 <- x %% 5 == 0
out <- data.table::fifelse(is3 & is5, "FizzBuzz",
data.table::fifelse(is3, "Fizz",
data.table::fifelse(is5, "Buzz", "")))
out[out == ""] <- as.character(x[out==""])
out
}
実行結果④
> system.time(out <- func2(x))
ユーザ システム 経過
0.48 0.03 0.51
0.5秒を切りました!
まとめ
結論、data.table::fifelseが爆速でした。