はじめに
本記事は,「PsychoPy Coderによる心理学実験作成チュートリアル」の第6回の記事です。第5回では実験データの保存方法を紹介しました。今回は,参加者情報の取得について紹介します。合わせて,Pythonの辞書型およびif文についても解説します。詳細は右側の見出しリストをご覧ください。
このチュートリアルシリーズの目的・概要等が気になった方はこちらの全体のまとめをご一読ください。
ダイアログの表示
PsychoPyには実験に必要な情報を入力するためのダイアログボックスを表示するためのツールgui
が用意されています。本当に便利ですね。
使い方は以下のコードの通りです。とりあえず以下のコードを実行して,適当な情報を入力してみましょう。なおsubj_info = {...}
で使用しているカッコは波カッコ{}
です。この{}
を使って生成できる辞書型データについては次節で説明します。
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に与えます。名前の通り,辞書(Dictonary)からダイアログボックス(Dia lo g)が生成・表示されます。
注目してもらいたいのは,初期値の与え方によって,ダイアログボックスの表示が変わるという点です。具体的にいうと,「参加者ID」や「年齢」のように文字列(この場合は空文字''
)を指定していると,その項目の入力欄では自由に値を入力することができます。一方で,「性別」のようにリストを指定すると,そのリストの要素が選択肢となる選択型の項目になります2。なお,必ず初期値を与えるようにしてください。{"参加者ID":,}
とコロン:
の後に何も入力しないとエラーになります。
以上で,PsychoPyで参加者ID等の実験情報を取得するためのコードの解説は終了です。しかし,これらのデータは実験データに保存したいですし,参加者のデータを区別するためにファイル名を参加者ID(+α)にしたいところです。
そのためにはこの辞書型データから値を取り出す必要があります。次節ではこのことについて説明します。
辞書型データ
辞書型(dictionary)はリストの親戚で,複数の要素を保持することができるデータ型です。ただし,リストと異なり,ただ要素をならべるのではなく,それぞれの要素はあるキーと対応させます。
{"参加者ID":'1', "年齢":'20', "性別":"男性"}
{"キー":値(要素)}
となります。例えば,'1'
という文字列3は'参加者ID'
というキーと対応しています。逆方向も言い方の方が分かりやすいと思います。つまり,'参加者ID'
というキーには'1'
という文字列が紐付いています。まさに,ある単語とそれに対応する意味が並べられた「辞書」なわけです。なお,"性別":["男性","女性"]
としたように,値にはリストを持ってくることも可能です。
辞書ではキーを用いることで値を取り出すことができます。実際の辞書でも単語からその意味を見つけることができるのと同じです。
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
文を使います。
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の略だと思います。
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でなければ実験を終了させる」ことができます。
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点です。
- ダイアログボックスを用いて参加者情報を取得する
- データファイル名に参加者IDが反映されるようにする
- データに参加者情報を保存する
- 課題中にescapeキーを押したら実験を中断する
下記のコードを見る前にご自身でコードの改変に挑戦してみてください。
コード例
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点目はデータの保存に関してです。保存する内容を増やしているので,それにあわせてファイルの列名,{}
の数を揃えるようにしましょう。
おわりに
今回はダイアログボックスによる参加者情報の取得について解説しました。それに合わせてPythonの辞書型,if文の解説も行いもました。特に,if文を用いることで任意のタイミングで実験を中止できるようにもなりました。これでサイモン課題はひとまず完成です。
これでサイモン課題を用いた実験を実施することができるようになりました。もう十分ではありますが,どうせなら課題の教示からプログラム上で行いたいところです。また,実際の実験では課題の練習も必要です。あと,注視点やITIも必要です。次回はこれらをテーマに解説を進めていきます。