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

JavaScript なしで自己ペース読みの実験を作る

YAML の設定から、ラテン方格のリストと理解度質問を備えた、ガーデンパス文の移動窓式の調査を実行し、単語ごとの読み時間を分析します。

Potato Team

PCIbex で自己ペース読みを実施している、あるいは jsPsych のプラグインを探したことのある心理言語学者は、このチュートリアルに従って、JavaScript を一行も書かずに、一つの YAML 設定と三つの JSON 調査ファイルから、Potato 2.13.1 で移動窓式の調査を構築できます。この調査は、公開されたベンチマークの 24 のガーデンパス項目を、フィラーとともに二つのラテン方格リストで提示し、各文の後に理解度質問を出し、完了コードを付けて参加者を Prolific に戻します。エクスポートは一単語につき一行で、短い Python スクリプトが、その行を曖昧性解消動詞での残差読み時間に変えます。調査のメイズ版と PCIbex からのインポートは、最後に扱います。このページの設定、コマンド、エクスポートされた行は、ヘッドレス Chrome 上の 12 人のシミュレートされた読み手による、Potato 2.12.1 の実行から得たものです。

自己ペース読みのためのツール

自己ペース読みは、読み手が文の各単語に費やす時間を測定します(Just, Carpenter & Woolley, 1982)。読み手はキーを押して、ある単語から次の単語へ進みます。移動窓式では、文はまずダッシュとして表示され、キーを押すたびに次の単語が現れ、その前の単語が再びマスクされます。ある単語での遅れは、読み手が困難を感じた箇所を示します。

PCIbex は、文処理の実験を中心に作られています。PCIbex の DashedSentence と Maze のコントローラーは、PennController のスクリプトから移動窓式の読みとメイズ課題を実行し、PCIbex Farm が実験をホストします。jsPsych のユーザーは、jspsych-contrib の plugin-self-paced-reading や plugin-spr などの自己ペース読みのプラグインを中心に、タイムラインを書きます。試行に、newTimer や newFunction 内のカスタム JavaScript のような、Potato にキーがない PennController の要素が必要な場合は、PCIbex のほうが適しています。Potato は、trial_timing の下の presentation ブロックから読みを実行し、同じ YAML ファイルが、同意から Prolific への復帰まで、セッションの残りを設定します。行動実験ツールの比較は、これらのツールを並べて比較しています。

調査

項目は、主動詞/縮約関係節(MV/RR)のガーデンパス文です。曖昧なバージョンは「The suspect sent the file deserved further investigation given the new evidence.」です。読み手は sent を主動詞と解釈し、deserved で再分析を迫られます。曖昧でないバージョンは who was を加え(「The suspect who was sent the file deserved further investigation given the new evidence.」)、動詞が来る前に主動詞の読みを排除します。予測は、曖昧なバージョンで deserved とその次の一語か二語の読み時間が長くなることです。

24 の MV/RR 項目、理解度質問とその答え、各曖昧性解消語の位置は、Syntactic Ambiguity Processing(SAP)ベンチマーク(Huang et al., 2024)のものです。SAP リポジトリは MIT ライセンスで公開されており、項目は Items for all subsets.xlsx のシート NPSNPZMVRR にあります。12 のフィラーと 2 つの練習文は、このチュートリアルのために書かれました。

各参加者は、すべての項目を、一つのバージョンで一度ずつ読みます。リスト 1 は、奇数番号の項目を曖昧なバージョンで、偶数番号の項目を曖昧でないバージョンで持ち、リスト 2 はその逆です。したがって、各リストは条件ごとに 12 項目を持ちます。どちらのリストにも同じ 12 のフィラーが加わり、2 つの練習文で始まります。各参加者は 38 の文を読み、すべての文の後に、はい・いいえの質問が続きます。

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

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

bash
pip install potato-annotation==2.13.1

プロジェクトフォルダーには、設定、二つのリストファイル、三つの調査ページがあります。

text
spr-study/
├── config.yaml
├── data/list1.jsonl
├── data/list2.jsonl
├── surveys/consent.json
├── surveys/instructions.json
└── surveys/background.json

二つのリストの作成

リストファイルは JSON Lines で、一行に一つの文があります。下のスクリプトは、SAP のスプレッドシートを読み込み、各項目を二つのリストに反対のバージョンで配置し、各リストを固定のシードで一度シャッフルして、ファイルを書き出します。スクリプトには pandas と openpyxl が必要で、フィラーと練習文を fillers.jsonl と practice.jsonl から読み込みます。この二つのファイルは、項目の行と同じフィールドを使います。

python
import json
import random
 
import pandas as pd
 
sap = pd.read_excel("Items for all subsets.xlsx", sheet_name="NPSNPZMVRR")
mvrr = sap[sap.condition == "MVRR_UAMB"]
fillers = [json.loads(line) for line in open("fillers.jsonl")]
practice = [json.loads(line) for line in open("practice.jsonl")]
 
rng = random.Random(20261029)
for n, name in enumerate(["list1", "list2"]):
    rows = []
    for _, r in mvrr.iterrows():
        item = int(r["item"])
        ambiguous = (item % 2 == 1) == (n == 0)  # the Latin square
        sentence = r["ambiguous"] if ambiguous else r["Unnamed: 3"]
        verb = r["disambPositionAmb"] if ambiguous else r["disambPositionUnamb"]
        rows.append({
            "id": f"mvrr{item:02d}_{'amb' if ambiguous else 'unamb'}",
            "item": item,
            "condition": "ambiguous" if ambiguous else "unambiguous",
            "sentence": " ".join(sentence.split()),
            "question": r["Question"].strip(),
            "answer": r["Option1"] if r["Answer"] == 1 else r["Option0"],
            "critical_region": int(verb) - 1,  # SAP counts words from 1
        })
    rows += [{**f, "id": f"{f['id']}_{name}"} for f in fillers]
    while True:  # no two items of one condition in a row
        rng.shuffle(rows)
        if all(a["condition"] == "filler" or a["condition"] != b["condition"]
               for a, b in zip(rows, rows[1:])):
            break
    with open(f"data/{name}.jsonl", "w") as out:
        for row in [{**p, "id": f"{p['id']}_{name}"} for p in practice] + rows:
            out.write(json.dumps(row) + "\n")

data/list1.jsonl の一行は次のようになります。

json
{"id": "mvrr01_amb", "item": 1, "condition": "ambiguous", "sentence": "The suspect sent the file deserved further investigation given the new evidence.", "question": "Did the suspect send the file to someone?", "answer": "No", "critical_region": 5}

critical_region は、Potato のエクスポートと同じく 0 から数えた、曖昧性解消動詞の位置です。answer フィールドは分析用で、Potato は参加者が押したキーを記録し、答えの採点は行いません。項目 ID は、両方のファイルにわたって一意でなければなりません。そのため、フィラーと練習文には、filler01_list1 のように、リスト名が接尾辞として付きます。

Potato は、リストの項目をファイルの順序で配信します。リスト 1 のすべての参加者は、練習文が最初に来る、同じ擬似ランダムな順序で読みます。別の順序が必要な場合は、list 要因に list1b のような水準を追加し、各水準に別々のファイルを用意します。

設定

下の設定は完全なもので、2.12.1 と 2.13.1 の両方で python -m potato.validate_cli config.yaml --strict を通ります。

yaml
annotation_task_name: Garden-path self-paced reading
task_dir: .
output_annotation_dir: annotation_output/
data_files: []
item_properties:
  id_key: id
  text_key: sentence
 
login:
  type: url_direct
  url_argument: PROLIFIC_PID
user_config:
  allow_all_users: true
  users: []
 
crowdsourcing:
  provider: prolific
  prolific:
    completion:
      code: C1READING
 
conditions:
  factors:
    list: [list1, list2]
 
assignment_strategy: batch
batch_assignment:
  auto_assign_annotators: true
  group_by_condition: list
  groups:
    - {name: list1, data_file: data/list1.jsonl}
    - {name: list2, data_file: data/list2.jsonl}
 
phases:
  order: [consent, instructions, annotation, background]
  consent:
    type: consent
    file: surveys/consent.json
  instructions:
    type: instructions
    file: surveys/instructions.json
  annotation:
    type: annotation
  background:
    type: poststudy
    file: surveys/background.json
 
trial_timing:
  enabled: true
  fixation_ms: 400
  iti_ms: 500
  presentation:
    mode: moving_window
    field: sentence
    advance_key: space
    question_field: question
  response:
    schema: comprehension
    keys: {f: "Yes", j: "No"}
 
annotation_schemes:
  - annotation_type: radio
    name: comprehension
    description: Answer the question
    labels: ["Yes", "No"]
    label_requirement:
      required: true
 
export_include_phase_data: true

presentation ブロックは、各項目を移動窓式の試行にします。field: sentence は表示する項目フィールドを指定し、region_separator がなければ、各単語が一つの領域になります。単語ではなく句の時間を測るには、文中の句の境界を | で示し、region_separator: "|" を設定します。question_field: question は、最後の単語の後に各文の質問を表示し、question が空の文は、質問なしで終わります。response ブロックは F をはい、J をいいえに対応付け、comprehension スキームが回答を保存します。

fixation_ms: 400 は、各文の前に固視十字を表示し、iti_ms: 500 は、文の間に半秒を置きます。読みにも質問にも制限時間はありません。2.13.1 では、trial_duration_ms は質問にのみ適用され、読みには適用されません。

list 要因と batch_assignment.group_by_condition: list は、各参加者に一つのリストを与え、Potato は二つのグループファイルから項目を読み込むため、data_files は空のままです。割り当てはデフォルトで balanced であり、新しい参加者は、参加者の少ないほうのリストに割り当てられます。設定は balance_on をデフォルトの assigned のままにしています。Potato 2.12.1 の balance_on: completed では、参加者は終了後にはじめてリストに数えられ、1 秒以内に開始した 12 人のシミュレートされた読み手は、デフォルトでは 6 対 6 のところ、リスト間で 8 対 4 に分かれました。Potato 2.13.1 は、最後の操作から in_progress_minutes(デフォルトは 60)以内でまだ作業中の参加者も数えるため、completed は、その偏りなしに脱落者を補います。

login.type: url_direct は、調査リンクの PROLIFIC_PID から参加者の ID を読み取ります。最後に、crowdsourcing ブロックは、完了コード C1READING を「Return to Prolific」ボタンとともに表示します。Prolific の調査 URL、スクリーナー用のスクリーンアウトコード、調査の公開サーバーへの移行については、jsPsych を使って自分のサーバーで Stroop 課題を実行するで説明しています。export_include_phase_data: true は、調査の回答をエクスポートに追加します。

調査ページ

各調査ページは、質問の JSON リストです。同意の質問は、一つの回答を required_label の下に列挙します。サーバーは調査ページで required_label を検査し、2.13.1 からは required: true も検査します。2.12.1 では、required: true はブラウザでのみ検査されていました。surveys/consent.json は次のとおりです。

json
[
  {
    "name": "study_information",
    "annotation_type": "pure_display",
    "description": "About this study",
    "labels": [
      "You will read 38 sentences one word at a time and answer a yes-or-no question after each. The study takes about 15 minutes.",
      "Your key presses and answers 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"]}
  }
]

surveys/instructions.json の教示ページは、練習文の前にキーを示します。

json
[
  {
    "name": "how_to_read",
    "annotation_type": "pure_display",
    "description": "How the reading works",
    "labels": [
      "Each sentence first appears as a row of dashes. Press the space bar to see the first word, and press it again for each next word. Each word disappears when the next one appears.",
      "Read at your normal pace. After the last word, a question appears. Press F for yes and J for no.",
      "The first two sentences are practice."
    ]
  }
]

surveys/background.json の背景ページは、読みの後に母語を尋ねます。両方の回答を required_label の下に列挙すると、サーバーは、特定の回答を求めずに、回答があることを要求します。

json
[
  {
    "name": "native_english",
    "annotation_type": "radio",
    "description": "Is English your native language?",
    "labels": ["Yes", "No"],
    "label_requirement": {"required_label": ["Yes", "No"]}
  },
  {
    "name": "comments",
    "annotation_type": "text",
    "description": "Anything else you want to tell us? (optional)"
  }
]

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

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

bash
potato start config.yaml -p 8000

テスト参加者ごとに新しい PROLIFIC_PID を付けて、http://localhost:8000/?PROLIFIC_PID=TEST1&STUDY_ID=s1&SESSION_ID=x1 を開きます。同意ページと教示ページの後、各文は固視十字で始まり、次にダッシュとして表示され、その下に「Space = next word」と出ます。表示は等幅フォントを使い、各マスクには、隠された単語の文字ごとに一つのダッシュがあります。そのため、単語が現れても行がずれません。スペースバーを最初に押すと、最初の単語が表示されます。以降の押下では次の単語が表示され、前の単語がマスクされ、最後の単語の後の押下で、質問の下に「F = Yes」と「J = No」を付けた質問が表示されます。回答すると、次の文が読み込まれます。

スペースバーが文を進めるため、参加者にはキーボードが必要です。trial_timing.disable_back_navigation はデフォルトで true なので、Potato は読みのページで戻るボタンを隠します。文の途中でページを再読み込みすると、文は最初からやり直しになります。その試行は reloads が 1 になり、フラグ reloaded_mid_trial が付き、その試行の単語の時間は二度目の読みから得られます。テスト参加者はリストに数えられ、エクスポートに現れます。公開前に、annotation_output/ と project.sqlite を削除してください。

読み時間のエクスポート

調査を CSV としてエクスポートします。

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

12 人のシミュレートされた読み手による実行は、次のファイルを書き出しました。

ファイル一行の単位内容
regions.csv表示された単語参加者、項目、リスト、0 から数えた単語位置、単語、rt_ms、試行の結果と開始時刻(6,132 行)
trials.csv文回答の key と label、回答までの時間、意図した固視時間と試行間時間と測定された値、frame_ms、timer_resolution_ms、reloads、サーバーのフラグ(456 行)
participants.csv参加者condition_list とスクリーンアウト
annotations.csv参加者と文理解度の回答。ラベルごとに一列
phase_responses.csv調査の回答同意、母語、コメント

下の行は、一人の読み手の、ある曖昧な項目の単語 4 から 8 です。項目は「The assistant manager assigned the training seemed unnecessarily demanding to new staff.」で、曖昧性解消動詞は 6 番目の単語 seemed です。

text
user_id,instance_id,condition_list,region,text,rt_ms,correct,side,errors,trial_outcome,onset_epoch_ms
r01,mvrr12_amb,list2,4,the,295.9,,,,response,1793287302649
r01,mvrr12_amb,list2,5,training,458.5,,,,response,1793287302649
r01,mvrr12_amb,list2,6,seemed,553.2,,,,response,1793287302649
r01,mvrr12_amb,list2,7,unnecessarily,564.2,,,,response,1793287302649
r01,mvrr12_amb,list2,8,demanding,433.9,,,,response,1793287302649

rt_ms は、単語が現れた表示フレームから、次のスペースバーの押下までの時間です。correct、side、errors の列は、メイズ課題以外では空のままです。文が回答された質問で終わったため、trial_outcome は response です。regions.csv には list 条件がありますが、項目自身のフィールドはありません。condition や critical_region のような項目のフィールドは、instance_id での結合を通じて、リストファイルから得られます。

trials.csv では、rt_ms は質問の開始から回答までの時間です。各試行には、参加者のブラウザが測定したフレーム間隔と、ブラウザのタイマー解像度(ヘッドレスのテストでは 8.3 ms と 0.1 ms)も記録されます。この二つの列は、各参加者のマシンが提供できたものを示します。

Python での残差読み時間

生の読み時間は、単語の長さとともに増え、読み手によって異なります。残差読み時間は、各読み手の時間を単語の長さに回帰し、残差を残すことで、その両方の効果を取り除きます(Ferreira & Clifton, 1986)。下の分析スクリプトは、練習文、質問への正答率が 80% 未満の読み手、再読み込みされた試行、100 から 2,000 ms の範囲外の時間を除外します。次にスクリプトは、読み手ごとに残差を計算し、動詞の前の単語、動詞、動詞の後の二語で、条件ごとに残差を平均します。最後のステップは、動詞とそれに続く二語について、参加者と項目のランダム切片を持つ混合モデルを当てはめます。

python
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
 
items = pd.concat([pd.read_json(f"data/{f}.jsonl", lines=True) for f in ("list1", "list2")])
items = items.rename(columns={"id": "instance_id"})
trials = pd.read_csv("results/trials.csv").merge(items, on="instance_id")
regions = pd.read_csv("results/regions.csv").merge(items, on="instance_id")
 
# Comprehension accuracy per participant, practice excluded.
trials = trials[trials.condition != "practice"]
trials["correct"] = trials.label == trials.answer
accuracy = trials.groupby("user_id").correct.mean()
keep = accuracy[accuracy >= 0.8].index
print(accuracy.round(2).to_dict())
 
# Drop practice, low-accuracy readers, reloaded trials, and extreme times.
flagged = trials[trials["flags"].fillna("").str.contains("reloaded_mid_trial")]
rt = regions[(regions.condition != "practice") & regions.user_id.isin(keep)]
rt = rt.merge(flagged[["user_id", "instance_id"]], how="left", indicator=True)
rt = rt[(rt._merge == "left_only") & rt.rt_ms.between(100, 2000)].copy()
 
# Residual reading time: each reader's times regressed on word length.
rt["length"] = rt.text.str.replace(r"[^A-Za-z]", "", regex=True).str.len()
 
 
def residuals(d):
    slope, intercept = np.polyfit(d.length, d.rt_ms, 1)
    return d.rt_ms - (intercept + slope * d.length)
 
 
rt["residual"] = pd.concat([residuals(d) for _, d in rt.groupby("user_id")])
 
# Positions relative to the disambiguating verb, critical items only.
crit = rt[rt.condition.isin(["ambiguous", "unambiguous"])].copy()
crit["position"] = (crit.region - crit.critical_region).astype(int)
window = crit[crit.position.between(-1, 2)]
window.to_csv("window.csv", index=False)
print(window.pivot_table(index="position", columns="condition",
                         values="residual", aggfunc="mean").round(1))
 
# Mixed model at the verb and the two words after it.
spill = window[window.position >= 0]
fit = smf.mixedlm("residual ~ condition", spill, groups="user_id",
                  vc_formula={"item": "0 + C(item)"}).fit()
print(fit.summary().tables[1])

テストのエクスポートに対してプロジェクトフォルダーから実行すると、スクリプトは次を出力しました。

text
{'r01': 0.92, 'r02': 0.83, 'r03': 0.89, 'r04': 0.97, 'r05': 0.94, 'r06': 0.67, 'r07': 0.92, 'r08': 0.89, 'r09': 0.89, 'r10': 0.94, 'r11': 0.81, 'r12': 0.94}
condition  ambiguous  unambiguous
position
-1               0.8          2.3
 0              80.7         -7.8
 1              56.4        -13.8
 2              18.5        -10.4
                            Coef. Std.Err.        z  P>|z|   [0.025   0.975]
Intercept                  51.865    2.559   20.268  0.000   46.850   56.881
condition[T.unambiguous]  -62.542    3.626  -17.249  0.000  -69.648  -55.435
item Var                    0.000    1.955

この実行は、Potato 2.12.1 とシミュレートされた読み手を使いました。ヘッドレスブラウザのドライバーは、各単語の遅延を、単語の長さにランダムなノイズを加えて設定し、すべての曖昧な項目の動詞とその次の二語に、90、60、25 ms を加えました。分析は、その三つの位置で、条件間の 89、70、29 ms の差を再現しました。したがって、キーの押下からエクスポート、残差までの経路は、ドライバーが挿入した効果を保持しており、その差は、人間がこれらの文をどう読むかについては何も述べていません。読み手 r06 は質問の 67% に正答したため、除外されました。シミュレートされた項目には項目効果がないため、項目の分散はゼロであり、statsmodels は、推定値がパラメータ空間の境界上にあると警告します。

スクリプトは、R での分析のために、動詞の周りの四つの位置を window.csv にも書き出します。下の lme4 のモデルは、参加者と項目に対する条件のランダム勾配を加えます。このページのために R は実行していません。

r
library(lme4)
window <- read.csv("window.csv")
spill <- subset(window, position >= 0)
fit <- lmer(residual ~ condition + (1 + condition | user_id) + (1 + condition | item),
            data = spill)
summary(fit)

メイズ版

メイズ課題は、各位置に二つの単語を並べて表示し、読み手は、文を続けるほうの単語を選びます(Boyce, Futrell & Levy, 2020)。調査のメイズ版にするには、上の設定の trial_timing ブロックと annotation_schemes ブロックを、下のブロックに置き換えます。

yaml
trial_timing:
  enabled: true
  fixation_ms: 400
  iti_ms: 500
  presentation:
    mode: maze
    field: sentence
    distractor_field: distractors
    maze_keys: {left: e, right: i}
    on_error: redo
 
annotation_schemes:
  - annotation_type: pure_display
    name: maze_instructions
    description: How to read
    labels:
      - Press E for the left word or I for the right word, whichever continues the sentence.

すると、各項目には、文の単語ごとに一つの誤った単語を持つ distractors フィールドが必要になります。フィールドの単語数が文より一つ少ない場合、Potato は最初の単語を x-x-x と対にします。

json
{"id": "mvrr01_amb", "item": 1, "condition": "ambiguous", "sentence": "The suspect sent the file deserved further investigation given the new evidence.", "distractors": "ran very of went ninety whose upon slept seemed of often.", "critical_region": 5}

上のディストラクターは、テスト実行のために手作業で書いたものです。A-maze は、言語モデルを使って、長さと頻度が近く、文を続けない単語から、各ディストラクターを選びます。Maze リポジトリに生成コードがあり、その生成は、リストファイルを作る前に実行します。

メイズ版は理解度質問を出さず、response ブロックを持ちません。response ブロックのない時間制限付きページでは、Potato は pure_display スキームだけを受け付け、教示がそのページの唯一のスキームです。on_error: redo は誤った選択に印を付けて正しい単語を待ち、on_error: stop は、代わりに最初の誤りで文を終了します。二人のシミュレートされた読み手によるテスト実行では、regions.csv はすべての単語について correct と side が埋まり、この設定ではメイズ文の後に質問が来ないため、trial_outcome は completed でした。一人の読み手が deserved で一度誤ったキーを押しました。deserved の行は errors が 1 になり、その行の rt_ms の 1,072.6 は、組が表示された時点から正しい押下までの時間です。

PCIbex 実験のインポート

potato import -f ibex は、既存の PCIbex 実験を変換します。コマンドに、data_includes/ と chunk_includes/ のあるフォルダーを指定します。

bash
potato import -f ibex -i my_experiment/ -o my-study/

テスト用の実験には、一つの Template("items.csv", ...) の試行と、水準 A と B を持つ Group 列がありました。試行は newController("DashedSentence", {s: row.Sentence}) を表示し、その後 newText(row.Question) と newKey("FJ") で質問をしました。インポーターは、list 要因とグループごとの一つのデータファイルを持つ移動窓式の設定を書き出し、その設定は validate_cli --strict を通りました。IMPORT_REPORT.md は、Sequence の順序付けと newHtml の教示画面を、変換されなかったものとして列挙しました。Potato 2.12.1 では、理解度質問はレポートに一行も記載されずに削除され、上の設定のように、question_field と response ブロックを手で追加する必要がありました。同じ実験に対して、2.13.1 のインポーターは、質問を question_field と response ブロックを持つ comprehension スキームとして保持し、レポートは、F と J のキーが Yes と No になったことを記載します。Ibex から移行するは、インポーターが変換する PCIbex の構成要素を一覧にしています。

次に読むもの

参考文献

  • Boyce, V., Futrell, R., & Levy, R. P. (2020). Maze Made Easy: Better and easier measurement of incremental processing difficulty. Journal of Memory and Language, 111, 104082. https://doi.org/10.1016/j.jml.2019.104082
  • Ferreira, F., & Clifton, C. (1986). The independence of syntactic processing. Journal of Memory and Language, 25(3), 348-368. https://doi.org/10.1016/0749-596X(86)90006-9
  • Huang, K.-J., Arehalli, S., Kugemoto, M., Muxica, C., Prasad, G., Dillon, B., & Linzen, T. (2024). Large-scale benchmark yields no evidence that language model surprisal explains syntactic disambiguation difficulty. Journal of Memory and Language, 137, 104510. https://doi.org/10.1016/j.jml.2024.104510
  • Just, M. A., Carpenter, P. A., & Woolley, J. D. (1982). Paradigms and processes in reading comprehension. Journal of Experimental Psychology: General, 111(2), 228-238. https://doi.org/10.1037/0096-3445.111.2.228