2024年1月2日火曜日

情報量規準による多重比較のRコード



AICなどの情報量規準を用いた多重比較の方法(Dayton 1998)を実装するRコードの紹介です。以前、共著論文で使用する機会があったのですが、久しぶりに掘り返して、だいぶスッキリしたコードへと改定できたので公開することにしました。手作業だと面倒くさかったのでだいぶ手間が省けるはずです。

非常によく見かける解析方法に、ANOVAの後で多重比較(post-hoc tests)というコンボがあリます。いわゆる多重比較の方法は検定(頻度論)に基づいており、Type I errorの回避に特化しているのが気になっていました。一方で情報量規準に基づくモデル選択による解析を行うケースも多いですが、旧来の多重比較(頻度論)も併用するとダブルスタンダードになってしまいます。実際に査読で突っ込まれたこともあります。また多重比較は正規分布を仮定できるデータにしか適用できないことも制約になります。この点についてはHolmの方法など原理的に分布型に制約されない方法もありますが、組み合わせ数が多くなると検出力が低下する問題もあります。

こういった問題をすっきりと解決できる、情報量規準に基づいたモデル選択による多重比較の方法があります。以前、北大・久保先生のスライドで知った手法ですが( https://kuboweb.github.io/-kubo/ce/2004/index.html )、下記のDayton(1998)が同様の手法(PCIC)を発表しており、根拠論文として参照可能です。特に手法名が提唱されているとは知らずに使っている例も多いかもしれませんが。また、名前が知られていないからこそ、統計ソフトにもRパッケージでも実装されてこなかったのかもしれません。

Dayton CM (1998) Information criteria for the paired-comparisons problem. The American Statistician, 52(2):144-151. DOI: 10.1080/00031305.1998.10480554(ResearchGateからも取得可能)

下記の10数行のコードで実装できました。lm, glm, glm.nbなどのモデル式を下記のpcic関数に食わせれば実行できるはずです。

require(partitions) 
# 組み合わせを表現する関数setpartsを使うのに必要なパッケージ、要インストール

# pcic関数の定義、ここから:
pcic <- function(Model,  Data=Model$model, Expl=names(Model$model)[2]) { 
    nLevels <- length(levels(Data[,Expl])) # グループのリスト
    groupPTs <- apply(setparts(nLevels), 2, function(x) as.numeric(factor(x, levels=unique(x))))
    # グルーピングの全パターン、setpartsの標準ではb,a,aなどが出るので正順に並べ替え
    groupPTA <- apply(matrix(letters[1:nLevels][groupPTs], nrow=nLevels), 2, paste, collapse=" ") 
    # アルファベット変換&結合
    AICs <- c(AIC(update(Model, 
        reformulate(c(setdiff(names(Data)[-1],Expl),1), response = names(Data)[1]))),     # 帰無モデル
    sapply(2:ncol(groupPTs), function(i) { # 帰無モデル以外
    Data$groupLv <- factor(letters[1:nLevels][groupPTs[,i]][Data[,Expl]], levels=letters[1:nLevels]) 
    # グループ分けの変数をDataに追加
    AIC(update(Model, reformulate(c(setdiff(names(Data)[-1],Expl),"groupLv"), 
        response = names(Data)[1]), data=Data)) } ) ) # 各モデル
    comps <- data.frame(AIC=AICs, deltaAIC=AICs-min(AICs), Grouping=groupPTA)
    print(paste(c(paste(levels(Data$Species), collapse=", "), "are labeled in this order.",
        Expl, "sharing the same alphabet do not differ in the", names(Data)[1]), collapse=" "))
    return(comps[order(comps$AIC),]) }
# 関数定義ここまで(これを一度R上で実行するとカスタム関数として利用可能になる)

・lm, glm, glm.nbなどのモデルに適用可能です。それ以外でもModel$modelを実行してモデルに使用しているデータ(y, x)が返ってくるならば使用可能、offset項にも対応。
・注意点として、グルーピングのための説明変数は "~"のすぐ後ろの変数と見なす。異なる時は文字で指定する(この例の場合は"Species")
・Data: Model$model # モデルに使用しているデータ:y, x。これで大丈夫なはずだが、自前で指定も可能


実行例:
data(iris) # Sepal.Lengthが種によって異なるかについて多重比較を実行
Model <- glm(Sepal.Length ~ Species, data=iris)
require(partitions)
pcic(Model)

[1] "setosa, versicolor, virginica are labeled in this order. Species sharing the same alphabet do not differ in the Sepal.Length"
       AIC  deltaAIC Grouping
5 231.4520   0.00000    a b c
4 265.6359  34.18393    a b b
3 295.6778  64.22581    a a b
1 372.0795 140.62758    a a a
2 373.1310 141.67904    a b a

一番上がベストモデルです。setosa: a, versicolor: b, virginica: c、のように異なるアルファベットで識別されており、種によってSepal.Lengthが異なることが示されました。このアルファベットによる識別は一般的な多重比較でよく見かける表現なので、多くの人が馴染みがあるかと期待します。

結果を表形式で取り出すには、こんな感じでOKです。
write.csv(pcic(Model), "保存場所/ファイル名.csv")

最後に、post-hoc testの用途について。実験系など個々のグループの意味が明確な場合に適した手法だと思いますが、その場合は頻度論の方が適していそうです。ここまで書いておいて何ですが、個人的にはpcicが必要なケースってそんなに多く無いと思っています。生態学系でよく見かける使用例の多くは回帰分析の方が適しているケースだし(説明変数が連続変数になっている等)、2way ANOVAなどの方が適しているケースも多いように感じます。何の仮定も置かずにpost-hoc testをやってみて、その結果を見てから演繹的に考察をするような使い方をしようとしていたら、少し立ち止まって考え直してみませんか。仮説を立ててそれを検証するための解析を行うという設計さえしていれば、より適切な解析方法が見つかるはずです。

cf. lme4は今のところ非対応です。Data=Model@frameでデータを引っ張って来れそうですが、モデルのアップデートのところの改定がすぐにはできそうもなく。

2023年4月9日日曜日

Rの常用速度を新旧macで比較:Apple Silicon M1 Max vs. Intel Core i9

今更ながら、macでのRの計算速度をIntel版とApple Siliconとで比較してみました。先行するテスト結果は検索するといくつか見つかるのですが、自分にとって普段よく使いそうな実用的用途で試してみたいと思いました。(2023.04.25追記:Intelも第12世代でCPU速度は飛躍的に向上して、以下の比較機種とは状況は異なっている模様)

比較をした計算環境は次の通りです。Rは登稿時の最新 ver. 4.2.3に揃えました。ハードウェアはintelとapple siliconとで実質1年半の差なので、ほぼCPUの差で比較できているかと思います。

(1)macbook pro 14-2021(以下、MBP_M1mと略します)
Apple Silicon M1 Max 10コアCPU、32コアGPU 64GBメモリ


(2)macbook pro 16-inch 2019(以下、MBP_Intelと略します)
Intel Core i9 2.4GHz 8コアCPU 64GBメモリ

(3)新旧macだけでもよかったのですが、Ubuntuを載せたカスタムPCも比較に加えました。
カスタムPC 2019年製(OS: Ubuntu 22.04.1 LTS)(以下、Intel_Ubuntと略します)
Intel Core i9-9980XE Extreme Edition 3.0-4.4GHz 18コアCPU 128GBメモリ

計算は次の4つのケースで実行し比較しました。概ねシングルコアの速度を試す形になっていますが、マルチコアを活用できるケースならば、単にコア数の多いマシンを使えばいいんじゃないかと思うので、制約の掛かりやすそうな事例をわざと選んでいます。


### Case 1: Markov chain
いわゆるMCMC等の計算過程で動いているマルコフ連鎖、値の更新が必要なので一括処理できない計算の例の1つかと。

x <- matrix(0, 10^6, 10)
# 10^6回分のマルコフ連鎖、10本の鎖を同時に計算
system.time( for(r in 2:(nrow(x))) x[r,] <- 0.9*x[r-1,] + rnorm(10) )



### Case 2 raster_distance: raster画像の値あり全地点からの距離計算
都市や島からの距離の計算と思ったら良いです。
require(raster)
require(maptools)
data(wrld_simpl)
rgrid <- raster(ex=extent(c(-180,180,-90,90)), res=0.5)
rwrld <- raster::rasterize(wrld_simpl, rgrid)
system.time(rdst <- raster::distance(rwrld))
0.5度解像度の全世界の陸地からの距離を計算、かなり時間のかかるケース。
(ちなみにwrld_simplは気軽に呼び出しやすい粗い世界地図なのですが、{maptools}が2023年いっぱいで提供終了とのこと。高機能な{rnaturalearth}もよいですが...)



### Case 3 raster_linear_model: raster stackへの回帰モデル適用
重ねたrasterの全値について回帰モデルを適用、calcのhelpにあるコードを高解像度にしただけのもの。単純な回帰モデル(lm)に限定(逆行列を利用した方法も紹介されていますが、応用しづらいので省きます)。
help(calc)
r <- raster(nrow=100, ncol=100) # ここを10倍にした
s1 <- lapply(1:12, function(i) setValues(r, rnorm(ncell(r), i, 3)))
s2 <- lapply(1:12, function(i) setValues(r, rnorm(ncell(r), i, 3)))
s1 <- stack(s1)
s2 <- stack(s2)

# regression of values in one brick (or stack) with another
s <- stack(s1, s2)
# s1 and s2 have 12 layers; coefficients[2] is the slope
fun <- function(x) { lm(x[1:12] ~ x[13:24])$coefficients[2] }
system.time(x1 <- calc(s, fun))



### Case 4: Stan (cmdstan)
コマンドライン版のStanで単純な状態空間モデルの計算、chain毎に異なるスレッドを割り当てているので、他の例と異なり、少しだけマルチコアの恩恵があるはず。コードは長いので、後に載せておきます。



こちらが結果になります。左に行くほど高速です。



概ねMBP_M1mはMBP_intelの2倍弱ほどの速度があることが分かりました。計算時間の掛かるraster_distanceで2.33倍と最も差が大きいのは嬉しい結果です。マルチコアの恩恵があるStanでは1.5倍とさほどの差になっていないのも予想通りではあります。またOSの違いはあまり速度に関係ないようでした。

なお、マルコフ連鎖については、2012年のマシンで5.457秒という手元の記録が残っているので(Late 2012 iMac Intel Core i7 3.4GHz クアッドコア 32GB)、倍速いというのがどれほどの進化かよく分かるかと思います。

ちなみに、関連してCase 2, 3ではrasterパッケージの後継?terraパッケージでも比較をしてみたのですが、ほぼ変わりはなかったです。もしかすると連繋が進んでいたりして内部でterraを利用しているかもしれませんが、あくまで想像です。


もうM2が登場していますが、今回のようなRのシングルコア計算での比較ならば、おそらく速度は10%向上程度かなと見ています。

その他、Apple SiliconはGPUコアや機械学習用のニューラルエンジンを積んでいるので、用途次第では次元の違う速度になる期待もあります。高速なGPUを使用可能なRパッケージってまだあまり聞かないようには思いますが。

あとはSSDがかなり高速になっているそうで、物理メモリが不足した時にSSD領域を仮想メモリとして使用しても、かつてほどの速度低下はしないみたいで、もうさほどメモリに拘らなくてもよいのかもしれません。



# Case 4のcmdstanの計算コード:

require(cmdstanr)
stan_program <- "
data {
    int T;
    vector[T] Y;
}
parameters {
    real mu_Y0;
    vector[T] mu_Y;
    real<lower=0> sigma;
    real<lower=0> SIGMA;
}
model {
    // state model
    mu_Y[1] ~ normal(mu_Y0, sigma);
    for(t in 2:T) {
        mu_Y[t] ~ normal(mu_Y[t-1], sigma);
    }

    // observation model
    for(t in 1:T) {
        Y[t] ~ normal(mu_Y[t], SIGMA);
    }
}"

modfile <- write_stan_file(stan_program)

mod <- cmdstan_model(modfile) # 一時ファイルへ書き出し

data(Nile)
data <- list(Y=Nile, T=length(Nile))

system.time(fit <- mod$sample(
  data = data,
  chains = 4,
  parallel_chains = 4, # 並列化するchainの数
  iter_warmup = 5000, #burn-in数
  iter_sampling = 20000 #butn-in後のsampling数
  )
)

2021年9月30日木曜日

問題を起こすプロセスを定期的に強制終了する

 macの動作を重くしたり不安定にさせたりするプロセス(アプリケーション含む)はなるべく終了させるか排除したくなるものです。純正のものではWindowServer, Falcond, mdworkerなどが重いですが、システムに必要そうなので我慢です。しかし、非純正のプロセスならいっそ落としてしまえば動作が改善する場合があるかもしれません。

このところmacが非常に不安定になっていて、Zoom会議で落ちる、Rで負荷の掛かる処理をすると落ちる、フォルダを移動しただけでもなど、処理内容が重い方が落ちやすいけれど軽くても構わず落ちる(カーネルパニック)現象がひどい時は1〜2日に一度くらいの頻度で起きるという困った状況に陥っていました。これではまったく使い物になりません。もしやと思い、心当たりの非純正アプリケーションを朝・夜にアクティビティモニタから強制終了させてみたところ、それらのトラブルは全て解消されました。残念ながら該当のアプリを削除するわけにはいかなさそうなのですが、この定期的なリフレッシュを自動で実行する仕組みを構築してみました。以下のような手順です。Automatorとカレンダー.appを使用します。

1. まず対象プロセスを強制終了させるアプリケーションを作成する。

1-1.「アプリケーション」から「Automator.app」を立ち上げる。


1-2. すると次のような新規書類が現れるはずなので「ワークフロー」を選択する。


1-3. 「アクション」の中から「シェルスクリプトを実行」を選び、右の枠の中へとドラッグアンドドロップする。


1-4. シェルスクリプトの入力は、「pkill プロセス名」とする。これはプロセス名のプロセス(アプリケーションである場合もある)を強制終了させるコマンドである。「ユーティリティ」の「アクティビティモニタ」にリストアップされるプロセス名で記す。プロセス名は先頭から一部の文字で構わない。また大文字と小文字を区別する必要がある。例えば、対象としたいプロセス名が「Hogehoge1」と「Hogehoge2」の場合、「pkill Hoge」で両方が対象となる。「pkill hoge」だと対象と見なされず何も起こらない。
 

1-5. 「ファイル」メニューから「アプリケーション」として保存する(ワークフローではなく)。保存場所はどこでも良さそう。一度、手動で立ち上げて、ちゃんと狙い通り動作(対象プロセスを強制終了)するか確かめておいた方が良いだろう。


2. 作成したアプリケーションを定期的に実行させる仕組みを作る。
6. 「カレンダー.app」の予定を作成する。アプリを実行したい時間を設定する。さらに「通知」の仕方として「ファイル(イベントの開始時刻)を開きます」を選択、開始時刻リストの下にある「カスタム」を選択する。


7.  「ファイルを開く」対象がデフォルトでは「カレンダー」のところを、「その他...」を選択し、先ほど作成したアプリケーションを選択する。

8.  「繰り返し」を毎日にすれば、決まった時刻に実行される。私の場合、朝用と夜用を作成し、1日に2回リセットさせるように設定した。カレンダー.appは落としておいても動作するようです。

難点といえば「カレンダー」に毎日この定期リセットのための予定が書き込まれてしまうことです。ただし予定のグループを専用に設けて非表示にしておけば見えなくすることができます。また、通知機能を利用しているので、定時のリセットの度に通知されるのも気になると言えば気になりますが、ちゃんと機能していることを意識はできます。









2020年12月20日日曜日

tidyverse版: 複数生物種の個体数集計データを縦に伸ばすor縮める(集計データ← →データフレーム変換)

 (以前の記事をtidyverseで作り直してみました。まだ旧来のRコードの方が馴染みがあるので、tidyverseに慣れている方からすると不格好かもしれません。)

多数の種が含まれる生物群集データを解析する時、エクセルなどにデータを整理していると、各種をインデックスにするか要因のひとつとして整理するか、ケースによって変わってくるかと思います。とくに各種を応答変数とするか説明変数に使用するかあたりで必要になってくるかと。完全なデータフレーム型にしておいた方が統計解析はやりやすいけれど、膨大な行数となってエクセルの限界にあっさりと到達したりもするし…(cf. エクセルのvlookup関数)。


こんな時に、一部集計データとデータフレーム型とを自在に一発変換できたら効率がよいと思い、やり方をまとめてみました。正直、入力の手間が一番省けるのは部分的には集計されたデータだったりしますよね(下記のdata1のような形)。

データの整形にはdplyrパッケージの関数などを使用します。tidyverseパッケージとして関連パッケージも一括でインストールが可能です。

require(tidyverse) # 要インストール、tidyverseのパッケージ群が一括で呼び出される。

# 例えば、こんな種毎に集計されているデータがある時
# (tidyverse標準のtibble形式で作成)
data1 <- tibble(
year = c(rep("y08", 3), rep("y09", 3)), 
site = c(1:3, 1:3), 
depth = seq(1, 5, length=6), 
sp1 = c(6:1), sp2 = c(1:6), sp3 = c(0:5))

# 出力するとこんな感じです。<chr>は文字型、<int>は整数型、<dbl>は連続変数型を示す。
> data1
# A tibble: 6 x 6
  year   site depth   sp1   sp2   sp3
  <chr> <int> <dbl> <int> <int> <int>
1 y08       1   1       6     1     0
2 y08       2   1.8     5     2     1
3 y08       3   2.6     4     3     2
4 y09       1   3.4     3     4     3
5 y09       2   4.2     2     5     4
6 y09       3   5       1     6     5



########################################################

# data1をデータフレーム型に縦に伸ばす場合
data2 <- data1 %>% pivot_longer(c(-year, -site, -depth), names_to="species", values_to="abundance")
# pivot_longerは集計された種名の表を縦長に伸ばす。種名を表す新たな種をspecies, 個体数をabundanceとする。
# 形式を保持したい変数は、c(-year, -site, -depth)のように記す(c()は必要)。ひとつだけならば-yearのように書けば良いが、複数の場合は-c(year, site, depth)ではないことに注意。一般的なRの記法と異なっており、厄介な点。
# "%>%"はパイプと呼ばれる。パイプの前のデータを後の処理に渡す意味。いちいちdata1$yearのように書かなくていい、with関数やattach関数と似ている。可読性は良くなっているかもだけれど、デバッグがしづらい気もする。

> data2 # 出力してみます
# A tibble: 18 x 5
   year   site depth species abundance
   <chr> <int> <dbl> <chr>       <int>
 1 y08       1   1   sp1             6
 2 y08       1   1   sp2             1
 3 y08       1   1   sp3             0
 4 y08       2   1.8 sp1             5
 5 y08       2   1.8 sp2             2
 6 y08       2   1.8 sp3             1
 7 y08       3   2.6 sp1             4
 8 y08       3   2.6 sp2             3
 9 y08       3   2.6 sp3             2
10 y09       1   3.4 sp1             3
11 y09       1   3.4 sp2             4
12 y09       1   3.4 sp3             3
13 y09       2   4.2 sp1             2
14 y09       2   4.2 sp2             5
15 y09       2   4.2 sp3             4
16 y09       3   5   sp1             1
17 y09       3   5   sp2             6
18 y09       3   5   sp3             5


########################################################
# もう一段階、伸ばしてみましょう。一個体あたり一行というデータセットへ。多項ロジットなど、カテゴリカルな解析に便利そうです。

data3 <- select(data2, -abundance) %>% slice(unlist(map2(1:nrow(data2), data2$abundance, rep)))
# data2のabundanceの数に応じてrepで行数を増やし、sliceで行を選択。map2はおおよそmapplyに相当、2変数を取るsapply。slice(unlist(map2(,,,,)))はもう少しスッキリ書けないものだろうか。

> data3 
# A tibble: 57 x 4
   year   site depth species
   <chr> <int> <dbl> <chr>  
 1 y08       1   1   sp1    
 2 y08       1   1   sp1    
 3 y08       1   1   sp1    
 4 y08       1   1   sp1    
 5 y08       1   1   sp1    
 6 y08       1   1   sp1    
 7 y08       1   1   sp2    
 8 y08       2   1.8 sp1    
 9 y08       2   1.8 sp1    
10 y08       2   1.8 sp1    
# … with 47 more rows




########################################################

# data3を一つ前の状態に戻してみます(観測地点あたりで集計、といったところ)
data4 <- data3 %>% count(year, site, depth, species, name="abundance") 
# year, site, depth, species毎にデータ頻度をカウントし、カウント結果をabundance変数として追加

> data4

# A tibble: 17 x 5
   year   site depth species abundance
   <chr> <int> <dbl> <chr>       <int>
 1 y08       1   1   sp1             6
 2 y08       1   1   sp2             1
 3 y08       2   1.8 sp1             5
 4 y08       2   1.8 sp2             2
 5 y08       2   1.8 sp3             1
 6 y08       3   2.6 sp1             4
 7 y08       3   2.6 sp2             3
 8 y08       3   2.6 sp3             2
 9 y09       1   3.4 sp1             3
10 y09       1   3.4 sp2             4
11 y09       1   3.4 sp3             3
12 y09       2   4.2 sp1             2
13 y09       2   4.2 sp2             5
14 y09       2   4.2 sp3             4
15 y09       3   5   sp1             1
16 y09       3   5   sp2             6
17 y09       3   5   sp3             5

# sp3 のゼロ個体データだけは消えてしまったが、当然と言えば当然




########################################################

# data4の種数部分を集計表状に横に伸ばす(最初の状態=data1の形にする場合)

data5 <- data4 %>% pivot_wider(names_from=species, values_from=abundance, values_fill=0)
 

# pivot_widerは縦長データから横長データへ変換する、見出しとなるspeciesと対応する値abundanceを展開する。生じるNAはvalues_fill=0で置き換えた。

> data5 # 出力してみます。
# A tibble: 6 x 6
  year   site depth   sp1   sp2   sp3
  <chr> <int> <dbl> <int> <int> <int>
1 y08       1   1       6     1     0
2 y08       2   1.8     5     2     1
3 y08       3   2.6     4     3     2
4 y09       1   3.4     3     4     3
5 y09       2   4.2     2     5     4
6 y09       3   5       1     6     5



########################################################
# 出現する種のリストにひどく変動がある場合など、1つのセルの中に種名を羅列したくなるケースもあると思います。そういうデータからの変換例も追加しておきます。

data6 <- tibble(site=paste0("s", 1:10), month=rep(c(1:2), each=5), 
species=c("スズメ", "","スズメ、ヒヨドリ、シジュウカラ", "ムクドリ、スズメ", "ヒヨドリ、スズメ", "スズメ、キジバト", "スズメ、ムクドリ", "スズメ、ヒヨドリ", "", "ムクドリ"))

# 例えばこんなかんじのデータセット。空欄さえあります…
# A tibble: 10 x 3
   site  month species                         
   <chr> <int> <chr>                           
 1 s1        1 "スズメ"                        
 2 s2        1 ""                              
 3 s3        1 "スズメ、ヒヨドリ、シジュウカラ"
 4 s4        1 "ムクドリ、スズメ"              
 5 s5        1 "ヒヨドリ、スズメ"              
 6 s6        2 "スズメ、キジバト"              
 7 s7        2 "スズメ、ムクドリ"              
 8 s8        2 "スズメ、ヒヨドリ"              
 9 s9        2 ""                              
10 s10       2 "ムクドリ"                      

# 注:このような日本語データのでの入力の場合、普通に読み込もうとすると文字化けしやすいので、readxlパッケージのread_excel関数などで読み込むのがトラブルが少なくていいと思っています。csvでは機種依存文字やmac/windows間での文字コード問題が混乱のもとなので。

# こんなデータでも次のようにすれば、通常の集計データに変換できます。data1の形態へ変換してみましょう。

sptabs <- data6 %>% # data6をベースに集計作業
pull(species) %>% # 種名部分をベクトルとして抽出
map(str_split, pattern="、") %>% # "、"で文字列を分割
map_dfr(table) %>% # 要素毎に集計テーブルを作成
map_dfr(replace_na, 0) %>% # naを0で置き換え
select(-starts_with("...")) # 出現無しを削除
data7 <- select(data6, -species) %>% # 元データの見出しを抽出
mutate(sptabs) # 種集計テーブルと結合

data7 # 出力してみます(出来上がり)

# A tibble: 10 x 7
   site  month スズメ  シジュウカラ ヒヨドリ ムクドリ キジバト
   <chr> <int> <table> <table>      <table>  <table>  <table> 
 1 s1        1          1       0            0        0        0       
 2 s2        1          0       0            0        0        0       
 3 s3        1          1       1            1        0        0       
 4 s4        1          1       0            0        1        0       
 5 s5        1          1       0            1        0        0       
 6 s6        2          1       0            0        0        1       
 7 s7        2          1       0            0        1        0       
 8 s8        2          1       0            1        0        0       
 9 s9        2          0       0            0        0        0       
10 s10       2          0       0            0        1        0       

2020年9月3日木曜日

Zoom / Teamsでスライドショーにならない/スライドが進まない問題の解決:macで外部ディスプレイ使用時?

コロナ禍のもと、オンラインツールを用いてプレゼンをする機会は多いと思います。Zoom / Teamsでスライドショーにならない/スライドが進まないことがあり、探しても解決策が見つからなかったため(書いてあっても単に私が理解していなかっただけかもですが)ここで紹介しておきます。いずれも外部ディスプレイの使用時のトラブル例です。macでと書いていますが、Windowsで同様のことが起こるのかどうか単に未確認です。


Zoom、Teamsの両方に共通の解決方法が分かってきたので、その手順を先に書いておきます:

0. とっさの場合には、外部ディスプレイを引っこ抜いてmac本体単体で共有をやり直すのが手っ取り早いです。以下は、外部ディスプレイを保持したまま正常に共有でスライドショーにするための対処法です。

1. まずPowerPointでスライドショーを開始します。するとメイン画面は発表者ツール、外部ディスプレイが全画面のプレゼンになっているはずです。

2. PowerPointの発表者ツールの左上に、ツールのボタンが並んでいますが、「ディスプレイの入れ替え」をクリックします。

すると、メイン画面が全画面のプレゼン、外部ディスプレイが発表者ツールに入れ替わるはずです。
(2023追記:いっそここで「スライドショーの使用」を選び、両方のディスプレイにスライドショーが表示されるようにするのも良い解決方法かと思います。相手方を待たせるよりはいいでしょう。)



3. この状態でZoom / Teamsへ移動し、通常通りにファイル共有ボタンをクリックします。この状態ではPowerPointのファイルを選択する形ではなく、ウィンドウ1または2の選択する形で全画面プレゼンになっているメイン画面を選択します。なお、PowerPointですでにプレゼンモードになっている状態でZoom / Teamsへ移動するには、command + タブ(このマークのボタン ->| )を押すと移動先のアプリを選択できるようになります。

4. これで相手側にも正常にZoom / Teams上でスライドショーが表示されているはずです。

5. プレゼンが終了したら、先に共有を終了してから、次にスライドショーを終了することをお勧めします。先にスライドショーを終了すると、デスクトップをさらけ出すことになります。

6. 一度この手順を踏んでおくと、その後、同じ環境では設定が保存されているようです。別のファイルを開こうとした際にも、初めから外部ディスプレイに発表者ツールが現れました。ただし、いざリアルのプロジェクターを用いたプレゼンに戻る際には注意が必要そうです。


参考までに、自分自身ではどういう現象が起きたのかを書いておきます。

●macのZoomファイル共有でPowerPointプレゼンのスライドショーが進まない場合

手元では共有したPowerPointをスライドショーに切り替え、手元ではスライドショーが進んでいるのに、相手側からは"最初のページから進んでいない"と言われました。後で実験してみると、相手側ではスライドショーモードにすら切り替わっていないことが分かりました。スライドショーを切って、編集モードの状態でスライドを進めると、相手側でも進みます。


●macのTeamsファイル共有でPowerPointプレゼン全画面できない場合 

手元では共有したPowerPointをスライドショーに切り替え、手元ではスライドショーが進んでいるのに、相手側からは"編集モードのままだ"と言われました。ディスプレイ設定をミラーリング、拡張のいずれもダメ、PowerPoint側を全画面/プレゼンツールへ切り替えなど試しましたが、自分の手元でしか全画面プレゼンにならずに困りました。

2020年8月30日日曜日

{cmdstanr}: Stan高速コンパイル、{rstan}代替としても有用

前回、最近mac版のRパッケージ、とくにコンパイル必要パッケージのインストールが困難になっているという記事を書いた。R3.6ではひどい状況だったが、R4.0.2の現在、CRANの手順に従えば、おおよそ問題はなくなっている。残る大きな問題は、rstanの動作不良だ。手元のR4.0.2では、rstanのインストール自体はできても、モデルのコンパイルができなくなっている。Macだけでなく、最近Windowsでもrstanの問題が出ている模様。

対策方法を調べていったところ、cmdstanrというコマンドラインベース(コンソール?macならTerminal)のRパッケージに辿り着いた。rstanのようにいちいちRcppでコンパイルしないので動作が速いし、またバージョンアップの際の依存環境との相性問題も生じにくいようだ。mac, linux, windowsのいずれでも使用可能なようだ。コマンドラインベースとは言うものの、Rから全ての作業を完結できることが分かったので、使い勝手も悪くない。

cmdstanrの公式ページが分かりやすいので、とくに説明も要らなさそうだが、実行サンプルを載せておく。

# インストール: まず以下をR上で実行(公式ページの記述そのまま)

install.packages("cmdstanr", repos = c("https://mc-stan.org/r-packages/", getOption("repos")))

# または、開発版のインストール(上記ではコンパイル失敗するモデルがあったが、そういう場合でも開発版では成功している2020.09.17追記 → 2021.09.30現在、通常インストールの最新版で十分にうまく行くようになった模様)

# おそらくもう不要→ # devtools::install_github("stan-dev/cmdstanr")

# 次に、以下でcmdstanrを呼び出し、

require(cmdstanr)

# R上で、下記のインストール関数を実行(coresの数値は、PCのコアをいくつ使用するかの指定、そこまで時間は掛からない)

install_cmdstan(cores = 2)

cmdstan_path() # インストールされるディレクトリを示す場合

# 2回目以降の使用で謎エラーが出てモデルがコンパイルできない場合、いったんcmdstanrを削除しインストールし直すことで使えている。いい対処法ではないだろうけれど、今のところ他にうまい手が見つからない。


####################################

# 計算実行:まず、モデル式を記述(公式サンプルではファイルからの読み込み)、

stan_program <- "

data {

  int<lower=0> N;

  int<lower=0,upper=1> y[N];

}

parameters {

  real<lower=0,upper=1> theta;

}

model {

  y ~ bernoulli(theta);

}

"


modfile <- write_stan_file(stan_program)

mod <- cmdstan_model(modfile) # 一時ファイルへ書き出し、この方が運用しやすいと思う


# あとはrstanと同様の計算実行(公式の計算例、そのまま)

data_list <- list(N = 10, y = c(0,1,0,0,0,0,0,0,0,1))


fit <- mod$sample(

  data = data_list,

  seed = 123, # 乱数の種

  chains = 4,

  parallel_chains = 4, # 並列化するchainの数

  refresh = 500 # コンソール上に経過を表示するiterationの更新間隔

)

# samplingの詳細設定の解説も充実
## 指定可能な代表的なもの
# iter_warmup: burn-in数
# iter_sampling: butn-in後のsampling数
# thin: sampleを保存する間隔
# init: 初期値、init=function() list(theta=runif(1))のように


# 計算結果はtibble形式になっている
fit$summary() # 計算結果の要約、tibble形式
fit$sampler_diagnostics() # 収束診断
fit$draws() # sample値(次元:iteration, chain, parameter)、
# "draws_array"形式だが、matrix(fit$draws()[,,1])あるいはmatrix(fit$draws("theta"))のようにparameter名で指定し、parameter毎に取り出し可能


# しかし、以下によってrstanの形式に変換することも可能
stanfit <- rstan::read_stan_csv(fit$output_files())

# rstanの形式にできた方が都合が良いだろうが、rstanのインストールが全くできない場合はtibbleの方がありがたい場合もあるだろう。

# 推定結果を保存する場合、以下のように.RDSで保存するのが推奨されている。
fit$save_object(file = "fit.RDS")
# 再読み込みの場合
fit2 <- readRDS("fit.RDS")



#(2021.12.31補足)個々のchainに1つのCPU threadを割り当てる並列化は上記のようにparallel_chainsの指定をするだけなので容易だが、chainあたり複数のthreadを割り当てるには、モデルコード自体にも工夫が要るようだ。GPUの利用なども可能なようだが、いずれにしてもモデル構造自体の制約が大きく、またN数が非常に大きいなどのビッグデータには効果が大きいが、生態学でよく用いるような複雑なプロセスモデルでは恩恵が少なそうである。https://mc-stan.org/docs/2_28/cmdstan-guide/parallelization.html

2020年4月3日金曜日

(2021.10月追記:もう大丈夫そう)当面要注意、mac版Rのアップデート

(2021.10.01追記)R4.1.1へアップグレードした様子では2020年に発生していた下記の数多くの問題は既に解消されたように見えます。またApple Silicon用のRバイナリーパッケージは既にCRANからダウンロード可能になっているのも確認しました。

(2020.08.29追記)クリーンインストール状態のR4.0.2をセットアップ試みました。CRANに、ソースからのパッケージコンパイルをする場合の方法が追記されていて(https://cran.r-project.org/bin/macosx/)、それ通りでrstan以外は適正にパッケージがインストールできるようになった模様。Xcode, command line tools, XQuartz, GNU Fortran 8.2をインストールせよとあります。注意点は、GNU Fortran 8.2はリンク先でfor Marvericsとなっていますが、Catalinaにインストールしても今のところ問題は起きていません(cf. https://thecoatlessprofessor.com/programming/cpp/r-compiler-tools-for-rcpp-on-macos/)。
Catalinaでrstanがうまく動作しない問題は深刻なようで、範囲も広くて把握できていないので、とりあえず放置しています。より問題の少なそうなrstan代替のパッケージ紹介もしておきました。

まとめると、上記のようにcranのガイドに従って手順を踏みさえすれば、rstan以外のインストール問題は解決しているようです。それでも以前のように、Rインストーラひとつで終わりとはいかなくなっているので、ハードルが上がってしまっており残念なことです。


*************(以下は、過去の参考履歴として残しておきます)****************************
(2020.04.14追記)CRANからRcpp1.0.4.6が通常版として配布され、ソースからのコンパイルが失敗する問題はある程度解決しているかもしれません。しかし依然、バイナリー版を見つけられない問題は残っていますし、ソースのコンパイルが通常の方法のみで行けない問題も残っています(tidyverseの必須パッケージの1つxml2など)。

現在(2020.03.31に確認)mac版のRをアップグレード後に、パッケージのインストールがうまくいかなくなるトラブルが生じるかもしれません。どれくらいの範囲、環境まで影響するかわからないのですが、CatalinaだけでなくEl Capitanでも発生しています。沢山の追加パッケージをインストールする必要のある方、とくにC++、Fortranコンパイル系のパッケージを利用している方は、しばらく様子を見た方がいいかもしれません。実態がわからず、復旧に相当な労力を要しました。通常ならばmac版Rのパッケージの再インストールなど、install.packages関数のリストのコピペ一発で済むはずでしたが、今回は日単位の時間を浪費する羽目に合いました。最近Linux版Rのセットアップをしたところでしたが、それよりもはるかに困難でした。

生じた問題は大きく分けて2つあります。

(1)install.packages関数がバイナリーパッケージを見つけられなかった。
バイナリーよりもソースが新しいversionである場合に、ソースパッケージが選択されることはこれまでも時折ありましたが、今回はCRANのアドレスは正しいのに、必ずソースパッケージを探しに行ってしまう現象が生じています。試しにネットワーク環境を変えてみても状況は変わらずで、また一方で同じネットワーク環境でも未アップデートのR(R3.5.1@El Capitan)では問題なくバイナリーを取得できました。エラーメッセージは以下のような感じに出てきました。

install.packages("パッケージ名") を実行すると、以下のようなエラーメッセージが出て、バイナリーをスキップしてソースファイルのインストールが試みられました:

警告:   リポジトリー https://cran.ism.ac.jp/bin/macosx/el-capitan/contrib/3.6 に対する索引にアクセスできません :
   URL 'https://cran.ism.ac.jp/bin/macosx/el-capitan/contrib/3.6/PACKAGES' を開けません 
 ソースパッケージ ‘パッケージ名’ をインストール中です 
 URL 'https://cran.ism.ac.jp/src/contrib/パッケージ名.tar.gz' を試しています 
(この後、ソースのコンパイルが始まる。つまりバイナリーの取得には失敗するけれど、ソースは取得できているので、CRAN自体には接続できている)

(2)大半のRパッケージでソースのコンパイルに失敗した。
強制的にソースを取得させられるのに、それがことごとく失敗しました。展開されたコード内に、'uuid_t'; 'uid_t'; といった大量のエラーが生じてコンパイルできない現象が生じました。

これらの現象は、El Capitan、クリーンインストール後のCatalina両方で発生しました(つまり私は原因がわからずに、いい機会だからとOSのクリーンインストールまでしてしまった!)。初めは、El CapitanのR3.6.1を3.6.3にアップしたときにこの症状が発生したので、いったんRを完全消去後に3.6.1を入れ直しました。しかし症状は改善せず。コンパイルに失敗しているので開発環境をクリアにする必要を感じて、この機会にCatalinaにアップしたら、まさかの再発に苦しめられたという顛末です。


****** 解決策 ******************************************************
(1)については、さしあたってCRANにある対象パッケージのバイナリーファイルのアドレスをコピーしてきて実行すれば、とりあえずインストールできました。多くのユーザにとって、これが最短の解決法だと思います。パッケージ名を入れ替えるだけなら簡単ですが、バージョンがわからないので、"cran パッケージ名"でググるなどして調べる必要があります(ターミナルで、R CMD INSTALLで行けないかと思ったけれど、これはソースしか取得できない模様)。私はこっちの解決法に気づくのが遅れて、(2)に取り組んでしまいましたが...。
# dplyrの場合の例、URLを用いたバイナリーからのインストールコード:
install.packages("https://cran.r-project.org/bin/macosx/el-capitan/contrib/3.6/dplyr_0.8.5.tgz”) 

(2)はより深刻です。心折れかけたところでu_riboさんに相談したところ、関連するトピックを見つけて教えてもらいました(https://github.com/RcppCore/Rcpp/issues/1060)。
これを読み解くと、問題として、Mac OSX SDKとRcppのバージョンが合っていない、Rcppに依存しているパッケージの大多数が影響を受けているようです。トピ主のパワーユーザがdplyrとhttpuvで試した限りでは、Mac OSX SDK 10.11の使用(ただし古いOSでしか使用不能とのこと)、あるいはMac OSX SDK 10.5とRcpp 1.0.4.4の組み合わせならば、とりあえずコンパイルに成功したとのこと。Rcpp 1.0.4.4はテスト版のようで、インストールするなら覚悟が必要な代物です。
こちらの問題の対処はRcpp 1.0.4.4だけでなく、複数の対処が必要です。重要な手順はいくつか判明していますが、効果があったかわからないものも含んでいます。それらを検証できる腕もないので、他の方の環境で、同じやり方で解決できるのかどうかは不明です。何の保証もなくお勧めできない最終手段だと思います。

(a)Rのインストールからやり直した方がよさそうなので、ターミナルから以下のコードでRを消去しました。"存在しない"とエラーが返ってくるものもありました。
rm -rf /Applications/R.app
sudo rm -rf /Library/Frameworks/R.framework
sudo rm /usr/bin/{R,Rscript}
sudo rm /usr/local/bin/R
sudo rm ~/library/R/

(b)Rのパッケージファイル(.pkg)のインストール(https://cran.ism.ac.jp/bin/macosx/)、現時点の最新版のR3.6.3を選択しました。Catalina用だけは分けられているので注意です。
なお、こちら(https://ryanhomer.github.io/posts/build-openmp-macos-catalina-complete)のおすすめに従い、カスタムインストールでTcl/TkとTexinfoを外しました。

(c)開発環境Command Line Toolsのインストール。ターミナルで以下のコードを実行、Gバイト単位のファイルを取得するので時間が掛かります。
sudo xcode-select --install

(d)ターミナルで以下のディレクトリを開き、SDKのversionを確認します:
open /Library/Developer/CommandLineTools/SDKs/ 
# 私のCatalina環境の場合、MacOSX10.15.sdkが既にインストールされていました。より下のversionの場合、別の対処が必要かもしれません(https://github.com/RcppCore/Rcpp/issues/1060)。

(e)(入れていなかったら)homebrewのインストール。ターミナルで以下のコードを打つ
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

(f)Tcl/TkとTexinfo、その他のインストール(homebrewユーザへは、bとは別途、こうした方がよいと勧められていました)
brew install tcl-tk texinfo
brew install llvm libomp
brew install gcc

(g)~/.R/Makevarsファイルの書き換え。こちらの4.の項目を実行。必要だったかどうか不明(https://thecoatlessprofessor.com/programming/cpp/r-compiler-tools-for-rcpp-on-macos/

h)clang7.0、gfortran6.1の.pkgファイルのインストール(これは随所で勧められていました)
https://cran.r-project.org/bin/macosx/tools/clang-7.0.0.pkg
https://cran.r-project.org/bin/macosx/tools/gfortran-6.1.pkg

(i)Rcpp1.0.4.6のインストール Rcpp1.0.4.4のインストール
以下のRコードで入りましたが、まだテスト版とのことなので、入れるなら覚悟して入れてください: 問題のあったversionのRcppはアップデートされ、通常のインストール方法でRcpp1.0.4.6がインストールされるようになっています。
install.packages("Rcpp", repos="https://rcppcore.github.io/drat")

(j)tidyverseの必須パッケージの1つxml2のコンパイルを試みる際に以下のようなエラーが頻発するので、これも実行。こちらを参考(https://github.com/r-lib/xml2/issues/223)。
ちなみに、ANTICONF ERRORはLinux版のRでのパッケージコンパイルでよく見かけますが、ここを見ると対処方法のヒントが得られやすいです。macでこれをやらなければならないかと思うと気が重いですが。

# ターミナルで、以下を実行
brew install pkg-config
# Rで、以下を実行
install.packages("xml2", configure.vars = c("INCLUDE_DIR=/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/usr/include/libxml2"))

* installing *source* package ‘xml2’ ...
** package ‘xml2’ successfully unpacked and MD5 sums checked
Found pkg-config cflags and libs!
Using PKG_CFLAGS=-I/usr/include/libxml2
Using PKG_LIBS=-L/usr/lib -lxml2 -lz -lpthread -licucore -lm
------------------------- ANTICONF ERROR ---------------------------
Configuration failed because libxml-2.0 was not found. Try installing:
 * deb: libxml2-dev (Debian, Ubuntu, etc)
 * rpm: libxml2-devel (Fedora, CentOS, RHEL)
 * csw: libxml2_dev (Solaris)
If libxml-2.0 is already installed, check that 'pkg-config' is in your
PATH and PKG_CONFIG_PATH contains a libxml-2.0.pc file. If pkg-config
is unavailable you can set INCLUDE_DIR and LIB_DIR manually via:
R CMD INSTALL --configure-vars='INCLUDE_DIR=... LIB_DIR=...'
--------------------------------------------------------------------

(k)さらに、頻発する "make: gfortran: No such file or directory"というエラーに対処するため、~/.R/Makevarsにパスを追記をする。こちらを参考( https://github.com/merliseclyde/BAS/issues/1 )。これによって大多数のエラーが解消されました。
open ~/.R/Makevars
# これによって開かれたファイルに以下のパスを書き込んで保存。置き換えでなく追記です。
F77 = /usr/local/gfortran/bin/gfortran
FC = /usr/local/gfortran/bin/gfortran

(l)ここまで95%くらいのパッケージのインストールに成功しましたが、ここまで来て、パッケージのURLから直接取得する方法にたどり着いたので、これ以上の対処はせず。


(2)まったくひどい対処方法だと思うので、現状では個々のRパッケージをダウンロードしてきてインストールする(1)の方法が無難だと思います。

2018年8月22日水曜日

生態系の熱帯化:藻場が狭まり、サンゴ群集が拡大する

渾身の論文がPNAS(米国科学アカデミー紀要)出版になりました(プレスリリースも出ました):
Naoki H. Kumagai, Jorge García Molinos, Hiroya Yamano, Shintaro Takao, Masahiko Fujii, Yasuhiro Yamanaka (in press) Ocean currents and herbivory drive macroalgae-to-coral community shift under climate warming. Proceedings of the National Academy of Sciences of the United States of America.
(https://doi.org/10.1073/pnas.1716826115)

研究内容や説明についてはプレスリリースに譲りますが、以下は書き切れなかった、もう少し個人的な部分や苦労についてメモしておきます。

研究開始から約5年、初投稿から1年9ヶ月も掛かった大作です(Nは1ヶ月でエディターキック、Sの査読まで進んだものの手が届かず、PNASで2回の改訂ののち受理)。これまでに培った、移動分散、メタ個体群の考え方、データベースの構築、多くの観察データを集めて用いる統計モデリング技術、ベイズ推定、GISなど、全てを込めました。

この論文では、主に1950〜2015年の、数多くの地道な観察記録(439文献、22,253調査記録)をしらみつぶしに探索し、観察年・位置情報を付加した上で、45種の生物種ごとの日本国内の分布変化を網羅しました。私はおもに海藻と魚類の分布変化記録の収集と整備を担当しました(サンゴも一部だけ)。興味のある方はSupporting InformationのFig S2を見ていただけると45種の個々の分布変化が分かります。
お陰で日本のあらゆる海岸線の地名には詳しくなり、市町村合併や海岸線の変化(埋め立て)の歴史も垣間見ました。

この論文ではさらに、藻場を構成する海藻や、より南方から分布を拡げてくる造礁サンゴ、海藻を食害する魚類の分布変化速度を指標として、藻場とサンゴ群集の分布変化を解析しています。気候変動影響のもとで、海流の輸送といった大スケールの物理的要因および魚類による海藻の食害といった生物間相互作用が組み合わさることで、観察された分布変化をモデルによってうまく再現できることを示しました。これらの解析・モデルを最終的な形態にもっていく過程では、共著者の皆さまに大きく助けていただきました。この研究を通じて、私自身のモデリング技術、GIS技術とその理解も大きく向上したので、実によい勉強機会になったと思います。

PNASなどのshort formatスタイルは限られた字数の中に、必要なことを凝縮しつつ、しかも読みやすくしなければならないという、実際やってみると非常に難しい作業でした。字数が短い=簡単と考える人も居るかもしれませんが、記述的な新発見の論文でもない限り、short formatの論文の執筆はフルペーパーを書くよりはるかに難しい(あるいは慣れていないと難しい)作業だと感じました。これについても論文を改訂する過程で共著者の皆さまには大きく助けていただきました。自分自身の文章力も向上できたと思います。

この論文で述べたように、今後の日本の温帯藻場は温暖化の直接的影響だけでなく、魚類やウニなどの摂食圧の影響がいっそう深刻になると予想されます。またサンゴの分布拡大も気候の変化速度よりも遅いため、海藻藻場もサンゴ群集も何かしら人の手を加えて保全する手立てを考えていく必要があると思います。この論文がそういう議論の必要性を感じる1つの切っ掛けになればと思います。

なお、実際の海中では同じ海(例えば1回のダイビングの中で共存するくらいの範囲)に藻場とサンゴ群集が居合わせることはよくあるけれど、いざこれを写真に収めようとすると適した場所はなかなかありません。査読2回目の改訂の際に偶然、この論文で取り上げた全生物要素がバランスよく生息するシンボル的な海(宇和島市津島町田之浜)を訪れることができました。論文のFig.1A中央、プレスリリースの写真1中央の写真は、南方性コンブ類、温帯性ホンダワラ類、南方性ホンダワラ類、南方性サンゴの全要素が凝縮された奇跡の一枚です。
(参照:田之浜の調査でお世話になった、愛媛ダイビングセンターさんのブログでも取り上げていただきました)

2018年5月29日火曜日

Rいろは:グラフ作成ggplot2編

###### ggplot2とは? #################
# 標準仕様でもそれらしい科学的グラフを作ってくれるRパッケージ
# (plot関数の場合、相当なアレンジをしないと使い物にならない)
# 色分けや複合グラフ、重ね合わせなどに強い、応用の利きやすいメリットがある
# さらにcowplotパッケージを適用して、より学術用に適したグラフ作成を紹介します

# 項や足し算形式でグラフ要素を追加する形態になっている。例えば、散布図、棒グラフのようなグラフ形式や、回帰直線など。
# 項の指定方法などが(plot関数と違って)英単語を元にしているものが多く連想しやすい

# 使い方の早見表も参考になります

install.packages("ggplot2", dep=T) # 未インストールの場合
install.packages("cowplot", dep=T) # 未インストールの場合
install.packages("reshape", dep=T) # 未インストールの場合
require(ggplot2) # 呼び出し

###### まず定番のdata"iris"を用いて、originalのggplot2で描いてみます #########

data(iris) # iris呼び出し
head(iris) # irisの構成をチェック
  #Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#1          5.1         3.5          1.4         0.2  setosa
#2          4.9         3.0          1.4         0.2  setosa
#3          4.7         3.2          1.3         0.2  setosa
#4          4.6         3.1          1.5         0.2  setosa
#5          5.0         3.6          1.4         0.2  setosa
#6          5.4         3.9          1.7         0.4  setosa



##### 散布図 ####################
ggplot(data=iris, aes(x=Sepal.Length, y=Sepal.Width, fill=Species)) + geom_point(pch=21, size=2)

# ggplot(data=iris, # データ元の指定
#  aes(x=Sepal.Length, y=Sepal.Width, # x, yに用いる変数の指定(aes()内で変数を指定)
#      fill=Species)) # Speciesによって異なる色を使用
# + geom_point( # + geom_xxx: xxxの部分でグラフ種類を指定
#  pch=21,  # pch=21: 塗りと枠に異なる色を適用可能(21~25) # 全種に適用する場合、aes外に記述
#  size=2) # プロットサイズ(少し大きくしました)

# 以下でも同様のグラフになります(fill=Speciesを後半に記述)
ggplot(data=iris, aes(x=Sepal.Length, y=Sepal.Width)) + geom_point(aes(fill=Species), pch=21, size=2)

# このままでは背景がうるさく、学術用に不向き
require(cowplot) # cowplotパッケージを呼び出し、同じようにグラフを作成します
theme_set(theme_cowplot()) # このコードを通します(すると以降、cowplotスタイルに変更される)

ggplot(data=iris, aes(x=Sepal.Length, y=Sepal.Width, fill=Species)) + geom_point(pch=21, size=2)
# 背景が白地のスッキリしたものに変わりました。文字のサイズもちょうどいいです。



##### 箱ひげ図 ####################
ggplot(data=iris, aes(x=Species, y=Sepal.Width, fill=Species)) + geom_boxplot()



##### 棒グラフ+SD ####################
# サンプルデータの準備
MEAN1 <- with(iris, tapply(Sepal.Width, Species, mean)) # 各種の平均("対象データ", "グループ分け", "関数名")
SD1 <- with(iris, tapply(Sepal.Width, Species, sd)) # 各グループの標準偏差
iris2 <- data.frame(Species=names(MEAN1), me=MEAN1, sd=SD1)
# グラフの作成
ggplot(data=iris2, aes(x=Species, y=me, fill=Species)) + geom_bar(stat="identity") + geom_linerange(aes(ymax=me + sd, ymin=me - sd))



##### 2要因の棒グラフ + SD ####################
# サンプルデータの準備
iris3 <- rbind( # 1.2倍したサンプルデータを追加
data.frame(Species=names(MEAN1), me=MEAN1, sd=SD1, treat="C"),
data.frame(Species=names(MEAN1), me=1.2*MEAN1, sd=1.2*SD1, treat="T") )
# グラフの作成
ggplot(data=iris3, aes(x=Species, y=me, fill=treat)) + geom_bar(stat="identity", position="dodge") + geom_linerange(aes(ymax=me + sd, ymin=me - sd), position=position_dodge(width=0.9))
# position, dodge: 接したグラフを作成するためのkeyword



##### ヒストグラム ####################
ggplot(iris, aes(x=Sepal.Width, fill=Species)) + geom_histogram()



##### 時系列データのグラフ ####################
data(airquality) # data"airquality"を使用
head(airquality) # データの中身をチェック
  #Ozone Solar.R Wind Temp Month Day
#1    41     190  7.4   67     5   1
#2    36     118  8.0   72     5   2
#3    12     149 12.6   74     5   3
#4    18     313 11.5   62     5   4
#5    NA      NA 14.3   56     5   5
#6    28      NA 14.9   66     5   6

# 時間形式に変換
airquality$Time <- as.POSIXct(strptime(with(airquality, paste(2018, Month, Day, sep="-")), "%Y-%m-%d")) # (2020.03.03少しコードを修正)
ggplot(airquality, aes(x=Time, y=Ozone)) + geom_point(pch=21, cex=2, fill="purple") + geom_line()



##### 時系列データのグラフ(複数系列) ####################
require(reshape) # 要インストール
airquality2 <- melt( # データ構成を変換
airquality[,c("Ozone","Temp","Time")], id.var="Time")
head(airquality2)
        #Time variable value
#1 2018-05-01    Ozone    41
#2 2018-05-02    Ozone    36
#3 2018-05-03    Ozone    12
#4 2018-05-04    Ozone    18
#5 2018-05-05    Ozone    NA
#6 2018-05-06    Ozone    28

# 2パネル構成
base <- ggplot(airquality2, aes(x=Time, y=value)) # ごちゃごちゃしてきたので分割します
poi <- geom_point(pch=21, cex=2, aes(fill=variable)) 
lin <- geom_line(aes(colour=variable)) 
base + poi + lin + facet_wrap(~ variable, scale="free_y", ncol=1) # variableでパネルを分割




###### 散布図+回帰直線 ##################
summary(iris) # 値の幅をチェック
  #Sepal.Length    Sepal.Width     Petal.Length    Petal.Width          Species
 #Min.   :4.300   Min.   :2.000   Min.   :1.000   Min.   :0.100   setosa    :50
 #1st Qu.:5.100   1st Qu.:2.800   1st Qu.:1.600   1st Qu.:0.300   versicolor:50
 #Median :5.800   Median :3.000   Median :4.350   Median :1.300   virginica :50
 #Mean   :5.843   Mean   :3.057   Mean   :3.758   Mean   :1.199                
 #3rd Qu.:6.400   3rd Qu.:3.300   3rd Qu.:5.100   3rd Qu.:1.800                
 #Max.   :7.900   Max.   :4.400   Max.   :6.900   Max.   :2.500                

md1 <- lm(Sepal.Length ~ Sepal.Width, iris[iris$Species=="setosa",]) # 1種に限定
coef(md1)
#(Intercept) Sepal.Width
  #6.5262226  -0.2233611

# 散布図 + 回帰直線
x.interval <- seq(2, 5, 0.1) # x(Sepal.Width)の最小〜最大を0.1刻みで細かく分割
est.Y <- predict(md1, newdata=data.frame(Sepal.Width=x.interval))
fitted <- data.frame(Sepal.Width=x.interval, Sepal.Length=est.Y)
# fitted <- data.frame(Sepal.Width=x.interval, Sepal.Length=coef(md1)[1] + coef(md1)[2]*x.interval) # 上と同じ
base2 <- ggplot(iris[iris$Species=="setosa",], aes(x=Sepal.Width, y=Sepal.Length)) 
poi2 <- geom_point(fill="turquoise", pch=21, size=2) 
lin2 <- geom_line(data=fitted, colour="turquoise", lwd=1) # ここは"data="を明記する必要あり
base2 + poi2 + lin2



##### 散布図 + 回帰直線 + 信頼区間 ##################
est.Y2 <- predict(md1, newdata=data.frame(Sepal.Width=x.interval), interval="confidence")
head(est.Y2)
       #fit      lwr      upr
#1 4.019981 3.753101 4.286860
#2 4.089030 3.839589 4.338470
#3 4.158079 3.925981 4.390177
#4 4.227128 4.012251 4.442004
#5 4.296177 4.098369 4.493984
#6 4.365226 4.184291 4.546160

fitted2 <- data.frame(Sepal.Width=x.interval, Sepal.Length=est.Y2[,1], upperCI=est.Y2[,3], lowerCI=est.Y2[,2])
band <- geom_ribbon(data=fitted2, aes(ymin=lowerCI, ymax=upperCI), alpha=0.5, fill="turquoise", linetype="blank") # alpha: 半透明(0~1)
base2 + poi2 + band + lin2 # 重ねたい順で



##### 散布図 + 回帰直線(複数グループ) ##################
x.interval3 <- seq(1,5,0.1)
new.dat <- data.frame( # 種数分、繰り返す
Species=rep(unique(iris$Species),each=length(x.interval3)), 
Sepal.Width=rep(x.interval3, length(unique(iris$Species))))
md3 <- lm(Sepal.Length ~ Sepal.Width*Species, iris) # 全種を対象、Speciesによる違い(ANCOVA)
est.Y3 <- predict(md3, newdata=new.dat)
fitted3 <- data.frame(Species=new.dat$Species, Sepal.Width=new.dat$Sepal.Width, Sepal.Length=est.Y3) # Species列を追加
base3 <- ggplot(iris, aes(x=Sepal.Width, y=Sepal.Length)) 
poi3 <- geom_point(aes(fill=Species), pch=21, size=2) 
lin3 <- geom_line(data=fitted3, aes(colour=Species), lwd=1) # ここは"data="を明記する必要あり
base3 + poi3 + lin3




##### 複数タイプのグラフの組み合わせ(cowplot利用) ##################
g1 <- ggplot(data=iris, aes(x=Sepal.Length, y=Sepal.Width)) + geom_point(aes(fill=Species), pch=21, size=2) 
g2 <- ggplot(data=iris, aes(x=Species, y=Petal.Width)) + geom_boxplot(aes(fill=Species)) # 箱ひげ図
+ theme(axis.text.x = element_text(angle=90, vjust=0.5)) # label方向

g12 <- plot_grid(g1, g2, labels="auto", align="h") 
# labels="auto":小文字でラベリング(大文字は"AUTO")
# 直接入力の例:labels=c("a","b")
# align="h":図の端を揃える("h", "v", "hv")

#### グラフの調整(例:凡例を統一、サイズ調整)
g1n <- g1 + theme(legend.position="none") # 一方の凡例消去
bd <- panel_border(colour=1, size=1) # 外枠追加
g1n2 <- plot_grid(g1n + bd, g2+ bd, labels="auto", 
   rel_widths=c(1, 1.4), # 横幅を1:1.4に変更
   scale=0.9, # パネル間の間隔を広げる
   vjust=0.2) # ラベル位置(a, b)の調整
 
 
 
#### ファイルの保存 ##############################
save_plot("directry/filename.pdf", g1n2) # cowplotの関数、お任せで適度なサイズになる。指定も可能(2020.03.03: ggsaveからsave_plotへ変更)

#### 水平線・垂線 ##############################
abl <- geom_hline(yintercept=0, linetype="dotted") # 水平 ("dotted": 点線)
abl2 <- geom_vline(xintercept=0, linetype="dotted") # 垂直


#### 軸とラベルの調整 ############################
x.ax <- scale_x_continuous(breaks=c(1:5), limits=c(1, 5), expand=c(0.001, 0.001), name="Sepal width (mm)")
# ex.
base3 + poi3 + lin3 + x.ax


#### 色の指定 ##############################
# グレースケール
set.fillG <- scale_fill_grey(start=0.5, end=1) # グレー 〜 白の場合(塗りつぶし色)
# "fill"の部分を"colour"にすると線の色の指定("shape": 形状、

set.fillC <- scale_fill_manual(values=c("turquoise", "salmon", "blue")) # 塗りつぶし色
set.colC <- scale_colour_manual(values=c("turquoise", "salmon", "blue")) # 線の色
# ex.
base3 + poi3 + lin3 + set.fillC + set.colC

#### cf. 色コードの選択 ##########################
"見やすい"色の組み合わせ例の参照サイト(ColorBrewer)
fill="#2ca25f" のようにしてカラーコードで色の指定が可能



#### 90° 回転(グラフを横向きにする)#######################
 + coord_flip()


#### 凡例の調整 ##############################
+ theme(legend.position="none") # 凡例消し
+ theme(legend.position="bottom") # ラベルの右揃え(cf. vjust)

# 長すぎるラベルを改行する( \n が使える)
scale_x_continuous(name = "Length\n (cm)")

# 上付き、ギリシャ文字など
expression(m^2) 
expression(Delta) # Δ



#### cowplotの調整 ##############################
+ panel_border(colour=1, size=1) # defaultの線・文字色がなぜかグレーなので黒に変更
+ theme_cowplot(font_size = 12) # 文字が大きく感じる場合はサイズ指定

# 日本語の利用(ただしアウトライン化されてしまう模様 )
ggsave(..., family="Japan1GothicBBB",...) 

2017年3月30日木曜日

Rで機種依存文字の混じったデータファイルを読み込む方法

Rへ読み込む際に非常に厄介な機種依存文字(=環境依存文字)、データを作成する際には絶対に避けてほしい代物ですが、丸に数字など、日本のあらゆる文書で愛されており、根絶は非常に困難です。

見渡せる程度の小さなファイルならば手作業で特定し、検索・置換で除去するのですが、巨大なファイルの場合、いったいどんな文字が悪さをしているのか見つけることさえできない場合がありますね。

今回、到底手作業で対処できないサイズのファイルに取り組んでいた際にいい方法を思いついたので挙げておきます(小ネタです)。

1) まず、機種依存文字のファイルは.xlsxファイルとして保存しておきます(ここでは"データファイル名.xlsx"とします)。

2) 通常、read.csv関数などでデータファイルを読み込むところを、openxlsxパッケージのread.xlsx関数を使用します(要インストール、install.packages("openxlsx") などで)。

3) 以下で読み込むことができます、fileEncodingの指定も特に不要の様子。
data <- read.xlsx("データファイル名.xlsx")

cf. もちろんファイル出力にも対応しています(outputというデータフレームを出力する場合の例)write.xlsx(output, "出力ファイル名.xlsx")

Excelファイルを直接読むのはまだ抵抗はあるのですが、気にしなければ汎用性のある方法なので便利です。あとはこのパッケージがちゃんと存続してくれることを祈るばかりですが。なお、他にもExcelファイルを読めるパッケージは複数存在しているようですが、他のは基本的にJavaに依存しているようなので却下しました。

(2019.09.17 追記)
列名に漢字が混じる場合にフリガナが追加されるエラーが出ていて対処方法が分からず。代替関数として、readxlパッケージのread_excel関数の方が良いかもしれません。こちらならこのエラーは起こらず、またJava不使用です。tidyverseの一部のようなので、将来性もあるかもしれません。一方、読み込むとデータフレームではなくtibleという新形式になります。tible非対応のパッケージもまだ多いので、通常のデータフレームに変換するには、as.data.frame(tible_no_data)としてやればよいです。

cf. 機種依存文字が混じっているファイルをread.csvで読もうとすると、"In scan(file, what, nmax, sep, dec, quote, skip, nlines, na.strings,  : 入力コネクション 'ファイル名.csv' に不正な入力がありました "のようなエラーメッセージが出る。
Rじゃなくとも、メールの送受信でも文字化けの温床となります。データ解析に限らず、トラブルの原因にしかなりません。

2016年12月21日水曜日

macOS10.12 (Sierra) でWinBUGSを動かす (Wine, R2WinBUGS使用)

メインマシンをクリーンインストールする羽目に会いました。今度こそWinBUGSは卒業したくはあるのですが、依然StanやJAGSでは通らないコードも残っておりまだ手放せずにいます。早くStanが離散パラメータを直接扱えるようになってほしい…

以下は、いったんMountain lion (OSX10.8)をクリーンインストール → Sierraにアップグレードしたマシンにインストールした場合の報告例です。El Capitan(OSX10.11)の時とほぼ同様です。

まず、XQuartzとHomebrew(パッケージ化されていないアプリを容易にインストールするための補助ツール?)を入れ、Homebrewを用いてWine(非Windows OS上でWin専用アプリケーションを実行する環境)をインストール、Wineのディレクトリ内にWinBUGSをインストールするという流れです。

cf. 以下の「ターミナル」の使い方:
「アプリケーション」→「ユーティリティ」にある「ターミナル」を立ち上げる。

コンピュータ名:~ ユーザ名$

このドルマーク $ の後にコマンドを打っていく。
なお、インストールに関わるところでパスワードを求められるが、その都度、自分のアカウントのパスワードを入れる。
(以降、パスワードを入れる作業は説明を省略)




<以下、作業手順(もしかするとSierraでは1〜4の行程は不要かもしれない>
0)実行環境
・Mountain lion (OSX10.8)をクリーンインストール → SierraにアップグレードしたiMac


1) 下準備の開始、/usr/local/フォルダを作る、ロックをいったん外して操作をするという動作をするのですが、その前にシステムの基本的なセキュリティを一旦外します。推奨されていない動作だということをお忘れなく。

リカバリモード(⌘+R を押しながら起動)で起動し、ターミナルを立ち上げる


2) ターミナルの $ マークの直後に、下記のコードを打ち込む(セキュリティを外す作業)これはコピペでOK、以降も同様。

csrutil disable


3) 通常の再起動をする


4) ターミナルに下記を打ち込む(改行されて見えているだろうが、改行無しで打ち込む)

sudo mkdir /usr/local && sudo chflags norestricted /usr/local && sudo chown $(whoami):admin /usr/local && sudo chown -R $(whoami):admin /usr/local

ただし、今回の実行環境では、このディレクトリは既に存在すると言われた。なので、この危なっかしい工程はもはや省略できるかもしれない。


5) 再度、リカバリモードで起動


6) 下記のコードを打ち込む(セキュリティを元に戻す)

csrutil enable


7) 今一度、通常の再起動をする


8) Xcodeのインストール、ターミナルに以下を打ち込む

xcode-select --install


9) homebrewをインストールする

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

(せいぜい数分でインストールは終了するはず)


10) XQuartzのインストール。もはやアプリとして付属しなくなった。コードでインストール可能であるようなのでインストール。

brew cask install xquartz


11) Wineのインストール、下記のコードをターミナルに打ち込む。

brew install wine


12) 回線速度にもよるが、良好な環境では1時間程度でインストールは終わるだろう。これが終わったら、次のコマンドを打つ。

winecfg


13) XQuartzのウインドウが表示され、"Gecko package"が必要だから入れてもいいか?と聞かれた。表示されているInstallボタンで承諾するとインストールされる。

しばらく処理がされた後、XQuartzからWineの環境設定のようなウインドウが表示される。単に一番下のOKをクリックすればよい。


14) WinBUGSのインストール
パッチなどを当ててある展開済みのWinBUGSフォルダを用意する。Windows7、Windows8へのインストールも現在はこのやり方で行くしかないことを考えれば、Macでも同様にすればいいだろう。適用済みのWinBUGSも公開されている。

下記のコードで不可視フォルダにあるProgram Filesフォルダを開く

open ~/.wine/drive_c/Program\ Files/

ここへWinBUGSフォルダを入れればインストール完了

15)RからWinBUGSを実行する。下記のような単純なサンプルコードで試してみる。Wineを経由するので、bugs()内にそのためのコードがたくさん必要。


# R2WinBUGSのインストールをお忘れなく
require(R2WinBUGS)
# 真の値は、a=3, b=2, sd=1
X <- c(1:100)
Y <- rnorm(100, mean=(3 + 2*X), sd=1)
data <- list(X=X, Y=Y)
inits <- function() list(a=0, b=0, tau=1) 
parameters <- c("a", "b", "sigma")

model <- function() {
a ~ dnorm(0, 1.0E-6)
b ~ dnorm(0, 1.0E-6)
tau ~ dgamma(1.0E-2, 1.0E-2)
for (i in 1:100) {
Y[i] ~ dnorm(mean[i], tau)
mean[i] <- a + b*X[i] }
sigma <- 1/sqrt(tau)
}
modelpath <- file.path(tempdir(), "model.bug")
write.model(model, modelpath)

mcmc <- bugs(
data=data, inits=inits, parameters=parameters, model.file=modelpath, 
n.chains=3, n.iter=5000, debug=T,
working.directory=NULL, clearWD=T, useWINE=T, newWINE=T,
WINE="/usr/local/bin/wine", WINEPATH="/usr/local/bin/winepath")

print(mcmc) # ちゃんと真の値(a=3, b=2, sigma=1)が推定できたかチェックしよう

# 今回、opt/localではなくusr/localにパスを通すよう変更する必要が出た。以前のWINE、WINEPATHは/opt/local/bin/になっていたが、ここは/usr/local/bin/に変更していることに注意。


17)まだR上で下記のエラーコードが出るが、これはこちら(http://ggorjan.blogspot.jp/2008/10/runnning-r2winbugs-on-mac.html)によると害のないエラーコードらしい。要は推定計算さえ無事に行われていればよいだろう。
err:ole:CoGetClassObject class {0003000a-0000-0000-c000-000000000046} not registered
err:ole:CoGetClassObject class {0003000a-0000-0000-c000-000000000046} not registered
err:ole:CoGetClassObject no class object {0003000a-0000-0000-c000-000000000046} could be created for context 0x3
err:ole:CoReleaseMarshalData IMarshal::ReleaseMarshalData failed with error 0x8001011d


2016年11月20日日曜日

潜水から無事に帰還する装備について考える

海中での潜水調査・作業をよくやる人の中には、ヒヤッとした経験のある人も多いだろう。私自身もかつてBC(浮力調節)ジャケットの弁のトラブルで強制浮上になってしまったことがあり、それ以降はとくに気になるようになってきた。

先週、知っている人の中で重大な潜水事故が起こってしまった。まったくもって今更ではあるけれど、ここでは海中または浮上後の海上でトラブルが起こった際に無事に帰還するための装備について考えてみたい。ご意見も歓迎します。

まず、流されてしまったか何かで、岸や船から遠い地点に浮上してしまった場合、BCに付属している笛を吹く程度では、よほど凪でもなければ気づいてもらえないだろう。他に入手しやすいものとしては、海面上に長く伸びるフロート(レギュレータからのエアを入れて膨らませられる)があるだろう。私も念のため持っているけれど、しかし実際これで十分に目立つのだろうか。

他の手段…例えばスマホを完全防水で携帯して、浮上したら電話するというのはどうだろうか。場合によってはアカウントを利用して、PCからスマホを探すことも可能な気がするのだけれど。

スマホに近いものとして、ココセコムというのがあるそうだ。子供やお年寄りに持たせておいて携帯電話網でマップ上で追跡が可能、料金的にもスマホ利用より安そうです。

無線を使うという手段は電波法上かなり難しいようだが、合法的にダイビングで使用できる製品もあるようだ(トランスポンダ SEAKER_1
船からのダイビングという形態を前提としているようだけれど、通常使用(自分の母船に連絡)と、緊急使用(母船とはぐれている場合に無線を受けられるすべての船に緊急発信が可能)の2通りの発信が可能というのが優れている。距離的には20 kmまで届くようなので頼もしい。

(2016.12.16 追記)意識があって海上で操作することが前提ですが、その条件ではかなり有効な製品が出たようです(個人用救難信号発信機 PLB)。精度100mで全世界対応。機器そのものは今年3月に日本で認可、防水ケースが先月末に発売された模様。無線局として申請・許可を得る必要があるなど取り扱いは注意ではあります。

では、意識がないけれど海面には浮かんでいる場合どうするか。上記の携帯を利用した方法が可能であったら使えそうではあるけれど、岸から遠くへ流されてしまっていたとしたら(携帯電話エリア外)アウトだろう。
登山用にはもう少し手軽な製品が出ているようだ(ヒトココ)。
こちらは最大距離 1 kmと短めだけれど、親機を安全なところに確保しておいて、わずか20gの子機を携帯していくという形で使用できる模様。完全生活防水とのことだけれど、ダイビングで使うなら何かしらの防水ケースが別途必要だろう。距離の制限はあるけれど、水面に浮いてさえしてくれればいいところは汎用性は高そうだ(意識の有無にかかわらず…)。

その他、ビーコンという案もいただいた。

またバイオロギングで海獣にGPS発信機を付けるという話もあるけれど、あれはダイビングの安全管理には使えないのだろうか?

しかし、もし沈んだままで浮いていなかったら、これらの手段はどうにもならないですね。。海底で作業している間に意識を失ったとしたら、中性浮力でなくエアは抜いた状態のはず。

2016年9月2日金曜日

(cowplotパッケージ)研究用にスッキリ簡潔にggplotを描画 & 複数パネル化

最近、Rでのグラフ作成の標準になりつつある気がするggplot2パッケージですが、デフォルトのテーマは研究用としては装飾過剰なので、自分用にアレンジしたテーマを使っている人も多いと思います。でもそのテーマを図示の度に毎回引っ張ってくるのはとても面倒。

それから、複数の異なる種類のグラフを組み合わせて描画するときに、gridExtraパッケージを使うというのがありますが、図示するまではいいけれど、保存する時に行・列数の指定ができないなど、こちらもいろいろ厄介でした。

この両方を一気に解決してくれたのがcowplotパッケージ、研究目的のスッキリしたグラフを作成するのに特化したようなパッケージです。ggplot2を基本にして拡張したようなものなのですが、使い方はかなり簡単です。以下、irisデータを用いて図示してみます。

data(iris) # iris呼び出し

# いずれも要インストール
require(ggplot2)
require(cowplot)

初めにggplot2とcowplotの両方を呼び出しておけば、あとは普段通りにggplotで図示するだけで論文ライクなスッキリしたグラフが描かれます。
→(2020.08.29追記)cowplotの仕様が変更になっており、下記のようなcowplotスタイルのグラフにするには、theme_set(theme_cowplot())、を通しておく必要があります。またはggplot()... + theme_cowplot()のように追加してもOKです。

# x=Sepal.Length, y=Sepal.Widthの散布図をSpecies毎に色分けして描く
g1 <- ggplot(iris, aes(x=Sepal.Length, y=Sepal.Width)) + geom_point(aes(fill=Species), pch=21, size=2) 
g1 # 図示



# 保存も普通にggplot2と同様にやるだけ
ggsave("cowsample1.pdf", g1, height=10, width=12, unit="cm")





次は、複数種類のグラフを並べる場合

# 先ほどのg1と新たにg2を用意し、横並びにします。
g2 <- ggplot(iris, aes(x=Petal.Length, y=Petal.Width)) + geom_point(aes(fill=Species), pch=21, size=2)

# 複数グラフを合成するにはplot_grid関数を使用します
g12 <- plot_grid(g1, g2, labels="auto", align="h") 

# labels="auto":小文字で各パネルにラベリング("AUTO"だと大文字)
# labels=c("a","b") のように直接入力することも可能
# align="h":グラフの上下が揃うようにサイズ調整してくれる



# このグラフのように凡例が共通の場合、一方は消してしまいましょうか。
g1n <- g1 + theme(legend.position="none") # ggplotのtheme関数と合わせ技が可能、1パネル目の凡例を消去
g1n2 <- plot_grid(g1n, g2, labels="auto", rel_widths=c(1, 1.4), scale=0.9, vjust=0.2
# rel_widthsで横幅を1:1.4に変更
# それからデフォルトのままだと、パネル間がくっつきすぎてしまうので、scaleパラメータで調整します(1→0.9)。vjustはラベル位置(a, b)の調整






# もう一つ増やして、二段重ねにしてみます(nrow=2で2行になる:gridExtraパッケージのarrageGrob関数と違って、行・列数の指定が可能。grid.arrange関数があるじゃないかと思うかもですが、ファイル保存できないので実用不可)
g3 <- ggplot(iris, aes(x=Species, y=Petal.Width)) + geom_boxplot(aes(fill=Species)) + theme(axis.text.x = element_text(angle=90, vjust=0.5))
# 追加のg3はx軸のラベルが重なってしまうので縦にする↑(themeを使用)
g123 <- plot_grid(g1, g2, g3, labels="auto", nrow=2, scale=0.9, vjust=0.2

# ちゃんとこの状態で保存も可能です
ggsave("cowsample123.pdf", g124, height=15, width=19, unit="cm")





さらに、ggplotらしく要因で分けたグラフと普通のグラフを組み合わせてみます。縦長と普通サイズの組み合わせをしようとしているのですが、この場合、plot_gridを重ねることで実現できます。ラベルをautoのままにすると重なってしまうので注意が必要です。

# g1, g3で縦に連結(横に持ってくるので、ラベルがb, cになるようにする)
# 幅も揃えておきます(align="v")
g13 <- plot_grid(g1, g3, labels=c("b","c"), ncol=1, vjust=0.5, scale=0.9, align="v") 

# 次にfacet_wrapで種毎のグラフ(ncol=1で1列になる)
g4n <- ggplot(iris, aes(x=Sepal.Length, y=Petal.Length)) + geom_point(aes(fill=Species), pch=21, size=2) + facet_wrap(~ Species, ncol=1) + theme(legend.position="none")

# 両者をさらにplot_gridの中に入れます(これによりplot_gridが入れ子になる)。
# 新たな左のグラフのみにラベルをつけるので、c("a","")のように指定しました。
g134 <- plot_grid(g4n, g13, labels=c("a",""), nrow=1, rel_widths=c(1, 1.4), scale=0.9, vjust=0.1)




単体のggplot2と比べて、とても便利。日常的に使いたくなるパッケージです。

2017.05.19追記:
theme関数で外枠を追加しようとすると上手く行かず。代わりにcowplotパッケージのpanel_border関数を使えば良い模様。
+ panel_border(colour=1, size=1) # defaultの線色がなぜかグレーなので黒に変更、sizeで線太さを調節

2016年8月31日水曜日

RのforループをC++で高速化する(Rcppパッケージ)

"時間が掛かるからRでforループを使うな"、"applyファミリーを使え(ほかにsapply, lapplyなど)"とよく言われる。かといって無理に使うと難解なコードになる場合もあるし、せっかく実現しても計測してみたら、むしろforループの方が早かったなんてこともあった(今回の動機)。並列化(複数のCPUコアを用いる)するという手も考えたが、大きなー繋がりのタスクを1回やるだけならよいが、入れ子になっていると呼び出す時間の方が律速になったりもする。

applyファミリーやforeachなどで置換えにくい計算例として、値を毎回更新するようなプログラムがあるだろう。例えば以下のような計算例("0.9* "さえ無ければapply(x, 2, cumsum)で一発なのだが、sapplyとapplyを組み合わせて書いたらforより遅くなった…)。

NN <- 10^6
x <- matrix(0, NN, 10) 
# NN回分の結果を格納するための入れ物(1回あたりは長さ10のベクトル)
system.time( for(r in 2:(nrow(x))) x[r,] <-  0.9*x[r-1,] + rnorm(10) )
# ひとつ前の結果に0.9を掛け、正規乱数を足していくマルコフ連鎖
#  ユーザ   システム       経過
#    5.457      0.363      5.778

手元の環境では6秒弱掛かった。

いろいろ調べて行き着いたのが Rcpp というパッケージ(要インストール)。RからC++("しーぷらぷら"と呼ぶみたい、なのでcppなのだろう)で作成した関数を呼び出せるというもの。Cで作られているというパッケージは最近よく見かけるようになったが、自分でカスタマイズできるのはすごい。もっとも、C++を書けるならばですが。それでも部分的にだけでも記述できればだいぶ速くすることができるはず。目的のforループ作成を達成するのに丸一日掛かりましたが、最終目的の計算に掛かる予想時間が数ヶ月だったことを考えれば大幅な時間短縮になりました。

手順として、まず開発環境のセットアップが必要です。Macの場合はXcode(AppStoreからフリーでダウンロード)とX11(ユーティリティ内にあるX11のアイコンをダブルクリックして立ち上げてライセンスにOKすれば大丈夫なはず)。Windows版はすみませんよく分かりません…コマンドプロンプトでC++をコンパイルする必要があると思います。
次に肝心の、C++で記述したコードが必要です。ただし随所に純粋のC++と異なる書き方を要する箇所があります(私はここでつまづきました)。C++で記述したファイルをRの作業ディレクトリに置いて読み込むと、Rの関数として使用できるようになります。





まずは最小限のコード例:(以下、C++のコードは紫にしておきます)
require(Rcpp) # Rcppパッケージを呼び出す(この行はRに直打ち)

#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]

double testF(double x) {
    return(x*x);
}

紫で書いた6行分をコードエディタなどで書いて test.cpp というファイル名で作業ディレクトリに保存します( .cpp ってC++ファイルの拡張子なのだと思います)。
C++用コードの初めの3行は決まり文句のようなものです。3行目は用途によって変える場合があるようですが、とりあえず忘れてよさそう。
まずRとの大きな違いはすべての変数型を逐一定義すること。この性質は嫌いじゃないです。むしろ勝手に変数型が変わるのはRが嫌われる点でもありますね。

double は実数の意味、整数なら int 、のように、使う変数の前に半角スペースを置いて書いて定義する必要があります。

testF は、いま定義しようとしている自作関数の名前です。括弧の中で x という変数が与えられた時に { } の処理を行う変数を作成することを宣言しています。

return(x*x); これは x*x の結果を返すことを示しています。もうひとつRと大きな違いとして各行の最後に ;(セミコロン)が入ることです(中括弧 { の後ろ以外)。


次にR上で以下のようにして、この自作関数を呼び出します(初回の呼び出しには数秒掛かるかもしれない)。
sourceCpp("test.cpp")

すると、testF 関数が使用できるようになります。例えば…
testF(1.41421356) # 一夜一夜に人見頃
結果はちゃんと 2 が返ってくるはず。


ちなみにRの上でC++コードを走らせることも可能です。""で挟んで、code=code.testで指定してやるだけです。
sourceCpp(code="
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]

double testF(double x) {
    return(x*x);
} ")

ただし、Rcpp.h以外のファイルを読み込む必要がある場合のやり方が今ひとつ分からず。inlineパッケージのcxxfunctionを使ってできそうなのですが。






次の例では段階を上げて行きます。
rnorm 関数をC++で書いてみましょう。なお、乱数の発生アルゴリズムとしてRの場合はMersenne Twister法が使用されていますが、C++でやろうとすると一工夫必要でした。これについては、詳しい解説とコードも配布されているサイトがありましたのでリンクを貼っておきます。MT.hファイルと内包されるinit_genrand関数、genrand_real3関数はこちらから取得しました。(追記:他にも、匿名さんから頂いたコメントで乱数発生アルゴリズムのまとめサイトがあるとのことです。

r_normal という名前でC++版rnormを作成します。
下準備としてMt.hと名付けたファイルをRの作業ディレクトリに置きます。下記がRcppから読み込む用のC++コードになります(もはや純粋なC++コードではないので、こう呼びます)。

#include <Rcpp.h>
#include "MT.h"

using namespace Rcpp;
// [[Rcpp::export]]

NumericVector r_normal( int NN, double mu, double sigma ) {  
  NumericVector x(NN);
init_genrand((unsigned)time(NULL));

for(int i=0; i<NN; i++) {
x[i] =  mu + sigma*sqrt( -2.0*log(genrand_real3()) ) * sin( 2.0*M_PI*genrand_real3() );
}
return(x);
}

新たに加わった include "MT.h" で上記のファイルを詠みこみます

#include <stdio.h>
#include "MT.h"

次に、この行ですが、これがクセモノです。
NumericVector r_normal( int NN, double mu, double sigma ) {

NumericVector は返り値がベクトルになるように定義しますという意味です(Rcpp特有の表現:cf. Rcpp Quick reference)。そして r_normal という名前の自作関数を作ろうとします。

int NN, double mu, double sigma は、ベクトルの要素数 NN(整数)、平均が mu(実数)、標準偏差が sigma(実数)の値を用いることを宣言しています。

NumericVector x(NN);
次にこれですが、xという、要素数が NN のベクトル(初期値は全部0になっている)を用意することを意味しています(Rcpp特有の定義のようだ)。

init_genrand((unsigned)time(NULL));
これは乱数のタネの指定、現在時刻を秒精度で使用します。

for(int i=0; i<NN; i++) {
C++版のforループ。整数 i を 0からNN未満まで適用しますという意味。0から始めるのが基本である模様(誤解があるかもしれないけれど、とりあえずこのコードではOK)。

x[i] =  mu + sigma*sqrt( -2.0*log(genrand_real3()) ) * sin( 2.0*M_PI*genrand_real3() );

xのi番目の要素に正規乱数を格納するコード。右辺はごちゃごちゃしていますが、genrand_real3が一様乱数を返す関数です。詳しい説明が参照元にあります。
return(x); で x を返り値で返すようにすれば、r_normal関数が使えるようになります。

rnormと速度比較をしてみます。

NN <- 10^7
system.time(rnorm(n, 0, 1))
#   ユーザ   システム       経過
#    0.677      0.006      0.679

system.time(r_normal(n, 0, 1))
#   ユーザ   システム       経過
#    0.363      0.005      0.367

速度は2倍弱といったところ。もう少し期待したかったですが、それでもベクトル化されているRコードよりもさらに高速です。


一方、Rの関数をC++側に渡すことも可能です。Rの関数をC++で計算させることができます。やり方としては、cppFunction関数を用いてrnormのC++版、rnormCppを定義します。
cppFunction( "
NumericVector rnormCpp(int N, double me, double sd) { 
return(rnorm(N, me, sd)); 
}" )

定義の仕方などはだいたい同じですが、rnormが使われていることに注意です。これはC++の関数ではなく、たしかにRのrnorm関数です。
これを通すと、rnormCppという関数が使えるようになります。気になる速度ですが…

system.time(rnormCpp(10^7, 0, 1))
#   ユーザ   システム       経過
#     0.577      0.019      0.594

Rのrnormより少し速くはなりましたが、C++で1から作るより遅いです。それでも手軽に計算速度を向上させることができるので、何かと試したくなる技です。





ではいよいよ、本題のforループを r_markov 関数として記述してみます(同様にファイル保存します)。

#include <Rcpp.h>
#include "MT.h"

using namespace Rcpp;
// [[Rcpp::export]]

NumericMatrix r_markov( int repl, int length, double mu, double sigma ) {

NumericMatrix x(repl, length);
int i,j;

init_genrand((unsigned)time(NULL));

for(i=1; i<repl; i++) {
for(j=0; j<length; j++) {
x(i,j) =  0.9*x(i-1,j) + mu + sigma*sqrt( -2.0*log(genrand_real3()) ) * sin( 2.0*M_PI*genrand_real3() );
}
}
return(x);
}

繰り返しを各行に割り当てるため、int repl、各回のベクトル要素数は int length です。
ひとつ前のr_normal関数との違いは、まず毎回の結果を行列に格納している点です。
NumericMatrixが行列型を示しています。
一行目でもxをrepl行、length列の行列として定義します。NumericMatrix x(repl, length);
毎回の繰り返しでは、ひとつ前の結果に0.9を掛けて正規乱数を足していくので、iのforループは1から始まるようにしています(1行目は0のままで放置、Rコードで除去します)。
もう一つ、分かりにくい違いが添字の書き方です。さっきはx[i] = ...と肩のある括弧を使いましたが、行列の場合はx(i,j)と普通の括弧を使用するようです。C++の解説を見るとx[i][j]とありますが、こうするとエラーになりました。x[i,j]もダメです。



それでは冒頭のR版forループと速度比較してみます。
NN <- 10^6
system.time(r_markov(NN+1, 10, 0, 1)[-1,])
 #  ユーザ   システム       経過
 #   0.439      0.024      0.460

R版では5.778秒だったので10倍以上早いです。C++の速さを思い知らされました。

Rcpp、少しずつ部分的にC++化できるので、練習にも持って来いだと思いました。たぶん純粋C++に移行することはなくハイブリッドでやっていくことになりそうな気がします。