私のデータ解析が行き着いたところは、以下のような流れです。現時点では、これで動いているのでよいのですが、随分色々試行錯誤した末なので、ある程度最適化されていると思いますが、ともかく正解の分からない世界です。今の私の答え、という以上のものではありませんが、カノニカル・パイプライン canonical pipeline ととりあえず呼んでいます。なんとなくカッコいいですから。
データ解析のカノニカル・パイプライン
Live Scriptを使用。探索目的のスクリプトの命名法は、scr20170725_112030_hogehoge.mlx
という形式。発表用のスクリプトは、区別できるようにmain1_hogehoge.mlx
(Figure 1に対応)という形式。
-
環境設定
- フォルダパス設定、パラメータなど
-
ファイルの同期・更新
- Excelワークシートからのデータ抽出とマージ
- 生理学データの持つ不規則性が障害となる
- 2つのファイルをマージして1つのファイルを出力するときの7つのシナリオ
-
データの解析計算
- 時間のかかる計算処理は結果を保存して次から読み込む
-
parfor
を使った高速化 waitbar
はできるだけ使わない- 特定のデータにアクセスするための仕掛け
-
統計検定
- table型のデータを徹底活用
- グループ化変数 grouping variables(MATLABドキュメンテーション) を最大限利用
- 図の作成
一番大変なのはどこ?
ここでクイズです。
上の工程のうち、一番たいへんだった箇所はどこでしょうか?
1番から5番でお答えください。
。。。
正解は、.... 2番です。ですが、いきなりファイルの同期・更新と言っても、意味が分かりませんね。
2番では、データ解析はしていなくて、データ解析のためのデータを正しいフォーマットにして、正しい場所へ格納するという作業を行っています。このとき、もし元のデータファイルが永久不変であれば、同期も更新も必要なく、一度正しい場所に保存したらそのままでよいはずです。
そして、データが正しいフォーマットで正しい場所にあれば、後はどんどんプログラムを書けばデータを処理できますから、3番がいわゆるデータ解析の作業に当たるのでしょうけども、私の場合は、これは所要時間も2番に比べればずっと短く、実際にデータの中身を見ることになるので、研究の醍醐味でもあり、楽しい作業だと言って良いでしょう。
ところが、2番のためのプログラミングというのは、データの中身をほとんど問題にしません。それよりもファイル名、データ形式、と言ったことばかりを考えることになります。
家の配管工事、町の水道管工事に似ていて、水そのもの(データの中身)を利用するのではなくて、そのためのパイプ作りです。ファイル名やデータ形式は、コンピュータープログラムにとってはとても重要ですが、何かファイル名やデータ形式の誤りがあると、このパイプから水が漏れて失敗、ですから使えません。そんなイメージです。
先にも書いたように、元のデータファイルが永久不変であれば、同期も更新も必要なく、一度正しい場所に保存したらそのままでよいのですが、私の場合は、永久不変とは言えないと結論しました。
二系統のデータの統合
永久不変と言えない理由はふたつでした。
- データの前処理の段階での何らかの人為的エラーはある程度防ぎようがなく、誤りが見つかる度に、そこを修正する必要が出てくる。
- 解析のスタートとなるデータは二系統あって、それらを統合・マージして解析をする必要がある。
1は誰にとっても当てはまるものですが、2の事情があるために、とりわけ1が重荷になる、というのが実情でした。
プログラミングやデータ解析の途中で、データファイルのほうに不備が見つかったりすると、修正してやり直しになります。データファイル修正の度にデータのマージを誤り無く行う作業の繰り返しとなり、最初は人力でやっていたのですが、何か問題が有る度にこれを人力でやっていると非常に大変だということが分かりました。間違いのないように、対応するデータを選び出して合流させるのは結構手間なのです。
人力で地獄を見たので、仕方なく自動化を決意しました。簡単だと思っていたのにやってみると案外と複雑で、こっちも地獄でした。
電気生理学の実験データは、記録セッション毎(一個のニューロンあるいは記録部位につき、通常は複数のセッションで記録する)に一つのデータファイル(私の場合は Spike2という記録・解析ソフトのデータ *.smr
形式)にまとまって入っています。しかし、実験中また、実験後に収集されるその他のデータ(実験動物の条件、記録部位の解剖学のデータなど)はSpike2では管理できないので、巨大なひとつのExcelのワークシートに入力してあります。実際にデータ解析をする際には、この二系統のデータを突き合わせて利用する必要があるわけです。
二系統のデータ(Spike2の生理学データとExcelの解剖データ)を突き合わせて利用するには、後で突き合わせるか、最初にまとめておくかのどちらかが考えられます。
- データとしては別々の場所に保管したままで、実行時(ランタイム)に対応するデータを探して読みに行く、というデザイン
- 初めから一箇所にすべてのデータをまとめて保管しておき、それを実行時に利用するというデザイン
どちらでもできるのでしょうが、感覚として、一つのデータファイルに全情報がまとまっているほうが、スッキリして良いだろうということで私はこの後者のほうを選びました。
すなわち、Spike2の生理学データとExcelのその他のデータ(解剖データなど)を、MATLABのデータファイルの中にまとめて保管し、それを読み込んでデータ解析をするという順序です。
記録・解析用ソフトウェアのSpike2の*.smr
形式から、MATLABの*.mat
形式にデータのエクスポートが出来ますから、こうして作った個別の*.mat
ファイルに、対応する実験データを巨大Excelファイルから探し出して、それを追加するというのがここでの作業です。二系統あったデータが一箇所にまとまるので、統合作業であり、マージと呼んでよいでしょう。
文章で書くといかにも大した事なさそうですが。。。そして、当時私も初心者だったので、学会などで、生理学のデータ解析の経験が豊富な人に合う度にこれについて相談を持ちかけたのですが、誰に聞いても「そんなの簡単でしょう」「何が難しいのか全く分かららない」と判で押したような反応でした。
二系統のデータの統合の自動化
私の場合、Excelは一個の巨大ファイルがあるだけですが、マージの前には、個別の実験データに対応する部分だけ抽出した小さなExcelファイルを、Spike2のファイルと同じ数だけ準備しておきます。
これらを合体させるわけで、二つの入力ファイル(*.smr
がメインで *.xlsx
が追加)を合流させて一つの出力ファイル(*.mat
)にします。この時、任意の *.smr
入力データファイルについて、マージ作業の取扱シナリオが7種類にも分かれることがベン図を描いてみると分かりました。
どうでしょうか、大変そうでしょう。s1が*.smr
形式のSpike2のデータファイル、s2が*.xlsx
形式の小さなExcelファイル、dが*.mat
形式のMATLABデータファイルの集合を表します。
以下で、A∩Bは、AとBの共通部分(intersection)を表します。B∖Aは、差集合(set difference)で、BからAに属する元を除いた集合を表します(集合要素の引き算)。MATLABならば、それぞれ、intersect(A,B)
とsetdiff(B,A)
という関数が対応しています。
- s1∩s2∩d: 3つの集合の共通部分。対応するファイルはすべて揃っているので、最終更新日を比較し、s1やs2のファイルよりもdが新しいことを確認。dのファイルのほうが対応するs1、s2のファイルよりも古い場合は更新が必要。
- (s1∩d)∖(s1∩s2∩d): s2に対応するファイルが欠けているので、これを準備した上で、s1とマージしてdのファイルも更新します。
- (s1∩s2)∖(s1∩s2∩d): dに対応するファイルが欠けているので、s1とs2のファイルをマージしてdにファイルを追加します。
- (s2∩d)∖(s1∩s2∩d): s1に対応するファイルが欠けている場合。s1にファイルが無いということは、これらのファイルは解析の対象外なので、s2およびdからファイルを削除する。
- s1 only: s1のファイルに対応するファイルがs2, dに欠けている。s2へファイルを準備し、s1とマージしてdへファイルを追加。
- s2 only: s2のファイルに対応するファイルがs1, dに欠けている。s1に対応するファイルがないということは、これらのファイルは解析の対象外なので、s2からファイルを削除する。
- d only: dのファイルに対応するファイルがs1, dに欠けている。s1に対応するファイルがないということは、これらのファイルは解析の対象外なので、dからファイルを削除する。
これらの作業を行った結果、最終的には、全てのファイルが 1. s1∩s2∩d のカテゴリに含まれるはずで、これでマージ完了です。
人力でこの作業を誤り無く行うのが難しいのはよく分かると思います。自動化の際も場合分けが複雑なので注意する必要があります。
実装の方針: makeか否か
このように、あるファイルから別なファイルへ、と変換しながら、途中でマージも含むというワークフロー(いたってありふれた状況だと思いますが)を実装するに当たっては、2つの選択肢がありました。
- データ解析はMATLAB中心に行うと決めていたので、ファイルの同期・更新もMATLABで実装する
- GNU Makeを利用して、Makefileに、データ・ファイルの依存関係を記述し、バッチ処理を行う
実は、私の経験豊富な相談相手から推奨されたのは2のGNU Makeを使った方法でした。
複雑に関連し合ったファイルの依存関係を解決するのがmakeの長所である。例えば、A というファイルを処理して B というファイルを生成するとき、makeはそれぞれのファイルの更新時刻(タイムスタンプ)を参照し、A が B よりも新しいときには作業を行うが、B が A より新しければ作業は不要と見なして何もしない。ファイル数が増え、依存関係が複雑になっても、makeはmakefileの記述を頼りに必要最低限の作業だけを自動で行う。『ウィキペディア make』
このように、そもそものmakeの存在意義は、上で挙げたような、複雑なファイルの依存関係を解決することにあります。これを使えば、7つのシナリオもあまり心配しなくて良い、はずでした。
makeについて多少実験を行ったのですが、結果的には1を選んでいます。理由を挙げるならば、
- makeの実験中に、7つのシナリオに対応するMATLAB関数ができてしまった
- Windows環境で GNU makeを動かし、しかもMakefileからMATLAB関数を呼び出して動かすのは、いろいろと面倒であること
があるでしょう。しかし、Makeの利点を考えると、今でもこの判断が良かったのか疑問は残ります。
makeを利用した場合…
不採用の方針ですので、あまり詳しくありませんが、備忘録として、もし仮にGNU Makeを使ったワークフローを選んでいたとしたら、どういう世界になったかだろうか、ということを書いておきます。次のプロジェクトでは採用するかもしれませんし。
元々GNU makeはUnixのためのツールですから、Macで動かすには terminalから簡単にできますが、Windowsでは移植されたヴァージョンを使います。インストール方法はこちら。
MATLABからExcelを書き出すための関数xlswrite
は、Windowsでないと機能が大幅に制限されるという問題があり、また他の理由もあってプラットホームはWindowsで方針が固まっていました。
Makefileは、基本的に次のような書法で、target
には目標となるファイル群、dependencies
にはtarget
を作るために必要となるファイル群、二行目はタブで始まり、OSのコマンドウィンドウ(macOSならばTerminal、Windowsならコマンドプロンプト)で実行する命令文を書きます。
target: dependencies
system command(s)
このときに、Windowsの場合、コマンドプロンプトからMATLABの関数を呼んで処理を実行する必要が出てきます。matlab
コマンドに続けて、
matlab -nodisplay -nosplash -nodesktop -r "myfunc(dependency1,dependency2);"
のようにすればよいはずです(未確認)。Makefileでは、以下のような記述がズラズラと並んでファイル相互の依存関係を記述します。
data001_step1.mat: data001.mat, data001.xlsx
matlab -nodisplay -nosplash -nodesktop -r "myfunc1('data001.mat','data001.xlsx');"
data001_step2.mat: data001_step1.mat
matlab -nodisplay -nosplash -nodesktop -r "myfunc2('data001_step1.mat');"
そして
make data001_step2.mat
を実行することで、data001_step2.mat
生成のために必要な処理が順次実行されます。(合ってるかな?)
この方針で行くと、myfunc
に相当する部分の関数は、バッチ処理ではなく、常に個別の入力ファイルに対して動くことになります。通常の発想で多数のファイルのバッチ処理をしようとするとどこかでfor
ループを使うことになりますが、Makefileで使用する関数はバッチ処理を内蔵する必要がありません。
代わりに、個別のファイル名に対応するような長大なMakefileを自動生成して実行する、あるいは、Makefileの中で変数などを利用してファイル名を動的に指定するような書き方をする、ということになるでしょう。Makefile側でバッチ処理に対応する必要があるということです。
個々の関数がバッチ処理を内蔵しないのは、関数が小さく出来るのでありがたいことです。しかし、100を越える数のデータファイルに対応するバッチ処理のためのMakefileを作るのが課題になります。
課題点として思いつくのは...
- データファイルは巨大なので、できれば多数生じる中間ファイルは作業終了後に削除して、最初のファイルと最後のファイルだけ残しておきたいが、そのような条件でmakeが動作するのか不明。dependencyのファイルが存在しないと、もう一度準備しようとして全ファイルについて処理を行うのではという懸念あり。
- Makefileの思考方法は通常のMATLABのプログラミングとはかなり違う。
- コマンドプロンプトからMATLABを実行する場合、ファイル名などは引数として指定できるが、ワークスペースの変数を引数として利用できないと思われ、できることにかなり制限があるのでは?
結語
一旦はMakefileの利用も検討しましたが、上のベン図を利用して、ほぼmakeとおなじことを実現する関数をMATLABで実装したので、結局私はそれを採用しました。
このマージの部分は、7つのシナリオを別々に考えるので相当に複雑ですが、そうだと分かってしまえばやることははっきりしているので、それほどひどい状況ではありません。
一番大変だったのはおそらく、実験データのファイル名とデータ形式につきまとう、不規則性です。コンピューター・プログラムは、不規則なものが嫌いですが、現実世界で生理実験を行っていると、その日その日の事情や思いつきで、記録電極の配置や数を変えたり、いろいろな状況が発生します。これに対処して正常に動くプログラムを書くのが大変だったという話はまたの機会に。