.. title:: Pythonで心理実験 - 例題4-1 例題4-1:手作業で記録なんてうんざりだ ======================================== **A:** お、B君、何かプログラムを作ったんだね。 **B:** あ、Aさんいいところに。pythonのプログラミングで聞きたいことがあるんですけど。 **A:** 今はあまり時間ないんだけどな。とりあえず聞いておこうか。 **B:** 講義でFlash Lagっていうのを聞いて、自分で見てみたいと思ってプログラムを書いたんです。 + 行番号なしのソースファイルをダウンロード→ `04-1a.py `_ .. literalinclude:: source/04-1a.py :language: python :encoding: shift-jis :linenos: :lineno-match: **A:** (実行画面を見て)おや、刺激の動かし方は教えていなかったと思うんだが、自分で考えたのかい? **B:** ええ、刺激のparameters.positionを+フレーム毎に変えたらいいのかな?と思ってやってみたらそれらしくなりました。 **A:** ふむふむ。35行目で動かす刺激を作ってmovStimに格納して、53~54行目と72~73行目でmovStim.parameters.positionに代入して位置を変更しているんだな。 代入されてる値はっと。X座標はSX/2で固定、Y座標は…。conditionlistとかmovingDurには何が入ってんのかな。 **B:** conditionlist[tn][0]には±1のどちらかが入っています。movingDurは全部で何コマ動かすかが入っていて、frameCountは今何コマ目を描いているかが入っています。 **A:** ふむ。movingStimは49行目で60を代入されたきり変更なし、frameCountは82行目でバッファのスワップが終わる毎に1増加するのか。じゃあこんな感じか。 VisionEggの標準設定ではY座標の正の方向が画面の上に対応しているから、conditionlist[tn][0]が1の時は下へ、-1の時は上に、画面の書き換え毎に3ピクセルずつ動くことになる。 .. csv-table:: :delim: $ 現在何コマ目を描いているか |HTMLBR| (frameCount)$movingDur/2-frameCount$3*conditionlist[tn][0]*(movingDur/2-frameCount) |HTMLBR| |HTMLBR| #conditionlist[tn][0]が1の場合$3*conditionlist[tn][0]*(movingDur/2-frameCount) |HTMLBR| |HTMLBR| #conditionlist[tn][0]が-1の場合 0$30$90$-90 1$29$87$-87 2$28$84$-84 …$…$…$… 58$-28$-84$84 59$-29$-87$87 **A:** ところでこれ、動く速さや時間はどうやって決めたの? **B:** えーと、ちょっと動かしてみて書き直して、またちょっと動かして書き直して… **A:** 試行錯誤か。まあそれもいいんだが、きちんと狙い通りの速さで動かす方法も知っておいた方がいいな。でも今日は時間が… **B:** あのー。聞きたかったのはそういう事じゃないんですけど。 **A:** おっと、そう言えばまだ何を聞きたいのか聞いてなかったな。ごめんごめん。で、何? **B:** 刺激が赤く変化した位置とか、被験者の反応とかを記録したいんですけど、こういうのってコンピュータで出来ないんでしょうか? いちいちメモをとるのは大変なんですけど。 **A:** ああ、そんなことか。それならテキストファイルに出力すればいい。出力だけならすぐ教えられるから今教えてあげよう。 ファイルを操作するためにはファイルオブジェクトを作成する。ファイルオブジェクトを作成する関数はopen( )だ。 **B:** ちょ、ちょっと待って下さい。メモ帳メモ帳。…よいしょっと。OKです! **A:** よっしゃ。open()関数は二つの引数をとる。ひとつはファイルへのパス、もうひとつは読み書きのモードだ。パスというのはわかってるよな? **B:** えーと。わかってるけど自信がないみたいな? **A:** …。ごく簡単に説明しておくぞ。WindowsでもMacOSでもLinuxでも、ファイルはディレクトリやフォルダというものを使って階層的に管理されている。 どのようにディレクトリをたどっていけば目的のファイルにたどり着けるかを示した文字列をファイルのパスという。ディレクトリの名前を区切る文字がOSによって違うので、人が書いたプログラムを自分のPCで動かす時には注意が必要だ。 さらに言うとWindowsの区切り文字\\は表示に使うフォントによって¥に見えたり\に見えたりする点も注意。 .. csv-table:: :delim: $ OS$区切り文字$例$意味 Windows$\\$C:\\Python25\\python.exe$CドライブのPython25というディレクトリの中にあるpython.exeというファイル Linux$/$/home/username/test.py$homeというディレクトリの中にあるusernameというディレクトリの中にあるtest.pyというファイル **B:** …MacOSは? **A:** MacOSは長い事使ってないから忘れちゃった。確かMacOS XからBSD系のUnixになってるはずだから、Linuxとそう違わないと思うけど。 まあ今は時間がないので気になったら自分で調べといて。 **B:** そんな無責任な。 **A:** MacOSのマシンを持ってないからすぐに確認できないのさ。一応これでも確認して書いてるんだぞ。 まあそれはさておき、相対パスやら絶対パスやら、パスについて詳しい事は自分で勉強してもらうとして、ひとつ大事なことを言っておこう。 ファイルへのパスとして区切り文字を含まない文字列、例えば'data.txt'を渡すと、カレントディレクトリという場所のファイルを指定した事になるんだ。 カレントディレクトリも詳しくは自分で勉強してもらうとして、プログラム開始直後はpythonのインタプリタを実行したディレクトリがカレントディレクトリになる。 **B:** ??? **A:** 例えばWindowsでpythonスクリプトのファイルをダブルクリックで起動した場合、そのスクリプトの中でopen('data.txt')とすると同じディレクトリにdata.txtというファイルが出来る。 **B:** ははあ、なるほど。 **A:** さて、もうひとつの引数は初めてファイル操作をするプログラムを書く人にはちょっとピンとこないかも知れないが、以下のようなモードを指定する。 .. csv-table:: :delim: $ r$ファイルを読み込みモードで開く。ファイルは存在していなければならない。第2引数が省略された場合はrが指定されたものと解釈される。 w$ファイルを書き込みモードで開く。 **指定した名前のファイルがすでに存在している場合は消去される** 。 r+$ファイルを読み書き両用モードで開く。ファイルは存在していなければならない。 a$ファイルを追加モードで開く。出力したデータは既存の内容の末尾に追加される。 rb,wb,r+b$それぞれr、w、r+と同じだが、ファイルを **バイナリ** モードで開く。 **B:** うわ、わからないところだらけで何から聞いていいのやら。 **A:** 心理実験に使うという事だけを考えると、rとwが分かっていればとりあえず何とかなる。 データを保存したい時にはwを使って新しくファイルを作成する。で、後から保存したデータを読み込んで処理をしたい場合はrを使って開けばいい。 実験の場合、一度出力したデータに追加したり改変したりするような事はあまりないはずだから、r+やaは使う場面が少ない。 **B:** データを改変しちゃったら大変ですもんね。 **A:** ひとつ気をつけないといけないのは、 **wでファイルを作成しようとした時に、すでに同名のファイルがあると削除されてしまう** 事だ。 ついうっかり前の実験データ保存ファイルと同じ名前を指定してしまって、データを消してしまったら大変だ。 **B:** 消さないようにすることは出来ないんですか? **A:** open( )を実行する前に、同じ名前のファイルが存在するかプログラム内で確認すればいい。 まあここまで説明していると時間がなくなっちゃうのでパス。 **B:** あと、バイナリモードってのが良くわからないんですけど。 **A:** あぁ。それも説明すると長くなるな。宿題!自分で調べときなさい! **B:** えー。 **A:** ごく簡単に言っておくと、pythonのファイルオブジェクトはOS間のテキストファイル形式の違いを吸収してくれるんだけど、 画像ファイルなどのテキストファイル以外のファイルにそれを適用されるとまずい事が起きる。そこでテキストファイル以外のファイルを開く時にはバイナリモードというのを使うんだ。 これ以上は宿題ね。 **B:** むむー。なんだかよくわからないけどややこしいですねえ。 **A:** さて、open()関数の戻り値はファイルオブジェクトというものだ。 こいつを使うと本当にいろんな事が出来るんだが、今日はwrite( )とflush( )、close( )を説明しておこう。これだけあれば実験データをテキストファイルに書き出すには十分だ。 .. csv-table:: :delim: $ fp.write(s)$ファイルオブジェクトfpで開いているファイルにsを書き出すようOSに指示を出す。 fp.close()$すべてのデータを書き出してファイルを閉じる。 fp.flush()$現時点で書き出し指示が出ているデータをすべて実際にファイルに書き出す。 **B:** 「書き出すようOSに指示を出す」…? **A:** ハードディスクなどの記憶媒体はメモリなどに比べると非常に読み書きが遅いデバイスだ。 そこで、「これをファイルに書いておいてね~」とOSに指示を出しておいて、OSのファイルストリームという領域にため込んでおく。 そして、OSは都合がいい時にこのストリームにため込まれたデータを実際にファイルに書き出す。そうすれば、OSは読み書きが遅いデバイスに足を引っ張られれずに 高いパフォーマンスを発揮する事が出来る。 **B:** うー。ややこしいですね。 **A:** 実際にインタプリタで試してみると分かりやすいかな。以下ではWindows上のpythonでC:\\Users\\username\\Desktopという場所にtest.txtというファイルを書き込み用に 開いている。自分のPCで試す場合はファイル位置を適当に変更してくれ。 :: >>> fp = open(r'C:\Users\username\Desktop\test.txt','w') **B:** あ、デスクトップにtest.txtというファイルが出てきました。 **A:** Windows Vistaの場合、C:\\User\\username\\Desktopというディレクトリはusernameという名前のユーザーのデスクトップに対応しているから、デスクトップにtest.txtが作成されたわけだ。 ここへwrite()を使って文字列を書き込んでみる。 :: >>> fp = open(r'C:\Users\username\Desktop\test.txt','w') >>> fp.write('Hello, World!') **B:** またHello, World!ですか。 **A:** この時点でファイルへデータを書き込むようにOSに指示を出したわけだが、test.txtをメモ帳で開いてごらん。 **B:** どれどれ…。あれ、空っぽのファイルですね。 **A:** だろ? OSが'Hello, World!'という文字列をファイルストリームにため込んでいて、まだ実際のファイルに書いていないのさ。だからファイルの中身は空っぽだ。 ここでいったんメモ帳を閉じて、flush()を実行してみる。 :: >>> fp = open(r'C:\Users\username\Desktop\test.txt','w') >>> fp.write('Hello, World!') >>> fp.flush() **A:** さあ、もう一度メモ帳でtest.txtを開いてごらん。 **B:** おお、今度はちゃんと書き込まれている。 **A:** じゃあclose()を実行してファイルを閉じておこう。close()を実行した時にも、ストリームの内容がすべてファイルに書き込まれる。 だから通常はいちいちflush()しなくても、単にすべてのデータを書き終えたらclose()すればいい。むしろ、flushをいちいち実行すると効率が悪いので、flush()は出来るだけ控えた方がいい。 **B:** じゃあflush()なんか紹介しなきゃいいじゃないですか。 **A:** flush()したほうがいい場合がある。それは後で説明しよう。とにかく、open()してwrite()してclose()すればデータをテキストファイルに保存できる。それだけだ。 **B:** ふーん。意外と簡単なんですね。 **A:** さて、じゃあB君のプログラムを改造していこうか。まずどこかでデータファイルを開かないといけない。 これはwrite()を実行する前ならどこでもいいんだが…。そうだな、一番最後に付け加えるとするか。ファイル名はresults.txtとでもしておこう。 この一行をプログラムの最後に追加する。 :: fp = open('results.txt','w') **B:** ふむふむ。 **A:** 続いて保存するデータを収集しなくちゃいけない。私がよく使うのは、実験を開始する前にresultsという名前の空リストを作っておいて、そこへ1試行終わるごとにデータを追加するって方法だな。 49行目、movingDur = 60の前にでも追加しておこう。 .. literalinclude:: source/04-1b.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 47-50 **A:** そして、各試行の最後で変数resultsに必要なデータをappend()していく。 今回はconditionlist[tn]に第tn試行の条件を記録したリストが入っていて、被験者の反応がkeyという変数に入っているんだから、conditionlist[tn]にkeyを付け加えたリストを append()すればいいだろう。whileループを抜けた後に実行しないといけないので、100行目と字下げが異なる事に注意。 .. literalinclude:: source/04-1b.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 100-104 **B:** えっと、append()の中身はなんでconditionlist[tn]+ **[** key **]** って[ ]が必要なんですか? **A:** keyはリストではないから、+演算子でconditionlist[tn]に付け加える事が出来ない。素直に付け加えるなら conditionlist[tn].extend(key)と書くべきなんだろうが、そうするとconditionlist[tn]自体にkeyが付け加わってしまうので、それが気持ち悪くて[key]という 要素がひとつのリストを作成してconditionlist[tn]と連結している。まあ、このあたりは完全に好みの問題だ。 **B:** なんか「好み」ってパターン多いですねえ。 **A:** 同じ結果を得る方法は何通りもある。正解はひとつではない。初心者にはそういうところがかえって勉強しにくいのかも知れないね。 とにかくテキストや他人の書いたプログラムを読んで勉強して、自分なりのスタイルを確立する必要があるんだろうな。 **B:** うーん、当分たどり着きそうにないですねえ。っていうかたどり着くまでプログラミングの勉強すんのかな。 **A:** それはどんな職業に就くかにも寄るだろうな。ともかく、これで104行目のopen( )にたどり着く前に、resultsという変数に各試行の条件と反応を並べたリストが格納されている状態になる。 ここからいよいよデータの出力だ。ここの書き方も何通りもあるが、ひとつの例を示しておこう。 .. literalinclude:: source/04-1b.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 104-106 **B:** ええと、ええと…。わかるようで、わかりません。 **A:** resultsは以下のような2次元のリストになっている。 .. code-block:: python [[1,-2,'U'], [-1,1,'D'], [-1,0,'D'], # 以下略 ] **A:** このリストをfor r in results:でループさせるんだから、rには[1,-2,'U']といった3要素のリストがどんどん代入されることになる。 これをwrite()で出力するわけだが、その時に後々便利なようにカンマ区切りのテキストとして出力したい。そこで登場するのが例題3-4で話した文字列に対する%演算子だ。覚えているか? **B:** ああ、よくわかんなかったってことは覚えています。 **A:** 今回は実践だぞ。出力したいデータは3つ、最初の2つは整数だ。整数を出力するためにはformat文字列に%dと指定するんだったよな。 データの間をカンマで区切りたいので、ここまでで"%d,%d,"となる。 **B:** ふんふん。 **A:** 最後のデータは'U'や'D'といったキーを押した方向に対応している文字列だ。文字列を出力するには%sを使うんだった。 なので先ほどの"%d,%d,"に%sをくっつけて"%d,%d,%s"。このままだとすべてのデータが1行にだだーっと連なって書き出されてしまうので、次のデータは次の行へ出力するように 指示する。そのためには「改行文字」というのを出力しないといけないが、これは\\nと書く。これを最後にくっつけて"%d,%d,%s\\n"だな。これでformat文字列は完成だ。 あとは%演算子でデータを埋め込んでやればいいんだが、残念ながらrはリストだ。例題3-4の時にも話したように、%演算子の右はタプルでなければいけない。 幸いにもタプルを生成する関数tuple()というのがあって、tuple(r)とすれば要素がrと同じタプルを生成する事が出来る。これを利用して "%d,%d,%s\\n" % tuple(r)としてやれば望みどおり各試行のデータをカンマで区切った文字列が完成する。 **B:** …。 **A:** おーい。しっかりしろー。 **B:** だめです、気が遠くなりそうです…。 **A:** まあ、聞いてるだけじゃわけわからんだろうから、自分でいろいろやってみなさい。たとえば被験者のキー押しを出力せずに条件だけ 出力するように書き替えてみるとか、第何試行かを示す数字を付け加えてみるとか。試行錯誤しているうちに少しずつ分かってくるよ。 以上でプログラムの書き換えは完了だ。書き換え後のファイルを04-1b.pyとして置いておくよ。( `こちらからダウンロード `_ ) **B:** あれ、close()はしなくていいんですか? **A:** pythonではプログラムが終了する時に開きっぱなしのファイルがあると自動的にclose()してくれる。 **B:** そりゃ楽でいいですね。あ、close()でさらに思い出しましたけど、さっき言ってたflush()した方が良い場合ってなんですか? **A:** あー。その話するの忘れてた。今回書き換えたプログラムだと、すべての試行が無事に終了してから初めてデータを出力し始める。 もし、実験の途中でPCにトラブルが発生して途中で止まってしまった時に、そこまでのデータがすべてパーになっちゃうだろう? 要所要所でflush()しておけば、flush()した時点までのデータは手元に残る。 **B:** なんか後ろ向きな理由ですね。 **A:** でも予期せぬエラーで止まる時がまれにあるからね。被験者さんの苦労を無駄にしないためにも重要だと思う。 **B:** 要所って例えばどんなところでしょう? **A:** そうだなあ。1試行終わった時、1セッション終わった時…。区切りが良くて、万一ほんのちょっとの遅延が入っても実験に支障がないポイントかな。 **B:** それはそうと、Aさん時間がないって言ってましたけど、何時までOKなんですか? **A:** 何時ですかって…って、もう6時過ぎてるじゃないか! こうしちゃおれん、じゃあ、後はよろしく!! **B:** お達者で~ .. |htmlbr| raw:: html