8. マウスで刺激を動かそう―鏡映描写課題

8.1. この章の実験の概要

これまでの章では実験参加者の反応計測のためにキーボード用いてきましたが、この章ではキーボードと並んで普及しているPCの入力機器であるマウスを反応に用いる方法を学びます。ただ今までカーソルキーの左右を押していたのをマウスの左クリック、右クリックに置き換えただけでは面白くないので、マウスならではの課題である鏡映描写課題をBuilderで作成したいと思います。

鏡映描写課題とは、鏡に映った自分の手の像を見ながら図形をペンでなぞる課題です( 図8.1 左)。ペンでなぞる図形の上には遮蔽版が置かれていて図形を直接見ることが出来ないようになっていて、奥に立てられた鏡に映った像を見ながら手を動かさなければいけません。鏡を見ながら描画するために前後方向(鏡に近づく、離れる)に手を動かしたときの視覚像の動きが逆転してしまうので、うまく図形をなぞるのはかなり困難です。しかし、何度も練習を繰り返していると、次第に手とその鏡像の動きの関係が学習され、素早く間違わずになぞることが出来るようになってきます。状況にふさわしい知覚と運動の関係を学習することを知覚運動学習といいます。鏡映描写課題は、知覚運動学習の課題として用いられます。

鏡映描写課題と類似の課題をPCで実現するために、この章では 図8.1 右のような課題を考えます。スクリーン上に何カ所か折れ曲がった白い太線(パス)と、小さな円(プローブ)が提示されています。実験参加者がマウスを動かすとプローブが動きますが、スクリーンの上下方向のみ通常のマウスカーソルとは逆向きに動きます。つまり、マウスを奥に向かって動かすとプローブはスクリーンの下方向へ移動し、マウスを手前へ動かすとプローブはスクリーンの上方向へ移動します。マウスを動かすテーブルに箱か何かを置いて手元が見えないようにすれば、鏡映描写に類似した状況が得られるはずです。

_images/mirror-drawing.png

図8.1 鏡映描写課題。遮蔽版の下に置かれたテスト用紙に描かれた図形を、鏡に映った像を見ながらペンでなぞります。スクリーンとマウスを用いてこの実験装置をシミュレートします。

もう少し実験の詳細を検討していきましょう。まず、パスをどのような形にするかを決めなければいけません。 図8.1 は実験のアイディアを記しただけですので適当なパスを描いてありますが、実際に実験を作成するとなるときちんとした形状を決定する必要があります。特にBuilderで作成するにはその大きさや中心位置を座標として計算できないといけません。また、パス間で移動すべき距離が異なるのは避けたいところです。そこで、この章では 図8.2 のように星形の図形の辺を時計回りまたは反時計回りに進んで半周するパスを採用します。星形の中心から鋭角の頂点までの距離は300pixで、星形の中心がスクリーンの中心に一致するように配置します。スタート地点は鈍角の頂点から選び、星形の中心をはさんでスタート地点と向かい合う鋭角の頂点がゴール地点となります。スクリーン上でゴール地点をすぐに判別できるように、直径30pixのディスク(円)をゴール地点に提示します。パスは幅20pixの長方形を用いて描画します。スタート・ゴールの組み合わせが5種類、進行方向が反時計回りか時計回りかの2種類ありますので、合計10種類のパスが得られます。

_images/mirror-drawing-paths.png

図8.2 実験で使用するパス。星形の辺を時計回りまたは反時計回りに進んで半周します。5種類のスタート・ゴールの組み合わせと2種類の進行方向で合計10種類のパスが得られます。

パスを描画するための座標値を計算してみましょう( 図8.3 )。座標値の計算は、Builderでマウスを操作する方法を学ぶという本章の目的とは直接関係ありませんので、よくわからなければ次の段落まで読み流していただいて構いません。まず、長方形を用いてパスを描画するのですから、BuilderではPolygonコンポーネントを使うのが適切です。Polygonコンポーネントで星形の辺を描画するには、その辺の長さと中点の座標が必要です。 図8.3 では、辺AEの長さと、辺AEの中点である点Fの座標が必要な値です。点AとBの座標がすぐに求まること、BとEのY座標が等しいこと、辺ACがy=-tan(72°) x+300上にあることを利用すれば、Fの座標は計算できます。点Fの座標が得られればY軸を挟んで向かい合った点Gの座標が直ちに得られます。点FとGの座標がわかれば、原点を中心としてこれらの座標を72度ずつ回転させればすべての辺の中点が得られます。点Fの座標を求める途中で点AとEの座標が得られますので、辺の長さも計算可能です。

_images/calc-midpoint-of-paths.png

図8.3 星形を描画するための座標値の計算。Fの座標が得られればGの座標も直ちに得られます。FとGの座標を72度ずつ回転させればすべての辺の中点が得られます。

座標値を計算して、 図8.2 の10通りのパスを描けるようにBuilderの条件ファイルの形で並べたものが章末の 図8.20 です。pathXpos(X=1から5)が各辺の中心の座標で、 位置 [x, y] $ の値として使用します。pathXoriは 回転角度 $ に使用する回転角です。startPosとgoalPosはそれぞれスタート地点とゴール地点の座標です。このファイルをexp08cnd.xlsxという名前で保存して使用します。

パスにおける各辺の長さと幅はすべて等しいので、条件ファイルを利用せずにBuilder上で直接 サイズ [w, h] $ に値を入力することにします。 図8.3 から計算した辺の長さは218.0pixなのですが、ぴったり218pixにすると頂点のところでパスが狭くなってしまうので、余裕を持たせて辺の長さは240pixにしましょう。パスの幅は20pixなので、 サイズ [w, h] $ に指定する値は[240, 20]です。

これで提示する図形についてはほぼ決まりましたが、まだプローブの大きさと色を決めていません。プローブの直径は10pixとします。プローブの色は、パスを白色で描画することを考慮すると、パスと重なっていても区別できるように白以外の色にする必要があります。パスや背景と区別が付けば何色でも構わないのですが、ここでは白い輪郭に赤色の塗りつぶしで描画することにしましょう。ついでに、ゴール地点に提示する円もわかりやすいように、白い輪郭に緑色の塗りつぶしで描画することにしましょう。

続いて実験の手続きを決定しないといけませんが、以下の解説ではBuilderの使い方を学ぶという目的を最優先して、ごくシンプルな手続きだけを作成します。 図8.4 をご覧ください。各試行の最初に、プローブと共に、ゴール地点を示す円と色と大きさが等しい円をスタート地点に提示します。実験参加者がマウスの左ボタンをクリックすると、スタート地点の円がゴール地点へ移動すると同時にパスが提示されます。実験参加者はマウスを操作して、出来る限りプローブがパスからはみ出ないように注意しながらプローブをゴール地点まで動かします。プローブの中心がゴール地点の円内に入ったことが検出された時点で試行は終了します。試行終了後は直ちに次の試行へ進みます。以上の手続きを、exp08cnd.xlsxに定義されている10種類の条件に対して1回ずつ、合計10試行を無作為な順序で実施したら実験は終了です。

_images/mirror-drawing-procedure.png

図8.4 実験の流れ。

この課題を実験実習などの授業で使用するならば、鏡映描写課題を練習した群としなかった群で比較したり、非利き手で練習して学習の効果が利き手にどの程度転移するかを調べたりするなどの工夫をする必要があります。この辺りは各自で工夫してみてください。

さて、以上で作成する実験の内容が決まりました。作成方法の解説を始めたいと思いますが、その前に本章で初登場のMouseコンポーネントについて解説しておきましょう。

8.2. Mouseコンポーネント

Mouseコンポーネントは、その名の通りBuilderからマウスを利用するためのコンポーネントです。3ボタンのマウスを想定しているので、4つ以上ボタンがあるマウスの場合は押されたことを検出できないボタンが出てきてしまいます。一般的なマウスの場合、3つのボタンはそれぞれ「左クリック」と「右クリック」に使うボタンと、これらのボタンの間にあるボタンに対応します。 図8.5 左に示したMouseコンポーネントのアイコンをクリックすると、 図8.5 右のプロパティ設定ダイアログが開きます。

_images/mouse-properties-dialog.png

図8.5 Mouseコンポーネントと設定ダイアログ。

Mouseコンポーネントのプロパティ設定ダイアログのうち、 名前開始終了 は今までのコンポーネントと共通ですので特に解説は必要ないでしょう。 ボタン押しでRoutineを終了 はKeyboardコンポーネントと同様に、チェックがついていればマウスのボタンが押されたときにルーチンが終了します。 マウスの状態を保存 はマウスのどのような情報が保存されるかを指定します。選択可能な項目については 表8.1 をご覧ください。最後の 時刻の基準 はMouseコンポーネントで使用される時刻の0秒がルーチン開始時(routine)か実験開始時かを選択します。

表8.1 Mouseコンポーネントの マウスの状態を保存 で選択可能な値
概要
最終 ルーチン終了時のマウスの状態が保存されます。 名前 がfooの場合、実験記録ファイルのfoo.xとfoo.yにマウスカーソルのX座標とY座標、foo.leftButton、foo.midButton、foo.rightButtonにマウスの左ボタン、中央ボタン、右ボタンの状態が保存されます(0=押されていない、1=押されている)。座標の単位は実験設定ダイアログの 単位 に従います。
クリック時 マウスのいずれかのボタンが押されるたびに、その時のマウスの状態が保存されます。「最終」の時に出力される項目に加えて、押された時刻を保存するfoo.time(fooは 名前 に入力した文字列)という項目が実験記録ファイルに出力されます。ボタンを押しっぱなしにしていると、その間1フレーム毎に状態が保存されていきます。
フレーム毎 マウスが有効な間( 開始 から 終了 の間)、フレーム毎にマウスの状態が保存されます。保存形式は「クリック時」と同じです。60Hzのモニターを使っている場合1秒間に60件もデータが記録されますのでご注意ください。
なし マウスの状態を保存しません。

注意する必要があるのは マウスの状態を保存 の値によってどのようなデータが保存されるかです。まず、「最終」ではボタン押しの有無にかかわらず、とにかくルーチン終了時のマウスの状態が保存されます。この点はKeyboardコンポーネントと異なっています。マウスカーソルの座標の単位は実験設定ダイアログの 単位 に従います。

「クリック時」では、 開始終了 で指定された期間内にマウスのボタンを押すとマウスの状態が記録されます。このように書くと「クリックした回数だけ記録される」と思われてしまうかも知れませんが、そうではなくて「ボタンが押されている間ずっとフレーム毎に」記録され続けます。つまり、リフレッシュレートが60Hzのモニターを使用していてマウスのボタンを1回、1秒間押し続けると、1件ではなく60件のデータが保存されます。実験記録ファイルには、カーソルのX座標の値が[0.48, 0.49, 0.51]、Y座標の値が[0.09,-0.03, -0.11]といった具合にPythonのリストの形で保存されたデータが出力されています。この例の場合、ボタンが押されたときのマウスカーソルの座標は記録された順番に[0.48, 0.09]、[0.49, -0.03]、[0.51, -0.11]だったということです。このように3件のデータある場合、「素早く3回クリックされた」のか「1回、3フレーム分の時間ボタンを押した」のか「1回素早くクリックし、1回2フレーム分の時間ボタンを押した」のかを区別するには、ボタンが押された時刻の出力を確認する必要があります。 表8.1 に記したとおり、時刻は実験記録ファイルのfoo.timeという列に同じ書式で出力されています(fooは 名前 に指定した文字列)。

また、当然ですが ボタン押しでRoutineを終了 がチェックされている状態では、「クリック時」が設定されていていても、ボタン押しが検出された時点でルーチンが終了してしまうので1件しかデータが保存されません。

続いて「フレーム毎」ですが、これを選択すると「クリック時」と同様のデータがMouseコンポーネントが有効な期間中ずっと記録され続けます。最後の「なし」を選択すると、Mouseコンポーネントは実験記録ファイルに何も出力しません。

これから作成する実験では、「最終」でルーチン終了時の状態を記録しても意味がありません。「クリック時」にしたら参加者が操作した軌跡が保存されるので役に立ちそうな気がしますが、実際に保存させるには実験参加者にずっとマウスのボタンを押さえつづけてもらわないといけません。参加者への負担になりますし、何よりちゃんと押してくれなかった場合にデータが取得できないのでは話になりません。「フレーム毎」にすると軌跡は保存されますが、同時にマウスのボタンの状態も保存されて記録ファイルが非常に大きくなってしまいます。操作記録にはCodeコンポーネントを使うことにして、とりあえず「なし」を使う事にしましょう。

チェックリスト
  • ルーチン終了時のマウスカーソルの位置およびマウスのボタンの状態を記録することが出来る。
  • ボタンが押された時点のマウスカーソルの位置およびマウスのボタンの状態を記録することが出来る。
  • 実験記録ファイルにマウスカーソルの座標やボタンの状態がリストとして複数件出力されている時に、個々のデータの座標値やその取得時刻を判断できる。

8.3. 実験の作成

Mouseコンポーネントの解説も終わったところで、実験の作成に入りましょう。まずは前章までに解説済みのテクニックで作成できる部分を作成します。Builderで新規に実験を作成して以下の作業を行い、exp08a.psyexpという名前で保存するものとします。

  • 実験設定ダイアログ

    • 「xlsx形式のデータを保存」をチェックする。
    • 単位 をpixにする。
  • trialルーチン

    • 最初からStaticコンポーネントが配置されている場合は削除する。

    • Mouseコンポーネントをひとつ配置して、 名前 をmouseTrialにする。

      • 終了 を空白にする。
      • ボタン押しでRoutineを終了 のチェックを外す。
      • マウスの状態を保存 を「なし」にする。
    • Codeコンポーネントをひとつ配置し、 名前 をcodeTrialにする。

    • Polygonコンポーネントを7つ配置して 終了 を空白にし、 名前 をpath1、path2、path3、path4、path5、goalDisc、probeにする。必ずgoalDiscはpath1~path5より上に描画されるようにし、probeはgoalDiscよりも上に描画されるようにする。

    • pathN (N=1~5)について以下の作業を行う。

      • 回転角度 $ をpathNori (NはpathNのNと一致させる)にして、「繰り返し毎に更新」に設定する。
      • 位置 [x, y] $ をpathNpos (NはpathNのNと一致させる)にして、「繰り返し毎に更新」に設定する。
      • サイズ [w, h] $ を[240, 20]に設定する。
    • goalDiscについて以下の作業を行う。

      • 頂点数 を32にする。
      • Fill colorをgreenにする。
      • 位置 [x, y] $ をgoalPosにし、「繰り返し毎に更新」に設定する。
      • サイズ [w, h] $ を[30, 30]にする。
    • probeについて以下の作業を行う。

      • 頂点数 を32にする。
      • Fill colorをredにする。
      • 位置 [x, y] $ を[px, py]にし、「フレーム毎に更新する」に設定する。
      • サイズ [w, h] $ を[10, 10]にする。
  • readyルーチン(作成する)

    • フローのtrialルーチンの前に挿入する。

    • Mouseコンポーネントをひとつ配置して、 名前 をmouseReadyにする。

      • 終了 を空白にする。
      • ボタン押しでRoutineを終了 のチェックが付いていることを確認する(初期状態で付いている)。
      • マウスの状態を保存 を「なし」にする。
    • Polygonコンポーネントを2つ配置して 終了 を空白にし、 名前 をstartDisc、probeReadyにする。probeReadyはstartDiscよりも上に描画されるようにする。

    • startDiscについて以下の作業を行う。

      • 頂点数 を32にする。
      • Fill colorをgreenにする。
      • 位置 [x, y] $ をstartPosにし、「繰り返し毎に更新」に設定する。
      • サイズ [w, h] $ を[30,30]にする。
    • probeReadyについて以下の作業を行う。

      • 頂点数 を32にする。
      • Fill colorをredにする。
      • 位置 [x, y] $ をstartPosにし、「繰り返し毎に更新」に設定する。
      • サイズ [w, h] $ を[10,10]にする。
    • Textコンポーネントをひとつ配置する。

      • 文字の高さ $ を24にする。
      • 位置 [x, y] $ [0, -320]にする。
      • 文字列 に「準備ができたらマウスの左ボタンをクリックしてください」と入力する。
  • trialsループ(作成する)

    • readyルーチンとtrialsルーチンを繰り返すように挿入する。
    • 繰り返し回数 $ を1にする。
    • 繰り返し条件 にexp08cnd.xlsxを指定する。

ここまで準備できれば、あとはreadyルーチンとtrialsルーチンのCodeコンポーネントに必要なコードを記入するだけです。まず、trialsルーチンのCodeコンポーネントで実現すべき処理を整理して、何が今まで学んできたことでは出来ないのかをはっきりさせましょう。まだexp08a.psyexpで実現できていない処理は以下の3点です。

  1. 現在のマウスカーソルの座標を得る。
  2. マウスカーソルの動きを上下反転させた場合の座標を計算する。
  3. 反転させた座標がgoalDiscに含まれているか判定し、含まれていたらルーチンを終了する。
  4. 試行開始時にマウスカーソルをスタート地点に移動させる。

1は未解説の処理です。先ほど解説したMouseコンポーネントの機能には、ルーチン実行中に座標を得るという機能は含まれていませんでした。2もちょっと考える必要がありそうです。3は6章、7章で扱ってきたif文が利用できそうです。「指定した条件に当てはまればルーチンを終了する」という処理は7章ですでに解説しました。ですから、残っているのは「座標がgoalDiscに含まれているか否かをどうやって判定すればよいか」という問題だけです。4も未解説の処理ですね。

1と4の問題を解決するには、PsychoPyのマウス制御用クラスであるpsychopy.event.Mouseクラス(以下Mouseクラス)を利用する必要があります。BuildernのMouseコンポーネントもこのクラスを利用しています。まずはMouseクラスの解説から始めることにしましょう。

8.4. psychopy.event.Mouseクラスのメソッドを利用してマウスの状態を取得しよう

第6章で解説したように、Builder上でGratingコンポーネントを配置すると、その 名前 に設定した変数にGratingクラスのインスタンスが格納されます。それと同様に、Builder 上でMouseコンポーネントを配置すると 名前 に設定した変数にMouseクラスのインスタンスが格納されます。現在作成中のexp08a.psyexpでは、trialルーチンにmouseTrialという 名前 のMouseコンポーネントを配置したのですから、CodeコンポーネントからはmouseTrialという変数を通してtrialルーチン上のMouseクラスを利用することが出来ます。

Mouseクラスは、データ属性に直接アクセスするのではなく、メソッドを通してすべての処理を行うように設計されています。ですから、ここではメソッドのみを紹介しましょう 表8.2 に代表的なMouseクラスの主なメソッドを示します。

表8.2 psychopy.event.Mouseクラスの主なメソッド
メソッド 概要
clickReset(buttons=[0,1,2]) マウスの反応時間計測用タイマーをリセットします。buttonsにはリセットしたいボタンを並べたリストを指定します。0=左ボタン、1=中央のボタン、2=右ボタンです。
getPos() 現在のマウスカーソルの座標をリストとして取得します。リストの最初の要素はX座標、次の要素はY座標を示しています。
getPressed(getTime=False) 現在のボタンの状態をリストとして取得します。リストの要素は順番に左ボタン、中央のボタン、右ボタンに対応していて、0=押されていない、1=押されている、です。getTime=Trueとすると、最後にclickResetを呼んでからの時間も返されます。
setPos(newPos=(0,0)) マウスカーソルの位置をnewPosに移動させます。
setVisible(visible) visibleにTrueを指定すると、マウスカーソルが描画されます。Falseを指定すると、マウスカーソルは描画されません。

getPos( )とgetPressed( )を利用すれば、Mouseコンポーネントで出力される程度の情報を取得することが出来ます。注目してほしいのは、clickReset( )やgetPressed( )におけるボタンの番号です。clickReset( )では、マウスの左ボタンが0、中央のボタンが1、右ボタンが2で表されます。一方、getPressed( )の戻り値として得られるマウスボタンの状態は[0, 0, 1]といった具合に0または1が3つ並んだリストで、1番目が左ボタン、2番目が中央のボタン、3番目が右ボタンに対応しています。clickReset( )のボタン番号とgetPressed( )の戻り値のリストにおけるボタンの順番が一致しているのは一目瞭然ですが、なぜボタン番号が1、2、3ではなく0、1、2なのでしょうか。それは、Pythonの文法ではリストの要素の順番を数える時には1からではなく0から始めるからです。ですから、Pythonにとっては0、1、2と番号が割り当てられている方が自然なのです。「0から数え始める」ということは次節以降の内容を理解する上で非常に重要なので必ず覚えておいてください。

以上を踏まえてBuilderの作業に戻りましょう。exp08a.psyexpのtrialルーチンを開いて、codeTrialの フレーム毎 に以下のコードを入力してください。

mousePos = mouseTrial.getPos()

これで、フレーム毎にmousePosという変数にマウスカーソルの位置が代入されるようになりました。続いてprobeの動きをマウスカーソルの動きと上下方向だけ反対にする方法を考えます。

なお、ひとつ補足しておきますと、今回とは違ってマウスカーソルの位置に合わせて刺激の位置を変更するのであれば、Codeコンポーネントを使う必要はありません。動かしたい刺激の 位置 [x, y] $ に以下のように入力して「フレーム毎に更新する」に設定するだけで目的を達成できます(Mouseコンポーネントの 名前 はmouseTrialとする)。

mouseTrial.getPos( )

なぜこれだけで済むか、おわかりでしょうか。 getPos( )メソッドの戻り値はX座標、Y座標の値を並べたリストであり、 位置 [x, y] $ に記述すべき値とデータの型が一致しているから、というのが理由です。関数やメソッドを見た時に、その戻り値の型を思い浮かべる習慣をつけるようにしてください。

チェックリスト
  • Codeコンポーネントでマウスカーソルの座標を取得するコードを記述できる。
  • Codeコンポーネントでマウスのボタンの状態を取得するコードを記述できる。
  • getPressed()メソッドを用いて取得したマウスのボタンの状態を示すリストの各要素がどのボタンに対応しているか答えられる。また、リストの各要素の値とボタンの状態の関係を答えられる。
  • Codeコンポーネントを用いずにマウスカーソルの位置に刺激を描画することが出来る。

8.5. リストの要素にアクセスしてマウスカーソルと上下反対にプローブを移動させよう

変数mousePosに現在のマウスカーソルの座標を代入することができたので、次はマウスカーソルと上下反対にプローブを移動させる方法を考えましょう。 図8.6 の左図をご覧ください。マウスカーソルと上下方向だけ反対にプローブが移動するということは、マウスカーソルが(dx, dy)移動したときにプローブは(dx, -dy)移動するということです。

_images/flip-vertical-mouse-movement.png

図8.6 マウスカーソルと上下方向だけ反転して移動するプローブ。マウスカーソルのY座標の符号を反転した座標をプローブの座標とすれば、最大の移動量(対角線上の運動)に対応することが出来ます。ただしsw、shはスクリーン幅、スクリーン高を表しているとします。

この要件さえ満たせばどのようにプローブの座標を決定しても構わないのですが、ここではマウスカーソルを最大限移動させてもプローブが追従できるように座標を決定する方法を考えます。PsychoPyでサポートしているスクリーンは長方形ですから、マウスカーソルの移動量が最大となるのは対角線上を頂点から頂点まで移動させた時です。マウスカーソルを幅sw、高さshのスクリーンの左下から右上の頂点へ移動させるとすると、マウスカーソルの座標は(-sw/2, -sh/2)から(sw/2, sh/2)まで移動し、移動量は(sw, sh)です。この時、プローブは(sw, -sh)だけ移動しないといけませんが、(sw, -sh)の移動量を確保するためにはプローブは(-sw/2, sh/2)から(sw/2, -sh/2)へ移動する以外あり得ません。これらの座標を比較すると、マウスカーソルのY座標の符号を反転した座標をプローブの座標とすれば、最大の移動量に対応できることがわかります( 図8.6 右)。

以上を踏まえたうえで、Codeコンポーネントに入力するコードを考えましょう。マウスカーソルのX座標とY座標がそれぞれmx、myという変数に格納されているのであれば、[mx, -my]と書けば目的を達成できます。しかし、今回はmousePosという変数にX座標とY座標をまとめてひとつのリストとして代入されているので、このような書き方が出来ません。mousePosに代入されたリストから、X座標とY座標を個別に抜き出す必要があります。

_images/sequence-index.png

図8.7 シーケンス型のデータからの要素の取出し。インデックスが0以上の整数なら先頭から、負の整数なら末尾から数えます。

Pythonの文法において、リストをはじめとするシーケンス型のデータから一部の要素を取り出すには、[ ]演算子を使用します。[ ]演算子は、mousePos[1]という具合にシーケンス型のデータの後に添えて、[ ]の中に何番目の要素を取り出すかを指定します。この指定のことをインデックスと呼びます。 図8.7 にインデックスとして整数を指定した時の動作を示します。インデックスが0以上の整数の場合、最初の要素を0として順番に先頭(左)から数えた位置にある要素を取り出します。現在変数に格納されているシーケンス型データに何個の要素が含まれているかわからない場合はlen( )という関数を用います。len( )は、引数に与えられたデータに含まれる要素数を返します。インデックスとして負の整数が指定された場合(負のインデックス)は、最後の要素を-1として末尾(右)から数えた位置にある要素を取り出します。0から数えるというのは先ほどgetPressed( )メソッドについて解説した時に述べたとおりですね。[ ]演算子を使用して、以下のようなコードを書くと変数mousePosからX座標、Y座標を取り出してそれぞれ変数px、pyに代入することが出来ます。

px = mousePos[0]
py = mousePos[1]

今回はY座標の符号を反転してプローブの座標として用いたいのですから、以下のように-演算子を組み合わせて代入と符号反転を一度に済ませると便利です。

::
px = mousePos[0] py = -mousePos[1]

これらの文をexp08.pyのtrialルーチンのcodeTrialの フレーム毎 に追加してください。追加後の フレーム毎 は以下のようになります。

mousePos = mouseTrial.getPos( )
px = mousePos[0]
py = -mousePos[1]

trialルーチンのprobeの 位置 [x, y] $ に[px, py]と設定されておりますので、これでマウスカーソルと上下反対方向に移動するプローブが完成しました。この節の目標は達成されましたが、せっかく[ ]演算子が出てきましたので、シーケンス型の扱いについてもう少し学んでおきましょう。まず、今回のように符号の反転などの操作が必要ないのであれば、以下の書式で、関数やメソッドの戻り値として渡されたシーケンス型データを要素に分解して別々の変数に代入することが出来ます。

px, py = mouseTrial.getPos( )

この書式を使用する場合、=演算子の左辺にカンマ区切りで並べられた変数の個数と、戻り値として渡されるシーケンス型データの要素数が一致している必要があります。一致していない場合、エラーでスクリプトの処理が停止してしまいます。

もうひとつ補足しておきたいのは、多重シーケンスからの要素の取出しです。シーケンス型は非常に柔軟なデータ型で、入れ子のようにシーケンスの要素としてシーケンスを含むことが出来ます。このような多重のシーケンスに対して[ ]演算子を適用した例を 図8.8 に示します。この例では[‘A’, ‘B’, ‘C’]というリストと[1, 2, 3, 4]というリストを要素とするリストが変数varに格納されています。var[0]は[‘A’, ‘B’, ‘C’]、var[1]は[1, 2, 3, 4]です。var[0]はリストなのですから、これに対してさらに[ ]を適用することが出来て、var[0][0]は’A’、var[0][1]は’B’です。同様に、var[1][3]とすると4が得られます。もちろん負のインデックスも組み合わせて使用できますので、var[1][3]はvar[1][-1]と書くことも出来ます。

_images/nested-sequence-index.png

図8.8 多重シーケンスからの要素の取出し。var[0]とするとリストが得られるので、さらに[ ]演算子を適用してvar[0][0]とするとvar[0]の最初の要素を取り出せます。

ここまでの章で、 位置 [x, y] $サイズ [w, h] $ といったプロパティにシーケンス型の一種であるリストをたくさん書いてきました。[0, 0]とか[240,20]とかいう具合に[ ]で括ってある値がリストでしたね。ちょっとややこしいのですが、この[ ]とシーケンス型の要素を取り出す[ ]演算子の違いをよく理解しておいてください( 図8.9 )。リストを作る[ ]の前には変数も値もありません。一方、要素を取り出す[ ]演算子は必ずその前にシーケンス型のデータがあります。「シーケンス型のデータがあればよい」ということは、[240, 20][0]という具合に変数ではなくリスト等のデータが前に直接置かれていても構わないということです。 [240, 20][0]は「[240, 20]という要素数2のリストの最初の要素」ということですから、その値は240です。よろしいでしょうか?

_images/blacket-operator.png

図8.9 リストを作る[ ]とシーケンス型の要素を取り出す[ ]。

シーケンス型に対する[ ]演算子の使い方には解説したいテクニックがたくさんあるのですが、どんどんこの章の実験から離れていってしまいますので、ひとまずこのくらいにしておきましょう。ここで述べただけでもBuilderを便利に拡張する様々なコードを書くことが出来ます。終わりと言いつつ最後にひとつだけ付け足しておきますと、[ ]演算子は文字列に対しても同様に使用することが出来ます。’Psychology’という文字列が格納されている変数sにs[3]とすると’c’が得られますし、s[-2]とすると’g’が得られます。覚えておいてください。

チェックリスト
  • シーケンス型のデータから要素をひとつ取り出して他の変数に代入したり関数の引数に使ったりすることが出来る。
  • シーケンス型データの前からN番目の要素を取り出す時の式を答えられる。
  • シーケンス型データの後ろからN番目の要素を取り出す時の式を答えられる。
  • 関数を用いてシーケンス型データに含まれる要素数を調べることが出来る。
  • シーケンス型のデータが入れ子構造になっている時に、要素であるシーケンス型データや、さらにその要素を取り出すことが出来る。
  • 文字列の中からN番目の文字を取り出して変数に代入したり関数の引数に使ったりすることが出来る。
  • [1,2,3][0]といった具合に[から始まって、]の後に[ ]演算子が続く式を評価した時の値を答えられる。

8.6. 刺激の重なりを判定しよう

プローブの動きが実現できたので、続いてプローブの中心がゴール地点の円内に入ったことを判定する方法を考えましょう。これは「プローブの座標がゴール地点の座標を中心とした半径15pixの円内に入る」ことと等しいので、以下の式で判定することが出来ます。さっそく[ ]演算子の復習になっているので、よくわからなければ前節を復習してください。

(goalPos[0]-px)**2 + (goalPos[1]-py)**2  < 15**2

念のため補足しておきますが、ゴール地点の座標は変数goalPosに格納されていて、ゴール地点に置かれているPolygonコンポーネントgoalDiscの サイズ [w, h] $ は[30, 30]です。 サイズ [w, h] $ は刺激の幅と高さ、すなわち直径に相当しますので、半径は15pixです。この式をif文の条件式に用いれば目的は達成できるのですが、この節ではもっと便利な方法を学びましょう。

_images/contains-overlaps.png

図8.10 Contains( )とOverlaps( )メソッドの例。

Polygonコンポーネントに対応するクラスが複数あるのは第6章で述べたとおりですが、 頂点数 が3以上の時に用いられるクラスでは、contains( )とoverlaps( )というメソッドが用意されています。これは今回の実験にはうってつけのメソッドで、contains( )は引数に指定された座標が内側に含まれていればTrue、いなければFalseを返します。overlaps( )は、引数に指定されたPolygonコンポーネントと重なっていればTrue、いなければFalseを返します。文章で説明するより図の方がわかりやすいでしょう。 図8.10 はrectangleという名前のPolygonコンポーネントからcontains( )とoverlaps( )を呼び出した例を示しています。判定の対象となる図形が 図8.10 のように傾いている場合、先ほどのような簡単な条件式では図形の中に点が含まれているかを判定することは出来ませんので、contains( )とoverlaps( )は非常にありがたいメソッドです。もちろん図形が三角形や五角形など、Polygonコンポーネントで描画できる多角形であれば適切に判定が行われます。

ここまで解説が進めば、もう「プローブの中心がゴール地点の円内に入ったらルーチンを終了する」という動作を実現するのは簡単です。円内に入った判定にはcontains( )を使用し、ルーチンを終了するにはcontinueRoutineにFalseを代入すればいいのですから

if goalDisc.contains([px, py]):
    continueRoutine = False

とすればよいはずです。このコードを、trialルーチンのcodeTrialの フレーム毎 に追加しましょう。念のため、追加後のコード全体を示しておきます。これでtrialルーチンは完成しました。

mousePos = mouseTrial.getPos( )
px = mousePos[0]
py = -mousePos[1]
if goalDisc.contains([px, py]):
    continueRoutine = False

これでマウスカーソルの現在位置の取得、マウスカーソルと上下反転した移動をするプローブ、プローブとゴール地点との重なりを判定してルーチンの終了、の課題をクリアしました。ここで一度、exp08a.psyexpを保存して実行してみてください。狙い通りの動作が実現できているはずです。残るは試行開始時のマウスカーソルの位置を設定するだけです。

チェックリスト
  • ある座標がPolygonコンポーネントで描画した多角形の内側に含まれているか判定するコードを書くことができる。
  • Polygonコンポーネントで描画した二つの多角形に重なっている部分があるが判定するコードを書くことができる。

8.7. カーソルの位置を設定し、カーソルの表示ON/OFFを制御しよう

Mouseクラスのメソッドを使用すれば、カーソル位置の設定は何も難しいことはありません。 表8.2 で紹介済みのsetPos( )メソッドを使用します。

注意しないといけないのは、今回の実験では「スタート位置にプローブを置くためには、マウスカーソルの位置はスタート位置の座標と上下反対の位置になければいけないという点です。前節ではマウスカーソルの位置をプローブ位置に変換しましたが、逆にプローブ位置をマウスカーソルの位置に変換しないといけないわけです。

変換といっても単にY座標の符号を反転すればよいだけですから、処理は非常に単純です。startPosにスタート位置の座標が格納されているのですから、[startPos[0], -startPos[1]]とすればよいでしょう。これをsetPos( )の引数にすればよいのですから、以下のコードを実行すればプローブのスタート位置に対応する位置へマウスカーソルを設定することができます。

mouseTrial.setPos([startPos[0], -startPos[1]])

試行開始時にカーソルがスタート位置にないといけないのですから、このコードはtrialルーチンの開始時に実行するべきです。trialルーチンに配置してあるcodeTrialの Routine開始時 に追加しましょう。

これで実験の基本的な部分は完成ですが、実験の間ずっとマウスカーソルが表示されたままになっているのが気になります。実験設定ダイアログにShow mouseというマウスカーソルを表示させるための項目はあるのですが、このチェックを外してもマウスカーソルは消えません。 Show mouseはMouseコンポーネントを使っていない時にマウスカーソルを表示したい(ウィンドウモードで他アプリケーションと一緒に使用したいときなど)場合に使用するためのもので、Mouseコンポーネントを使用するとマウスカーソルは強制的に表示されます。まあ普通はマウスを使用する時にはカーソルが表示されているのが当然なのでこのような仕様になっているのでしょうが、今回の実験の場合では余計です。マウスカーソルを非表示にするには、 表8.2 のsetVisible( )メソッドを使用します。以下のコードをtrialルーチンのCodeコンポーネントの 実験開始時 に入力してください。

mouseTrial.setVisible(False)
mouseReady.setVisible(False)

入力を終えたら、exp08a.psyexpを保存して実行してください。今度はマウスカーソルが表示されません。

これで 図8.4 に示した実験の流れは完成しましたが、現在の状態では参加者の反応が一切記録されません。心理学実験の実習であれば、ストップウォッチなどを利用しながら手作業で記録するのも良いのですが、せっかくPCを使っているのですからやはり記録もPCで行いたいところです。次節では、反応の記録方法について考えてみましょう。

チェックリスト
  • マウスカーソルの位置を設定するコードを書くことができる
  • マウスカーソルの表示ON/OFFを切り替えるコードを書くことができる。

8.8. for文を用いて複数の対象に作業を繰り返そう

今回の実験では、実験参加者のどのような反応を記録すればよいでしょうか。真っ先に思い浮かぶのは、課題遂行中の(つまりtrialルーチン実行中の)プローブの軌跡をすべて記録することです。しかし、ただプローブの軌跡を記録しただけでは、プローブがパスの上にきちんと乗っているのか後から計算しないといけません。パスに用いる長方形の中心座標値の計算( 図8.3 )よりもさらに面倒な計算なので、出来ることならしたくありません。せっかくcontains( )、overlaps( )というメソッドを覚えたのですから、これを利用してプローブがパス上にあるか否かを判定しましょう。

今回の課題では、パスを構成するPolygonコンポーネントが5個あり、このいずれかの上にプローブがあれば、パス上にプローブが乗っています。すでに今まで学んだPythonの文法の範囲でも何通りかの方法でこの処理を記述することができますが、ここでは以下のようなコードを考えてみましょう。

onPath = False
if path1.contains([px, py]):
    onPath = True
if path2.contains([px, py]):
    onPath = True
if path3.contains([px, py]):
    onPath = True
if path4.contains([px, py]):
    onPath = True
if path5.contains([px, py]):
    onPath = True

最初にonPathという変数にFalseを代入しておいて、path1からpath5まで順番にcontains( )を実行して戻り値がTrueであればonPathをTrueにします。最後の行まで進んだ時点でonPathがTrueであればいずれかの上にプローブが乗っています。2つ目のifからはelifのほうが効率がいいじゃないの?と思われた方は良い点に注目しておられますが、以下の解説と対応づけるためにわざとこうしていますのでご理解ください。

さて、このコードは確かに目的とする処理は達成できるのですが、、同じような文がだらだらと続いて読みづらいですし、「やっぱりcontains( )じゃなくてoverlaps( )を使うことにしよう」などと思ったときに書き換えが面倒です。うっかり1行だけ書き換え忘れたりすると、間違えた場所を見つけるのは非常に厄介です。この例のように「変数が異なるだけで同じ処理を繰り返す」時に非常に有効なforという文がPythonには用意されています。

_images/for-statement.png

図8.11 for文による繰り返し処理。

図8.11 にfor文の例を示します。for文は「for 変数 in シーケンス型データ:」という形で使用し、シーケンス型データの先頭から要素をひとつずつ取り出して変数に代入しながら処理を繰り返します。繰り返しの範囲はif文と同様に、字下げ量がforの出現する行と同じかそれより少ない行の手前まで及びます。また、forが出て来る行の最後にコロンが必要な点もif文と同じなので忘れないようにしてください。 図8.11 左の例では、forに続く変数としてxを使用していますので、xにx1を代入して繰り返し処理を行い、続いてxにx2を代入して繰り返し処理を…という具合に実行されます。結果的に、 図8.11 左のfor文は、 図8.11 右に示したコードと等価な処理を行います。for文を使うと、path1からpath5までcontains( )メソッドを実行するコードは以下のように短縮できます。

onPath = False
for path in [path1, path2, path3, path4, path5]:
    if path.contains([px, py]):
        onPath = True

とりあえずこれで「プローブがパス上にあるか判定する」という目標は達成されましたが、for文についてもう少し勉強しておきましょう。今回は5個ある長方形のどれにプローブが乗っていても構わないので、もしpath1の上にプローブが乗っていれば(すなわちpath1.contains([px, py])の戻り値がTrueであれば)、path2以降についてcontains( )を実行する必要がありません。今時のPCの性能であれば残り4回余分にcontains( )を実行しても大した負担になりませんが、PCの処理遅延による刺激の描画漏れや反応時間計測の遅延が起こる可能性を少しでも下げたいと思うのであればPCに余分な処理をさせたくありません。このように、for文を最後まで繰り返す前に「これ以上for文を繰り返す必要はない」という状況になった場合は、breakという文を使います。 図8.12 にbreakの使用例を示します。for文による繰り返し処理を遂行中にbreakが実行されると、まだ繰り返すべき処理が残っていても直ちにfor文が終了します。 図8.12 の場合、プローブの中心がpath3の上に乗っていますので、pathにpath1、path2を代入して実行している時はif文の式がTrueになりません。breakはif文の中にあるので実行されず、次の繰り返しへ進みます。そしてpathにpath3が代入された時、if文の式がTrueになるのでbreakが実行され、直ちにfor文による繰り返しが終了します。したがって、path4、path5に対する処理は実行されません。

この節最初でif文をずらずらと並べたコードを示したときに「2番目以降はelif文のほうが効率がいいんじゃないのか?」と思った方は、このbreakによるfor文の中断がelifによる効率化と対応することがおわかり頂けると思います。

_images/break-statement.png

図8.12 breakによるfor文の中断。if文などと組み合わせて使用するのが一般的です。

この章の実験を完成させるためにはここまで学んでいれば十分なのですが、今後皆さんが自分の実験を作成する時のための知識を高める、という観点でもうひとつfor文について紹介しておきたい点があります。少々高度な内容を例題として取り上げますので、その前にいったん節を区切りましょう。

チェックリスト
  • ある変数に格納されている値だけが異なる処理を繰り返し実行しなければいけない時に、for文を用いて記述することが出来る。
  • for文を継続する必要がなくなった時に直ちに終了させることが出来る。

8.9. ルーチンに含まれる全コンポーネントのリストから必要なものを判別して処理しよう

この節で紹介するコードは、この章の実験の完成版では使用しませんので、ここまで作業してきたexp08a.psyexpに手を加えずに読み進めてください。コードを入力して実行させたい場合は、exp08a.psyexpを別名で保存して作業してください。

それではfor文の勉強を再開しましょう。for文の実行を制御す文として、breakと併せて覚えておきたいのがcontinueです。continueは、現在実行中の繰り返しを終了させて、次の繰り返しに移行させる時に使用します。 図8.13 はbreakとcontinueの働きを示した図です。breakはもうfor文をこれ以上続ける必要がないときに、continueは現在の要素に対してはもう処理する必要がないけれども残りの要素に対して処理を続ける必要あるときに使用します。この節では、continueを使用例として、「ルーチンに含まれるコンポーネントから特定のコンポーネントを探して処理をする」という処理をするコードを作成します。この「特定のコンポーネントを探す」という処理が少々難解なので、「とりあえずBuilderの基本的な使い方をマスターして自分の実験を作ってみたい」という方はよくわからなくても先に進んでいただいて構いません。

_images/continue-statement.png

図8.13 breakとcontinueの違い。残りの要素に対して処理をする必要がない場合はbreak、必要がある場合はcontinueを使用します。

例題の題材は前節に引き続き「プローブがパス上にあるか判定する」処理です。前節ではパスを構成するPolygonコンポーネントのオブジェクトを並べたリストを手で入力してfor文へ渡しましたが、オブジェクトの個数が多くなるとリストが長くなって後から読み返す時に非常に読みにくいです。また、コンポーネントの名前を変更したら入力済みのコードを全部修正必要があり、間違いが生じやすいです。

Builderは、この問題を解消するのにうってつけの内部変数を持っています。Builderは各ルーチンの処理を開始する前に、「ルーチン名+Components」という名前の変数に、ルーチン内に配置されている全てのコンポーネント(正確に言うとコンポーネントに対応するクラスのインスタンス: Builderの内部変数trialComponentsに含まれるオブジェクトについて 参照)を列挙したリストを格納します。trialルーチンでしたら変数名はtrialComponents、readyルーチンでしたらreadyComponentsです。この変数をfor c in trialComponents: という具合にfor文に渡せば、ルーチンに含まれるインスタンスを持つすべてのコンポーネントに対して処理を行うことが出来るのです。そうすればコンポーネントの数が増えてもコードの長さは変わりませんし、 名前 を変更した時にいちいちコードを修正する必要もありません。

ただ、今回の「プローブがパス上にあるか判定する」という処理に変数trialComponentsを利用するには、少々面倒な問題を解決する必要があります。trialComponentsにはtrialルーチンに置かれたすべてのコンポーネントが列挙されているので、Mouseコンポーネントやプローブ自身、ゴール地点の円も含まれています。これらに対してcontains( )を実行しようとすると問題が生じるので、パスを構成しているPolygonコンポーネントに対してだけ処理をしなければいけません。このような時に、continueは非常に有効です。具体的には以下のようにcontains( )を実行する前に、現在処理しようとしているコンポーネントがpath1からpath5ではない時にはcontinueを実行します。このようにすれば、continue以下の文が実行されずにfor文は次の繰り返しへ進みます。

onPath = False
for c in trialComponents:
    if (path1からpath5ではないことを判別する式)
        continue
    if c.contains([px, py]):
        onPath = True
        break

残る問題は、現在cに代入されている要素がpath1からpath5でないかをどうやって判別するかです。これにはPsychoPyの刺激描画用クラスが持っているnameというデータ属性を使用します。データ属性nameには対応するBuilderのコンポーネントの 名前 に入力した文字列が格納されているので、nameに’path’という文字列が含まれているかを判別すればよいでしょう。この判別にはシーケンス中にある要素が含まれるか否かを判定するときに用いたin演算子が使用できます(第7章参照)。

'path' in c.name

c.nameに’path’という文字列が含まれていれば、この式はTrueになります。これで万全、と言いたいところなのですが、残念ながらもう一工夫必要です。データ属性nameはPsychoPyの刺激描画用クラスが持っているものであり、trialルーチンで使われているMouseクラスにはnameがないのです。ですから、cにMouseクラスのインスタンスが代入されている状態で’path’ in c.nameを実行すると「nameというデータ属性はありません」というエラーが出て実験が停止してしまいます。この問題に対応するためには、まずcに格納されているオブジェクトがnameというデータ属性を持っていることを確認する必要があります。確認のためにはhasattr( )という関数を使います。hasattr( )は2個の引数を持ち、第1引数に指定されたオブジェクトが第2引数に指定された名前のデータ属性を持っていればTrueが返されます。以下のように使用すると、cに格納されているオブジェクトがnameというデータ属性を持っている時にTrueの値を得ることが出来ます。

hasattr(c, 'name')

この二つの条件式を組み合わせれば、cが’path’という文字列を 名前 に含むコンポーネント(に対応するインスタンス)であるか判別できます。どちらか一方の条件式を満たさないオブジェクトはパスを構成しているPolygonコンポーネントではありませんので、

not hasattr(c, 'name') or not 'path' in c.name

がTrueとなった時にはcontinueを使って次のオブジェクトへ処理を進めればよいということになります (orの前後はこの順序である必要があります: 論理式の評価順序について 参照)。

if not hasattr(c, 'name') or not 'path' in c.name:
    continue

このコードをfor文に追加したものを 図8.14 左に示します。コードがどのように動作するのかを追った流れをその右側に示してあります。continueの働きを確認してください。

for文にはまだまだ解説したい機能があるのですが、いくらなんでも脱線しすぎですのでそろそろ実験の作成に戻りましょう。次節では、プローブがパス上にあるかを判定した結果とプローブの座標を実験記録ファイルに出力することに取り組みます。

_images/find-object-from-trialComponents.png

図8.14 continueによる繰り返し処理のスキップ。

チェックリスト
  • for文で現在処理中の要素に対してこれ以上処理を続ける必要がなくなった時に、次の要素の処理へ直ちに移行するコードを書くことができる。
  • ルーチン内に含まれるコンポーネントの一覧を格納したBuilderの内部変数の名前を答えられる。
  • オブジェクトがある名前のデータ属性(たとえば’foo’)を持っているか判別するコードを書くことができる。
  • ある文字列が別の文字列の中に含まれているか(例えば’psych’が’psychophysics’に含まれるか)判別するコードを書くことができる。

8.10. リストにデータを追加してマウスの軌跡を保存しよう

この章の実験作成も最終段階です。プローブの軌跡と、プローブがパス上にあるかを判定した結果を実験記録ファイルに出力しましょう。具体的には、Mouseコンポーネントの出力のように、実験記録ファイルにprobe_x、probe_y、on_pathという列を設けて、そこへそれぞれX座標、Y座標、判定結果の値を並べたリストを出力したいと思います。

この章までにリストは何度も扱ってきましたが、以前の章に出てきたリストはすべて事前に長さが決まっていました。 位置 [x, y] $ に入力する座標でしたら長さは2ですし、 にRGBで色を指定するのでしたら長さは3です。それに対して、今回実験記録ファイルに出力しようとしているリストは事前に長さがわかりません。実験参加者が課題遂行に時間がかかればかかるほど、プローブの座標を記録したリストは長くなります。課題遂行に要する時間が予測できないので、記録に必要なリストの長さも予測できないのです。このような場合に有効なのが、リストの要素を追加したり削除したりする機能です。実は今までリストと呼んできたデータはPythonに標準で備わっているListというクラスのインスタンスであり、ListはPsychoPyのMouseクラスなどと同様にメソッドを持っています。 表8.3 にListクラスの主なメソッドを示します。これらのメソッドを活用すれば問題を解決できそうです。

表8.3 Listクラスの主なメソッド
メソッド 概要
append(x) リストの末尾に新たな要素としてxを追加します。リストの長さは1増加します。
extend(seq) リストの末尾に新たな要素としてseqの要素を追加します。seqはシーケンス型や文字列型など、長さが定義されるデータ型である必要があります。リストの長さはseqの長さ分増加します。
insert(i, x) リストのi番目の要素としてxを追加します。iは整数でなければいけません。
remove(x) リストの先頭からチェックして、最初に現れたxを削除します。xがリストに含まれていない場合はエラーになります。
index(x) リストの先頭からチェックして、最初に現れたxのインデックスを返します。xがリストに含まれていない場合はエラーになります。
count(x) リストにxが何個含まれているかを返します。
sort( ) リストの要素を昇順に並び替えます。
reverse( ) リストの要素を現在の順番と逆順に並び替えます。

さて、じっくりと 表8.3 を眺めると、要素を追加するメソッドとしてappend( )とextend( )の2種類があることがわかります。両者の違いをまとめたものが 図8.15 です。ポイントは、append( )は引数をそのままひとつの要素として付け加えるのに対して、extend( )は引数の要素を付け加えます。ですから、append( )を実行すると必ずリストの要素が1個増えます。それに対してextend( )の場合は引数に含まれる要素数だけリストの要素が増えます。また、extend( )は長さがないデータ、すなわち[ ]演算子を付けて要素を取り出すことが出来ないデータを引数にすることは出来ません。具体的には数値や真偽値(True/False)などはextend( )の引数にできません。今回の目的では、保存しようとしているプローブのX座標、Y座標は数値、判定結果は真偽値ですから、いずれもextend( )で追加することが出来ません。append( )を使用しましょう。

_images/append-extend.png

図8.15 append( )とextend( )によるリストへのデータの追加。

それではコードを入力していきます。まず、exp08a.psyexpを開いてtrialルーチンのCodeコンポーネントのプロパティ設定ダイアログを開いてください。 Routine開始時 に以下のコードを追加してください。

probeX_list = [ ]
probeY_list = [ ]
onPath_list = [ ]

ここでは、trialルーチン開始時にこれから実行する試行のデータを追加していくためのリストを用意しています。=演算子の右辺に奇妙な式が出てきましたが、 図8.9 をよく思い出してください。式の中に[ ]が出てきた場合、[の直前が変数やデータであれば、要素を取り出す[ ]です。そうでなければ、リストを作成する[ ]です。上記の式の場合、[の前は=演算子であって変数やデータではありませんので、これはリストを作成する[ ]です。[ ]の中身に何も書いてありませんから、中身が空っぽ(長さは0)のリストが作成されます。

続いて、 フレーム毎 に入力済みのコードの最後に以下のコードを追加します。

probeX_list.append(px)
probeY_list.append(py)
onPath_list.append(onPath)

ここでは新しいプローブ座標(px, py)が得られるたびに、先ほど作成したリストにプローブのX座標、Y座標、判定結果を追加しています。そして最後に、 Routine終了時 でこれらのリストを実験記録ファイルに出力します。この方法については第7章ですでに解説済みですので、わからない人は第7章をしっかり復習してください。

trials.addData('probe_x', probeX_list)
trials.addData('probe_y', probeY_list)
trials.addData('on_path', onPath_list)

probeX_list、probeY_list、onPath_listの内容は、実験記録ファイルに出力さえしてしまえばもう用済みです。ですから、次の試行を開始する時には内容を初期化(=空っぽに)してしまっても構いません。このように、現在実行中のルーチン内でだけ必要なリストは、 Routine開始時 で作成するのが定番です。一方、実験を通じてデータを蓄積して、実験終了時に何かの処理をするためのリストを作成するのであれば、 実験開始時 で作成するべきです。

_images/output-list-to-xlsx.png

図8.16 trial-by-trial記録ファイルにおけるプローブ座標と判定結果の出力。Pythonのリストと同様に、[ ]で囲まれたカンマ区切りの値として出力されています。

これで作業は完了しました。exp08a.psyexpを保存して実行し、数試行実行してみてください。適当なところでEscapeキーを押して実験を終了し、Excelでtrial-by-trial記録ファイルを開いてみましょう。 図8.16 のようにprobe_x、probe_y、on_Pathという列が追加されていて、Pythonのリストのように[ ]で囲まれたカンマ区切りの値が並んでいるはずです。Excelにはこれらの値がまとめて文字列として認識されてひとつのセルに収められています。ちょっと面倒ですが、これらのセルの文字列を別の場所にコピーして「データの区切り位置」でカンマを区切り文字に指定すれば、ひとつひとつの値が1個のセルを占めるように変換できます。この状態まで変換すれば、後は 図8.17 のようにExcelの機能でグラフをプロットしたり、さまざまな関数を利用して分析したり出来ます。

_images/plot-trajectory.png

図8.17 probe_x、probe_y、on_PathをExcelの「データの区切り位置」機能を用いて復元し、軌跡をグラフとしてプロットしたもの。軌跡の青い部分はパス上にプローブの中心が乗っていたことを、赤い部分は乗っていなかったことを示しています。

これで完成!と言いたいところですが、この節で解説した方式だと一般的な60fpsのモニターを使用している場合1秒ごとに60件ものデータが追加されてしまうので、参加者がじっくりと課題に取り組む人の場合、膨大な量のデータが出力されてしまいます。軌跡をどの程度詳細に分析するかに寄って適切なサンプリング頻度は異なりますが、おおよその軌跡が把握できればよいだけでしたら、1秒間に10件程度でも充分でしょう。次節では、このようなデータの間引きを行うコードを考えます。

チェックリスト
  • リストにデータを追加するメソッドであるappend( )とextend( )の違いを説明できる。
  • 中身が空のリストを作成することが出来る。
  • ルーチン内でのみ必要なデータを蓄積するリストを作成するコードはどの時点で初期化すべきか判断できる。

8.11. 軌跡データを間引きしよう

軌跡データの間引きといってもいろいろな方法があるのでしょうが、もっとも単純な方法はサンプルを1つおきに保存すると言った具合に、一定間隔でサンプルを保存してその間のサンプルは捨ててしまうといったものでしょう。このように「順番に並んでいる(取得される)データを一定間隔で取り出して処理する」という作業には定番のコードがありますので、ぜひ覚えておきましょう。

exp08a.psyexpをexp08b.psyexpという別名で保存して、そちらを使って作業しましょう。exp08b.psyexp を開いて、trialルーチンに配置してあるCodeコンポーネントの フレーム毎 の最後を見てください。プローブの座標とプローブがパス上にあるかの判定結果のデータをappendしている3行のコードがあります。frameN % 6==0という式がTrueになるときだけこの3行のコードを実行するように、以下のようにif文を書き足します。これだけの書き換えで、で6件につき1件のペースでデータを間引いて記録するようになりました。

if frameN % 6 == 0:
    probeX_list.append(px)
    probeY_list.append(py)
    onPath_list.append(onPath)

さて、なぜこれだけでデータを6件に1件に間引くことが出来るのでしょうか。ポイントは%演算子にあります。第5章でPythonの算術演算子を紹介しましたが、その中に%演算子が含まれていたのを覚えていますでしょうか。%演算子は、x % y の形で使用して、xをyで割った時の余りが得られます。余りの事を「剰余」と呼ぶことから、%演算子のことを「剰余演算子」と呼びます。frameNは第5章で紹介したBuilderの内部変数で、ルーチンの実行が開始してから描画したフレーム数が格納されています。ということは、frameN % 6という値は 6フレーム毎に0になります( 図8.18 )。この性質を利用して、frameN % 6==0がTrueとなる時だけにappendを行えば、6件につき1件のペースでデータを間引いて記録できるのです。

_images/skipping-using-mod-operator.png

図8.18 剰余演算子を利用したデータの間引き。

%演算子のもう一つの重要な応用例を 図8.19 に示します。この例では、数値がカレンダーのように左上から右下に向かって並べられています。このように、升目状に数値が並んでいるデータ構造を2次元配列と呼びます。2次元配列のデータはさまざまなプログラミングで使用されますが、その際に「m行n列の位置にあるデータは先頭から数えて何番目か?」とか、逆に「先頭から数えてi番目のデータは何行目、何列目にあるか?」といった計算が求められることがよくあります。Pythonのインデックスが0から数え始めることに注意すると、m行n列の位置にあるデータはm×列数+nであることがわかります。インデックスiが与えられた時の行数は、Pythonの整数同士の除算が余りを切り捨てることを思い出せば(第5章)、i / 列数で計算できることがわかります。そして、列数を計算する時が%演算子の出番です。i % 列数を計算すれば、インデックスiの要素が何列目にあるかわかります。この章の実験では使用しないテクニックですが、非常に有効ですので覚えておいてください。

_images/calc-row-and-column.png

図8.19 2次元配列を1次元に展開した時の要素のインデックスと行、列の相互変換。インデックスは左上から右方向へ順番に割り当てて、右端に達したら次の行の左端へ移るものとします。

%演算子の話はこのくらいにしておいて、exp08b.psyexpの作業に戻りましょう。もう先ほどのif文を入力しただけでこの節の目的はもう達成できているのですが、このまま実行しても間引きの効果が大変わかりづらいです。そこで、プローブ座標などを保存したフレーム番号をframeNという列名で実験記録ファイルに出力するように改造し、ついでに実験情報ダイアログにIntervalという項目を設けて何フレーム毎に保存するかを指定できるようにしましょう。これはすでに解説済みのテクニックの復習ですので、自信がある人はノーヒントで取り組んでみてください。うまく動作したら「チェックリスト」まで飛ばしていただいて構いません。

では、作業内容を順番に説明します。まず、フレーム番号を保存するためのリストが必要なので、trialルーチンのCodeコンポーネントの Routine開始時 に以下のコードを追加しましょう。

frameN_list = [ ]

続いて、 フレーム毎 の最後でリストにデータをappendしているところの最後にframeN_listへframeNをappendするコードを追加しましょう。また、剰余演算の部分で実験情報ダイアログのIntervalという項目から値を取得するようにしておきましょう。実験情報ダイアログから取得した値は文字列なので、数値に変換するためにint( )を使用しないといけない点に注意してください。

if frameN % int(expInfo['Interval']) == 0:
    probeX_list.append(px)
    probeY_list.append(py)
    onPath_list.append(onPath)
    frameN_list.append(frameN)

以上のコードを入力したら、実験設定ダイアログを開いて、実験情報ダイアログにIntervalという項目を追加しておきましょう。そして最後に、trialルーチンのCodeコンポーネントに戻って Routine終了時 の最後にframeN_listを実験記録ファイルに出力するコードを付け足しておきましょう。

trials.addData('frameN', frameN_list)

以上で実験は完成です。exp08b.psyexpを保存して実行してみましょう。Intervalの値をいろいろと変更して、実験記録ファイルのframeNの値の間隔が変化することを確認してください。

チェックリスト
  • %演算子を用いてNフレーム(N=2,3,4,…)に1回の頻度で処理を実行させることが出来る。
  • 2次元配列の要素のインデックスから、その要素が何行目、何列目にあるかを計算することが出来る。

8.12. ゴール地点でクリックして終了するようにしてみよう

この章も長くなりましたし、そろそろ練習問題にしたいのですが、その前にもう一つだけ作業をします。本当はこの作業を練習問題にしたいのですが、今後皆さんが他人が書いたコードを読まないといけなくなった時に知っておくと役立つポイントがありますので解説しておきます。

この節で行う作業は、「プローブがゴールに到着したときに自動的に試行を終了するのではなく、ゴールに到着したうえでマウスをの左ボタンをクリックしないといけないようにする」というものです。ここまでの作業ではMouseクラスのgetPressed( )の出番がなかったので、このメソッドを使う練習をしておこうというわけです。

ここまでの作業で作成したexp08a.psyexpのtrialルーチンの終了判定は、以下のようなコードで行われています。

if goalDisc.contains([px, py]):
    continueRoutine = False

このif文の条件に「マウスの左ボタンが押されている」という条件を付けくわえれば、目的は達成できます。両方の条件を満たさないといけないので、and演算子を使って以下のように書けるはずです。

if startDisc.contains([px, py]) and マウスの左ボタンが押されている:
    continueRoutine = False

表8.2 のgetPressed( )メソッドを利用すれば、ボタンの状態を保持したリストが得られます。これをbuttonStatusという変数に格納しておきましょう。このリストの最初の要素が左ボタンの状態に対応しているのですから、以下のように書けば左ボタンが押されていることを判定できます。

buttonStatus = mouseReady.getPressed( )
if startDisc.contains([px, py]) and buttonStatus[0]==1:
    continueRoutine = False

これで全く問題ないのですが、Pythonに慣れるためにちょっと頭の体操をしましょう。getPressed( )の戻り値はリストです。リストから要素を取り出すには、リストの後ろに[ ]演算子を添えればよいのでした。ですから、上記のコードにおけるbuttonStatusという変数は必要ではなくて、以下のように書くことができます。

if startDisc.contains([px, py]) and mouseReady.getPressed( )[0] ==1:
    continueRoutine = False

Python以外のプログラミング言語に慣れている方の中には、関数を呼び出す( )の後ろに[ ]演算子があるのを見て奇妙に思う方もおられるかも知れません。しかし、これはvar[0]がリストの時にvar[0][1]という具合に[ ]演算子を連続して書くことができたのと同じことです。このような書き方はweb上のさまざまなPythonのサンプルコードでしばしば見かけますので、しっかりと理解しておいてください。

この節で取り上げた「ある座標が刺激に含まれていて、かつマウスがクリックされている」という状態を判定するテクニックは、Builderで作った実験のスクリーン上にボタンを表示してマウスでクリックして選択させるユーザーインターフェースを実装したいときなどに有効なので、ぜひ覚えておきたいところです。PsychoPyのMouseクラスにはisPressedIn(shape, buttons)という、shapeに指定されたオブジェクト上でbuttonsに指定されたボタンが押されたときにTrueを返すというメソッドがあり、本実験のような座標の上下反転などを行わない場合は、このメソッドを使用すればボタン風のユーザーインターフェースは実現できます。しかし、「カーソルが重なっただけで色を変化させる」といった凝った動作をさせる必要がある場合などには、contains( )を使う方が柔軟に対応できます。

チェックリスト
  • 関数やメソッドの戻り値に直接[ ]演算子を適用した式を理解できる。
  • Polygonコンポーネントで描画した多角形にマウスカーソルを重ねてクリックするとルーチンの終了や反応の記録などの処理を行うコードを書くことができる。

8.13. 練習問題:反転方向切り替え機能とフィードバック機能を追加しよう

以上でこの章の解説は終わりです。この章の内容まで理解できていれば、Builderで相当複雑な実験を作成することが出来るはずです。この章の仕上げとして、以下の練習問題に取り組んでください。ここまで理解できた人であればノーヒントでできるものと期待します。

  • 実験情報ダイアログにDirectionという項目を追加してください。Directionの値に応じて以下のようにプローブの動き方を変更してください。

    • DirectionがUDという文字列であればexp08a.psyexpと全く同一の動作。
    • DirectionがLRという文字列であれば、プローブがマウスカーソルの動きと左右反対に動く。
    • DirectionがUDでもLRでもなければ、プローブとマウスの動きが一致。
  • プローブの中心座標がパス上にある時にはプローブの 塗りつぶしの色 が赤色、パス上にない時には 塗りつぶしの色 が黒色になるようにしてください。

8.14. この章のトピックス

8.14.1. Builderの内部変数trialComponentsに含まれるオブジェクトについて

本文中で述べたように、Builderでは各ルーチンの処理を開始する前に、「ルーチン名+Components」という名前の変数(以下trialComponents)が作成されます。ここには、ルーチン実行中に毎フレーム処理しなければいけないオブジェクトがリスト型のデータとして列挙されています。オブジェクトはそれぞれ対応するコンポーネントの機能を実現するためのクラスインスタンスです。 BuilderのコンポーネントとPsychoPyのクラスの対応 で述べたとおり、コンポーネントによってGratingのように一対一にクラスが対応している物と、Polygonのように対応するクラスが変化するものがあります。また、Codeコンポーネントは直接PythonのコードをBuilderに埋め込むという性質上、他のコンポーネントとは異なる方法で管理されており、trialComponentsに列挙されません。

8.14.2. 論理式の評価順序について

本文中に出てきた「cが’name’というデータ属性を持っていないか、pathという文字列をデータ属性nameに含んでいない」という条件を検査する式について補足しておきます。この式は以下の通りで、「orの前後はこの順序である必要がある」と本文中で述べました。

not hasattr(c, 'name') or not 'path' in c.name

なぜこの順序でなければいけないのでしょうか。その理由は、Pythonが論理式を評価する順番にあります。Pythonでは、論理演算子は算術演算子や比較演算子より優先順位が低く、論理演算子の中ではnot、and、orの順に評価されます。優先順序に差がない部分は左側から評価されます。上記の順序であれば、左から評価されるのでnot hasattr(c, ‘name’)が評価されます。もしnot ‘path’ in c.nameがorの左に書いてあれば、いきなりcのデータ属性nameの値を参照するのでcがデータ属性nameを持っていなければエラーで停止してしまいます。

not hasattr(c, ‘name’)を左に書いても結局データ属性nameが無かったらnot ‘path’ in c.nameでエラーになるんじゃないの?と思われるかもしれませんが、Pythonには「論理式の評価途中で値が確定してしまったら、残りの部分の評価を行わない」という規則があります。どういう事かと言うと、A or Bという式でAがTrueであれば、BがTrueであろうとFalseであろうとA or Bの結果はTrueです。このような時に、Pythonはわざわざ時間をかけてBの評価を行わず、A or Bの結果はTrueと判断します。

今回の例に当てはめて考えましょう。not hasattr(c, ‘name’)がTrueだったら、この時点で条件式はTrueになるので、not ‘path’ in c.nameは評価しません。not hasattr(c, ‘name’)がFalseの場合は、式の真偽が確定しませんのでnot ‘path’ in c.nameを評価します。この時、not hasattr(c, ‘name’)がFalseだったのですから、cは必ずデータ属性nameを持っています。従って、not ‘path’ in c.nameで「データ属性nameが無い」というエラーが生じることはありません。

同様の理由で、A and Bという式を評価する時に、AがFalseであればBの評価は行われません。

なお、このif文がわかりにくいという方は、以下のように二つのif文に分けて書くことも出来ます。覚えやすい方を覚えていただければと思います。

if not hasattr(c, 'name')
    continue
if not 'path' in c.name
    continue
_images/mirror-drawing-condition-file.png

図8.20 本章で用いる条件ファイル