Rを使ったプログラミング演習2: シミュレーション †はじめに Rでプログラミングの授業を始めたころ、受講生から次のような質問があった。 「こういうシミュレーションのプログラミングって、これから生物学を学ぶのに、どのくらい必要なんですか?」 これまでプログラミングなどしたことの無い人にとって、とても素朴な疑問だろうと思う。私からの答えとしては、 画一的な答えは難しいです。シミュレーションそのものが直接必要になるような分野もあれば、 そうでない分野もあります。 ただ、プログラミングは、生物学における発展的な情報処理をする上で欠かせないものだと思います。 千葉大の研究室に限って言えば、プログラミング技術が無いと卒業研究が終わらないというところは 現時点では(たぶん)無いでしょう。但し、プログラミングができると、卒業研究や修士の研究でより 進んだことができたり、理解が深まったり、研究が楽になるようなところはいくつもあります。 将来、数理生物学、理論集団遺伝学、進化生態学、バイオインフォマティクスを志す人には、 不可欠な技術でしょう。 第11回授業の獲得目標: †
繰り返し・代入・条件分岐のおさらい †次のそれぞれのプログラムの_部分に何を入れるべきかを選択肢から選びなさい。また、Rを使って実行してみよう。 繰り返し †
代入 †
条件分岐 †
プログラミングTips †・1行に1つの命令を書いたら、必ず改行 ・{ } のそれぞれの後で改行すると見やすい(このページの例を参照) ユーザー定義関数 †「ユーザー定義関数」なんていうと難しそうだが、ようするに、先ほどまでに作ったプログラムに名前をつけて、いろいろと数値を変えて解析できるようにしようというものだ。こういうときに関数を定義する関数、function()を使う。 例えば、円の面積を計算する関数を作るなら、 menseki=function(r){r*r*pi} とすれば良い。これで自分独自の関数menseki()ができた。 では、実行してみよう。 menseki(10) #好きな数字を入れて円の面積を計算 演習 1: 1から入力した数値までの全てを横一列に表示させるプログラムを作りdisplayという名前の関数として定義しなさい display=____(a){ #関数定義の始まり kekka=c() #kekkaに空ベクトルを代入して初期化 for(i in 1:a){ #a回(iの値を1からaまで変化させる間)繰り返し kekka=c(kekka,i) #kekkaというベクトルにiを要素として代入 } print(kekka) #kekkaの内容を表示 } #関数定義の終わり ↑を使って何回も実行してみると、結果がいろいろ変わるのがわかる。例えば10を入れて実行するには、 display(10) 演習2: 上の関数の定義方法に従って、入力した数までの合計値を計算するsumupという名前の関数を作成する。下の囲みの中の_の部分(1文字に対応するとは限らない)を埋めて、プログラムを完成しなさい。 sumup = ______ #関数sumpuを定義 kotae = _ #kotaeを初期化 _ (i in ___){ #1からaまで繰り返し kotae = _____ #kotaeにiの値を足したものとkotaeに代入 } #繰り返し終了 _____ #kotaeを表示 } #関数定義の終了 授業で使ったRの基本関数 †→授業/H20/情報処理/R関数一覧 これまでに学んだRのいろんな関数を一覧できる。 Rを用いたシミュレーション †これまでに学んだ繰り返し・代入・条件分岐の3つの命令それぞれは単純だが、組み合わせれば、かなり複雑なこと表現できる。そこで、今回は、これらの命令を組み合わせて、Rを用いたシミュレーションに挑戦してみよう。 円周率(π)を「繰り返し」・「乱数」・「条件分岐」で求める †
正方形の面積は 1 扇形の面積は π/4 この図形の上に、砂粒をばらまいてみる。 砂粒が図形の上にランダムに散らばるとすると、砂粒をすごく沢山まいて数を数えれば、 正方形の中の砂粒の数(n) : 扇形の中砂粒の数(m) = 正方形の面積 :扇形の面積 = 1 : π/4 になると考えられる。つまり n : m = 1 : π/4 だから、 π = 4m/n もしあなたが、砂粒をたくさんランダムにまいて、正方形の中の砂粒と扇形の中の砂粒の数を数えることができれば、πの値がわかるということになる。自分で実際に実験することを想像すると、恐ろしく大変だが、そういう面倒な実験はコンピュータにやらせてしまえばいい! シミュレーションの考え方 †さて、砂粒を沢山まいて、それが円の中か外かを判定する実験を、コンピュータにやらせるにはどうすればよいだろうか。
まず、点(砂粒)を大量に発生させる必要がある。さて、どうすればいいだろうか? #とりあえず、1,000つぶの砂をまくことをfor文で表現する for (i in 1:1000){ <砂を1粒図形の上にまく> } 次に、砂を1粒図形の上にまいて、それが扇形の中か外かを判定することを考える。 砂を1粒まくことは、無作為に(ランダムに)図形の上に点を打つことと同じことだ。「ランダム」というと、我々はすでに乱数を発生させる方法をしっている。図形の上に1つの点を打つということは、2つの乱数を発生させることで表現できるだろう。
sqrt(0.230^2+0.782^2) < 1 となり、ランダムに発生させた(図形の上に偶然落ちた)1つの点は、扇形の中だったということになる。 point=runif(2) #この命令で、0-1の範囲の乱数を2つ発生させ、pointというベクトルに入れる このとき、pointに入ったそれぞれの値を指定するには、 point[1] point[2] のようにカギ括弧[]を使う。 例: []の使い方を、試してみよう > x=runif(2) > x > x[1] > x[2] 砂粒(点)が扇形の中かどうかを判断するにはどうすれば良いか?これは条件分岐で表現できる。つまり、runif(2)で発生させた2つの数値のそれぞれを2乗して足しあわせたものの平方根が1より小さいか否かを判断する。 #扇形の中かどうかを判断する条件文 distant=sqrt(point[1]^2+point[2]^2) #発生させた2つの乱数の2乗の和の平方根 if(distant<1){ #原点からの距離が1より小さければ扇形の中 <個数を数える> } では、個数を数えるにはどうすればよいか?ここでは代入文を使う #個数を数える部分 kaisu=0 #回数を数えた結果を入れる場所なので、最初の値(初期値)は0にしておく if(distant<1){ #原点からの距離が1より小さければ扇形の中 kaisu = kaisu + 1 #条件にあったとき、kaisuの値を1増やす } 以上のように、これまでに学習した、繰り返し、条件分岐、代入で、図形の上に砂粒を1000個まく実験が表現できた。πの値は砂粒の個数を4倍して、全体の個数で割ったものだから、以上をまとめると次のようになる。 kaisu=0; #個数の初期値を0にする for (i in 1:1000) { point=____(2) ____=sqrt(point[1]^2+point[2]^2) if(distant<1){ kaisu=kaisu+1 } } answer=4*____/1000 print(answer) さらにユーザー定義関数:function()を使って、好きな回数繰り返せる関数を定義してみよう simpai=function(x){ kaisu=0; #個数の初期値を0にする for (i in 1:x) { point=runif(2) distant=sqrt(point[1]^2+point[2]^2) if(distant<1){ kaisu=kaisu+1 } } answer=4*kaisu/x print(answer) } > simpai(10) [1] 2.8 > simpai(100) [1] 3.16 > simpai(1000) [1] 3.156 > simpai(10000) [1] 3.1624 > simpai(100000) [1] 3.15652 > simpai(1000000) #ここから先は時間がかかるので、授業中はやらない方がいい [1] 3.13994 > simpai(10000000) #ここから先は時間がかかるので、授業中はやらない方がいい [1] 3.140556 せっかくだから、点をうつところを図で表現してみよう †plot()関数でylimというオプションを使うと、グラフの軸の範囲を指定できる(詳しくはマニュアルを参照) simpai=function(x){ kaisu=0; #回数の初期値を0にする for (i in 1:x) { point=runif(2) par(new=T) #複数のグラフを重ね合わせる plot(point[1], point[2], ylim=c(0,1), xlim=c(0,1)) #0-1の範囲で乱数の値をプロット distant=sqrt(point[1]^2+point[2]^2) if(distant<1){ kaisu=kaisu+1 } } answer=4*kaisu/x print(answer) } 参考: 円周率の計算を乱数を使わずに行うプログラム †
simpai=function(x){ kaisu=0; #個数の初期値を0にする for (_ in 1:_) { for (_ in 1:_) { point=c(___,___) distant=sqrt(point[1]^2+point[2]^2) if(distant<1){ kaisu=kaisu+1 } } } answer=4*kaisu/x^2 print(answer) } 遺伝的浮動のシミュレーションに挑戦する! †次に、生物学科の学生なら、入試のときに必ず勉強している遺伝的浮動のシミュレーションをやってみよう。 大きさの決まった集団で起きる、配偶子のランダムサンプリングによる遺伝子頻度の変動 のことだ。例えば、集団中の2つの対立遺伝子を、青玉と赤玉で表すと、袋(配偶子の集団: 配偶子の数はものすごく多く、青と赤の比率はそれぞれ50%)から取り出される青玉・赤玉の割合(集団中の遺伝子頻度)には、下の図に示したような変動が生じる(下の例で、青玉の遺伝子頻度は、0.8, 0.7, 1.0, と変動している)。もし、袋から取り出されたのが青玉ばっかり(あるいは、赤玉ばっかり)だと、それ以降、何度繰り返しても変化は生じない(下の例だと、「集団は青玉に固定した」などという)。 遺伝的浮動のシミュレーションをプログラミングして、赤玉と青玉の変動の度合いをグラフ化する †それでは、赤玉と青玉が同じ数だけたくさん入った袋から、10個の玉を取り出して、その中に含まれる青玉の割合に従って袋に含まれる青玉配偶子の個数を変え、また10個取り出す。。。こういう操作を何度も繰り返すと、取り出した玉のうちの青玉と赤玉の割合の変動がわかる。 自分で何度も実験を繰り返してもいいけれど、とても面倒! そこで、この実験をコンピュータにやらせ、取り出した玉の割合がどのように変化するかをグラフにしよう!
シミュレーションの考え方 †もう皆さんは"繰り返し、代入、条件分岐''というプログラミングの基本技を修得しているので、今問題にしている実験を、この3つのワザを使ってどのように表現するかが、プログラミングの重要なポイントだ。
実行してみると、うまくいってるようだが、1回ずつ、グラフが変わってしまう。グラフを重ねて描くには、par(new=T) というコマンドをplot()の前に使う。もしいくつもグラフを重ねて描いて、グラフが汚くなってしまったら、 frame() か plot.new() というコマンドを使ってグラフをクリアする。
結果=c() #空ベクトル 青玉の頻度=0.5 #初期値は0.5 for ( j in 1:100 ) { #さっきiという変数を使ったので、ここではjを使う カウンタ = 0 for ( i in 1:10 ) { if (runif(1) < 青玉の頻度 ) { カウンタ = カウンタ + 1 } } 青玉の頻度 = カウンタ/10 #10個の中の青玉の割合を算出 結果=c(結果,青玉の頻度) #青玉の頻度を結果というベクトルに追加 } par(new=T) #グラフを一度クリアする plot(結果, type="l", ____=c(0,1)) #★★ylimを使ってy軸の目盛りを0から1に固定する
発展:様々な変数を指定できる関数を作ってしまう †上のシミュレーションは集団の遺伝子の数が10の場合だけのシミュレーションだった。そこで、遺伝子の数(集団サイズ)、遺伝子頻度の初期値、繰り返し世代数、繰り返し実験数の全てを指定できるような関数を作ってみよう。複数の引数は、function()の中でカンマを使って指定できる drift=function(実験回数, 初期頻度,集団サイズ,世代数){ frame() #グラフをクリアする for(k in 1:実験回数){ #関数に与えた実験回数だけ実験を繰り返す 青玉の頻度=初期頻度 結果=c() #空ベクトル for ( j in 1:世代数 ) { #さっきiという変数を使ったので、ここではjを使う カウンタ = 0 for ( i in 1:集団サイズ ) { if (runif(1) < 青玉の頻度 ) { カウンタ = カウンタ + 1 } } 青玉の頻度 = カウンタ/集団サイズ #集団サイズ個の中の青玉の割合を算出 結果=c(結果,青玉の頻度) #青玉の頻度を結果というベクトルに追加 } par(new=T) plot(結果, type="l", ylim=c(0,1)) #★★ylimを使ってy軸の目盛りを0から1に固定する } } 第11回授業の課題 †
課題1.意見調査 †
課題2:(基本課題:7点)コインを投げる実験のシミュレーション †下のプログラムは、正常なコイン(表、裏の出る確率はそれぞれ0.5)を100回投げたとき表の出た回数を記録するという実験を、1000回繰り返すシミュレーションのプログラムです。 omote=0 #表の出る回数を入れるomoteという変数を初期化 kekka=c() #結果を入れるkekkaという空ベクトルを作る for(【1】){ for (【2】){ if(【3】){ omote=【4】 } } kekka=c(kekka,omote) omote=0 #表の出る回数を初期化 } print(kekka)
課題3:(発展課題:max2点)遺伝的浮動のシミュレーション †プログラミングをもっとやってみたいという人だけ提出してください 下のプログラムは遺伝的浮動をシミュレートするRのプログラムです。このシミュレーションで観察する集団の数(num_repeats)、1集団あたり観察する世代数(num_generations)、集団サイズ(遺伝子数)(size_population)、0世代目における対立遺伝子aの数(num_a_allele)を与えて実行すれば、、ランダムに選ばれる遺伝子が、次世代の集団の遺伝子頻度にどのように影響するかを、シミュレートする集団数(観察集団数)の数だけ色つきの折れ線グラフで表示することができます。下のプログラムを見て以下の問に答えなさい。 drift= function(num_repeats,num_generations,size_population, num_a_allele){ results=c() a=num_a_allele for(【1】){ for(【2】){ count_a=0 for(k in 1:size_population){ if (【3】){ count_a=count_a+1 } } a=count_a results=append(results, 【4】) } a=num_a_allele } rmatrix=matrix(results, nrow=num_generations, ncol=num_repeats) return(matplot(rmatrix, type="l")) }
|