はじめに
事業会社で働いているデータサイエンティストです。普段の業務ではRを利用していますが、Pythonの言語仕様も理解しているつもりです。会社が作成していただいた記事はこちらです:
昨日会社のエンジニアの後輩社員にPython使いがキモいと感じるかもしれないRの文法をいっぱい見せたら、いい反応をいただいたので、Qiitaにまとめることにしました。
こんなことを紹介します:
キモいポイントその1:恐ろしい関数
キモいポイントその2:スキーマ大破壊
キモいポイントその3:そのxはどのx?
キモいポイントその4:if文とfor文なんてないよ、if関数とfor関数だよ
キモいポイントその5:あれ?足し算できなくなったけど
まず強調しておきたいのは、Rは決していわゆる「統計ソフト」ではなく、しっかりとしたプログラミング言語で、正しく書けばプロダクトとしてリリースできるほどの堅牢性と保守性が担保されます。
これからはRのキモいところをたっぷり紹介しますが、正直どれもいたずらに近いです。「包丁で自分の腕を切るな、肉を切れ」です。いたずらせずにRの言語仕様を理解しておけば、特に心配することはないかと思います。
むしろ、Pythonでは実現が難しいことを簡単にかつロバストに実現できるかもしれませんので、是非本記事のいたずらを通じてRをプログラミング言語として理解してみてください。
では本題に入りましょう!
キモいポイントその1:恐ろしい関数
ここでは、まずRのパイプについて説明します。
パイプとは、簡単にいうと、左側のものを右側の関数に渡す処理です。特に指定がなければ第一引数になりますが、指定したい引数がある場合、magrittrのパイプは"."、新しいbase Rのパイプは"_"でそれぞれ指定できます。具体的にはこんな感じです。
# magrittrというパッケージからmagrittrのパイプ(%>%)をインポートする
`%>%` <- magrittr::`%>%`
c(1,2,3,4,5) %>%
sum()
まあ、1から5までのベクトルを右側(改行はしてもしなくてもいいです)のsum関数に渡す処理なので、15が返却されます。
理解できました?
なので、こんな関数も作れます:
strange_function <- function(x){
x %>%
sum %>%
return
}
この関数を実行したら同じく15が返却されます。
> strange_function(c(1,2,3,4,5))
[1] 15
でもなかなかキモいですね。キモいポイントは
1、関数の呼び出しをしろ笑
2、何をreturnしてんの?!
ですが、ここでmagrittrパッケージとRの仕様について少し説明します。
まず、magrittrのパイプの右側の関数は別に呼び出しをしなくても使えます。ただ、個人的には呼び出しを明示的にしないと関数なのかどうかの判断がつかないため推奨しません。
次に、R言語の仕様なんですが、私の理解が正しければ、RにはPythonでいう「文」の概念がなく、全てが関数で表現されます。よって、「return」も第一引数が関数の返却物を決める機能を持つ関数のイメージなので、上のキモい書き方を実現できます。
ただ、これは流石によろしくないので、新しいパイプ(|>)は関数の呼び出しを必須にしています。また、returnを普通の関数として扱うのもやばいので、禁止になっています。具体的には:
> base_pipe_strange_function <- function(x){
x |>
sum() |>
return()
}
Error: function 'return' not supported in RHS call of a pipe
returnさんに変なことをしないでくださいということです。
ただ、ここで強調しておきたいのは、パイプはコードの順番を処理の順番と一致させる強力な概念で、正しい使い方で使えば、中間変数の削減と可読性の向上に寄与します。私が以前書いた記事でも、パイプを活用して自作進化版word2vecのための前処理を行いました。
もちろん書き方にもよりますが、Pythonと比べて中間変数が少ないところに注目していただければと思います。
キモいポイントその2:スキーマ大破壊
まずは、データフレイムを作ります:
> kawaisou_na_df <- data.frame(
x = 1:10,
y = 1:10
)
> kawaisou_na_df
x y
1 1 1
2 2 2
3 3 3
4 4 4
5 5 5
6 6 6
7 7 7
8 8 8
9 9 9
10 10 10
数字の1から10までのintegerのカラムが二つ入っています。型も確認しましょう。
> class(kawaisou_na_df[,1])
[1] "integer"
> class(kawaisou_na_df[,2])
[1] "integer"
問題ないですね!
でも
> kawaisou_na_df[2,2] <- "こんにちは"
> kawaisou_na_df
x y
1 1 1
2 2 こんにちは
3 3 3
4 4 4
5 5 5
6 6 6
7 7 7
8 8 8
9 9 9
10 10 10
「こんにちは」はなんと投入できて、かつ
> class(kawaisou_na_df[,1])
[1] "integer"
> class(kawaisou_na_df[,2])
[1] "character"
2列目が勝手にcharacter型に変換されました。そうなんです、エラーを吐くのではなく、こっそりとスキーマを変えてエラーを回避しています。バリデーションはないです。
なので、base Rのdata.frame(そう、dataパッケージのframeではなく、データフレイムです)を推奨せず、モダンなtidyverseのtibbleを推奨します。
> good_df <- tibble::tibble(
x = 1:10,
y = 1:10
)
> good_df
# A tibble: 10 × 2
x y
<int> <int>
1 1 1
2 2 2
3 3 3
4 4 4
5 5 5
6 6 6
7 7 7
8 8 8
9 9 9
10 10 10
tibbleはまずカラムの型を全部書いてくれるところから安心感ありますね。ここで同じことをしようとすると:
> good_df[2,2] <- "こんにちは"
Error in `[<-`:
! Assigned data `"こんにちは"` must be compatible with existing data.
ℹ Error occurred for column `y`.
Caused by error in `vec_assign()`:
! Can't convert <character> to <integer>.
Run `rlang::last_trace()` to see where the error occurred.
ちゃんとエラーを吐いてくれました。「Assigned data "こんにちは" must be compatible with existing data.」、既存のスキーマとの整合性がないと認めないぞ!ですね。
ここでPython利用者、多分どうして昔の人が考えたよくない実装(data.frame)を捨てないのかと疑問に思うと思いますが、これはRのコミュニティの特性に由来します。
Rは元々統計学、政治学、経済学、生物学、医学などの分野の統計分析ツールとして誕生したので、分析の再現性のために後方互換性を重視する文化があります。なので基本的には20年前のコードが今のRで動かなくなるみたいな現象を極力避ける方式で開発します。
Pythonの開発方針で考えれば、確かに大きなバージョンアップの段階でdata.frameを捨ててtibbleに置き換えるべきという結論に至ることは理解できますが、再現性と昔の知識を大事にするフィロソフィーもまたその良さはあるのかなと思います。
キモいポイントその3:そのxはどのx?
Rに慣れていない人にとって、Rの非標準評価はなかなかキモいですが、まず非標準評価の実例を見せます:
> tibble::tibble(
x = 1:10,
y = 1:10
) |>
dplyr::filter(x > 5)
# A tibble: 5 × 2
x y
<int> <int>
1 6 6
2 7 7
3 8 8
4 9 9
5 10 10
Pythonでいうとこんな処理をやりました:
import pandas as pd
# ごめんなさい、Rの1始まりに合わせます
pd.DataFrame({"x" : [i + 1 for i in range(10)], "y" : [i + 1 for i in range(10)]}).query("x > 5")
ここでPython使いにとってキモいのは、dplyr::filter関数で定義されていないxをそのまま使えるところなんですが、これは非標準処理といって、tibble型のデータフレイム内のスコープで存在しているから使えると理解できます。
pandasの場合わざわざ文字列型でx > 5の条件を処理しないといけない仕様とは対照的です。
Rの非標準処理のおかげでデータフレイム操作がスムーズになるメリットがありますが:
x <- 1
tibble::tibble(
x = 1:10,
y = 1:10
) |>
dplyr::filter(x > 5)
の時はどのxを優先するんでしょうか。tibbleの外のxを優先すれば、結果はfalseなので、空のデータブレイムが返却されるのに対して、tibble内のxを優先すれば、上の結果と変わりません。
> x <- 1
> tibble::tibble(
x = 1:10,
y = 1:10
) |>
dplyr::filter(x > 5)
# A tibble: 5 × 2
x y
<int> <int>
1 6 6
2 7 7
3 8 8
4 9 9
5 10 10
正解は中のxを優先します。
では「中のxが外のxより大きいレコードだけ欲しい」時はどうすればいいのか?もちろん変数名を変えるのが正論なんですが、.data$x
と.env$x
で明示的に指定できます。.data$
はデータフレイム内のことで、.env$
はその名の通り、データフレイムが存在している環境から変数を持ってきてくれます:
> x <- 1
> tibble::tibble(
x = 1:10,
y = 1:10
) |>
dplyr::filter(.data$x > .env$x)
# A tibble: 9 × 2
x y
<int> <int>
1 2 2
2 3 3
3 4 4
4 5 5
5 6 6
6 7 7
7 8 8
8 9 9
9 10 10
会社での経験からすると、ビジネスサイド向けの分析レポートの分析用コードに関しては、.data$
と.env$
を明示的に書く必要がないが、レコメンドシステムのAPIとShinyアプリを作る時は、.data$
と.env$
で明示的に指定した方が安全な気がします。
あ、そうなんです。Rは普通にREST規格のAPIも、UIを動的に制御するアプリも作れます。普通に。
キモいポイントその4:if文とfor文なんてないよ、if関数とfor関数だよ
ここでmagrittrのパイプのキモい使い方をもうひとつ紹介します。
> (10 > 1) %>%
if(.){
print("どうもー")
}
[1] "どうもー"
はい、このコードは動きます。
前も話したように、RはPythonなど他の言語でいう「文」の概念がなく、全てが関数です。ifもforも全部関数です。
> runif(10) %>%
for (i in .){
print(i)
}
[1] 0.9426044
[1] 0.6171426
[1] 0.9224991
[1] 0.2190397
[1] 0.397292
[1] 0.03934394
[1] 0.2722292
[1] 0.8432082
[1] 0.2088407
[1] 0.002536454
for文なんてないです。for関数です。理解できる方も多いと思いますが、「キモいポイントその1:恐ろしい関数」で説明したパイプの使い方をもう一回復習して、ifとforは関数になりうることを受け入れてみましょう。
ただ、文法上書けるものの、自分としてはこのような恐ろしいパイプの使い方は推奨しませんし、個人的にはコーディング規約でこのようなパイプの使い方を禁止にしたいと思います。
キモいポイントその5:あれ?足し算できなくなったけど
さて、forもifもreturnも全部関数だと説明しましたが、実はRでは足し算の+も関数で、普段はこう書くかもしれないですが:
1 + 1
正体(?)はこれです。
> `+`(1,1)
[1] 2
関数です。
関数なので、上書きももちろんできます。
> `+` <- function(x, y){
print("足し算が壊れました。うまい棒を買ってくれれば直します笑")
}
> 1 + 1
[1] "足し算が壊れました。うまい棒を買ってくれれば直します笑"
最高ですね。
でももちろんうまい棒を買う必要はなく、rm関数で意味不明な+関数を削除できます:
> rm(`+`)
> 1 + 1
[1] 2
これで直りました。
で、ここでプログラミングに深い知識がない初心者だと多分理解できないエラーも作れます:
> `+` <- function(x, y){
return(x + y)
}
> 1 + 1
Error: C stack usage 7956536 is too close to the limit
何が起きているのかというと、+関数の無限再帰が起きています。
全てを関数として扱うメリットは一見なさそうなんですが、ひとつ有名な例を紹介します。
tibble::tibble(
x = rnorm(10000)
) |>
dplyr::mutate(
noise = rnorm(dplyr::n())
) |>
dplyr::mutate(
y = 1.2 * x + noise
) |>
ggplot2::ggplot() +
ggplot2::geom_point(ggplot2::aes(x = x, y = y), color = ggplot2::alpha("blue", 0.3)) +
ggplot2::ggtitle("可視化です") +
ggplot2::theme_gray(base_family = "HiraKakuPro-W3")
ggplot2::ggplot()以降のコードに着目して欲しいですが、ggplot2のパッケージの処理は全て+で繋がっています。これはggplot2のオブジェクトに「+」のメソッドを定義できるからこそ、実現できる柔軟な書き方です。
また、少しパイプの話に戻りますが、Pythonのチェーンメソッドでもパイプのように処理の順番でコードを記述できますが、このRのコードのように、tibbleパッケージとdplyrパッケージとggplot2パッケージの処理をつなげるのは一般的には難しいです。私は普段自分の研究でも業務でもPythonを触っていないので、もしPythonでも実は簡単に実現できたらぜひご教示いただければと思います。
結論
ここで色々Python使いがキモいと感じるRの仕様を紹介しましたが、本質的にはキモいわけでなく、ただプログラミング言語が違うだけです。Pythonにも素晴らしい仕様があれば、Rの仕様にも素晴らしさがある。また、他のプログラミング言語を理解することで、ご自身が普段使っているプログラミング言語の知識も深まると思いますので、ぜひRを深く勉強してみてください。楽しいですよ!