関数の説明

最終更新日: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")

では、この関数で行われる処理について簡単に説明します。 英語でいうと、次のような流れです:

  1. 提供されたデータを年で絞り込みます(year 引数が空でない場合)。
  2. その結果を国でさらに絞り込みます(country 引数が空でない場合)。
  3. 上記の2つの手順から得られたサブセットに対してGDPを計算します。
  4. サブセットに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

関数の本体を詳しく見てみましょう:

R

calcGDP <- function(dat, year=NULL, country=NULL) {

ここでは、yearcountry の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() を呼び出すと、変数 datgdpnew は関数本体内にのみ存在します。 同じ名前の変数がインタラクティブRセッション内にあっても、それらは実行中の関数に影響を受けません。

R

  gdp <- dat$pop * dat$gdpPercap
  new <- cbind(dat, gdp=gdp)
  return(new)
}

最後に、新しいサブセットに基づいて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つの引数 textwrapper を取る関数 fence() を作成し、textwrapper で囲んだテキストを出力してください:

R

fence(text=best_practice, wrapper="***")

注: paste() 関数には sep という引数があり、テキスト間の区切り文字を指定します。 デフォルトはスペース " " です。paste0() のデフォルトはスペースなしの "" です。

2つの引数 textwrapper を取る関数 fence() を作成し、textwrapper で囲んだテキストを出力します:

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 にあるこのを読んで学びを深めてください。

ヒント:テストとドキュメント

関数をテストしてドキュメント化することは非常に重要です: ドキュメントは、関数の目的、使い方を理解する助けとなり、関数が意図した通りに動作するか確認することも重要です。

初めのうちは、ワークフローは以下のようになるでしょう:

  1. 関数を書く
  2. 関数の動作をドキュメント化するためにコメントを追加する
  3. ソースファイルを読み込む
  4. コンソールで関数を試して期待通りに動作するか確認する
  5. 必要なバグ修正を行う
  6. 繰り返す

関数の正式なドキュメントは、.Rd ファイルに記述され、ヘルプファイルで見ることができるドキュメントに変換されます。 roxygen2 パッケージを使用すると、関数コードに沿ってドキュメントを記述し、適切な .Rd ファイルに処理できます。 より複雑なRプロジェクトを書くようになったら、この形式的なドキュメント作成方法に切り替えることを検討してください。 実際、パッケージとは、形式的なドキュメント付きの関数の集合です。 自分の関数を source("functions.R") を使用して読み込むのは、他人の関数(または将来的には自分のパッケージ!)を library("package") を通じて読み込むのと本質的に同じです。

正式な自動化テストは、testthat パッケージを使用して記述できます。

まとめ

  • Rで新しい関数を定義するには function を使用する。
  • 関数に値を渡すにはパラメータを使用する。
  • Rで stopifnot() を使って関数引数を柔軟にチェックする。
  • source() を使用してプログラムに関数を読み込む。