Chapter 08

【フランカー課題】データの追加・if文

snishiyama
snishiyama
2022.08.08に更新

はじめに

jsPsych ではある程度の情報(提示された刺激や押されたキー,キー押しのタイミングなど)については自動で保存されるようになっています。しかしながら,それら以外にも保存しておきたい情報がある場合も多いでしょう。今回のフランカー課題では,「反応の正誤」や「条件」がそれに該当します。本章では任意の情報を出力データに追加する方法について紹介します[1]

前章に引き続き,本章の内容もタフかもしれません。もしかしたら,本チュートリアルで一番難しい内容かもしれません。それでも,これまで(特に前章)の知識を援用しながらじっくり時間をかければきっと理解できるはずです。

やりたいことを日本語で整理する

const trial = {
  // typeなどは省略
  on_finish: (data) => {
    もし反応キーが正しければ;
    data.isCorrect = 1;
    正しくなければ;
    data.isCorrect = 0;
  };
}

前章で確認したとおり,(htmlKeyboardResponseの場合)反応キーはresponseというプロパティ名で保存されているので,data.responseで反応キーにアクセスできそうです。

反応が正しいかどうかはどう判定すればいいでしょうか?反応キーと正解のキーと一致していると正反応と言えると思います。それでは正解のキーがデフォルトの保存データに含まれているかと言うと,含まれていません。したがって,正解のキーはこちらで用意する必要があります。

今回のフランカー課題では,正解のキーは真ん中のターゲット刺激<>の向きと対応していました。具体的には,<ならf>ならjでした。この判定もon_finishに加える必要があります。

ということで,on_finishの関数内で実装したい処理を日本語で書き起こすと以下のようになります。

const trial = {
  // typeなどは省略
  on_finish: (data) => {
    真ん中の記号が'<'なら
    正解のキーは'f'
    真ん中の記号が'>'なら
    正解のキーは'j'

    もしdata.responseが正解のキーと一致していれば
    data.isCorrect = 1
    そうでなければ
    data.isCorrect = 0
  };
}

これを順を追って実装していきましょう。

if 文

正反応キーは真ん中のターゲット刺激<>の向きと対応していて,<ならf>ならjでした。この「〜なら...」のようにある条件に応じて処理を変えるにはif文を利用します。JavaScript の if 文は以下のように書きます。

if (条件式 A) {
  条件式 B が真(true)の時に実行する処理
} else if (条件式 B) {
  条件式 A が偽(false)で条件式 Btrue の時に実行する処理
} else {
  条件式 A, B の両方とも false の時に実行する処理
}

例えば以下のような if 文を使った処理を考えてみます。

const a = 1;
const b = 0;
let num = 0;

if (a > 0) {
  num = num + 1; // 実行される
}

if (b > 0) {
  num = num + 1; // 実行されない
} else {
  num = num * 1000; // 実行される
}

このコード内では 2 つの if 文のブロックがあります。1つ目はa > 0を条件とする if 文,2つ目はb > 0を条件とする if 文です。変数aには1が代入されているので,a > 0trueになり,num = num + 1が実行されます。一方で,bには0が代入されているので,b > 0falseとなり,else{}で指定されている方のnum = num * 100の処理が実行されます。

なお,コード例のように,else if (){}else {}は必ずしも設ける必要はありません。ifしかなく,その条件式がfalseの場合は何の処理も実行されません。

かつ&&,または||で論理積・論理和を条件式に指定することも可能です。

// 論理積(b > 0 が false なので論理積も false)
if (a > 0 && b > 0) {
  num = num + 1; // 実行されない
}

// 論理和(a > 0が true なので 論理和も true)
if (a > 0 || b > 0) {
  num = num + 1; // 実行される
}

正解キーの判定

それでは正解キーを取得する if 文を考えてみましょう。最初に日本語で書き起こした内容に if 文を持ち込むと以下のようになります。なお,刺激の真ん中の記号の向きが重要といったものの,(詳しくは説明しませんが)今回の状況では,刺激の真ん中を参照するのはかなり面倒です。そこで,刺激全体(例えば,<<<<<)を取り上げています。

if (刺激が'<<<<<'または'>><>>'または'--<--') {
  正反応キー = 'f';
} else {
  正反応キー = 'j';
}

この if 文を実際のコードのon_finishの関数内に書いていきます。

const createTrial = (stim, fontSize) => {
  const trial = {
    type: jsPsychHtmlKeyboardResponse,
    // stimulus などは省略
    on_finish: (data) => {
      if (data.stimulus.includes('<<<<<') || data.stimulus.includes('>><>>') || data.stimulus.includes('--<--')) {
        data.correctKey = 'f';
      } else {
        data.correctKey = 'j';
      }
    },
  };
  return trial;
};

さて,刺激が'<<<<<'または'>><>>'または'--<--' という処理をdata.stimulus.includes()で実装しています。data.stimulusはその試行で提示された刺激を参照しています。前章に例示した保存データにもstimulusというプロパティ名がありました。

.includes()は文字列のメソッドで,引数に渡した文字列を含んでいるかを判定し,含まれていればtrueが返されます。これによって,'<<<<<','>><>>','--<--'のいずれかが含まれている場合はdatacorrectKeyというプロパティ名で'f'が追加されます。そうでない場合はcorrectKeyのプロパティに'j'が割り当てられます。

これで,各試行の正解キーが参照できるようになりました。これを用いて反応の正誤が判定できそうです。

反応の正誤判定

入力されたキーが正反応キーと一致していれば正反応,不一致なら誤反応になります。本チュートリアルでは正反応なら1,誤反応なら0を記録することにします。1, 0 にしておけば,正誤データの合計値が正反応数,平均が正反応率になり集計が少し楽になるからです。

これも判定しているのでif文を使うとよさそうです。ここでは jsPsych.pluginAPI.compareKeys()という関数を用いて一致判定を行ってます。

const createTrial = (stim, fontSize) => {
  const trial = {
    type: jsPsychHtmlKeyboardResponse,
    // stimulus などは省略
    on_finish: (data) => {
      if (data.stimulus.includes('<<<<<') || data.stimulus.includes('>><>>') || data.stimulus.includes('--<--')) {
        data.correctKey = 'f';
      } else {
        data.correctKey = 'j';
      }
      // 正誤判定・保存
      if (jsPsych.pluginAPI.compareKeys(data.response, data.correctKey)) {
        data.isCorrect = 1;
      } else {
        data.isCorrect = 0;
      }
    },
  };
  return trial;
};

compareKeysの結果がtrue,つまり一致しているのであれば,dataisCorrectというプロパティ名で1を追加し,そうでなければ,isCorrect0を入れています。

演習

  • dataconditionというプロパティ名で各試行の条件を保存しよう。条件は提示される刺激に応じて以下のルールで決定されます。
    • 記号がフランカーが同じ方向を向いていたら「一致」
    • ターゲットとフランカーの方向が逆なら「不一致」
    • フランカーが-なら「中性」
コード例
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script src="../jspsych/dist/jspsych.js"></script>
    <script src="../jspsych/dist/plugin-html-keyboard-response.js"></script>
    <link rel="stylesheet" href="../jspsych/dist/jspsych.css" />
  </head>
  <body></body>
  <script>
    const jsPsych = initJsPsych({
      on_finish: () => {
        jsPsych.data.displayData();
      },
    }); // <-- 波括弧を忘れない! 括弧の始まりにも!

    const createTrial = (stim, fontSize) => {
      const trial = {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: `<p style="font-size: ${fontSize}px">${stim}</p>`,
        choices: ['f', 'j'],
        post_trial_gap: 500,
        on_finish: (data) => {
          if (data.stimulus.includes('<<<<<') || data.stimulus.includes('>><>>') || data.stimulus.includes('--<--')) {
            data.correctKey = 'f';
          } else {
            data.correctKey = 'j';
          }
          if (jsPsych.pluginAPI.compareKeys(data.response, data.correctKey)) {
            data.isCorrect = 1;
          } else {
            data.isCorrect = 0;
          }

          // 試行の条件の判定・保存
          if (data.stimulus.includes('<<<<<') || data.stimulus.includes('>>>>>')) {
            data.condition = '一致';
          } else if (data.stimulus.includes('<<><<') || data.stimulus.includes('>><>>')) {
            data.condition = '不一致';
          } else {
            data.condition = '中性';
          }
          // 追加 ここまで
        },
      };
      return trial;
    };

    const fixation = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: '<p style="font-size: 48px">+</p>',
      choices: 'NO_KEYS',
      trial_duration: 500,
    };

    const createBlock = (stim, fontSize) => {
      const trial = createTrial(stim, fontSize);
      const block = {
        timeline: [fixation, trial],
      };
      return block;
    };

    // フォントサイズの指定は実用的ではないですが...
    const block1 = createBlock('<<<<<', 48);
    const block2 = createBlock('>>>>>', 48);
    const block3 = createBlock('<<><<', 32);
    const block4 = createBlock('>><>>', 30);

    const blocks = [block1, block2, block3, block4];
    const blocksRandom = jsPsych.randomization.repeat(blocks, 2);

    jsPsych.run(blocksRandom);
  </script>
</html>

試行間で共通のデータを追加する

ここまでは,試行ごとに内容が変わるデータの保存方法を説明しました。ここでは(ある課題の)全ての試行に共通のデータを保存する方法を紹介します。

実は,試行変数にはdataという設定項目があります。data:{キー: 値}とオブジェクトをしてしておくと,試行変数の定義時点で,保存されるプロパティを追加することができます。例えば今回のフランカー課題では,注視点とフランカー試行の両方がjsPsychHtmlKeyboardResponseで実施されるため,デフォルトの保存内容だけではどちらの結果なのかが見分けづらくなってしまいます。

これを回避するために,試行変数にdataというプロパティを用意して,そこに{task: 'flanker'}というようにオブジェクトを指定します。

const createTrial = (stim, fontSize) => {
  const trial = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `<p style="font-size: ${fontSize}px">${stim}</p>`,
    choices: ['f', 'j'],
    post_trial_gap: 500,
    data: {
      task: 'flanker', // <--
    },
    // on_finish: (data) => { 省略 },
  };
  return trial;
};

const fixation = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<p style="font-size: 48px">+</p>',
  choices: 'NO_KEYS',
  trial_duration: 500,
  data: {
    task: 'fixation', // <--
  },
};

これを実行してみると,最後の画面に,taskというプロパティが追加されているのがわかると思います。

余談

必ずしも皆さんの手元のコードに反映する必要はないですが,知っておくと便利なことがいくつかあるので,余談として紹介します。

余談 1: 正誤判定に if 文はいらない

正誤判定に以下の if 文を書きました。

if (jsPsych.pluginAPI.compareKeys(data.response, data.correctKey)) {
  data.isCorrect = 1;
} else {
  data.isCorrect = 0;
}

実は,この 5 行は次の 1 行で置き換えられます。

data.isCorrect = Number(jsPsych.pluginAPI.compareKeys(data.response, data.correctKey));

compareKeys()Number()という関数で括って,compareKeysの返り値を数値に変換しています。なぜこれでうまくいくのかというと,compareKeysは真偽値(truefalse)を返しますが,真偽値を数値に変換すると,true1に,false0になるからです。

Number(true); // 1
Number(false); // 0

余談 2: 正解キーと条件は事前にdataに指定しておく

正誤判定は参加者の反応次第で変化してしまうため,if 文を書いて試行の終了時に判定をするという対応を取らないといけないのですが,正解キーと条件は刺激に対応しているので,実験の作成時に確定させておくことができます。

そして,その確定情報を先ほど紹介した試行変数のdataプロパティで指定します。dataプロパティの値は,data.rt, data.stimulusと同様に,on_finishの関数内でdata.プロパティ名で参照することができます。そうすると,on_finishに煩雑な if 文を書く必要がなくなります。

ここまで試行変数は関数で生成していたので,関数が取れる引数を増やして対応します。具体的には以下のとおりです。

// 引数で正解キーと条件を受け取れるようにする
const createTrial = (stim, fontSize, corrKey, cond) => {
  const trial = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `<p style="font-size: ${fontSize}px">${stim}</p>`,
    choices: ['f', 'j'],
    post_trial_gap: 500,
    data: {
      task: 'flanker',
      correctKey: corrKey, // <--
      condition: cond, // <--
    },
    on_finish: (data) => {
      // 正解キーの判定のコードはもういらない

      // 正誤判定・保存
      // 正解キーは data.correctKeyで参照できる
      data.isCorrect = Number(jsPsych.pluginAPI.compareKeys(data.response, data.correctKey));

      // 条件判定のコードももういらない
    },
  };
  return trial;
};

createTrialcreateBlock内で呼び出している場合は,createBlockの引数も増やしておくことを忘れないようにしましょう。

// fixationの定義
const createBlock = (stim, fontSize, corrKey, cond) => {
  const trial = createTrial(stim, fontSize, corrKey, cond);
  const block = {
    timeline: [fixation, trial],
  };
  return block;
};

これらの変更の上で,以下のようにブロック変数を生成すれば OK です。

// フォントサイズの指定は実用的ではないですが...
const block1 = createBlock('<<<<<', 48, 'f', '一致');
const block2 = createBlock('>>>>>', 48, 'j', '一致');
const block3 = createBlock('<<><<', 32, 'j', '不一致');
const block4 = createBlock('>><>>', 30, 'f', '不一致');

余談 3: map を使いたい人へ (発展)

余談 2 を読んだ上で,配列のmapメソッドを使ってブロックの配列を作成したい方はぜひ読んでみてください。そうでない方は飛ばしてもらって構いません。

自作関数でオブジェクトを引数に受け取れば map が使える

mapを初めて紹介した際には,createTrialの引数は一つでした。そのため,非常にシンプルにmapを書くことができました。しかし,関数の引数が増えると,mapでどう取り扱ったらいいかわからなくなってしまったかもしれません(フォントサイズの変更のために引数を増やしたときにすでにその状況になっていたかもしれません)。

一つの解決策は,オブジェクトを利用して無理やり関数の引数を一つにすることです。前章でも説明したとおり,オブジェクトは.プロパティ名でオブジェクトの値を参照することができます。引数に与えるオブジェクトに必要なプロパティを詰め込み,関数内で呼び出すようにしておけば,引数の数は 1 つにもかかわらず,複数の引数を指定するのと同じような柔軟性を関数に持たせることができます。具体的には以下のようにします。

// setting は stim, fontSize, corrKey, cond をプロパティとするオブジェクト
const createTrial = (setting) => {
  const trial = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `<p style="font-size: ${setting.fontSize}px">${setting.stim}</p>`,
    choices: ['f', 'j'],
    post_trial_gap: 500,
    data: {
      task: 'flanker',
      correctKey: setting.corrKey,
      condition: setting.cond,
    },
    on_finish: (data) => {
      data.isCorrect = Number(jsPsych.pluginAPI.compareKeys(data.response, data.correctKey));
    },
  };
  return trial;
};

// fixationの定義
// createBlock も 一つだけ引数を受け取るようにする
const createBlock = (setting) => {
  const trial = createTrial(setting);
  const block = {
    timeline: [fixation, trial],
  };
  return block;
};

createTrialを上記のように変更したことに伴い,必要なプロパティを持つオブジェクトの配列を作成します。それをmapを使って関数に流し込めば OK です。

const trialSettings = [
  { stim: '<<<<<', fontSize: 48, corrKey: 'f', cond: '一致' },
  { stim: '>>>>>', fontSize: 48, corrKey: 'j', cond: '一致' },
  { stim: '<<><<', fontSize: 32, corrKey: 'j', cond: '不一致' },
  { stim: '>><>>', fontSize: 30, corrKey: 'f', cond: '不一致' },
];

const blocks = trialSettings.map((s) => createBlock(s));

このように関数を設計しておくと,「余談 2」であったような引数の増加(減少)にも対応が簡単になりますね。オブジェクトを引数にする手法には色々テクニックがあるのですが,本章の趣旨から逸れすぎてしまうので,またの機会にしたいと思います。関連記事のリンクを載せておきます。

オブジェクトと分割代入 | JavaScript Primer
関数の引数と分割代入 | JavaScript Primer

おわりに

本章では,任意のデータを追加で保存する方法について紹介しました。デフォルトで保存されるデータだけで十分ということはあまりないと思います。今回の内容をうまく活用して実験データの分析をスムーズに進められるようにしましょう。

本章を乗り切った皆さんは jsPsych あるいは JavaScript がかなり身についてきたはずです。実験の完成に向けてはまだいくつかやるべきことはありますが,本章までの知識をベースにして難なくこなしていけるでしょう。もう少し,がんばっていきましょう!

脚注
  1. フランカー課題の反応の正誤は,必ずしも実験コード内で処理する必要はありません。正反応のキーが事後的に容易に確認できるので,出力されたデータから事後的に処理できます。とはいえ,実験コードか分析コードのいずれかでどうせ処理するのであれば,実験コード上で予め処理しておくのが楽だと思います。 ↩︎