Rで良いコードを書く(R style guide)
はじめに
tidyverseスタイルガイドとGoogleスタイルガイドをもとに、R言語で良いコードを書くための記事をまとめました(一部省略しています)。
tidyverseスタイルガイド
Tidyverse 全体で使用されているスタイルについてまとめました。このスタイルは、Google の元々の R スタイルガイドを基にしていますが、現在の Google のガイドは Tidyverse スタイルガイドから派生しています。
スタイルガイドの最も重要な点は一貫性を提供することであり、これによって作業が効率化されます。
以下2つのRパッケージには、このRスタイルガイドを確認したり校正したりする機能があります。
- styler:選択したテキスト、ファイル、またはプロジェクト全体をインタラクティブに再スタイル化することができます。RStudio のアドインが含まれており、既存のコードを再スタイル化する最も簡単な方法です。
- lintr:このスタイルガイドに準拠しているかどうかを自動的に確認します。
1 ファイル
1.1 名前
ファイル名は意味のあるもので、必ず .R で終わるようにしましょう。ファイル名には特殊文字を使わず、<font color="red">数字、アルファベット、ハイフン(-)、アンダースコア(_)のみ</font>を使用します。
fit_models.R
utility_functions.R
fit models.R
foo.r
stuff.r
ファイルを特定の順序で実行する必要がある場合は、数字を接頭辞として付けます。10 ファイル以上になる可能性がある場合は、ゼロで左詰めして名前を付けると良いです。
00_download.R
01_explore.R
...
09_model.R
10_visualize.R
後でステップを追加する必要があると気づいた際には02a、02b などを使う誘惑に駆られますが、全ファイルをリネームする方が一般的に良いです。
また、大文字と小文字には注意しましょう。Microsoft Windows や OS Xのような大文字・小文字を区別しないファイルシステムを使用している人がいるかもしれません。これは、大文字・小文字を区別するバージョン管理システムで問題を引き起こす可能性があります。すべて小文字のファイル名を使用し、大文字と小文字だけで区別される名前は避けてください。
1.2 組織化
複数のファイルにコードを分割して整理する方法を正確に説明するのは難しいですが、簡潔な命名によってその内容が示唆されるならば、良い整理方法と言えるでしょう。
1.3 内部構造
ファイル内を読みやすく区切るために、- や = でコメント行を使って区切ります。
# データの読み込み ---------------------------
# データのプロット ---------------------------
スクリプトで追加のパッケージを使用する場合は、ファイルの冒頭ですべて一度に読み込みます。library() 呼び出しをコード内に散らばせたり、.Rprofile などのスタートアップファイルで依存関係を隠すよりも、この方法の方が透明性があります。
2 文法
2.1 オブジェクト名
変数名や関数名は、小文字のアルファベット、数字、アンダースコア (_) のみを使用するべきです。単語間の区切りにはアンダースコア(スネークケース)を使用します。
day_one
day_1
DayOne
dayone
Base Rでは関数名(contrib.url())やクラス名(data.frame)にドットが使われますが、ドットはS3オブジェクトシステム専用にするのが良いです(← Googleのコーディングスタイルとはやや異なる点)。S3ではメソッドがfunction.classの形で命名されるため、関数名やクラス名にドットを使うと、as.data.frame.data.frame()のように混乱を招く命名になります。
変数名にデータを詰め込もうとしている場合(例: model_2018, model_2019, model_2020)、リストやデータフレームを使用することを検討してください。
一般に、変数名は名詞、関数名は動詞であるべきです。短く、意味のある名前を目指しましょう(これは簡単ではありません)。
Copy code
day_one
first_day_of_the_month
djm1
可能な限り、共通の関数や変数の名前を再利用しないようにしましょう。コードの読者に混乱を与える可能性があります。
T <- FALSE
c <- 10
mean <- function(x) sum(x)
2.2 スペースの使い方
2.2.1 コンマ
常にコンマの後にスペースを入れ、前にはスペースを入れないようにしましょう。
x[, 1]
x[,1]
x[ ,1]
x[ , 1]
2.2.2 括弧
通常の関数呼び出しでは、括弧の内外にスペースを入れないようにします。
mean(x, na.rm = TRUE)
mean (x, na.rm = TRUE)
mean( x, na.rm = TRUE )
if、for、whileと一緒に使う場合は、括弧の前後にスペースを入れます。
if (debug) {
show(x)
}
if(debug){
show(x)
}
2.2.3 波括弧
波括弧 ({{ }}) を使用する場合、内部にスペースを入れて、その特別な動作を強調します。
max_by <- function(data, var, by) {
data %>%
group_by({{ by }}) %>%
summarise(maximum = max({{ var }}, na.rm = TRUE))
}
max_by <- function(data, var, by) {
data %>%
group_by({{by}}) %>%
summarise(maximum = max({{var}}, na.rm = TRUE))
}
2.2.4 中置演算子
ほとんどの中置演算子(==, +, -, <-など)は、常にスペースで囲むべきです。
height <- (feet * 12) + inches
mean(x, na.rm = TRUE)
height<-feet*12+inches
mean(x, na.rm=TRUE)
ただし、いくつかの例外があります。以下の演算子はスペースで囲むべきではありません:
優先順位の高い演算子: ::, :::, $, @, [, [[, ^, 単項-, 単項+, :
sqrt(x^2 + y^2)
df$z
x <- 1:10
sqrt(x ^ 2 + y ^ 2)
df $ z
x <- 1 : 10
単一の識別子を持つ片側の数式の場合も、スペースは不要です。
~foo
tribble(
~col1, ~col2,
"a", "b"
)
~ foo
tribble(
~ col1, ~ col2,
"a", "b"
)
2.3 関数呼び出し
2.3.1 名前付き引数
関数の引数は、一般的に二つの広いカテゴリに分かれます。一つは計算対象のデータを提供するもので、もう一つは計算の詳細を制御します。関数を呼び出すときには、データ引数の名前を省略することが一般的です。デフォルト値をオーバーライドする場合は、引数の完全な名前を使用してください。
mean(1:10, na.rm = TRUE)
mean(x = 1:10, , FALSE)
mean(, TRUE, x = c(1:10, NA))
2.3.2 代入
関数呼び出し内での代入は避けてください。
x <- complicated_function()
if (nzchar(x) < 1) {
# 何かをする
}
if (nzchar(x <- complicated_function()) < 1) {
# 何かをする
}
2.4 制御フロー
2.4.1 コードブロック
波括弧 {} は R コードの最も重要な階層を定義します。この階層を見やすくするために:
{ は行の最後の文字であるべきです。関連するコード(例:if 条件、関数宣言、末尾のコンマなど)は、開き括弧と同じ行に置くべきです。
内容は二つのスペースでインデントします。
} は行の最初の文字であるべきです。
if (y < 0 && debug) {
message("y is negative")
}
if (y == 0) {
if (x > 0) {
log(x)
} else {
message("x is negative or zero")
}
} else {
y^x
}
test_that("call1 returns an ordered factor", {
expect_s3_class(call1(x, y), c("factor", "ordered"))
})
tryCatch(
{
x <- scan()
cat("Total = ", sum(x), "\n")
},
warning = function(w) {
cat("Warning: ", w$message, "\n")
},
error = function(e) {
cat("Error: ", e$message, "\n")
}
)
Copy code
if (y < 0 && debug)
{
message("y is negative")
}
if (y == 0)
{
if (x > 0)
{
log(x)
}
else
{
message("x is negative or zero")
}
}
else
{
y^x
}
test_that("call1 returns an ordered factor", { expect_s3_class(call1(x, y), c("factor", "ordered")) })
tryCatch(
{
x <- scan()
cat("Total = ", sum(x), "\n")
},
warning = function(w) {
cat("Warning: ", w$message, "\n")
},
error = function(e) {
cat("Error: ", e$message, "\n")
}
)
2.4.2 if 文
if を使用する場合、else は } と同じ行に置くべきです。
& と | は if 節内で使用しないでください。これらはベクトルを返す可能性があります。代わりに && と || を使用してください。
注: ifelse(x, a, b) は if (x) a else b の代わりにはなりません。ifelse() はベクトル化されており(つまり、x の長さが 1 より大きい場合、a と b は一致するように再利用されます)、また、即時評価されます(つまり、a と b は常に評価されます)。
単純だが長い if ブロックを再構築したい場合は、すべてを 1 行に書いてしまいましょう。
message <- if (x > 10) "big" else "small"
if (x > 10) {
message <- "big"
} else {
message <- "small"
}
2.4.3 インライン文
副作用がない非常に単純な文については、1 行に収まる限り波括弧を省略しても問題ありません。
y <- 10
x <- if (y < 20) "Too low" else "Too high"
制御フローに影響を与える関数呼び出し(例えば return()、stop()、continue など)は、常に自分専用の {} ブロック内に置くべきです。
if (y < 0) {
stop("Y is negative")
}
find_abs <- function(x) {
if (x > 0) {
return(x)
}
x * -1
}
if (y < 0) stop("Y is negative")
if (y < 0)
stop("Y is negative")
find_abs <- function(x) {
if (x > 0) return(x)
x * -1
}
2.4.4 暗黙の型変換
if 文内での暗黙の型変換(例えば、数値から論理値への変換)は避けるべきです。
if (length(x) > 0) {
# 何かをする
}
if (length(x)) {
# 何かをする
}
2.4.5 switch 文
位置ベースの switch() 文は避け、名前ベースを好むべきです。 各要素はそれぞれの行に置きます。 次の要素に流れる要素には、= の後にスペースを入れます。 入力を事前に検証していない限り、フォールスルーエラーを提供してください。
switch(x,
a = ,
b = 1,
c = 2,
stop("Unknown `x`", call. = FALSE)
)
```r:悪い例
switch(x, a = , b = 1, c = 2)
switch(x, a =, b = 1, c = 2)
switch(y, 1, 2, 3)
2.5 関数の引数
関数の引数には、通常、説明的でわかりやすい名前を使用します。オプション引数が複数ある場合は、必須の引数から始めると良いでしょう。
mean(x, na.rm = TRUE)
mean(x, TRUE)
2.6 文字列
2.6.1 ダブルクオーテーション
文字列には常にダブルクオーテーションを使用してください。
"my string"
'my string'
2.7 コメント
コメントの各行は、コメント記号とその後に単一のスペースで始めるべきです: #
データ分析コードでは、重要な発見や分析の決定を記録するためにコメントを使用してください。コードが何をしているのかを説明するためにコメントが必要な場合は、コードをより明確にするために書き直すことを検討してください。もしコメントの数がコードの数より多くなってしまった場合は、R Markdown に切り替えることを検討してください。
# これからデータをフィルタリングします
data <- data %>%
filter(variable > threshold)
data <- data %>%
filter(variable > threshold) # 変数のしきい値を超えているか確認
2.8 データ
2.8.1 文字ベクトル
テキストを引用する際には、' ではなく " を使用します。唯一の例外は、テキストにダブルクォートがすでに含まれていて、シングルクォートが含まれていない場合です。
"Text"
'Text with "quotes"'
'<a href="http://style.tidyverse.org">A link</a>'
'Text'
'Text with "double" and \'single\' quotes'
2.8.2 論理ベクトル
T と F よりも TRUE と FALSE を使用することを推奨します。
2.9 コメント
コメントの各行は、コメント記号と単一のスペースで始めるべきです: #
データ分析コードでは、重要な発見や分析の決定を記録するためにコメントを使用します。コードが何をしているのかを説明するためのコメントが必要な場合は、コードをより明確に書き直すことを検討してください。コメントがコードよりも多くなることに気づいた場合は、R Markdown に切り替えることを検討してください。
3 関数
3.1 名前付け
オブジェクト名に関する一般的なアドバイスに従うだけでなく、関数名には動詞を使用するよう努めます。
Copy code
add_row()
permute()
Copy code
row_adder()
permutation()
3.2 長い行
関数名と定義が1行に収まらない場合には、2つのオプションがあります。
関数インデント: 各引数を別々の行に配置し、関数の開き括弧 ( に合わせてインデントします。
long_function_name <- function(a = "a long argument",
b = "another argument",
c = "another long argument") {
# 通常のコードは2スペースでインデントされます。
}
ダブルインデント: 各引数を別々のダブルインデントされた行に配置します。
long_function_name <- function(
a = "a long argument",
b = "another argument",
c = "another long argument") {
# 通常のコードは2スペースでインデントされます。
}
いずれの場合も、閉じ括弧 ) と開き中括弧 { は最後の引数と同じ行に配置するべきです。
関数インデントスタイルをダブルインデントスタイルよりも推奨します。これらのスタイルは、関数定義とその本体を明確に分けるために設計されています。
long_function_name <- function(a = "a long argument",
b = "another argument",
c = "another long argument") {
# ここでは定義がどこで終わり、コードがどこから始まるのか
# を見つけるのが難しく、3つの関数引数すべてを確認するのも
# 難しいです。
}
関数引数が1行に収まらない場合、それは引数を短くて分かりやすく保つ必要があるというサインです。
3.3 return()
早期リターンのためにのみ return() を使用し、それ以外の場合は R に最後に評価された式の結果を返させるようにします。
find_abs <- function(x) {
if (x > 0) {
return(x)
}
x * -1
}
add_two <- function(x, y) {
x + y
}
add_two <- function(x, y) {
return(x + y)
}
リターン文は制御フローに重要な影響を与えるため、常に独立した行に配置するべきです。インラインステートメントも参照してください。
find_abs <- function(x) {
if (x > 0) {
return(x)
}
x * -1
}
find_abs <- function(x) {
if (x > 0) return(x)
x * -1
}
関数が主に副作用(印刷、プロット、ディスクへの保存など)のために呼び出される場合、その関数は最初の引数を見えなくして返すべきです。これにより、関数をパイプの一部として使用することが可能になります。print メソッドは通常このように行います。例えば httr からの例です:
print.url <- function(x, ...) {
cat("Url: ", build_url(x), "\n", sep = "")
invisible(x)
}
3.4 コメント
コード内では、「何を」や「どうやって」ではなく「なぜ」を説明するためにコメントを使用します。コメントの各行は、コメント記号と単一のスペースで始めるべきです: #
# データフレームのようなオブジェクトはリーフとして扱われます
x <- map_if(x, is_bare_list, recurse)
# データフレームのようなオブジェクトはリーフとして扱われます。
x <- map_if(x, is_bare_list, recurse)
コメントは文のケース(センテンスケース)で書き、2つ以上の文が含まれている場合にのみ句点で終わるべきです。
# データフレームのようなオブジェクトはリーフとして扱われます
x <- map_if(x, is_bare_list, recurse)
# `is.list()` は使用しないでください。データフレームのようなオブジェクトは
# リーフとして扱われるべきです。
x <- map_if(x, is_bare_list, recurse)
# データフレームのようなオブジェクトはリーフとして扱われます
x <- map_if(x, is_bare_list, recurse)
4 パイプ
4.1 はじめに
アクションのシーケンスを強調するために %>% を使用し、アクションが適用されるオブジェクトを強調するのではなく、アクションの流れを示します。
複数のオブジェクトを一度に操作する必要がある場合にはパイプを使用しないでください。パイプは、1つの主要なオブジェクトに対して適用される一連のステップに予約してください。
意味のある中間オブジェクトがあり、それに情報を与えた名前を付けることができる場合には、パイプの使用を避けるべきです。
4.2 空白
%>% の前には常にスペースを入れ、通常は新しい行に続けて書くべきです。最初のステップの後は、各行を2スペースでインデントします。この構造により、新しいステップを追加したり(または既存のステップを再配置したり)するのが容易になり、ステップを見逃すことが少なくなります。
iris %>%
group_by(Species) %>%
summarize_if(is.numeric, mean) %>%
ungroup() %>%
gather(measure, value, -Species) %>%
arrange(value)
iris %>% group_by(Species) %>% summarize_all(mean) %>%
ungroup %>% gather(measure, value, -Species) %>%
arrange(value)
4.3 長い行
関数の引数が1行に収まらない場合は、各引数を別々の行に配置し、インデントします。
iris %>%
group_by(Species) %>%
summarise(
Sepal.Length = mean(Sepal.Length),
Sepal.Width = mean(Sepal.Width),
Species = n_distinct(Species)
)
4.4 短いパイプ
1ステップのパイプは1行に収めても構いませんが、後で拡張する予定がない場合は、通常の関数呼び出しに書き換えることを検討してください。
iris %>% arrange(Species)
iris %>%
arrange(Species)
arrange(iris, Species)
短いパイプを長いパイプ内の関数の引数として含めることが有用な場合もあります。コードが短いインラインパイプ(他の場所での検索が不要)で読みやすいか、コードをパイプの外に移動させて説明的な名前を付ける方が良いかを慎重に考慮してください。
x %>%
select(a, b, w) %>%
left_join(y %>% select(a, b, v), by = c("a", "b"))
x_join <- x %>% select(a, b, w)
y_join <- y %>% select(a, b, v)
left_join(x_join, y_join, by = c("a", "b"))
4.5 引数なし
magrittr は引数がない関数で () を省略することを許可しますが、この機能は避けるべきです。
x %>%
unique() %>%
sort()
Copy code
x %>%
unique %>%
sort
4.6 代入
3つの許可される代入形式があります:
変数名と代入を別々の行にする形式:
iris_long <-
iris %>%
gather(measure, value, -Species) %>%
arrange(-value)
変数名と代入を同じ行にする形式:
iris_long <- iris %>%
gather(measure, value, -Species) %>%
arrange(-value)
パイプの末尾に -> を使用する形式:
iris %>%
gather(measure, value, -Species) %>%
arrange(-value) ->
iris_long
これは書くのが最も自然だと考えられますが、読みやすさが少し難しくなります。名前が最初に来ることで、パイプの目的を思い出させることができます。
magrittr パッケージは、オブジェクトをインプレースで修正するためのショートカットとして %<>% 演算子を提供しています。この演算子は避けるべきです。
```r:良い例
x <- x %>%
abs() %>%
sort()
x %<>%
abs() %>%
sort()
5 ggplot2
5.1 はじめに
- を使用して ggplot2 レイヤーを分ける際のスタイリングの提案は、パイプラインで %>% を使用する際のスタイルと非常に似ています。
5.2 空白
- の前には常にスペースを入れ、次の行に続けるべきです。たとえプロットが2つのレイヤーしかない場合でも同様です。最初のステップの後は、各行を2スペースでインデントします。
dplyr パイプラインから ggplot を作成する場合、インデントのレベルは1段階だけにしてください。
iris %>%
filter(Species == "setosa") %>%
ggplot(aes(x = Sepal.Width, y = Sepal.Length)) +
geom_point()
iris %>%
filter(Species == "setosa") %>%
ggplot(aes(x = Sepal.Width, y = Sepal.Length)) +
geom_point()
iris %>%
filter(Species == "setosa") %>%
ggplot(aes(x = Sepal.Width, y = Sepal.Length)) + geom_point()
5.3 長い行
ggplot2 レイヤーの引数が1行に収まらない場合は、各引数を別々の行に配置し、インデントします。
ggplot(aes(x = Sepal.Width, y = Sepal.Length, color = Species)) +
geom_point() +
labs(
x = "Sepal width, in cm",
y = "Sepal length, in cm",
title = "Sepal length vs. width of irises"
)
ggplot(aes(x = Sepal.Width, y = Sepal.Length, color = Species)) +
geom_point() +
labs(x = "Sepal width, in cm", y = "Sepal length, in cm", title = "Sepal length vs. width of irises")
ggplot2 では、データ引数内でフィルタリングやスライシングなどのデータ操作を行うことができますが、これを避けて、プロットを開始する前にパイプラインでデータ操作を行うようにしてください。
iris %>%
filter(Species == "setosa") %>%
ggplot(aes(x = Sepal.Width, y = Sepal.Length)) +
geom_point()
ggplot(filter(iris, Species == "setosa"), aes(x = Sepal.Width, y = Sepal.Length)) +
geom_point()
Googleスタイルガイド
Google の R スタイルガイドは、Hadley Wickham 氏の Tidyverse スタイルガイドの派生版であることに注意してください。Tidyverse ガイドと比較した Google の主要な違いと、その背景について説明します。
命名規則
Google では、関数を他のオブジェクトと明確に区別するために、BigCamelCase(キャメルケース)を推奨しています。
DoNothing <- function() {
return(invisible(NULL))
}
プライベート関数の名前は、ドット(ピリオド)で始めるべきです。これにより、その関数の起源や用途が明確になります。
.DoNothingPrivately <- function() {
return(invisible(NULL))
}
以前はオブジェクト名にドットケースを推奨していましたが、S3 メソッドとの混同を避けるために、この方法は推奨しない方向に変わっています。
attach() を使わない
attach() を使用すると、多くのエラーが発生する可能性があるため、Google ではこれを推奨していません。
右辺代入
Google では、右辺代入の使用をサポートしていません。
iris %>%
dplyr::summarize(max_petal = max(Petal.Width)) -> results
このスタイルは他の言語の慣例と大きく異なり、オブジェクトがどこで定義されているのかをコード中で見つけにくくします。例えば、foo <- を検索する方が、foo <- と -> foo(複数行に分かれる可能性がある)の両方を探すよりも簡単です。
明示的に return() を使用
R の暗黙的な return 機能に頼らないでください。オブジェクトを返す意図が明確になるように、return() を使用する方が良いです。
AddValues <- function(x, y) {
return(x + y)
}
AddValues <- function(x, y) {
x + y
}
名前空間の明示
すべての外部関数に対して、名前空間を明示的に指定する必要があります。
purrr::map()
すべての関数を NAMESPACE に取り込むために、@import の Roxygen タグを使用することは推奨していません。Google の R コードベースは非常に大規模であり、すべての関数をインポートすると、名前の衝突のリスクが高まります。
:: の使用にはわずかなパフォーマンスの低下がありますが、コード内の依存関係を理解しやすくするために推奨されています。このルールにはいくつかの例外があります。
- 中置関数(%name%)は常にインポートする必要があります。
- 特定の rlang のプロナウンス(特に .data)はインポートする必要があります。
- 標準 R パッケージ(datasets、utils、grDevices、graphics、stats、methods)の関数。必要に応じて、パッケージ全体をインポートしてもかまいません。
関数をインポートする際には、外部依存関係が使用される関数の Roxygen ヘッダーに @importFrom タグを配置します。
ドキュメンテーション
パッケージレベルのドキュメンテーション
すべてのパッケージには、packagename-package.R ファイルにパッケージドキュメンテーションファイルを用意する必要があります。
Discussion