12. 正答率や経過時間で終了する課題を作成しよう

12.1. この章の実験の概要

Builderの繰り返しは、基本的に回数を指定して使用します。しかし、心理学実験では事前に繰り返し回数が決まらない手続きもよく用いられます。 「連続して5問正答するまで繰り返す」、「正答率が80%に達するまで繰り返す」といった「ある条件を満たすまで繰り返す」といった手続き、「制限時間1分の中で出来る限りたくさん繰り返す」といった「一定時間経過するまで繰り返す」といったものです。 この章ではまとまった「実験」を作るのではなく、これらのような特殊な繰り返し回数の課題を作成してみたいと思います。

12.2. 繰り返しを中断しよう

本章で中心的な役割を果たすテクニックが「繰り返しの中断」です。 第7章 でcontinueRoutineを使ってルーチンを抜ける方法を解説しましたが、これはあくまでルーチンを抜けるだけで、ループによる繰り返しが中断するわけではありません。ループが中断されないからこそ、 第7章 のように「次の試行(=次の繰り返し)へ進む」ということができたわけです。 ループに含まれる全てのルーチンに対して、開始したら直ちにcontinueRoutine=Falseを実行するようにすると、見た目上はルーチンを全く実行せずにループを進行させることができます。 例えばtask_completedという変数をFalseで初期化しておいて、「連続して5問正答するまで繰り返す」などの条件を満たしたらtask_completedにTrueを代入するとします。 そのうえで、ループ内のすべてのルーチンにCodeコンポーネントを配置して [フレーム毎]

if task_completed:
    continueRoutine = False

というコードを挿入しておけば、ひとたびtask_completedがTrueになるとすべてのルーチンが即座に終了していずれループが終了します。 これでも「条件を満たしたら繰り返しを終了する」ように見えるので構わないかも知れませんが、ループそのものは繰り返されているので、実験記録ファイルにはループの [繰り返し回数] や条件ファイルの条件数などから決定される繰り返し回数ぶんだけデータが出力されてしまいます。 Builderの実験では一度繰り返しが始まってから繰り返し回数を増減させるのは難しいので、この方法で「条件を満たしたら繰り返しを終了する」を実現するためには「どれだけ長く続いてもこの回数内におさまるだろう」という繰り返し回数を設定しなければいけません。その回数分だけ実験記録ファイルに出力されてしまうのですから、ファイルサイズの無駄になりますし分析の時に邪魔です。

ではどうすればいいかというと、ループの実体であるTrialHandlerのfinishedというデータ属性を利用します。finishedにはまだ繰り返し回数が残っていればFalse、繰り返しを終えていればTrueが代入されているのですが、finishedにTrueを代入することで無理やり「繰り返しが終了した」状態にすることができます。従って、先ほどのコードは、ループの名前がtrialsとすると

if task_completed:
    trials.finished = True

と書けばよいということです。 実際の使用例を見た方が早いと思いますので、さっそく作業に入りましょう。 まずはストループ課題で、直近10試行を90%以上の正答率を達成したら終了する課題を組んでみます。 実験開始前におこなう、指示内容を理解していることの確認のための練習試行などに適しています。

12.3. 直近10試行の正答率が基準値に達したら終了するループを組もう

  • 準備作業
    • この実験のためのフォルダを作成して、その中にexp12a.psyexpという名前で新しい実験を保存する。

    • 実験設定ダイアログの「スクリーン」タブの [単位] がheightであることを確認する。

    • 実験設定ダイアログの「入力」タブの [キーボードバックエンド] がPsychToolboxか確認する。

  • trialルーチン
    • Textコンポーネントをひとつ配置し、以下のように設定する。

      • 「基本」タブの [名前]textTrial にする。 [文字列]本試行をここで実行する と入力する。

  • abortルーチン (作成する)

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

    • Textコンポーネントをひとつ配置し、以下のように設定する。

      • 「基本」タブの [名前]textAbort に、 [終了] を空欄にする。 [文字列]実験者の指示に従ってください と入力する。

  • practiceルーチン (作成する)
    • abortルーチンの前に挿入する。

    • Textコンポーネントをひとつ配置し、以下のように設定する。

      • 「基本」タブの [名前]textPracticeStim に、 [開始]0.5[終了] を空欄にする。 [文字列]$stim_word と入力して「繰り返し毎に更新」にする。

      • 「外観」タブの [前景色]$stim_color と入力し、「繰り返し毎に更新」にする。

      • 「書式」タブの [文字の高さ]0.1 にする。

    • Textコンポーネントをひとつ配置し、以下のように設定する。

      • 「基本」タブの [名前]textPracticeInst に、 [終了] を空欄にする。 [文字列]←文字が赤色   ↓文字が緑色   文字が青色→ と入力する。

      • 「レイアウト」タブの [位置 [x, y] $](0, -0.4) にする。

      • 「書式」タブの [文字の高さ]0.03 にする。

    • Keyboardコンポーネントをひとつ配置し、以下のように設定する。

      • 「基本」タブの [名前]key_resp_practice に、 [開始]0.5[検出するキー $]'left', 'down', 'right' にする(キー名の順番は問わない)。

      • 「データ」タブの [正答を記録] をチェックして、 [正答]$correct_ans と入力する。

    • Codeコンポーネントをひとつ配置し、 [名前]codePractice にする。コードは後で入力する。

  • practicesループ(practieルーチンのみを繰り返すように作成する)

    • [Loopの種類] をrandomにする。

    • [繰り返し回数 $]5 にする。

    • [条件]exp12a.xlsx と入力する。

  • exp12a.xlsx (条件ファイル)

    • 図12.1 に示すように、表示する単語を定義するstim_word、文字色を定義するstim_color、正答キーを定義するcorrect_ansの3つの列からなる条件ファイルを作成する。内容は 図12.1 とまったく同じでなくてもよいが、key_resp_practiceがright、down、rightの3つのキーを検出しているので、3種類の語と色を用意すること。作成した条件ファイルをexp12a.xlsxの名前でexp12.psyexpと同じフォルダに保存する。

_images/stroop-conditions.png

図12.1 ストループ課題のための条件ファイル

以上で準備はOKです。 practiceルーチンで練習試行をおこなって、直近10試行の正答率が0.8以上であればtrialルーチンへ進みますが、30試行(条件ファイルに6条件×5回繰り返し)の間にこの基準に達しなかった場合は参加者が課題を理解していないなどの問題が生じている可能性があると考えてabortルーチンで「実験者の指示に従ってください」と表示したいと思います。abortルーチンにはルーチンを終了させるコンポーネントが配置せず、ESCキーで実験を中断しないといけないようにしてあります。

practiceルーチンに配置したCodeコンポーネントですることをまとめると

  1. 直近10試行の正答率を求めること

  2. 直近10試行の正答率が基準を満たしていたらループを中断すること

  3. 練習試行が基準を満たして終了したのか否かを次のルーチンへ伝えること

といったことが挙げられるでしょう。まず1は、複数回の繰り返しにわたってpracticeルーチンでの正誤を保持しないといけないので、正誤を保持するリストを用意するといいでしょう。 以下のようにcodePracticeの [実験開始時] で空のリストを用意します。名前はpractice_resultsとしておきました。

practice_results = []

続いて、codePracticeの [Routine終了時] で反応の正誤をpractice_resultsに追加します。 反応の正誤を得るにはKeyboardコンポーネントのデータ属性corrを使えばよいのでした( 表6.3 )。

practice_results.append(key_resp_practice.corr)

直近10試行の正答率を求めるには、 第11章 で出てきたスライスが早速役に立ちます。practice_results[-10:]とすればpractice_resultsの末尾から10個の要素を取り出せますので、sum( )関数で合計すれば直近10試行中で正答した試行数が得られます(正答なら1、誤答なら0であることを思い出してください)。10で割れば正答率となります。

正答率が求められたので、次は2のループの中断です。今回は「正答率が0.8以上」を基準としたいので、この条件がTrueになったらpractices.finished = Trueを実行するif文を書けばいいでしょう。 ここまでまとめると、codePracticeの [Routine終了時]

practice_results.append(key_resp_practice.corr)

if suum(practice_results[-10:])/10 >= 0.8:
    practices.finished = True

を実行すれば目標を達成できます。最後の3.は、上記if文の条件式がそのまま「基準を満たして終了したか否か」の判定に使えますが、後から見てわかりやすいように変数に代入しておいた方が良いと思います。 ここではpractice_completeという変数にしておきます。

practice_results.append(key_resp_practice.corr)

if suum(practice_results[-10:])/10 >= 0.8:
    practices.finished = True
    practice_completed = True

これだけでは、基準を満たして終了しなかったときにpractice_completedに値が代入されていない状態になるので、どこかでFalseを代入しておく必要があります。上記のコードのif文にelse節をつけてそこで代入してもいいのですが、基準を満たすまで何度もFalseを代入し続けるのも無駄な作業なので、 [実験開始時]

practice_results = []
practice_completed = False

と代入しておくとよいでしょう。これでpracticeCodeでするべき3つの作業がすべてできました。

後はこのpractice_completedを後のルーチンでどう活用するかですが、abortルーチンの「Routineの設定」をクリックしてプロパティ設定ダイアログを開き、「Flow」タブの [条件に合致する場合はスキップ... $] に practice_completed と入力しましょう。基準を満たしてpracticesループが終了したなら、practice_completedがTrueになっているので、abortルーチンがスキップされてtrialルーチンへ処理が進みます。 もし基準を満たさずにpracticesループが終了していれば、practice_completedがFalseなのでabortルーチンが実行されます。先述した通り、abortルーチンに進むと参加者は先へ進めなくなりますので、実験者がESCキーを押して実験を中断して、改めて課題を説明するなり実験を中止するなりすることになるでしょう。 exp12a.psyexpを実行してみて、基準を満たせば「本試行をここで実行する」と画面に表示されて終了すること、わざと間違え続けて基準を満たさずに終われば「実験者の指示に従ってください」と表示されてESCキーで中断しないといけなくなることを確認してください。

チェックリスト
  • ループ内で反応の正誤を順番にならべたリストを作ることができる。

  • リストの末尾から指定された個数の値を取り出せる。

  • ループを中断することができる。

12.4. 練習試行で基準を満たさなければ自動的に実験が中断されるようにしよう

前節では練習試行の成績が基準に達しなかったときに通常のキー押し等で終了しないメッセージを表示して、実験者が実験の中断操作をすることを想定していましたが、自動的に実験が終了した方が望ましいケースもあるでしょう。そのような場合、「基準に達しなかったらフローの残りをすべて実行しない」という処理が必要になります。 [条件に合致する場合はスキップ... $] を後続の全てのルーチンに設定するという方法でもできなくはありませんが、後続のルーチンがたくさんある場合はかなり面倒ですし、ループがある場合はループも中断させないと実験記録ファイルに出力されてしまいます。 フローに条件分岐の機能があればいいのですが、残念ながら現在のバージョンのBUilderにはありません。

そこで注目したいのが、「 4.9:動作確認のために一部の動作をスキップしよう 」で紹介した「ループの [繰り返し回数] を0にする」というテクニックです。これは「ループが0回実行される = 1度も実行されない」ためループ内のルーチンがまとめてスキップされるというものでした。 4.9 章 で紹介したときは、実験の動作確認の時に確認不要な部分をスキップするために、実行前に手作業で0に設定するという使い方をしたのですが、 [繰り返し回数] に変数を指定して実行時の条件に応じて変更してやろうというわけです。 前節で作成したexp12a.psyexpで引き続き作業しましょう。

  • trialsループ (trialルーチンのみを繰り返すように挿入する)

    • [繰り返し回数 $] にnum_repeats_trialsと入力する

  • abortルーチン

    • textAbortの「基本」タブの [終了] に3.0と入力する。 [文字列] に 「実験を中断します」 と入力する。

abortルーチンでどういうメッセージを表示するかは、実験者が横についているのか、別室で待機しているのか、中断した場合にその理由をメッセージで参加者に伝えるか否かなど、状況によって異なると思いますので、ここでは簡潔に「実験を中断します」だけにしておきました。3秒で自動的に終了するようにしましたが、参加者にキーを押させるなどの方法もあり得るでしょう。

さて、ここから本題ですが、practiceルーチンに配置してあるcodePracticeの [Routine終了時] のコードに以下のようにif文に num_repeats_trials = 1 という行を追加してください。

practice_results.append(key_resp_practice.corr)

if suum(practice_results[-10:])/10 >= 0.8:
    practices.finished = True
    practice_completed = True
    num_repeats_trials = 1

practice_completedの時と同様、基準を満たさなかったときに num_repeats_trials の値が未定義になってしまわないよう、 [実験開始時] に num_repeats_trials = 0 を追加しておきましょう。

practice_results = []
practice_completed = False
num_repeats_trials = 0

以上で作業は終了です。exp12a.psyexpを実行して、わざと間違え続けて基準を満たさずに終われば「実験を中断します」と表示されて、3秒経過すると自動的に実験が中断される(=trialルーチンが実行されない)ことを確認してください。 この方法であれば、trialsループ内にルーチンがいくつあってもまとめてスキップすることができます。 また、練習試行の成績に応じて3通り以上にフローを分岐するといったことも可能になります。 応用範囲が広いので、ぜひ覚えておいてください。

チェックリスト
  • 実験実行時に条件に応じてループの実行をスキップできる。

  • ループを活用して、フローで条件分岐を実現できる。

12.5. グローバルクロックを利用して一定時間経過したら終了するループを組もう

続いてもう一つの例として、記憶課題などで、再生や再認を開始するまでの間に「1分間計算問題を繰り返す」といった遅延課題をBuilderで実現することを考えてみましょう。 前節の練習課題の例と異なるのは、練習課題では参加者が反応したタイミングでループの中断判断をおこなえばいいのに対して、遅延課題では反応を待っている期間であっても制限時間がきたら直ちにループ(と現在のルーチン)を中断しないといけない点です。

ルーチンの中断もループの中断もすでに解説しましたので、特に難しいことはないように思われるかもしれませんが、ひとつ問題があります。それは「課題を始めてから何秒経過したか?」を知る方法です。 これまで時間というとRoutineが開始してからの時間を表す内部変数tが出てきましたが、tはルーチンを繰り返すたびに0にリセットされてしまうので、「ループが始まってから何秒経過したか」を測ることはできません。そこで登場するのがBuilderのグローバルクロックです。 これは実験開始時に0に初期化されるストップウォッチのようなもので、globalClockというBuilderの内部変数に格納されています。 その実体はpsychopy.core.Clockというクラスのオブジェクトで、getTime( )というメソッドを用いて現在の経過時間を取得することができます。 さっそく実験を作成して確かめてみましょう。

  • 準備作業
    • この実験のためのフォルダを作成して、その中にexp12b.psyexpという名前で新しい実験を保存する。

    • 実験設定ダイアログの「スクリーン」タブの [単位] がheightであることを確認する。

    • 実験設定ダイアログの「入力」タブの [キーボードバックエンド] がPsychToolboxか確認する。

  • delay_taskルーチン (trialルーチンの名前を変更する)
    • Codeコンポーネントをひとつ配置し、codeDelayTaskという名前にする。

    • Textコンポーネントをひとつ配置し、以下のように設定する。
      • 「基本」タブの [名前] をtextDelayTaskQuestionにする。 [開始] を0.1にして、 [文字列] に $delay_task_question と入力する。

      • 「書式」タブの [文字の高さ] を0.1にする。

    • Textコンポーネントをもうひとつ配置し、以下のように設定する。
      • 「基本」タブの [名前] をtextDelayTaskInstにする。 [文字列] に「←割り切れない    割り切れる→」と入力する。

      • 「レイアウト」タブの [位置 [x, y] $] を (0, -0.4)にする。

      • 「書式」タブの [文字の高さ] を0.05にする(初期値でそうなっている)。

    • Keyboardコンポーネントをひとつ配置し、以下のように設定する。
      • 「基本」タブの [名前] を key_resp_DelayTask にする。 [開始] を0.1にして、 [検出するキー $] を'left', 'right' にする。

      • 「基本」タブの [Routineを終了] がチェックされていることを確認する。

      • 「データ」タブの [正答を記録] をチェックして、 [正答] に $delay_task_correct_ans と入力する。

  • delay_tasksループ (delay_taskルーチンを繰り返すように挿入する)
    • [繰り返し回数 $] を 12 にする。

    • [条件] に exp12b.xlsxと入力する。

  • exp12b.xlsx (exp12b.psyexpと同じフォルダに作成する)
    • delay_task_question と delay_task_correct_ans という2列のパラメータを持つ条件ファイルを作成する。

    • delay_task_question には 78÷7 のような2桁の整数を1桁の整数(0, 1は除く)で割る式を入力し、delay_task_correct_ans にはその式が割り切れないならleft、割り切れるならrightと入力する。

    • 少なくとも10条件は作成する。

以上で準備終了です。あとはCodeコンポーネントに入力するコードを考えていきましょう。 まず、遅延課題を開始してからの経過時間がわからないことにはどうしようもありませんから、開始時刻を測らないといけません。 これにはglobalClock.getTime()を使うとして、Codeコンポーネントのどこへコードを入力すればいいでしょうか。 delay_tasksループによる繰り返し全体が「遅延課題」を構成しているので、「delay_tasksループ開始時」に入力するというのが正解ですが、残念ながらCodeコンポーネントには「ループ開始時」というタブがありません。 候補としては

  1. delay_tasksループに入る直前の [Routine終了時] に入力する

  2. delay_tasksループに入って最初の [Routine開始時]初回に一度だけ実行されるように 入力する

の2つが考えられます。1.の方法はここまでに解説したテクニックの範囲で十分にできますし、「遅延課題のための準備をその前の課題で行わなければならない」点が「この遅延課題を別の実験に使いまわせるようにしたい」というところまで考えると望ましくないので、ここでは2.の方法を考えます。

[Routine開始時] で初回に一度だけ実行される」ようにするには、「実行済みか否か」を保持する変数を利用します。こういった変数をフラグと呼びます。 フラグがFalseであれば「まだ実行されていない」と判断して時刻の測定を行うと同時にフラグをTrueにします。 以後、ふたたび [Routine開始時] になってもすでにフラグがFalseではないので時刻の測定は実行されないというわけです。 ここでは「遅延課題の実行中である」ということで delay_task_running という名前の変数をフラグにしましょう。 以下のように codeDelayTask の [Routine開始時] に入力します。

if not delay_task_running:
    delay_task_start = globalClock.getTime()
    delay_task_running = True

遅延課題開始時刻を保持する変数は delay_task_start という名前にしておきました。 このままだと初回実行時には delay_task_running という変数が一度も値を代入されないままif文で参照されてしまうため、 [実験開始時]

delay_task_running = False

と入力しておきましょう。当然、実験開始時には遅延課題は開始していないのでFalseを代入しています。 続いて [フレーム毎] ですが、ここでは1分間経過したらルーチンの中断とループの中断を一気に行う必要があります。 「1分間経過したら」の判断は、globalClock.getTime()からdelay_task_startを引けば現在の経過時間が得られるので、この値が60を超えたか否かを条件とするif文を書けばよいでしょう。 ルーチンの中断は continueRoutine=False 、 ループの中断はdelay_tasks.finished=Trueですから、以下のような文になります。

if globalClock.getTime() - delay_task_start > 60.0:
    continueRoutine = False
    delay_tasks.finished = True
    delay_task_running = False

最後のdelay_task_running=Falseは「遅延課題を終了する」という処理自体には不要ですが、もしこの遅延課題を別のループの中に入れて繰り返し実行するなら(というか繰り返し使う方が一般的でしょう)、delay_task_runningをFalseに戻しておかないと次の繰り返しの時に「遅延課題が開始された時刻を測る」という処理ができません。 次の繰り返しまでのどこかでFalseにすれば問題ないのですが、実際に遅延課題が終了する直前のこのタイミングでFalseにするのがスマートです。

以上でコードは完成です。exp12b.psyexpを実行して、画面に表示される式が割り切れるか割り切れないかカーソルキーを使って反応すると次々と問題が表示されることと、時間が経過したらキーを押さなくても直ちに終了することを確認してください。

なお、ここでは一定時間で終了するループを組むテクニックを学ぶことが目的だったので、exp12b.xlsxの条件数は「少なくとも10条件作成してください」とだけしか指定しませんでしたが、実際の実験では「遅延課題の長さを何秒にするか」、「同じ問題が繰り返し表示されても構わないか」といった事に注意して条件数を決定する必要があります。

最も避けなければいけないことは、条件数と繰り返し数が足りなくて、遅延課題の時間が終了する前に繰り返しが完了してしまうことです。 exp12b.psyexpの場合、Keyboardコンポーネントの [開始] が0.5秒に設定されているため、参加者が全力でキーを連打しても絶対にループ1回に0.5秒かかります。そんなことをする参加者はいないだろうと思うかもしれませんが、ずっと付き添っていない実験ならあり得ないとは言えません。 ループ1回に0.5秒以上かかるということは、1分(=60秒)÷0.5秒で120問以上の問題があれば、最短の反応時間であっても遅延時間内に終わってしまうことはありません。 exp12b.psyexpではdelay_tasksループの [繰り返し] を12に設定しているので、exp12b.xlsxに10条件あれば12×10=120問となりギリギリこの条件を満たすことができます。 同じ問題が出現してはいけない場合は、条件ファイルに定義された条件数だけで確実に遅延時間以上かかるようにしないといけません。今回の場合なら120条件以上です。 実際には最短の反応時間で反応し続けるなんて不可能でしょうからもう少し問題数が少なくても大丈夫でしょうが、確実に1分以上かかる問題数を確保しておいた方がいいと個人的に思います。

チェックリスト
  • ループを中断することができる。

  • 実験開始からの経過時間を取得することができる。

  • 開始から一定時間経過したら終了するループを組むことができる。

12.6. 残り時間を表示しよう

この節では、前節の遅延課題を題材として、残り時間の表示をしてみたいと思います。 時間の表示は 第5章 でフレーム毎のアニメーションをする際にも少し触れたのですが、その時は小数点以下すごい桁数が表示されてしまって非常に見難いものでした。 この問題を解決する方法を学ぶのが目的です。

文字列オブジェクトにはformat( )というメソッドがあり、このメソッドを用いると引数として与えられた他の文字列や数値などを書式指定しながら埋め込むことができます。 以下にformat()の例を示します。

'平均反応時間:{} 正答率:{}'.format(mean_rt, n_correct)

文字列の中に{}という部分が2か所あることに注目してください。format()は、引数として与えられた値を文字列中の{}の場所へ順番に埋め込んでいきます。いま、引数として与えている変数mean_rtの値が1218.7、n_correctの値が31ならば、このメソッドの実行結果は

'平均反応時間:1218.7 正答率:31'

という文字列になります。 format( )はただ引数の値を埋め込むだけでなく、{}のなかに書式指定文字列と呼ばれる文字を書き込むことによって、埋め込まれた後の文字列を細やかに指定することができます。 非常に多くの機能があるのですが、ここで紹介したいのは数値を埋め込むときの桁数指定です。 以下に小数点以下3桁まで表示するよう指定する例を示します。

'実験開始から{:.3f}秒'.format(t)

ちょっと難しいですが、{}内の : が「これ以降は書式指定」ということを表す記号だと思ってください。 : の後ろの .3f というのが書式指定で、fは小数として出力することを指定してします。fの前の.3が小数点以下3桁を出力するという指定です。.の前に整数を書くと全体の桁数の指定となり、さらに+記号をつけると符号つきで出力されます。文章で説明してもなかなかわかりにくいと思いますので、具体例を 図12.2 に示します。

_images/flag-string.png

図12.2 数値の書式指定の例。変数pには3.14159264359という値が代入されているものとします。

書式指定には小数を指定するfの他にも、10進数の整数を指定するdや、16進数の整数を指定するxなどがあります。小数を10進数整数や16進整数で出力しようとするとエラーで実験が停止してしまうのでご注意ください。小数の値を整数で出力する場合は以下のようにint()を使って整数に変換するか、小数点以下の桁数として0を指定すればよいでしょう。

'実験開始から{:d}秒'.format(int(t))  # intで整数に変換
'実験開始から{:.0f}秒'.format(t)     # 小数点以下の桁数として0を指定

ここまで数値を埋め込む例ばかり挙げてきましtが、format( )は教示文や刺激文へ単語の埋め込む場合などにも便利です。 以下の例では実験情報ダイアログの値(=文字列)を埋め込んでいます。

'あなたの参加者IDは{}です'.format(expInfo['participant'])

埋め込み後の文字列に { や } の記号を出力したい場合は {{ や }} といった具合に2つ並べて書きます。以下の例で、変数idの値が712なら '{参加者ID}: 712'という文字列が得られます。

'{{参加者ID}}: {}'.format(id)

第6章 では+演算子とstr( )関数を使用する方法を紹介しましたが、文字列に埋め込む値が多数ある場合はformat()を使った方が簡潔に記述できるので、ぜひ活用してください。

それでは、前節のexp12b.psyexpに残り時間を表示してみましょう。

  • delay_taskルーチン

    • Textコンポーネントをひとつ配置して以下のように設定する。

      • 「基本」タブの [名前] を textDelayTaskRemaining にして、 [終了] を空欄にする。

      • 「基本」タブの [文字列] を $'残り{:3.1f}秒'.format(delay_task_remaining_time) にして、「フレーム毎に更新」にする。

      • 「レイアウト」タブの [位置 [x, y] $] を (0, 0.3)にする。

    • codeDelayTaskの [フレーム毎] の先頭に以下のように delay_task_remaining_time への代入文を挿入し、if文の条件節を変更する。

delay_task_remaining_time = delay_task_start + 60 - globalClock.getTime()
if delay_task_remaining_time < 0.0:
    # 以下の行はそのままでよい

変更前は開始からの経過時刻を [フレーム毎] で計算していましたが、残り時間を表示したいのでdelay_task_start+60として終了時刻を求め、そこからglobalClock.getTime()で得た現在の時刻を引いています。この式を textDelayTaskRemaining の [文字列] に伴うformat( )の中と if文の条件説の両方に書くのは長ったらしいですし、変更する必要が生じた時(例えば遅延時間を変更するとき)などに2箇所修正するのも面倒ですので、delay_task_remaining_timeという変数に代入しています。 delay_task_remaining_timeという変数名自体長ったらしいじゃないかと怒られるかもしれませんが、これは次のことを考えてわざとこのような名前にしてあります。

あと、ひとつ注意すべき点として、codeDelayTask が textDelayTaskRemainingより先に実行されないと、delay_task_remaining_timeという変数が定義されていなくてエラーになってしまいます。ルーチン内でのコンポーネントの順序を間違えないようにしましょう(今回は解説通りの手順で作業していれば適切な順番になるはず)。そういった順序関係を気にしたくない人は、codeDelayTask の [実験開始時] でdelay_task_remaining_time = 60 などと適当な値を代入して変数を用意しておくと良いでしょう。

以上で作業は終了です。実験を実行して、残り時間が表示されることを確認してください。

チェックリスト
  • 小数点以下の桁数を指定して数値を文字列に埋め込める。

  • ひとつの文字列に複数の数値や文字列を位置を指定して埋め込める。

12.7. テンプレートにしてみよう

前節で「delay_task_remaining_timeという長ったらしい名前をつけたのは次のことを考えて…」と書きましたが、本章の締めくくりとしてやっておきたいことは「テンプレート化」です。 「 3.12.9:独自のルーチンテンプレートを登録する方法 (上級) 」で少し触れたように、Builderで新しいルーチンを追加するときに選択できるテンプレートは独自に追加することができます。 十分に活用するにはよく考えてテンプレートを設計する必要があるので 第3章 では詳しく触れなかったのですが、そろそろ解説しても問題ないでしょう。 前節までに作成した遅延課題のルーチンを使って、実際にテンプレートを作成してみます。

テンプレートは通常のpsyexpファイルで、Builderが起動するときにユーザー設定フォルダのroutine_templatesというサブフォルダ内に置かれていればテンプレートとして登録されます。ですから、作業としてはexp12b.psyexpをこのフォルダへコピーするだけです。 問題は「ユーザー設定フォルダ」とはどこかということですが、Windowsではアプリケーション固有のデータを保存するフォルダ中にあるpsychopy3というフォルダが該当します。この「アプリケーション固有のデータを保存するフォルダ」はWindowsのAPPDATAという環境変数で参照することができるので、ファイルエクスプローラのアドレスバーに%APPDATA%と入力してEnterキーを押せば開くことができます(図12.3)。 一度でもそのWindows上でPsychoPyのアプリケーション(Builder, Coder, Runner)を起動したことがあれば、そのフォルダの中にpsychopy3というフォルダが見つかるはずです。

_images/windows-appdata.png

図12.3 WindowsでPsychoPyのユーザー設定フォルダを見つける方法

MacOSやLinuxでは、PsychoPyのアプリケーションを一度でも立ち上げたことがあれば、ユーザーのホームディレクトリに.psychopy3という隠しフォルダが作成されているはずです。これがユーザー設定フォルダです。 隠しフォルダなので標準の状態では見えませんが、MacOSではFinderでCommand + Shift + . (ピリオド)キーを同時押しすると名前が . から始まる隠しフォルダおよび隠しファイルを表示することができます。 Linuxではウィンドウマネージャによって操作が違うでしょうから自分が使用している環境での表示方法は各自で調べてください。 Linuxを使う人ならshellでの操作に抵抗がない人が多いでしょうから、shellで操作した方がいいかも知れません。

ユーザー設定フォルダを開いたら、その中に routine_templates というフォルダがあるか確認し、もしなければ作成してください。そして、本章で作成した exp12b.psyexp を routine_temples フォルダにコピーして、PsychoPyを一旦すべて閉じてから起動してください。以後、Builderで新しいルーチンを追加しようとすると、 図12.4 のようにテンプレートの中にexp12bというカテゴリができていて、その中にdelay_taskというテンプレートが追加されているはずです。このテンプレートを選んでルーチンを追加すれば、exp12b.psyexpで作ったdelayルーチンが追加されます。

_images/new-template.png

図12.4 routine-templatesフォルダに追加したpsyexpファイルと同名のカテゴリが追加されていて、そのファイルに含まれていたルーチンがテンプレートとして表示される。

このdelay_taskルーチンが適切に機能するためには、exp12b.psyexpでそうしたようにdelay_tasksというループで囲む必要があります。delay_taskルーチン内に配置したCodeコンポーネントに delay_tasks.finished=Trueというコードが含まれているので、ループの名前はdelay_tasksでなければいけません。ループの名前を変えたければ、コードを修正する必要があります。 delay_tasksループで [条件] に指定する条件ファイルも用意しなければいけませんし、Codeコンポーネントで使用しているdelay_task_startやdelay_task_runningといった変数を他のルーチンで使用しないよう注意しなければいけません。 こういったことを考えると、コードなどを駆使した複雑なルーチンは作るのが面倒なので、繰り返し使うのならどんどんテンプレートしていきたいところですが、「このテンプレートを使う時には何を用意しないといけないか、何をしてはいけないか」を理解しておく必要があります。先ほど「十分に活用するにはよく考えてテンプレートを設計する必要がある」と書いたのはこういった点を指しています。

実は、exp12b.psyexpの作成手順を執筆していた時、テンプレート化を考えて決めたことがいくつかあります。まず、変数名としてdelay_task_remaining_timeなどという「長ったらしい名前」をわざわざ選んだのは、他のルーチンで使用する名前と重複しにくくするためです。配置するコンポーネントの名称や、条件ファイルの列名などにも、テンプレートの名前にちなんでdelay_taskとかdelayTaskといった文字列を組み込んでいます。また、遅延課題の測定時刻の測定は「ループの開始直前の [Routine終了時] 」におこなった方がフラグ変数が不要となってシンプルになるのですが、それではテンプレート内にコードを含めることができません。「ループに入って最初の [Routine開始時] に初回だけ実行する」方法であればdelay_taskルーチンに配置するCodeコンポーネント内ですべて完結するので、テンプレート化したときに考えないといけない点が減ります。

ルーチンの独立性を高めることにこだわるなら、exp12b.psyexpで条件ファイルで定義することにしていた計算問題と回答を 第11章 のような方法で無作為に生成するという事も考えられます(「 12.8.1:遅延課題の計算問題を実行時に生成する 」)。どの実験でも同じ計算問題を遅延課題として使用するならそれも良いでしょうが、exp12b.psyexpのように条件ファイルとして問題を切り離しておけば、計算問題以外のカーソルキーの左右で反応できる課題(例えば単語が生物か無生物かを判断する課題など)にも使いまわすことができます。 どちらが優れているというわけではないので、自分が使いやすいようにカスタマイズしてください。

さて、テンプレート化について、あといくつか確認してから本節を終えたいと思います。 ユーザー設定フォルダ内のroutine_tenplatesフォルダにコピーした方のexp12b.xlsx で以下の作業をしてださい。

  • exp12b.psyexp の名前を 遅延課題.psyexp に変更する。

  • Builderで 遅延課題.psyexp を開く。

  • delay_taskルーチンの「Routineの設定」をクリックして設定ダイアログを開き、「基本」タブの [説明] に以下のように入力する。

遅延課題のテンプレートです。delay_task_question と delay_task_correct_ans という2つのパラメータを定義した条件ファイルを指定したdelay_tasksというループで囲んで使用します。

delay_task_question は問題文、 delay_task_correct_ans は正答となるキーを right、left のいずれかで指定します。

以下の変数をCodeコンポーネントで使用しているので、このテンプレートを使用する実験では他の場所で使用しないでください。
- delay_task_running
- delay_task_start
- delay_task_remaining_time
  • delay_task_feedbackルーチン (新たに作成し、delay_taskルーチンの後ろに挿入する)
    • Codeコンポーネントをひとつ置き、 [名前] を codeDelayTaskFeedback にする。

    • Textコンポーネントをひとつ置き、以下のように設定する
      • 「基本」タブの [名前] を textDelayTaskFeedback にする。 [終了] が1.0であることを確認する(初期状態でそうなっている)。

      • 「基本」タブの [文字列] を $delay_task_feedback_msg として「繰り返し毎に変更」にする。

    • codeDelayTaskFeedback、textDelayTaskFeedbackの順番に実行されるよう並んでいることを確認する(ここまで手順どおりに作業していればそうなっているはずである)。そうしないとルーチン開始時にTextコンポーネントでdelay_task_feedback_msgが定義されていないと言われてエラーになる。

    • delay_taskルーチンから textDelayTaskRemaining をコピーして、delay_task_feedbackルーチンに貼り付ける。 [名前] は textDelayTaskRemaining_2 でよい。以下のように設定する。
      • 「基本」タブの [終了] 1 にする (textDelayTaskFeedbackと同時に終了するようにする)。

    • 「Routineの設定」をクリックして設定ダイアログを開き、以下の通り設定する。
      • 「flow」タブの [条件に合致する場合はスキップ... $] に delay_tasks.finished と入力する。

      • 「基本」タブの [説明] に以下のように入力する。

遅延課題のテンプレートdelay_taskと組み合わせて使用するフィードバック画面です。単独で使用しないでください。

以下の変数をCodeコンポーネントで使用しているので、このテンプレートを使用する実験では他の場所で使用しないでください。
- delay_task_feedback_msg

続いて delay_task_feedbackルーチン の codeDelayTaskFeedback にコード入力しましょう。まず [Routine開始時] に以下のコードを入力します。

if key_resp_DelayTask.corr:
    delay_task_feedback_msg = '正解'
else:
    delay_task_feedback_msg = '不正解'

[フレーム毎] に以下のコードを入力します。 delay_taskルーチンに配置されている codeDelayTask の [フレーム毎] に書いてあるコードと同じなので、コピー&ペーストするとよいでしょう。

以上で作業は終了です。遅延課題.psyexpを保存してPsychoPyをすべて終了し、もう一度PsychoPyを起動してください。そしてBuilderで新しいルーチンを挿入しようとしたら、 図12.5 のように、「遅延課題」と言うカテゴリができていて delay_task と delay_task_feedback というテンプレートが含まれているはずです。 以上のことから、routine-templatesフォルダに保存したpsyexpファイル名がテンプレートのカテゴリとして利用されること、カテゴリ名(=psyexpファイル名)は日本語では問題ないこと、psyexpファイルに含まれているルーチンはそのファイル名のカテゴリのテンプレートになることがわかります。

_images/new-template-2.png

図12.5 routine-templatesフォルダに追加したpsyexpファイルと同名のカテゴリが追加されていて、そのファイルに含まれていたルーチンがテンプレートとして表示される。

ルーチン名に日本語などの非ASCII文字を使用することは引き続きお勧めしませんが、筆者がバージョン2024.2.5 + Python 3.10で試した限り、テンプレートとして使用するpsyexpファイルのルーチンに非ASCIIの文字を使ってもテンプレート名として問題なく機能しました。 テンプレートを使って新しいルーチンを挿入するときには名前をつけないといけませんが、その際に非ASCII文字を使わないように心がければ、テンプレート名に日本語を使っても問題ないかもしれません。

delay_taskまたはdelay_task_feedbackテンプレートを使って新しいルーチンを挿入したら、「Routineの設定」をクリックして設定ダイアログを開き、「基本」タブの [説明] に先ほど入力した注意書きが入力されていることを確認してください。「 [説明] にテンプレートを使用する際の注意事項を書く」ということを研究室内で徹底しておけば、かなり便利にテンプレートを使えるのではないかと思います。

チェックリスト
  • PsychoPyのユーザー設定フォルダを開くことができる。

  • Builderにルーチンのテンプレートを追加することができる。

  • ルーチンテンプレートのカテゴリ名を設定することができる。

  • 複数のルーチンテンプレートをカテゴリにまとめることができる。

  • テンプレートの [説明] にテンプレート使用の際の注意点を記入しておくことができる。

12.8. この章のトピックス

12.8.1. 遅延課題の計算問題を実行時に生成する

遅延課題を条件ファイルに分けずにテンプレートに組み込んでしまうには、 [実験開始時] などに以下のような問題と正反応のペアを並べたリストを用意すると良いでしょう。

重複を許さないならshuffle( )で順序を無作為化し、delay_tasks.thisNを使って順番に問題と正反応のペアを取り出せばいですし、重複しても良いならrandchoice( )でペアを取り出すという方法もあります。 ただ、randchoice( )だと本当に無作為になってしまうので、可能性としては極めて低いもののひとつの問題が極端に多い回数選出されてしまう可能性があります。 それでは困ると言う場合は、「Pythonにおいてリストと正の整数の積はリストの要素をその回数だけ繰り返したリストを返すことを利用して

として各問題を5回含むリストを作成し、shuffe( )して順番に取り出せばよいでしょう。

問題と正反応のペアが大量に必要な場合、それらをすべてコードとして記入するのも大変ですので、実行時に生成することも考えてみます。 numpyを使った方が速いかも知れませんが、ここではBuilderが標準でimportしていないモジュールは使わないことにします。 まず、被除数を10から99、除数を2から9まで変化させていきながら、割り切れる場合はdivisible、割り切れない場合はindivisibleというリストへ追加していきます。もうここでTextコンポーネントとKeyboardコンポーネントに渡せる形式にしてしまいましょう。 2で割れる値は他の除数(3から9)より多いうえに判断も簡単なので、2から9ではなく3から9にしてもいいかも知れません。

divisible = []
indivisible = []
for dividend in range(10,100):
    for divisor in range(2,10):
        if dividend % divisor == 0:
            divisible.append(('{}÷{}'.format(dividend, divisor),'right'))
        else:
            indivisible.append(('{}÷{}'.format(dividend, divisor),'left'))

divisibleとindivisibleをそれぞれ無作為に並び替えて、スライスで必要な問題数を取り出して結合します。 リスト同士の結合は + 演算子が使えます。 割り切れる問題と割り切れない問題の比率を1:1以外にしたい場合は取り出す問題数で調整しましょう。

shuffle(divisible)
shuffle(indivisible)
delay_task_question_set = divisible[:100] + indivisible[:100]

あとは問題を直接コードに記入する場合と同じです。