関数の説明
最終更新日:2024-11-22 | ページの編集
所要時間: 60分
概要
質問
- Rで新しい関数を書くにはどうすればよいですか?
目的
- 引数を受け取る関数を定義する。
- 関数から値を返す。
- 関数内で
stopifnot()
を使って引数の条件を確認する。 - 関数をテストする。
- 関数引数のデフォルト値を設定する。
- プログラムを小さく、単一目的の関数に分割する理由を説明する。
1つのデータセットだけを分析するのであれば、スプレッドシートにデータをロードして単純な統計をプロットする方が速いかもしれません。 しかし、gapminder データは定期的に更新されるため、将来的に新しい情報を取得して分析を再実行する必要があるかもしれません。 また、別のソースから類似のデータを取得することも考えられます。
このレッスンでは、関数の書き方を学び、複数の操作を1つのコマンドで繰り返せるようにします。
関数とは?
関数は一連の操作をまとめ、継続的に使用できるようにします。 関数には以下の利点があります:
- 覚えやすい名前で呼び出すことができる。
- 個々の操作を覚える必要がなくなる。
- 定義された入力と期待される出力がある。
- プログラミング環境全体との豊かな接続が得られる。
ほとんどのプログラミング言語の基本的な構成要素として、ユーザー定義関数は「プログラミング」を代表する抽象概念と言えます。 関数を書いたことがあれば、あなたはコンピュータープログラマーです。
関数の定義
functions/
ディレクトリ内に新しい R
スクリプトファイルを開き、functions-lesson.R
という名前を付けます。
関数の一般的な構造は次のようになります:
R
my_function <- function(parameters) {
# 操作を実行
# 値を返す
}
Fahrenheit から Kelvin に温度を変換する関数
fahr_to_kelvin()
を定義してみましょう:
R
fahr_to_kelvin <- function(temp) {
kelvin <- ((temp - 32) * (5 / 9)) + 273.15
return(kelvin)
}
fahr_to_kelvin()
を function
の出力として割り当てることで定義します。
引数名のリストは括弧内に含まれています。 次に、関数の本体である実行される文を波括弧
({}
) 内に記述します。
本体内の文はインデント(スペース2つ)されています。これによりコードが読みやすくなりますが、コードの動作には影響しません。
関数を作成することはレシピを書くことに似ています。 最初に関数が必要とする「材料」を定義します。この場合、材料は「temp」の1つだけです。 次に、材料をどのように処理するかを記述します。この例では、材料に数値操作を適用しています。
関数を呼び出すとき、引数として渡された値は関数内で使用できる変数に割り当てられます。 関数内ではreturn文を使用して、呼び出し元に結果を返します。
ヒント
Rの特有の機能として、return文は必須ではありません。 Rは関数本体の最後の行にある変数を自動的に返します。 ただし、明示的に return 文を定義することでコードがより明確になります。
関数を実行してみましょう。 自分で定義した関数を呼び出すのは、他の関数を呼び出すのと同じです:
R
# 水の凝固点
fahr_to_kelvin(32)
出力
[1] 273.15
R
# 水の沸点
fahr_to_kelvin(212)
出力
[1] 373.15
チャレンジ 1
Kelvin の温度を受け取り、Celsius に変換して返す関数
kelvin_to_celsius()
を作成してください。
ヒント: Kelvin を Celsius に変換するには、273.15 を引きます。
Kelvin の温度を受け取り、Celsius に変換して返す関数
kelvin_to_celsius
を作成します:
R
kelvin_to_celsius <- function(temp) {
celsius <- temp - 273.15
return(celsius)
}
関数の組み合わせ
関数の本当の力は、目的に応じて組み合わせたり再利用したりすることで得られます。
Fahrenheit を Kelvin に、Kelvin を Celsius に変換する2つの関数を定義してみましょう:
R
fahr_to_kelvin <- function(temp) {
kelvin <- ((temp - 32) * (5 / 9)) + 273.15
return(kelvin)
}
kelvin_to_celsius <- function(temp) {
celsius <- temp - 273.15
return(celsius)
}
チャレンジ 2
上記の2つの関数を再利用して、Fahrenheit を直接 Celsius に変換する関数を定義してください。
Fahrenheit を直接 Celsius に変換する関数を定義します:
R
fahr_to_celsius <- function(temp) {
temp_k <- fahr_to_kelvin(temp)
result <- kelvin_to_celsius(temp_k)
return(result)
}
インタールード: 防御的プログラミング
関数を書くことで R コードを再利用可能かつモジュール化できる効率的な方法を理解したところで、関数が意図した用途でのみ動作するようにすることが重要です。 引数の条件を確認することは、防御的プログラミングの概念に関連しています。 防御的プログラミングでは、頻繁に条件を確認し、何か問題があればエラーをスローします。
stopifnot()
を使った条件の確認
例えば、fahr_to_kelvin()
を以下のように改良できます:
R
fahr_to_kelvin <- function(temp) {
stopifnot(is.numeric(temp))
kelvin <- ((temp - 32) * (5 / 9)) + 273.15
return(kelvin)
}
適切な入力では正常に動作しますが、不適切な入力では即座にエラーをスローします。
チャレンジ 3
fahr_to_celsius()
関数で、防御的プログラミングを使用して、引数 temp
が不適切に指定された場合にエラーをスローするようにしてください。
stopifnot()
を追加して、引数を明示的に確認するようにします:
R
fahr_to_celsius <- function(temp) {
stopifnot(is.numeric(temp))
temp_k <- fahr_to_kelvin(temp)
result <- kelvin_to_celsius(temp_k)
return(result)
}
関数の組み合わせについてさらに詳しく
次に、データセットから国の国内総生産(GDP)を計算する関数を定義します:
R
# データセットを受け取り、人口列と
# 1人当たりGDP列を掛け合わせます。
calcGDP <- function(dat) {
gdp <- dat$pop * dat$gdpPercap
return(gdp)
}
calcGDP()
は function
の出力として定義されます。 引数名のリストは括弧内に含まれています。
次に、関数を呼び出したときに実行される文(関数の本体)は波括弧
({}
) 内に記述されます。
本体内の文をインデント(スペース2つ)しました。これにより、コードが読みやすくなりますが、動作には影響しません。
関数を呼び出すと、渡された値が引数に割り当てられ、それが関数の本体内の変数になります。
関数内では、return()
関数を使用して結果を返します。
ただし、return()
関数は任意です。Rは関数本体の最後の行にあるコマンドの結果を自動的に返します。
R
calcGDP(head(gapminder))
出力
[1] 6567086330 7585448670 8758855797 9648014150 9678553274 11697659231
あまり情報量が多くありませんね。では、引数を追加して、年や国ごとにデータを抽出できるようにしましょう。
R
# データセットを受け取り、人口列と
# 1人当たりGDP列を掛け合わせます。
calcGDP <- function(dat, year=NULL, country=NULL) {
if(!is.null(year)) {
dat <- dat[dat$year %in% year, ]
}
if (!is.null(country)) {
dat <- dat[dat$country %in% country,]
}
gdp <- dat$pop * dat$gdpPercap
new <- cbind(dat, gdp=gdp)
return(new)
}
これらの関数を別の R
スクリプトに書き出している場合(おすすめです!)、source()
関数を使用してRセッションに読み込むことができます:
R
source("functions/functions-lesson.R")
では、この関数で行われる処理について簡単に説明します。 英語でいうと、次のような流れです:
- 提供されたデータを年で絞り込みます(
year
引数が空でない場合)。 - その結果を国でさらに絞り込みます(
country
引数が空でない場合)。 - 上記の2つの手順から得られたサブセットに対してGDPを計算します。
- サブセットにGDP列を新しい列として追加し、これを最終結果として返します。
この出力は、単なる数値ベクトルよりもはるかに有用です。
年を指定するとどうなるか見てみましょう:
R
head(calcGDP(gapminder, year=2007))
出力
country year pop continent lifeExp gdpPercap gdp
12 Afghanistan 2007 31889923 Asia 43.828 974.5803 31079291949
24 Albania 2007 3600523 Europe 76.423 5937.0295 21376411360
36 Algeria 2007 33333216 Africa 72.301 6223.3675 207444851958
48 Angola 2007 12420476 Africa 42.731 4797.2313 59583895818
60 Argentina 2007 40301927 Americas 75.320 12779.3796 515033625357
72 Australia 2007 20434176 Oceania 81.235 34435.3674 703658358894
特定の国の場合:
R
calcGDP(gapminder, country="Australia")
出力
country year pop continent lifeExp gdpPercap gdp
61 Australia 1952 8691212 Oceania 69.120 10039.60 87256254102
62 Australia 1957 9712569 Oceania 70.330 10949.65 106349227169
63 Australia 1962 10794968 Oceania 70.930 12217.23 131884573002
64 Australia 1967 11872264 Oceania 71.100 14526.12 172457986742
65 Australia 1972 13177000 Oceania 71.930 16788.63 221223770658
66 Australia 1977 14074100 Oceania 73.490 18334.20 258037329175
67 Australia 1982 15184200 Oceania 74.740 19477.01 295742804309
68 Australia 1987 16257249 Oceania 76.320 21888.89 355853119294
69 Australia 1992 17481977 Oceania 77.560 23424.77 409511234952
70 Australia 1997 18565243 Oceania 78.830 26997.94 501223252921
71 Australia 2002 19546792 Oceania 80.370 30687.75 599847158654
72 Australia 2007 20434176 Oceania 81.235 34435.37 703658358894
または、両方を指定する場合:
R
calcGDP(gapminder, year=2007, country="Australia")
出力
country year pop continent lifeExp gdpPercap gdp
72 Australia 2007 20434176 Oceania 81.235 34435.37 703658358894
関数の本体を詳しく見てみましょう:
ここでは、year
と country
の2つの引数を追加しました。 これらにはデフォルト引数として
NULL
を設定しています。これは、ユーザーが別の値を指定しない限り、引数がこの値を取ることを意味します。
R
if(!is.null(year)) {
dat <- dat[dat$year %in% year, ]
}
if (!is.null(country)) {
dat <- dat[dat$country %in% country,]
}
ここでは、各追加引数が NULL
かどうかを確認し、NULL
でない場合は、dat
に格納されているデータセットを該当する引数に基づいて絞り込みます。
条件を組み込むことで、関数を柔軟に使用できるようにしました。 これで以下の用途に対応できます:
- データセット全体の計算
- 単一の年の計算
- 単一の国の計算
- 年と国の組み合わせの計算
さらに %in%
を使用することで、複数の年や国を引数に渡すこともできます。
ヒント:値渡し (Pass by value)
R の関数はほぼ常に、関数本体内で操作するためにデータをコピーします。
関数内で dat
を変更しても、それは関数に渡されたオリジナルの
gapminder
データセットではなく、コピーされた
dat
に対してのみ適用されます。
これは「値渡し」と呼ばれ、コードを書く際に安全性が高まります。 関数本体内で行った変更は、本体内にとどまることが保証されます。
ヒント:関数スコープ (Function scope)
もう1つの重要な概念はスコープです。
関数本体内で作成または変更された変数(または関数)は、その関数の実行中にのみ存在します。
たとえば、calcGDP()
を呼び出すと、変数
dat
、gdp
、new
は関数本体内にのみ存在します。
同じ名前の変数がインタラクティブRセッション内にあっても、それらは実行中の関数に影響を受けません。
最後に、新しいサブセットに基づいてGDPを計算し、その列を追加した新しいデータフレームを作成しました。 このようにすると、関数を後で呼び出したときに、返されたGDP値の文脈を確認でき、最初の試行で得られた数値ベクトルよりもはるかに有用です。
チャレンジ 4
1987年のニュージーランドのGDPを計算してみましょう。 1952年のニュージーランドのGDPとどう違いますか?
R
calcGDP(gapminder, year = c(1952, 1987), country = "New Zealand")
1987年のニュージーランドのGDP: 65,050,008,703
1952年のニュージーランドのGDP: 21,058,193,787
チャレンジ 5
paste()
関数は、テキストを結合するために使用できます。例:
R
best_practice <- c("Write", "programs", "for", "people", "not", "computers")
paste(best_practice, collapse=" ")
出力
[1] "Write programs for people not computers"
2つの引数 text
と wrapper
を取る関数
fence()
を作成し、text
を wrapper
で囲んだテキストを出力してください:
R
fence(text=best_practice, wrapper="***")
注: paste()
関数には sep
という引数があり、テキスト間の区切り文字を指定します。
デフォルトはスペース " "
です。paste0()
のデフォルトはスペースなしの ""
です。
2つの引数 text
と wrapper
を取る関数
fence()
を作成し、text
を wrapper
で囲んだテキストを出力します:
R
fence <- function(text, wrapper){
text <- c(wrapper, text, wrapper)
result <- paste(text, collapse = " ")
return(result)
}
best_practice <- c("Write", "programs", "for", "people", "not", "computers")
fence(text=best_practice, wrapper="***")
出力
[1] "*** Write programs for people not computers ***"
ヒント
Rには、複雑な操作を行う際に活用できる独自の機能があります。 ここでは、これらの高度な概念を必要とする内容を書くことはありません。 将来的にRで関数を書くことに慣れたら、R言語マニュアル や Hadley Wickham の Advanced R Programming にあるこの章を読んで学びを深めてください。
ヒント:テストとドキュメント
関数をテストしてドキュメント化することは非常に重要です: ドキュメントは、関数の目的、使い方を理解する助けとなり、関数が意図した通りに動作するか確認することも重要です。
初めのうちは、ワークフローは以下のようになるでしょう:
- 関数を書く
- 関数の動作をドキュメント化するためにコメントを追加する
- ソースファイルを読み込む
- コンソールで関数を試して期待通りに動作するか確認する
- 必要なバグ修正を行う
- 繰り返す
関数の正式なドキュメントは、.Rd
ファイルに記述され、ヘルプファイルで見ることができるドキュメントに変換されます。
roxygen2
パッケージを使用すると、関数コードに沿ってドキュメントを記述し、適切な
.Rd
ファイルに処理できます。
より複雑なRプロジェクトを書くようになったら、この形式的なドキュメント作成方法に切り替えることを検討してください。
実際、パッケージとは、形式的なドキュメント付きの関数の集合です。
自分の関数を source("functions.R")
を使用して読み込むのは、他人の関数(または将来的には自分のパッケージ!)を
library("package")
を通じて読み込むのと本質的に同じです。
正式な自動化テストは、testthat パッケージを使用して記述できます。
まとめ
- Rで新しい関数を定義するには
function
を使用する。 - 関数に値を渡すにはパラメータを使用する。
- Rで
stopifnot()
を使って関数引数を柔軟にチェックする。 -
source()
を使用してプログラムに関数を読み込む。