PsychoPyのTrialHandlerとExperimentHandler

Builderを使っているとほとんど意識することはないと思いますが、Coderを使いたいときTrialHandlerってどうやって使うの？　TrialHandler2というのもあるけど、どう違うの？ え？さらにExperimentHandlerてのもあるんだけど・・・となりがちです。私だけかもしれませんが。

ということで、同じ悩みを抱えているかたへの解説です。

この例では、次の４種類の視覚刺激を呈示することにします。

<img width="816" alt="スクリーンショット 2020-03-04 16.47.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495823/296b9c9f-88c1-f7c7-a378-9c9632a001d0.png">

上の４つの中からひとつが呈示され、それに対して f または j のキーを押すという課題を考えましょう。

こういったケースでは、色の要因、形の要因の影響を見たいことが一般的です。色の要因としては赤と青の2水準があり、形の要因としては三角形と四角形の2水準があります。2 x 2 で4条件を設けることになりますが、PsychoPyには便利な関数があります。

```python
conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})
```

`print(conditions)`をしてみると分かりますが、conditionsは次のような内容です。専門用語では、[辞書型](https://qiita.com/E_Schaf/items/9ef26c0c514325d69e80)を要素として持つ[リスト](https://www.sejuku.net/blog/23633)になります。各要素が実験条件を表しています。関数ひとつで、ぱっとこれを作ってくれるので便利ですね。ちなみに[importConditions](https://www.psychopy.org/api/data.html#psychopy.data.importConditions)を使えばエクセルファイルに記述した実験条件を読み込むことができます。

```python:conditionsの中身
[
{'stim_color': 'red', 'stim_shape': 'triangle'}, 
{'stim_color': 'blue', 'stim_shape': 'triangle'}, 
{'stim_color': 'red', 'stim_shape': 'rectangle'}, 
{'stim_color': 'blue', 'stim_shape': 'rectangle'}
]
```

# TrialHandler

TrialHandlerの動作を確認するためのコードです。こちらのコードはBuilderではなく、Coderで記述します。

```python
from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }
    
trials = data.TrialHandler(conditions, 2) # 各条件を２回繰り返す

win = visual.Window()

thisTrial = trials.next() # 1試行目

win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
stim.draw()
win.flip()

key_loop = True
while key_loop:
    ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False) # f,jキーのみ入力可能
    if len(ptb_keys):        
        trials.addData('RT', ptb_keys[0].rt) # 反応時間の記録
        trials.addData('choice', ptb_keys[0].name) # どのキーを押されたかの記録
        key_loop = False
        
# 以下はプログラムの書き方としてはスマートではありませんが、TrialHandlerの動作確認のため、
# 1試行目と同じ作業を繰り返しています。
thisTrial = trials.next() # 2試行目

win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
stim.draw()
win.flip()

key_loop = True
while key_loop:
    ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)
    if len(ptb_keys):        
        trials.addData('RT', ptb_keys[0].rt)
        trials.addData('choice', ptb_keys[0].name)
        key_loop = False


trials.saveAsWideText("results.csv", appendFile=False)
```

動作確認のため、ちょっと変則的な使い方になっています。全試行は、4条件 x 2繰り返し = 8試行ですが、2試行のみを実施します。

本題からそれますが、推奨されている[Psychtoolbox由来のキーボード](https://www.psychopy.org/api/hardware/keyboard.html)を使っています。取得された反応時間が`psychopy.event.getKeys()`より正確になるらしいです。

```python
kb = keyboard.Keyboard()
```

また反応時間を正確に取得するため、次の２行を加えています。

```python
win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア
```
callOnFlipで指定された関数は、次のFlipのタイミングで実行されます。上の例では、刺激を呈示した瞬間にクロックをリセットして、反応時間の計測を開始しています。またすでに押されていたキーがあった場合には、その情報をクリアするようにしています。

さて、[TrialHandler](https://www.psychopy.org/api/data.html#psychopy.data.TrialHandler)のパラメーター（引数）はたくさんあるのですが、今回は必要最小限にしています。第1パラメーターで実験条件のリストを指定します。第2パラメーターで繰り返し数を指定します。

```python
trials = data.TrialHandler(conditions, 2) # 各条件を２回繰り返す
```

TrialHandlerはすべての実験条件を管理していて、`trials.next()`によって現在の試行を参照します。上のコードは2試行だけが実施されますが、`trials.next()`が2回呼び出されていることを確認してください。各試行に入る**直前**で呼び出します。しかし、後述しますが、`trials.next()`を手動で呼び出すことはまれです。

```python
ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)
```
で、現在のキー押し状況を確認しています。fまたはjキーの入力のみを受け付けています。またキーから指を離すことを待ちません。

fまたはjキーが押されていたら、反応時間とどのキーを押していたかの情報を保存します。押されているキーの情報は複数存在する可能性があるため（つまり、同時に複数のキーを押している）、その先頭（0番目）のみを受け付けるようにしています。

```python
trials.addData('RT', ptb_keys[0].rt)
trials.addData('choice', ptb_keys[0].name)
```

`trials`が`TrialHandler`によって作成されていることを思い出してください。つまり`TrialHandler`はすべての実験条件を管理し、かつ、データの保存まで請け負っているわけです。

```python
trials.saveAsWideText("results.csv", appendFile=False)
```

実験結果を`results.csv`に保存します。上書きをしないようにしています。

では実験結果を見てみましょう。たぶん、こんな感じになっていると思います。

<img width="577" alt="スクリーンショット 2020-03-05 11.12.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495823/fdf2d2b9-9725-d53b-9795-520f1c2dbfdb.png">

反応時間やどのキーを押したかは2試行分しか記録されていませんが、それ以外の情報については8試行すべてのデータが保存されていますね。

# TrialHandler2

次に`TrialHandler2`の説明です。

上のコードをそのまま使います。ただし、次の行のみ、`TrialHandler`から`TrialHandler2`に変更してください。

```python:TrialHandler2に変更
trials = data.TrialHandler2(conditions, 2) # 各条件を２回繰り返す
```

実験結果（results.csv）を見てみましょう。

<img width="590" alt="スクリーンショット 2020-03-05 11.16.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495823/23db57f1-23ec-b72a-ca66-ec71ababae05.png">

TrialHandlerを使ったときとの違いが分かるでしょうか？

そうです、3試行目から8試行目までのデータがまったくないですね。これがTrialHandlerとTrialHandler2の違いです。

うん、違いは分かった！　でもこの違いがどんな役に立つの？　と思いますよね。実は正直なところ、私もよく分かっていないのですが、私の理解では、事前に割り当てたすべての条件を出力すると結果が見にくくなるケースがあり、そういうときには `TrialHandler2`を使うようです。ですので、基本的には`TrialHandler`を使って、余計な内容が出力されていると感じたときに`TrialHandler2`を試すということでよいと思います。

# TrialHanlder を for文と組み合わせる

先に「`trials.next()`を手動で呼び出すことはまれです」と書きました。
ふつうは以下のようにします。なお、`TrialHandler2`から`TrialHandler`に戻しています。

```python
from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }
    
trials = data.TrialHandler(conditions, 2) # 各条件を２回繰り返す

win = visual.Window()

for thisTrial in trials:
    win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
    win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

    stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
    stim.draw()
    win.flip()

    key_loop = True
    while key_loop:
        ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)
        if len(ptb_keys):        
            trials.addData('RT', ptb_keys[0].rt)
            trials.addData('choice', ptb_keys[0].name)
            key_loop = False
        
trials.saveAsWideText("results.csv", appendFile=False)
```

for文を使って、trialsの各条件を参照します。自動で`trials.next()`が呼び出されていると思ってかまいません。

# ExperimentHandler

上のfor文を使ったサンプルにExperimentHandlerを加えてみましょう。変更は4箇所だけです。[ExperimentHandler](https://www.psychopy.org/api/data.html#psychopy.data.ExperimentHandler)のパラメーター（引数）もたくさんありますが、今回は必要最小限にしています。

```python
from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }
    
trials = data.TrialHandler(conditions, 2) # 各条件を２回繰り返す

win = visual.Window()

exp = data.ExperimentHandler(name='testExp', dataFileName='exp_results') # 変更(追加)1
exp.addLoop(trials) # 変更(追加)2

for thisTrial in trials:
    win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
    win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

    stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
    stim.draw()
    win.flip()

    key_loop = True
    while key_loop:
        ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)
        if len(ptb_keys):        
            trials.addData('RT', ptb_keys[0].rt)
            trials.addData('choice', ptb_keys[0].name)
            key_loop = False
            
    exp.nextEntry()    # 変更(追加)3

#trials.saveAsWideText("results.csv", appendFile=False) 変更4 不要なのでコメントアウト

```

`exp.nextEntry()`を呼び出すタイミングに注意が必要です。この関数は現在の試行のデータの保存が終わったあとに呼び出す必要があります。`trials.next()`と違って、こちらは明記しなくてはなりません。

さて、以上の例からも分かるように、実は`ExperimentHandler`は実験に必須ではありません。`TrialHandler`だけでも実験はできます。`ExperimentHandler`を使うメリットは、実験結果のファイル出力を明記しなくても自動で保存される点です。

なーんだ、と思ったかもしれませんが、メリットはもうひとつあります。ていうか、こちらのメリットのほうが重要。

上の実験プログラムに練習試行を加えたいとします。そして練習の結果と、実験の結果を同一のファイルに保存したいとします。こういうケースでExperimentHandlerは効果を発揮します。

```python
from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }

win = visual.Window()

exp = data.ExperimentHandler(name='testExp', dataFileName='exp_results')

for block in ['practice', 'experiment']:
    if block == 'practice': # 練習試行
        trials = data.TrialHandler(conditions, 1, name='practice') # 練習では各条件を1回ずつ
        instruction = visual.TextStim(win, text="練習を始めます")
    else: # 本試行
        trials = data.TrialHandler(conditions, 2, name='exp') # 実験では各条件を2回ずつ
        instruction = visual.TextStim(win, text="実験を始めます")
        
    exp.addLoop(trials)

    instruction.draw()
    win.flip()
    core.wait(2)

    for thisTrial in trials:
        win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
        win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

        stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
        stim.draw()
        win.flip()

        key_loop = True
        while key_loop:
            ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)
            if len(ptb_keys):        
                trials.addData('RT', ptb_keys[0].rt)
                trials.addData('choice', ptb_keys[0].name)
                key_loop = False

        exp.nextEntry()
```

練習では各条件を1回ずつ、実験（本試行）では各条件を2回ずつ呈示しています。同一ファイルに実験の結果が保存されることを確認してください。なお、Coderのデモにある、実験制御 => experimentHandler.py では階段法を使用していて参考になります。

ExperimentHandlerは、ひとつの実験内で複数のループを使いたいときに役に立ちます。

# ひとつの試行内で複数のキー入力を許容する

最後に、いっぷう変わった`TrialHandler`の使い方を説明しておきます。

いままでの例では、各刺激に対して１回だけ反応するというものでした。これを少し変更して、好きなだけキーを押すような実験を考えてみましょう。なおすべてのキー押しを保存するものとします。エンターキーを押したら、次の試行に移ることとしましょう。

例えば、次のコードで希望通りの動作になります。

```python
from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }
    
trials = data.TrialHandler(conditions, 2) 

win = visual.Window()

exp = data.ExperimentHandler(name='testExp', dataFileName='exp_results')
exp.addLoop(trials)

instruction = visual.TextStim(win, text="複数のキーを押してください。\nエンターキーで次の試行に移ります。")
instruction.draw()
win.flip()
core.wait(2)

for thisTrial in trials:
    win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
    win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

    stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
    stim.draw()
    win.flip()
    

    responses_times=[]
    responses_keys = []
    
    while True:
        ptb_keys = kb.getKeys(waitRelease=False) # すべてのキーを受け付ける
        if len(ptb_keys):   
            if 'return' in ptb_keys:
                break
            responses_times.append(ptb_keys[0].rt)
            responses_keys.append(ptb_keys[0].name)

    trials.addData('RT', responses_times)
    trials.addData('choice', responses_keys)
    exp.nextEntry()
```

実験結果として、例えば以下のようなものが得られます。

<img width="987" alt="スクリーンショット 2020-03-05 16.26.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495823/a18f15b2-a52d-a394-fb90-4177e0957325.png">

悪くはないんですが、反応時間（G列）と反応キー（H列）を見ると、なんか分析しにくいですよね。できれば各行をひとつの反応に対応させたいと思うかもしれません。

ということで、以下のコードです。

```python
from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }

exp = data.ExperimentHandler(name='testExp', dataFileName='exp_results')

trials = data.TrialHandler(conditions, 2) 

# ポイント1. 反応を記録するためのTrialHandlerを作成します。
# ここでは実験条件をあえて指定しません。繰り返し数はキーボードが押される回数を見越して多めに設定します。
responses = data.TrialHandler([], 100) 

# ポイント2. responsesとtrialsの両方をExperimentHandlerに加えます。
# 加える順番はどちらが先でも問題はありませんが、出力される結果が微妙に異なります。
# responsesを先に加えたほうが、thisRepN, thisTrialN, thisIndexなどが適切に保存されます
exp.addLoop(responses) 
exp.addLoop(trials)

win = visual.Window()

instruction = visual.TextStim(win, text="複数のキーを押してください。\nエンターキーで次の試行に移ります。")
instruction.draw()
win.flip()
core.wait(2)

for thisTrial in trials: # trialsについてはここでnext()される
    win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
    win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

    stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
    stim.draw()
    win.flip()

    while True:
        ptb_keys = kb.getKeys(waitRelease=False) # すべてのキーを受け付ける
        if len(ptb_keys):   
            if 'return' in ptb_keys: # returnキーが入力されたら、その試行を終了
                break
            responses.next() # ポイント3. responsesについては、addDataをする前に手動でnext()
            responses.addData('RT', ptb_keys[0].rt)
            responses.addData('choice', ptb_keys[0].name)
            exp.nextEntry() # ポイント4. ExperimentHandlerの参照点をひとつ進める
```

実験条件を管理する`TrialHandler (trials)`とデータを保存する`TrialHandler (responses)`を分けているところがポイントです。`trials`については`for`文によって自動で`next()`されますが、`responses`については手動で`next()`しないといけません。ちょっと分かりにくいですね・・・

結果は次のようになります。

<img width="662" alt="スクリーンショット 2020-03-09 10.36.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495823/94a9d7f0-45a7-c338-8f61-f9cc043f4fd0.png">


各行が、各反応に対応するので、分析がしやすくなりました。`.thisN`を見ると試行番号が分かります。試行によっては複数行にまたがる（３回反応していたら3行）になる点に注意してください。