【第6回】参加者情報の取得

# はじめに

本記事は，「PsychoPy Coderによる心理学実験作成チュートリアル」の第6回の記事です。[第5回](https://qiita.com/snishym/items/f80607cbe462f8a4e1d4)では実験データの保存方法を紹介しました。今回は，参加者情報の取得について紹介します。合わせて，Pythonの辞書型およびif文についても解説します。詳細は右側の見出しリストをご覧ください。

このチュートリアルシリーズの目的・概要等が気になった方はこちらの[全体のまとめ](https://qiita.com/snishym/items/8b52db0d901cf5744463)をご一読ください。

# ダイアログの表示

PsychoPyには実験に必要な情報を入力するためのダイアログボックスを表示するためのツール`gui`が用意されています。本当に便利ですね。
使い方は以下のコードの通りです。とりあえず以下のコードを実行して，適当な情報を入力してみましょう。なお`subj_info = {...}`で使用しているカッコは波カッコ`{}`です。この`{}`を使って生成できる辞書型データについては次節で説明します。

```python:show_gui.py
from psychopy import gui # guiモジュールを使用する

subj_info = {"参加者ID":'', "年齢":'', "性別": ["男性", "女性"]} # ダイアログに表示する項目と初期値を設定する {"項目名":初期値}

info_dlg = gui.DlgFromDict(subj_info) # subj_infoをベースにinfo_dlgという名前のダイアログを表示する

print(subj_info)
```

出力エリアに`subj_info`が表示されているので確認してみてください。OKを押した場合は，ダイアログボックスで入力した値が表示されているはずです。例えば，`{"参加者ID":'1', "年齢":'20', "性別": "男性"}`というような出力が得られます。

まず，ダイアログボックスで表示する項目とその初期値を辞書型データで作ります。`{"項目1":初期値, "項目2":初期値, "項目3":初期値}`というように，項目とその初期値を並べ，`{}`で括ります。今回は`subj_info`という名前にしています。

次に，その辞書型データを`gui.DlgFromDict()`という関数[^1]に与えます。名前の通り，辞書（**Dict**onary）からダイアログボックス（**D**ia **l**o **g**）が生成・表示されます。

[^1]: 正しくはDlgFromDictクラスのインスタンスを生成しています。わからなくても大丈夫です。

注目してもらいたいのは，初期値の与え方によって，ダイアログボックスの表示が変わるという点です。具体的にいうと，「参加者ID」や「年齢」のように文字列（この場合は空文字`''`）を指定していると，その項目の入力欄では自由に値を入力することができます。一方で，「性別」のようにリストを指定すると，そのリストの要素が選択肢となる選択型の項目になります[^2]。なお，必ず初期値を与えるようにしてください。`{"参加者ID":,}`とコロン`:`の後に何も入力しないとエラーになります。

[^2]: 初期値を数字で指定することも可能です。その場合，項目のダイアログボックスは，文字と同様に自由に値を入力できる項目となりますが，欄に数字以外を入力してOKすると，エラーが発生するので注意が必要です。

以上で，PsychoPyで参加者ID等の実験情報を取得するためのコードの解説は終了です。しかし，これらのデータは実験データに保存したいですし，参加者のデータを区別するためにファイル名を参加者ID（+α）にしたいところです。

そのためにはこの辞書型データから値を取り出す必要があります。次節ではこのことについて説明します。

# 辞書型データ

辞書型（dictionary）はリストの親戚で，複数の要素を保持することができるデータ型です。ただし，リストと異なり，ただ要素をならべるのではなく，それぞれの要素はあるキーと対応させます。

```python:example_dict.py
{"参加者ID":'1', "年齢":'20', "性別":"男性"}
```

`{"キー":値（要素）}`となります。例えば，`'1'`という文字列[^3]は`'参加者ID'`というキーと対応しています。逆方向も言い方の方が分かりやすいと思います。つまり，`'参加者ID'`というキーには`'1'`という文字列が紐付いています。まさに，ある単語とそれに対応する意味が並べられた「辞書」なわけです。なお，`"性別":["男性","女性"]`としたように，値にはリストを持ってくることも可能です。

[^3]: `''`でくくっているので'1'は文字列です。

辞書ではキーを用いることで値を取り出すことができます。実際の辞書でも単語からその意味を見つけることができるのと同じです。

```python:get_dict_value.py
subj_info = {"参加者ID":'1', "年齢":'20', "性別": ["男性", "女性"]}
print(subj_info["参加者ID"]) # '1'
print(subj_info["年齢"]) # '20'
print(subj_info["性別"]) # ["男性", "女性"]
```

ただし，実際の辞書には順番があります（50音順，アルファベット順）が，Pythonの辞書には順番がないので，リストのように，要素の位置を指定して値を取り出すことはできません。

これで，辞書から値を取り出せるようになりました。つまり，実験データのファイルの名前を参加者IDに応じて変更することができますし，参加者情報を実験データに保存することができるようになりました。

これらのことをこれまでのサイモン課題のコードに組み込んで今回は終了です。しかし，今のままではダイアログボックスでキャンセルを押したとしても，実験は中止されません。さらに`subj_info`は初期値のままなので保存されるデータもおかしくなってしまいます。そこで，ダイアログボックスでキャンセルが押されたら実験を中断する方法を紹介します。そのあとに，ダイアログボックスのコードをこれまでのコードに組み込みます。

# if文

PsychoPyのダイアログでOKがクリックされたかどうかは`info_dlg.OK`で知ることができます（詳細は次節）。問題は，OKが押されたかそうでないか（=キャンセルが押されたか）によって処理を変える必要があるということです。これは**条件分岐**と呼ばれます。ダイアログボックスの話であれば，OKが押されたかどうかという**条件**によって実験を行うか実験を終了するかに処理が**分岐**するということです。

条件分岐は`if`文を使います。

```python:if_else.py
num_list = [1,2,3,4,5,6]

for num in num_list:
    if num % 2 == 0: # もし，numを2で割った余りが0なら
        print("偶数です")
    else: # そうでないなら
        print("奇数です")
```

上記のコードを実行すると，「奇数です」「偶数です」という文字列が交互に出力されます。

まず，`if 条件式:`と宣言します。この条件式が真`True`ならifブロックの処理を実行します。ブロックはfor文の時と同様，インデント（半角スペース4つ分下げ）で作ることができます。上記のコードの場合，`num % 2 == 0`が評価される条件式になります。`%`は左の項を右の項で割ったときの余りを返す演算子で，`==`は左の項と右の項が同値かどうかを判定する演算子です。イコール2つだということに注意してください。等しいことを書く際，日常的には=を使うので，慣れるまで間違えやすいです。したがって，今回の条件式は，「2で割った余りが0である」ということになります。偶数であればこの条件式はTrueとなるので，ブロック内の`print("偶数です")`が実行されます。

次に，`else:`と書くことができます。これは，条件式が偽`False`だったすべての場合に実行されます。上記のコードでは，`num`が`1,3,5`のいずれかの場合は，2で割った余りが1で0ではないため，条件式が`False`となり，`print("奇数です")`が実行されます。なお，`else:`は必ずしも書く必要はありません（次節参照）。

なお，上記の例では2つの条件分岐でしたが，3つ以上の条件分岐が必要な場合は，`elif 条件式:`を使います。else ifの略だと思います。

```python:if_elif_else.py
num_list = [1,2,3,4,5,6]

for num in num_list:
    if num % 4 == 0: # もし，numを4で割った余りが0なら
        print("4の倍数です")
    elif num % 3 == 0: # もし，numを3で割った余りが0なら
        print("3の倍数です")
    elif num % 2 == 0: # もし，numを2で割った余りが0なら
        print("2の倍数です?")
    else: # そうでないなら
        print("1,5です")
```

この例のようにif, elif, elseを並べると，上から順に条件式が評価されます。そして，どこかの条件式が`True`になると，それよりも下の条件式は評価されません。そのため，`num`が4の時は4および2の倍数ですが，「4の倍数です」とだけ表示され，「2の倍数です」とは表示されません。さらに，`num`が6の時も，3および2の倍数ですが，「3の倍数です」だけが表示されます。今回このような3つ以上の分岐は扱いませんが，ご自身で使用される場合には**条件式の並べる順番には注意してください**。

# OKでなければ実験を終了する

`info_dlg.OK`には`True`か`False`が割り当てられています。OKボタンがクリックされていれば，`True`ですし，そうでないなら`False`になります。これをif文と組み合わせます。そして，実験を終了させるには，`core.quit()`を使います。こうして，「OKでなければ実験を終了させる」ことができます。

```python:allow_cancel.py
from psychopy import gui, core, visual

subj_info = {"参加者ID":''}

info_dlg = gui.DlgFromDict(subj_info)

if not info_dlg.OK: # もしOKボタンが押されなかったら
   core.quit() # 実験を終了させる

print("実験へ")
win = visual.Window()
core.wait(1)
win.close()
```

もし，OKを押せば「実験へ」という出力がある上でウィンドウが1秒だけ表示されます。一方で，キャンセルを押せば，ウィンドウは表示されずにプログラムは終了します。

このコードについて2点補足します。まず，ifの条件式で`not`という演算子を使いました。これは，右の項が`False`なら`True`を，`True`なら`False`を返す演算子です。今回の場合，`info_dlg.OK`が`False`であれば条件式は`True`になり，core.quit()が実行されます。

次に，今回の条件分岐では`else:`を使用して，そのブロックにウィンドウを表示させるコードを書くというようなことはしませんでした。これは，ifの条件式が`True`だった場合にはプログラムが終了して後続のコードは処理されないし，`False`だった場合はelseがなくてもそのまま後続の処理に移るからです。

以上でようやく参加者情報取得のために必要な知識（+α）の説明が終わりました。これをサイモン課題のプログラムに組み込みます。

# サイモン課題に取り入れる

それでは，サイモン課題の前に参加者情報を取得し，それをデータファイル名，データの中身に反映させます。ついでに，課題中にescapeキー（左上にある）を押すと実験を終了できるようにしてしまいましょう。整理すると，追加・変更点は以下の4点です。

1. ダイアログボックスを用いて参加者情報を取得する
2. データファイル名に参加者IDが反映されるようにする
3. データに参加者情報を保存する
4. 課題中にescapeキーを押したら実験を中断する

下記のコードを見る前にご自身でコードの改変に挑戦してみてください。

<details><summary>コード例</summary><div>

```python:simon_with_subjInfo.py
from psychopy import visual, core, event, gui # 追加し忘れない
import random
import pathlib

# ダイアログボックスの表示
subj_info = {"参加者ID":'', "年齢":'', "性別": ["男性", "女性"]}

info_dlg = gui.DlgFromDict(subj_info)

if not info_dlg.OK:
   core.quit()

# それぞれの情報をプログラム内で短い名前で使えるようにする
subjID = subj_info["参加者ID"]
age = subj_info["年齢"]
sex = subj_info["性別"]

win = visual.Window()

letter_stim = visual.TextStim(win) 
letter_pos = [
    ["L",-0.3],
    ["R",0.3],
    ["L",0.3],
    ["R",-0.3]
]
random.shuffle(letter_pos)

# ファイルを開く
current_folder = pathlib.Path(__file__).parent
new_filename = "{}_simon.csv".format(subjID) # 参加者IDをファイル名に入れる
new_filepath = current_folder/"data"/new_filename

datafile = open(new_filepath, mode = 'a')
datafile.write('参加者ID,年齢,性別,trialID,刺激,位置,反応キー,反応時間\n') # 列名を増やしておく

trialID = 0

stopwatch = core.Clock()

for letter, x_axis in letter_pos:
    letter_stim.setText(letter)
    letter_stim.setPos([x_axis, 0])
    letter_stim.draw()
    win.flip()

    stopwatch.reset()
    resp = event.waitKeys(keyList = ['left', 'right', 'escape'], timeStamped = stopwatch)
    
    # データの保存
    key, rt = resp[0]
    if key == 'escape': # 入力キーがescapeなら実験を終了する
        datafile.close() # ファイルを閉じるのを忘れない
        core.quit()
    
    data = '{},{},{},{},{},{},{},{}\n'.format(subjID, age, sex, trialID, letter, x_axis, key, rt) # subjID, age, sexを追加
    datafile.write(data)
    
    trialID = trialID + 1
    
datafile.close()
win.close()
```

無事，コードを改変することができたでしょうか？もし私のコードと違う点があったとしても，想定通り課題が走っていれば問題ありません。

上記のコードについて3点補足です。1点目は取得した参加者情報にそれぞれ`subjID`，`age`，`sex`と名前つけていることについてです。必要なタイミングで毎回`subj_info["参加者ID"]`などと書くのはコードの読みやすさを低下させていると思うので，この処理を書きました。なくても大丈夫ですし，無い方がいいという人もいるかもしれません。

2点目は，課題中の実験の中止についてです。ここでもelseは使っていません。理由は前節に書いた通りです。

最後3点目はデータの保存に関してです。保存する内容を増やしているので，それにあわせて**ファイルの列名，`{}`の数を揃えるようにしましょう**。

</div></details>

# おわりに

今回はダイアログボックスによる参加者情報の取得について解説しました。それに合わせてPythonの辞書型，if文の解説も行いもました。特に，if文を用いることで任意のタイミングで実験を中止できるようにもなりました。これでサイモン課題はひとまず完成です。

これでサイモン課題を用いた実験を実施することができるようになりました。もう十分ではありますが，どうせなら課題の教示からプログラム上で行いたいところです。また，実際の実験では課題の練習も必要です。あと，注視点やITIも必要です。次回はこれらをテーマに解説を進めていきます。

[【第7回】教示と練習課題](https://qiita.com/snishym/items/3c4b801c25bb98dd0d4e)