コンテンツへスキップ
チュートリアル17分で読めます

jsPsych を使って自分のサーバーで Stroop 課題を実行する

Potato で jsPsych の Stroop 課題を大学のサーバーにホストし、色覚のスクリーンアウトと Prolific の完了コードを追加して、R で反応時間を分析します。

Potato Team

jsPsych で課題を書いている認知心理学者は、このチュートリアルに従って、Potato 2.13.1 がページを配信してデータを保存する形で、研究室の管理下にあるサーバー上で Stroop 課題を実行できます。この調査には、同意ページ、赤、緑、青の区別が難しい人のセッションを終了させる色覚スクリーナー、参加者の半数に試行ごとのフィードバックを与える jsPsych のタイムライン、課題についての二つの評定、そしてデブリーフィングがあります。参加者は Prolific から来て、完了コードを受け取って終了し、試行のデータは R に取り込まれます。このページの設定、コマンド、エクスポートの出力は、ヘッドレスブラウザ上のシミュレートされた参加者による、Potato 2.12.1 の実行から得たものです。

jsPsych の調査のホスティングの選択肢

jsPsych は参加者のブラウザ内で動作し、それ自体は何もホストしません。jsPsych の調査には、ページを配信してデータを受け取るサーバーが必要です。PsychoPy の料金ページ(2026 年 10 月 21 日更新)は、Pavlovia のクレジットを参加者一人あたり 0.26 ポンド、大学ライセンスを年額 2,000 ポンドと記載しています。Gorilla は調査全体をホストし、その料金 FAQ は、学術向けの従量課金トークンを参加者一人あたり 1.09 ポンド、最低購入数 25 トークンと記載しています。どちらのサービスも、データを自社のサーバーに保管します。PsychoPy フォーラムのスレッド「Running Experiments on Own Server」で、PsychoPy のスタッフは 2021 年に、Pavlovia のローカルインストールがサイトライセンスを持つ機関に提供されていると書きました。

JATOS は Apache 2.0 ライセンスの無料のオープンソースで、自分のサーバーにインストールでき、jsPsych、lab.js、OpenSesame の調査を実行します。調査が jsPsych のタイムラインだけである場合は、JATOS が定番の選択肢です。Potato は、タイムラインが、同意とスクリーニングのページ、条件へのバランスのとれた割り当て、評定や質問紙、Prolific への復帰も必要とするセッションの一部である場合に適しています。Potato は、これらの各部分を、JavaScript ではなく YAML の設定で構成します。行動実験ツールの比較は、これらのツールを並べて比較しています。

調査

セッションは次の順に進みます。

  1. 同意。 ページが調査を説明し、同意を求めます。
  2. スクリーナー。 赤、緑、青の区別が難しいと報告した参加者はスクリーンアウトされ、スクリーンアウトコードとともに Prolific に戻されます。
  3. Stroop 課題。 jsPsych のタイムラインが、各色名を各インク色で二回ずつ、合計 18 試行を提示します。immediate 条件の参加者は、各反応の後に「Correct」「Incorrect」「Too slow」を目にし、none 条件の参加者はフィードバックを目にしません。
  4. 課題の評定。 二つの 5 段階評定が、課題がどれだけ難しかったか、参加者がどれだけうまくできたかを尋ねます。
  5. デブリーフィング。 ページが Stroop 効果とフィードバックの操作を説明し、コメント欄を提供します。

フィードバックは被験者間要因で、各参加者はセッション全体で一つの条件を保ちます。Potato は到着時に条件を割り当て、その条件をタイムラインに渡します。

Potato のインストールとプロジェクトの構成

このチュートリアルで確認したリリースをインストールします。

bash
pip install potato-annotation==2.13.1

プロジェクトフォルダーには、設定、三つの調査ページ、タイムライン、一つのデータファイルがあります。

text
stroop-study/
├── config.yaml
├── data/task.jsonl
├── surveys/consent.json
├── surveys/screener.json
├── surveys/debrief.json
└── tasks/stroop.js

設定

下の設定は完全なもので、2.12.1 と 2.13.1 の両方で python -m potato.validate_cli config.yaml --strict を通ります。バリデーターは、tasks/stroop.js が存在することも確認します。

yaml
annotation_task_name: Stroop feedback study
task_dir: .
output_annotation_dir: annotation_output/
data_files:
  - data/task.jsonl
item_properties:
  id_key: id
  text_key: text
 
login:
  type: url_direct
  url_argument: PROLIFIC_PID
user_config:
  allow_all_users: true
  users: []
 
crowdsourcing:
  provider: prolific
  prolific:
    completion:
      code: C1STROOP
      screened_out_code: C1SCREEN
 
conditions:
  factors:
    feedback: [none, immediate]
  assignment: balanced
  balance_on: completed
 
phases:
  order: [consent, screener, stroop, annotation, debrief]
  consent:
    type: consent
    file: surveys/consent.json
  screener:
    type: prestudy
    file: surveys/screener.json
    exit_when:
      show_when:
        - {schema: color_vision, operator: equals, value: "Yes"}
  stroop:
    type: jspsych
    timeline: tasks/stroop.js
    plugins: [instructions, html-keyboard-response]
    save: incremental
  annotation:
    type: annotation
  debrief:
    type: poststudy
    file: surveys/debrief.json
 
annotation_schemes:
  - annotation_type: likert
    name: difficulty
    description: The color-naming task was difficult.
    size: 5
    min_label: Strongly disagree
    max_label: Strongly agree
    label_requirement:
      required: true
  - annotation_type: likert
    name: performance
    description: I did well on the color-naming task.
    size: 5
    min_label: Strongly disagree
    max_label: Strongly agree
    label_requirement:
      required: true
 
export_include_phase_data: true

login.type: url_direct は、ログインページなしで、調査リンクの PROLIFIC_PID で各参加者をログインさせます。balance_on: completed は、スクリーンアウトされずに終了した参加者に基づいて、二つのフィードバック条件のバランスをとります。途中でやめた参加者やスクリーナーで不合格になった参加者は、どちらの条件にも数えられません。Potato 2.13.1 からは、作業中の参加者が、最後の操作から in_progress_minutes(デフォルトは 60)の間、自分の条件に数えられるため、同時に始めた人々が両方の条件に振り分けられます。save: incremental は、参加者が各試行を終えるたびに保存します。デフォルトの save: on_finish では、途中でタブを閉じると、試行が何も残りません。export_include_phase_data: true は、同意、スクリーナー、デブリーフィングの回答を CSV エクスポートに含めます。

すべての Potato の調査には、少なくとも一つの項目を持つアノテーションフェーズがあり、2.13.1 からは、そのない設定に対して potato validate がエラーを報告します。Stroop の調査は、このフェーズを、一つの項目に対する二つの課題評定に使います。data/task.jsonl には一行があります。

json
{"id": "stroop_task", "text": "The color-naming task you just finished"}

同意と色覚スクリーナー

Prolific は研究者に、同意しない参加者を戻るための経路へ案内し、そのような参加者を決して拒否しないよう求めています(Prolific の調査内スクリーニングのガイド)。Potato 2.13.1 では、すべての exit_when ルールが同じスクリーンアウトコードを与えます。そのため、同意ページには exit_when がありません。同意しない参加者は、Prolific で調査を返却するよう求められ、スクリーンアウトコードは、スクリーナーが除外した参加者にのみ渡されます。surveys/consent.json は次のとおりです。

json
[
  {
    "name": "study_information",
    "annotation_type": "pure_display",
    "description": "About this study",
    "labels": [
      "You will press keys for the ink color of color words, then answer a few questions. The study takes about 10 minutes.",
      "Your responses are stored on a university server under your Prolific ID.",
      "If you do not want to take part, close this page and return the study on Prolific."
    ]
  },
  {
    "name": "consent",
    "annotation_type": "radio",
    "description": "Do you agree to take part in this study?",
    "labels": ["I agree to take part"],
    "label_requirement": {"required_label": ["I agree to take part"]}
  }
]

調査ページでは、サーバーが required_label を検査し、2.13.1 からは required: true も検査します。2.12.1 では、required: true はブラウザでのみ検査されていました。2.12.1 で、同意の質問を飛ばしてページを直接送信したテストリクエストは、400 の応答を受け、同意ページに留まりました。surveys/screener.json のスクリーナーは、両方の選択肢を required_label の下に列挙します。するとサーバーは、特定の回答を求めずに、回答があることを要求します。

json
[
  {
    "name": "color_vision",
    "annotation_type": "radio",
    "description": "Do you have difficulty telling red, green, or blue apart?",
    "labels": ["Yes", "No"],
    "label_requirement": {"required_label": ["Yes", "No"]}
  }
]

「Yes」と答えた参加者は、C1SCREEN とともに、セッションの終わりに直行します。スクリーナーは、課題の前に独自のページを持ちます。Potato は、jsPsych ページの exit_when を、タイムラインが終了した後、つまり参加者がすでに課題を行った後にしか検査しません。Prolific は、可能な場合は、参加者が調査に到達する前に、Prolific 自身のプリスクリーナーでフィルタリングすることも勧めています。

Stroop のタイムライン

タイムラインファイルは、window.potatoTimeline に関数を代入します。Potato は jsPsych のインスタンスを作成し、そのインスタンスと potato オブジェクトを渡して関数を呼び出し、タイムラインが終了するとデータを保存します。下のタイムラインは、Potato に同梱されている examples/advanced/jspsych-stroop の例を改変したものです。

javascript
window.potatoTimeline = function (jsPsych, potato) {
    var inks = [
        {name: 'RED', color: '#c62828', key: 'r'},
        {name: 'GREEN', color: '#2e7d32', key: 'g'},
        {name: 'BLUE', color: '#1565c0', key: 'b'}
    ];
 
    // Every word in every ink: 3 congruent and 6 incongruent pairs.
    var stimuli = [];
    inks.forEach(function (word) {
        inks.forEach(function (ink) {
            stimuli.push({
                word: word.name,
                ink: ink.name,
                color: ink.color,
                correct_response: ink.key,
                congruent: word.name === ink.name
            });
        });
    });
 
    var instructions = {
        type: jsPsychInstructions,
        pages: [
            '<p>A color word will appear in colored ink.</p>' +
            '<p>Press the key for the <strong>ink color</strong>, not the word:</p>' +
            '<p><strong>R</strong> = red, <strong>G</strong> = green, <strong>B</strong> = blue</p>',
            '<p>Answer as quickly and accurately as you can.</p>'
        ],
        show_clickable_nav: true
    };
 
    var fixation = {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: '<div style="font-size:48px;">+</div>',
        choices: 'NO_KEYS',
        trial_duration: function () {
            return jsPsych.randomization.sampleWithoutReplacement([400, 500, 600], 1)[0];
        },
        data: {task: 'fixation'}
    };
 
    var trial = {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: function () {
            return '<div style="font-size:64px;font-weight:700;color:' +
                jsPsych.evaluateTimelineVariable('color') + ';">' +
                jsPsych.evaluateTimelineVariable('word') + '</div>';
        },
        choices: ['r', 'g', 'b'],
        trial_duration: 2500,
        data: {
            task: 'stroop',
            word: jsPsych.timelineVariable('word'),
            ink: jsPsych.timelineVariable('ink'),
            congruent: jsPsych.timelineVariable('congruent'),
            correct_response: jsPsych.timelineVariable('correct_response')
        },
        on_finish: function (data) {
            data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
        }
    };
 
    // Runs only for participants in the feedback: immediate condition.
    var feedback = {
        timeline: [{
            type: jsPsychHtmlKeyboardResponse,
            stimulus: function () {
                var last = jsPsych.data.get().last(1).values()[0];
                if (last.response === null) { return '<p>Too slow</p>'; }
                return last.correct ? '<p>Correct</p>' : '<p>Incorrect</p>';
            },
            choices: 'NO_KEYS',
            trial_duration: 600,
            data: {task: 'feedback'}
        }],
        conditional_function: function () {
            return potato.conditions.feedback === 'immediate';
        }
    };
 
    var block = {
        timeline: [fixation, trial, feedback],
        timeline_variables: stimuli,
        repetitions: 2,
        randomize_order: true
    };
 
    return [instructions, block];
};

potato.conditions.feedback は、Potato が参加者に割り当てた水準で、conditional_function は、immediate 条件でのみフィードバック画面を表示します。このファイルは initJsPsych を呼び出さず、実験レベルの on_finish も設定しません。どちらも Potato が行うためです。試行レベルの on_finish は、通常の jsPsych の調査と同じように動作します。

Potato 2.13.1 は、jsPsych 8.3.0 とそのプラグイン 15 個をパッケージ内に同梱しており、ページは両方を CDN ではなく Potato サーバーから読み込みます。設定の plugins リストは、タイムラインが使う二つのプラグインを指定します。jspsych-contrib のプラグインのように、Potato が同梱していないプラグインは、ブラウザ向けのビルドとして extra_scripts に入れます。18 試行では、各参加者に 6 つの一致試行しかなく、実際の調査では repetitions を増やします。

評定とデブリーフィング

二つの評定は、課題の後のアノテーションフェーズで実行されます。どちらの評定も label_requirement: required: true を設定します。サーバーはアノテーション項目でこの要件を検査し、参加者は両方に回答しなければデブリーフィングに到達できません。surveys/debrief.json のデブリーフィングは、操作を説明し、コメントは任意のままにします。

json
[
  {
    "name": "debriefing",
    "annotation_type": "pure_display",
    "description": "What this study measured",
    "labels": [
      "Naming the ink color takes longer when the word names a different color. This slowdown is the Stroop effect. Half of the participants saw feedback after each response, and the study compares the Stroop effect between the two groups."
    ]
  },
  {
    "name": "comments",
    "annotation_type": "text",
    "description": "Anything else you want to tell us? (optional)"
  }
]

お使いのマシンでの調査のテスト

プロジェクトフォルダーからサーバーを起動します。

bash
potato start config.yaml -p 8000

http://localhost:8000/?PROLIFIC_PID=TEST1&STUDY_ID=s1&SESSION_ID=x1 を開き、テスト参加者ごとに新しい PROLIFIC_PID を使います。調査を、一度は最後まで、一度はスクリーナーで「Yes」と答えて、一度は Stroop ブロックの途中でページを再読み込みして、通して試してください。終了した参加者には C1STROOP と「Return to Prolific」ボタンが表示され、スクリーンアウトされた参加者には C1SCREEN が表示されます。2.12.1 では、スクリーンアウトされた参加者のページに、終了した参加者のページと同じ「You have completed the annotation task」の文が表示され、違いはコードだけです。

jsPsych のページには「Next」ボタンがありません。タイムラインが終了してサーバーが実行を保存するまで、サーバーは先へ進むあらゆるリクエストにエラーで応答し、ページを飛ばす直接のリクエストは、テストで 400 を返しました。jsPsych は途中から再開できないため、再読み込みするとタイムラインは最初からやり直しになります。save: incremental では、再読み込み前の試行は、ステータスが partial の実行としてエクスポートに残り、新しい実行には試行番号 2 が付きます。

テスト参加者は条件に数えられ、エクスポートに現れます。公開前に annotation_output/ と project.sqlite を削除してください。

調査のサーバーへの設置

Prolific の参加者は、公開されたアドレスを通じてのみ調査に到達します。本番環境のセットアップは、大学のサーバーで TLS を使って nginx または Apache の背後で Potato を実行する方法を扱い、VPS への Potato のデプロイは、レンタルサーバーを扱います。試行データはプロジェクトフォルダーの project.sqlite に、評定と調査の回答は annotation_output/ に残り、どちらもサーバーを実行するマシン上にあります。

Prolific では、URL パラメーターで Prolific ID を記録する方法を選び、調査の URL を、サーバーのアドレスと三つのパラメーターにします。

text
https://your-server.example/?PROLIFIC_PID={{%PROLIFIC_PID%}}&STUDY_ID={{%STUDY_ID%}}&SESSION_ID={{%SESSION_ID%}}

Prolific に二つの完了経路を追加します。終了した参加者向けの C1STROOP と、Prolific の「Screened out」ラベルを付けた C1SCREEN です。Prolific は、スクリーンアウトされた参加者に、あなたが設定した固定のスクリーンアウト報酬を支払い、その提出は募集人数に数えられません。同意しない参加者は、Prolific で調査を返却し、コードは受け取りません。提出を追跡するための Prolific API の設定は、クラウドソーシング統合にあります。

試行データのエクスポート

Potato は、各 jsPsych の実行を、参加者の user_state.json ファイルではなく、project.sqlite(テーブル runtime_runs)に保存します。jspsych エクスポート形式は、実行と試行を CSV として書き出します。

bash
python -m potato.export --config config.yaml --format jspsych --output export/

7 人のシミュレートされた参加者による Potato 2.12.1 でのテスト実行は、次のファイルを書き出しました。

ファイル一行の単位内容
jspsych_runs.csvタイムラインの実行参加者、ページ、試行番号、ステータス(complete または partial)、開始と終了の時刻、試行数、condition_feedback
jspsych_trials.csvjsPsych の試行run_status、attempt、condition_feedback、および rt、response、plugin_version、タイムライン独自の task、word、ink、congruent、correct を含む、jsPsych が記録したすべての列
jspsych_raw/<participant>/stroop-<run>.json実行jsPsych が生成したままの、実行のデータ

四つ目のファイル jspsych_interaction.csv は、jsPsych がブラー、フォーカス、フルスクリーンのイベントを記録した場合に現れます。下の行は、フィードバック条件の一人の参加者の最初の Stroop 試行で、列の一部を示しています。

text
user_id,attempt,run_status,trial_index,rt,response,condition_feedback,plugin_version,task,word,ink,congruent,correct
5f1a02,1,complete,2,604,r,immediate,2.2.0,stroop,BLUE,RED,False,True
5f1a02,1,complete,5,538,b,immediate,2.2.0,stroop,RED,RED,True,False
5f1a02,1,complete,8,545,g,immediate,2.2.0,stroop,BLUE,GREEN,False,True

rt はミリ秒です。時間切れになった試行では、rt と response は空で、correct は False です。jsPsych は、すべての試行にプラグインのバージョン(ここでは html-keyboard-response が 2.2.0、instructions が 2.1.0)を記録します。jsPsych 本体のバージョンはデータに含まれないため、方法のセクションには、「jsPsych 8.3.0、Potato 2.13.1 による配信」のように、両方のバージョンを記載してください。CSV ファイルは真偽値に True と False を使い、生の JSON は JSON の真偽値を保持します。

CSV エクスポートは、調査のその他の部分を書き出します。

bash
python -m potato.export --config config.yaml --format csv --output results/

results/participants.csv には、参加者ごとに一行があり、condition_feedback、screened_out、screened_out_page、Stroop の実行の試行回数とステータスを示す runtime_runs 列があります。results/annotations.csv には二つの評定が尺度の点ごとに一列(difficulty.1 から difficulty.5)で入り、results/phase_responses.csv には、同意、スクリーナー、デブリーフィングの回答が入ります。データリポジトリ向けには、--format psych_ds が、五つの表すべてを Psych-DS のファイル名で data/ の下に書き出し、すべての列を一覧にした dataset_description.json も書き出します。

R での反応時間の分析

下の R のコードは、試行の表を読み込み、完了した実行の Stroop 試行を残し、各参加者の Stroop 効果を、不一致試行での正答の平均反応時間から、一致試行での平均を引いたものとして計算します。

r
trials <- read.csv("export/jspsych_trials.csv")
stroop <- subset(trials, task == "stroop" & run_status == "complete")
stroop$congruent <- as.logical(stroop$congruent)
stroop$correct <- as.logical(stroop$correct)
 
accuracy <- aggregate(correct ~ user_id + condition_feedback, data = stroop, FUN = mean)
 
fast <- subset(stroop, correct & rt >= 200)
cells <- aggregate(rt ~ user_id + condition_feedback + congruent, data = fast, FUN = mean)
wide <- reshape(cells, idvar = c("user_id", "condition_feedback"),
                timevar = "congruent", direction = "wide")
wide$stroop_effect <- wide$rt.FALSE - wide$rt.TRUE
 
aggregate(stroop_effect ~ condition_feedback, data = wide, FUN = mean)
t.test(stroop_effect ~ condition_feedback, data = wide)

as.logical は True と False の文字列を読み取り、read.csv がすでに変換した列はそのままにします。200 ms の下限は、予期的なキー押下を除外し、事前登録されたカットオフがある場合は、それが下限に代わります。時間切れの試行は correct が false になるため、正答率の表は時間切れを誤りとして数えます。

完了した実行の attempt が 2 の参加者は、ページを途中で再読み込みし、一部の試行を二度見ています。subset(stroop, attempt == 1) は最初の試行のみを残し、この選択は分析計画に属します。試行レベルのモデルには、lme4 が、一致性の効果、フィードバック条件、その交互作用を、参加者ごとの一致性のランダム勾配とともに当てはめます。

r
library(lme4)
fit <- lmer(rt ~ congruent * condition_feedback + (1 + congruent | user_id), data = fast)
summary(fit)

congruentTRUE:condition_feedbacknone の係数は、二つのフィードバック条件の間で Stroop 効果がどれだけ異なるかを推定します。参加者フローのためにスクリーンアウトを数えるには、results/participants.csv を読み込み、screened_out_page を集計します。

次に読むもの