【第5回】データの保存

# はじめに

本記事は，「PsychoPy Coderによる心理学実験作成チュートリアル」の第5回の記事です。[第4回](https://qiita.com/snishym/items/0b10e714363656094181)ではキー反応を取得する方法を紹介しました。今回は，反応データを含めた実験データの保存を行います。詳細は右側の見出しリストをご覧ください。

書くコードは少ないですが説明・注意点が多いので，気合いを入れる必要があるかもしれません。

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

# csvファイル

データを保存する際のファイル形式は様々ありますが，今回はcsvという形式でのデータの保存方法を紹介します。csvというのは**c**omma **s**eparated **v**alue(s)の略で，値（各データ）がカンマ`,`区切りで並べられて書かれているファイルです。 

対応しているアプリケーションも多く，エクセルのような表計算ソフトで開くことはもちろん可能ですし，PythonやRといったプログラミング言語でデータ分析をする際にも扱いやすいファイル形式です。

# 新規フォルダの作成

これから実験データを保存していくためのフォルダを作成しましょう。psychopy練習用のフォルダに`data`という名前のフォルダを作成してください[^1]。

[^1]: 異なるフォルダ名でも構いませんが，以後の説明では`data`という名前を前提にコードを書いていることに注意するとともに，コードを書く際には`data`の部分をご自身が決めたファイル名に変更するようにしてください。

# パス

早速csvファイルを作っていく，，，前にファイルを扱うために重要な話をします。ファイルをプログラミング言語で扱う際に知っておかなければならないのが，**パス**（path）です。パスとはあるフォルダやファイルのPC内における住所です。プログラミング言語（というかPCのシステム）はこのパスで特定のフォルダやファイルを認識しています。例えば，これまで作成したPsychoPyのプログラムを実行するとCoderの「出力」には以下のような表示があるはずです。

```
C:\Users\あなたのPCアカウント名\Desktop\psychopy練習用のフォルダ名\ファイル名.py
```

これは，実行しているファイルのパスです[^2]。`ファイル名.py`というファイルは第1回で作成した`デスクトップ`上のpsychopy練習用のフォルダにあるはずです。これは表示されたパスの最後の3つの要素に一致します。このことから，このパスというものが，ファイルのPCにおける住所であることを実感できると思います。例えば，私の場合は第1回に`psychopy-practice`という名前でフォルダを作成したので，`Desktop\psychopy-practice\ファイル名.py`となっています

[^2]: これはpsychopy練習用フォルダを第1回で指定したようにデスクトップに作成している場合の表示です。

本シリーズでは，先ほど作ってもらった`data`というフォルダに，実験データが書き込まれたcsvファイルを保存していくので，コードを書く際にはcsvファイルのパスが以下のようになるようにしていきます。

```
C:\Users\あなたのPCアカウント名\Desktop\psychopy練習用のフォルダ名\data\ファイル名.csv
```

もしPsychoPyのプログラムを実行した時の表示が`...\Desktop\psychopy練習用のフォルダ名\ファイル名.py`となっていない場合は，ファイルがpsychopy練習用のフォルダ内に保存されていません。その場合，以降の説明通りにコードを書いて実行するとエラーになります。そのため，以降のコードはpsychopy練習用フォルダに保存するようにしましょう。

# 新規ファイルの作成

ということで，新しいcsvファイルを作成してみましょう。

```python:make_new_file.py
import pathlib # pathlib モジュールを使用

current_folder = pathlib.Path(__file__).parent # 実行しているプログラムが入っているフォルダを特定
new_filename = "first_datafile.csv" # 新しいファイル名をnew_filenameという名前でプログラム内で使用
new_filepath = current_folder/"data"/new_filename # 新しいファイルのパスを指定

datafile = open(new_filepath, mode = 'a') # 新しいファイルを作成
datafile.close() # ファイルを閉じる
```

このプログラムを実行すると，練習用フォルダ下の`data`内に`first_datafile.csv`というファイルが作成されたはずです。各行の処理は以下の通りです。

1. `pathlib`というモジュールの使用を宣言しています。このモジュールはPython3.4から使用できるようになったファイル操作に便利なモジュールです[^3]。
2. `pathlib`の`Path(__file__)`で実行している`.py`ファイルのパスを取得し[^4]，`.parent`でそのファイルが入っているフォルダ，つまりPsychoPy練習用フォルダ，のパスを取得しています。
3. 作成するファイルの名前をnew_filenameという名前でプログラム内で使用できるようにしています。
4. 新しいファイルがPsychoPy練習用フォルダ内の`data`フォルダに作成されるように，パスを`PsychoPy練習用フォルダ\data\ファイル名`と指定しています。
5. 指定したパスのファイルを`open()`で開いています。ここで指定しているモード`mode = 'a'`は，新しいファイル（すでに存在している場合には既存ファイルの続き）に書き込んでいくというモードです。詳細はググってください。
6. 開いたファイルを閉じる。開いたら必ず閉じましょう。

[^3]: 最新のPsychoPyをstandaloneでインストールした場合はPython3.6が一緒にインストールされるので，安心して使えます。

[^4]: jupyter notebook/lab では`__file__`使用できないそうです。本シリーズではCoderを用いているので気にする必要はありませんが，念のため。

次に，作成したファイルにカンマ区切りで適当な値を書いてみましょう。`.write(書きたいこと)`で開いたファイルに文字列を書くことができます。実験データをカンマ区切りで保存に向けて，適当な文字列をカンマ区切りで入力してみましょう。

```python:write_something.py
import pathlib

current_folder = pathlib.Path(__file__).parent
new_filename = "first_datafile.csv"
new_filepath = current_folder/"data"/new_filename

datafile = open(new_filepath, mode = 'a')
datafile.write('a,b\nc,d\n') # ファイルに文字列を書く。\nは改行記号。Windowsでは"¥"キーで"\"を入力できる
datafile.close()
```

特にエラーなくプログラムが実行されたら，`first_datafile.csv`を開いてみてください。エクセルの入っているWindows PCならエクセルでファイルが開かれると思います。

| a | b |
| c | d |

と値が入っているはずです。

まず，`.write()`で入力した際にはただカンマで文字を区切っただけですが，エクセルで表示するとちょうど`,`の位置で値（aとb，cとd）が別々のセルに入力されるようになっています。カンマ一つでセルを表現できるのは簡単ですね。`a1,,b1`のように変化させるとどうなるでしょうか？ぜひ試してみてください。

次に，cとdが2行目に表示されています。これは改行記号`\n`の作用によるものです。`\n`はそれ以降の文字列を次の行に表示させます。もしなかったら，2つあったらどうなるのか，ぜひ`.write()`の中身を変更して試してみてください。

<!-- ### 日本語データの保存・エンコーディングの罠

上記コードを`.write('あ,い,う,え,お\n')`に変更して実行して，結果のファイルをエクセルで開いてみましょう。

文字化けはPythonのデフォルトとエクセルのデフォルトで用いるエンコーディングが異なるために生じます。Pythonではデフォルトで`utf-8`というエンコーディング方式が用いられますが，Windowsエクセルでは`Shift-JIS`という方式が使用されています[^2]。

[^4]: この問題はWindowsとMacの間でも生じます。Macでも`utf-8`が使われています。このため，これらのPC(OS)間でファイルの交換をすると文字化けが生じることがあります。

ファイルを`Shift-JIS`でコーディングすれば，日本語の文字化けを防ぐことができます[^5]。エンコーディングの指定は`.open`

[^5]: csvファイルをエクセルで開く際の方法を少し工夫すれば，`Shift-JIS`でコーディングされていないファイルでも文字化けせずに開くことは可能です。

```python

```

そもそも極力，日本語（全角文字）を使わないというのも有効な手段です。-->

# forループごとに異なった文字列を生成する

これまで作成したサイモン課題では，for文で課題ブロックを作成し，forループの各繰り返しが，課題の各試行に対応していました。したがって，for文のブロック内に`.write()`を組み込めば，各試行の実験データを保存することができそうです。しかし，上記の`.write('a,b\nc,d\n')`をそのまま組み込んでも，全く同じ`'a,b\nc,d\n'`が毎試行csvファイルに書き込まれるだけです。ループごとに異なった文字列を生成する必要があります。

ループごとに異なった文字列を生成するには`.format()`というメソッドが便利です。以下のコードを実行してみてください。

```python:make_diff_text.py
moji_list = [
    ['あ','a'],
    ['い','i'],
    ['う','u']
]

for jp, en in moji_list: # moji_listから出したリストの2つの要素にそれぞれ jp, enと名前をつけてブロック内で使用する
    print('{},{}'.format(jp, en)) # jp, enにそれぞれ紐づけられた値を順番に{}に入れる
```

見事，ループごとに異なった文字列が生成されたはずです。第3回で紹介したリストのリストに対するfor文の使い方を利用し，ひらがなとアルファベットをそれぞれjp, enという名前でブロック内で使用しています。for文ですので，jp, enに紐づけられた値はループごとに異なります。

ここで重要なのは，'{},{}'.format(jp, en)とすることで，jp, enに紐づけられた値が順番に応じて`'{},{}'`の`{}`の部分に代入されているということです。これによって，ループごとに異なる文字列を生成することができます。代入したい要素が増えたら`{}`の数を増やすだけで対応することができます。

# リストの要素の取り出し方

ということで，これまでの内容を実験のプログラムに早速反映していきたいところですが，最後にもう一つだけ，説明しなければならないことがあります。それは，`event.waitKeys()`で得られるキーや反応時間の保存のしかたです。というのも，`event.waitKeys()`はリスト（反応キーだけを取得する場合）やリストのリスト（反応時間も取得する場合）を渡してくれますが，保存する際には，反応キー（や反応時間）をリスト（のリスト）から取り出さなければならないからです。

といってもその方法は難しくはありません。Pythonの場合，リストの要素には最初から順に0番目，1番目，2番目，，，とリスト内での位置番号が割り振られており，取り出したい値の位置を指定することでその値を取り出すことができます。**日常的な数え方と異なり，0番目から始まることに注意してください**。

```python:get_list_value.py
letter_list = ['a','b','c']
print(letter_list[0]) # a
print(letter_list[1]) # b
print(letter_list[2]) # c
```

リストのリストの場合も位置の指定を繰り返すことで値を取り出すことができます。

```python:get_list_list_value.py
moji_list = [
    ['あ','a'],
    ['い','i'],
    ['う','u']
]
print(moji_list[0]) # ['あ','a']
print(moji_list[0][0]) # 'あ'（['あ','a']の0番目）
print(moji_list[0][1]) # 'a'（['あ','a']の1番目）
```

リストのリストの場合，1つ目の位置の指定で「リストのリスト」内にあるリストを取得することができます。続けてもう一度位置を指定することで1番目に指定した位置のリストからある要素を取り出すことができます。

サイモン課題では反応時間も取得するので，`waitKeys()`は`[[key, RT]]`というリストのリストを渡してくれます。したがって，`[0][0]`で反応キーを`[0][1]`で反応時間を取り出すことができるということになります。

以上でデータの保存のために必要な前提知識の解説は終了です。

# サイモン課題のデータの保存

それでは，サイモン課題のデータを保存できるようにしていきましょう。コード例を見る前にご自身で作成してみましょう。前回のコードから増やすのは以下の3点です。

- ファイルを開く（新規作成する）
- 試行ごとに，trialID，提示刺激，位置，反応キー，反応時間をファイルに書き込む
- 課題が終わったらファイルを閉じる

trialIDは試行ごとに1ずつ増えるようにしてください。また，前回の最後に紹介した刺激のランダマイズをしていなければランダマイズにも挑戦してください。

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

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

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 = "sample_data_simon.csv"
new_filepath = current_folder/"data"/new_filename

datafile = open(new_filepath, mode = 'a')
datafile.write('trialID,刺激,位置,反応キー,反応時間\n') # 列名をつけておく

trialID = 0 # trialIDの初期化

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'], timeStamped = stopwatch)
    
    # データの保存
    key = resp[0][0] # 反応キーの取得
    rt = resp[0][1] # 反応時間の取得
    # key, rt = resp[0] # 実は上の2行の代わりにこれでもいける。
    data = '{},{},{},{},{}\n'.format(trialID, letter, x_axis, key, rt) # カンマ区切りの文字列にする
    datafile.write(data) # ファイルに書き込む
    
    trialID = trialID + 1 # ループごとにtrialIDを1増やす
    
datafile.close() # ファイルを閉じる
win.close()
```

うまく保存のためのコードを組み込むことができたでしょうか？上記のコードでは見やすさのために，`.format`の行を`.write`の行と分けて書いています。**列名や試行ごとのデータ文字列を生成する際，改行`\n`を最後に入れ忘れないようにしましょう**。

</div></details>

# おわりに

今回は実験データの保存方法について紹介しました。これまで主に説明してきたPsychoPyの使い方から離れて，パスの話など全く新しい話ばかりだったので大変だったかもしれません。この回が最後の山場だったかもしれません。

これを乗り切った今，このプログラムに必要な要素は，「参加者情報の取得」を残すところとなりました。この勢いで頑張りましょう！

[【第6回】参加者情報の取得](https://qiita.com/snishym/items/5e01641150581c632bea)