Skip to content

Getting started

This page covers the three things you need before any of the rest is useful: getting the package installed with the right extras, driving it from the command line, and reading a design file. The design file is where nearly all of the work happens. Almost nothing about a study is expressed in Python code, which is deliberate, because a YAML file can be attached to a pre-registration and handed to someone running the other engine.

Install

pip install lexsync

That gives you the library and the lexsync console script. The corpora and experiment extras go in brackets, either on their own or together:

pip install "lexsync[corpora]"
pip install "lexsync[corpora,experiment]"

The development version comes straight from the repository, where the Python package sits in the python_workflow/ subdirectory:

pip install "git+https://github.com/pablobernabeu/lexsync.git#subdirectory=python_workflow"

If you want the 21 worked designs, the derived corpora and the committed outputs as well, clone the repository and install the package in editable mode from inside it:

git clone https://github.com/pablobernabeu/lexsync.git
cd lexsync
python -m pip install -e "python_workflow[dev]"
python python_workflow/run_pipeline.py     # runs every demonstration design

Python 3.10 or newer is required. The core dependencies are pandas, NumPy, PyYAML, rapidfuzz and SciPy, and they are enough to select stimuli and generate every experiment target.

Extras

Three optional extras are declared in pyproject.toml. Only the first is likely to matter to you early on.

Extra Installs What it unlocks
corpora wordfreq>=3.0,<4 The wordfreq connector, which derives a lexicon for a language that is not bundled.
experiment psychopy>=2024.1, pyserial>=3.5 Running a generated laboratory experiment on hardware.
dev pytest, build, twine, streamlit The test suite, including the tests that cover the Streamlit app.

The experiment extra deserves emphasis because it is easy to assume otherwise. It is needed only to run a generated experiment, never to generate one. Script generation imports neither PsychoPy nor pyserial. It writes text. The whole demonstration therefore reproduces on a laptop with no parallel port, no EEG amplifier and no PsychoPy installation, and the test suite runs the same way in continuous integration.

The floor on PsychoPy is there because the extra tracks PsychoPy's own supported range of Python versions, which trails the newest interpreter by a release or two. PsychoPy currently declares 3.10 up to 3.12, so on Python 3.13 there is nothing for pip to install and it stops with a message naming psychopy. That is the intended outcome. Without the floor pip would keep looking further back until it reached a 2022 release whose dependencies no longer build, and the install would die part-way through a compilation having installed nothing. On Windows with Python 3.12 the resolve succeeds, but pyWinhook, which PsychoPy requires there, publishes no wheel for that version and is built from source, so swig has to be on the path. Python 3.11 avoids both and is the easiest interpreter to use for hardware work. None of this touches the rest of the package, which runs on any version from 3.10 onwards.

The corpora extra pins wordfreq to its frozen 3.x line, which is a stable snapshot of language usage through roughly 2021. That is deliberate. A lexicon derived from a live, drifting source would quietly stop reproducing.

Check that it worked

import lexsync

print(lexsync.__version__)
print(lexsync.list_corpora().to_string(index=False))
0.1.0
       name              language   iso status   connector                                                                                                                                                                                                                                                                                             citation
 subtlex_uk     English (British) en-GB manual openlexicon                                                                                                                                       van Heuven, Mandera, Keuleers & Brysbaert (2014). SUBTLEX-UK. Quarterly Journal of Experimental Psychology, 67(6), 1176-1190. doi:10.1080/17470218.2013.850521
subtlex_esp               Spanish    es listed openlexicon                                                                                                                                                                                                         Cuetos, González-Nosti, Barbón & Brysbaert (2011). SUBTLEX-ESP. Psicológica, 32(2), 133-143.
 subtlex_us    English (American) en-US listed openlexicon                                                                                                                                                                                                          Brysbaert & New (2009). Behavior Research Methods, 41(4), 977-990. doi:10.3758/BRM.41.4.977
 subtlex_nl                 Dutch    nl listed openlexicon                                                                                                                                                                                    Keuleers, Brysbaert & New (2010). SUBTLEX-NL. Behavior Research Methods, 42(3), 643-650. doi:10.3758/BRM.42.3.643
 subtlex_de                German    de listed openlexicon                                                                                                                                                                                          Brysbaert et al. (2011). SUBTLEX-DE. Experimental Psychology, 58(5), 412-424. doi:10.1027/1618-3169/a000123
 lexique_fr                French    fr listed openlexicon                                                                                                                                                     New, Pallier, Brysbaert & Ferrand (2004). Lexique 2. Behavior Research Methods, Instruments, & Computers, 36(3), 516-524. doi:10.3758/BF03195598
 subtlex_it               Italian    it listed openlexicon                                                                                                                                                                                                           Crepaldi, Amenta, Mandera, Keuleers & Brysbaert (2015). SUBTLEX-IT. (verify on ingestion).
 subtlex_ch    Chinese (Mandarin)    zh listed openlexicon                                                                                                                                                                                                         Cai & Brysbaert (2010). SUBTLEX-CH. PLoS ONE, 5(6), e10729. doi:10.1371/journal.pone.0010729
 subtlex_pl                Polish    pl listed openlexicon                                                                                                                                                                Mandera, Keuleers, Wodniecka & Brysbaert (2015). SUBTLEX-PL. Behavior Research Methods, 47(2), 471-483. doi:10.3758/s13428-014-0489-4
 subtlex_pt Portuguese (European)    pt listed openlexicon Soares, Machado, Costa, Iriarte, Simoes, de Almeida, Comesana & Perea (2015). On the advantages of word frequency and contextual diversity measures extracted from subtitles: The case of Portuguese. Quarterly Journal of Experimental Psychology, 68(4), 680-696. doi:10.1080/17470218.2014.964271

list_corpora reads the corpus registry. It looks for corpora/registry.yaml relative to the working directory first, then one level up, then falls back to the copy bundled inside the package, so it answers whether or not you are standing in a clone. Setting LEXSYNC_REGISTRY overrides the search.

The command line

Installing the package puts a lexsync command on the path. python -m lexsync is an alias for it and takes the same arguments. There are three subcommands.

lexsync run config/design_en_freqcontrast.yaml    # one design, end to end
lexsync run                                       # every design_*.yaml in --config-dir
lexsync corpora list                              # the registry, as a table
lexsync fetch fr                                  # derive a French lexicon via wordfreq

lexsync run is the orchestrator. It reads the schema and the design, loads the lexicon, derives whatever dimensions the design matches on, selects and counterbalances the items, then writes the stimuli, the realised-control report, the materials datasheet, the run log and the three experiment scripts. Its flags:

Flag Default Meaning
--schema config/schema.yaml The global schema and defaults.
--config-dir config Where to look for designs when no design is named.
--outdir output Root of the written artefacts (stimuli/, reports/, experiments/).

Paths in a design are resolved relative to the working directory, so run these from the root of a clone. With no design argument, lexsync run globs design_*.yaml and design_*.yml from --config-dir, sorts them and runs each in turn.

lexsync corpora list prints the registry as a table of name, language, ISO code, status, connector and citation. lexsync fetch takes either a registry entry name or a language code. If the name is one of the thirty language codes registered for the wordfreq connector, a lexicon is built with wordfreq and cached under ~/.lexsync/cache. Otherwise the registered delimited file is downloaded there. Either way the path is printed and the citation for the source goes to standard output with it. Fetching is Python-only. The R package can read the result as an ordinary corpus but cannot build one, so an R laboratory reaches the wider language set through lexica derived here.

Anatomy of a design

A design is a YAML file. config/schema.yaml holds everything global and every numeric default, while the design holds what is specific to the study. Both are read identically by the R and Python packages, which is what lets a design travel. Here is the English frequency contrast in full, from config/design_en_freqcontrast.yaml:

name: en_freqcontrast
language: english
lexicon: corpora/derived/en.csv
description: 'High versus low frequency English words, matched on length, neighbourhood density and OLD20.'
n_per_condition: 80
pool_filters:
  length: [3, 8]
  frequency: [3.8, 7.0]
conditions:
  - name: high_frequency
    define_by:
      frequency: [5.2, 7.0]
  - name: low_frequency
    define_by:
      frequency: [3.8, 4.4]
match_on: [length, n_density, old20]
counterbalance:
  lists: 1
timing:
  fixation_frames: 30
  word_frames: 30
  isi_frames: 15

The keys

name and language are required, and together they form the slug that every written file is named after. en_freqcontrast plus english becomes en_freqcontrast_english, and from that come en_freqcontrast_english_stimuli_py.csv, en_freqcontrast_english.osexp and the rest. language is a free-text label rather than a code, because it is what the experiment displays. When the browser target needs a real BCP 47 tag it maps the common labels and falls back to und, or you can state language_tag outright.

lexicon names the derived corpus to read. It may also be written as items.lexicon, which is the form the lexical-decision designs use, since they also need to say where the items come from.

items selects the source of the stimuli, and there are four of them:

items.source Where stimuli come from Also needs
corpus (the default) Words selected from the lexicon by matching or by spanning a predictor. lexicon, conditions or continuous, match_on
generate Real words plus a deterministically generated pseudoword for each. lexicon, optionally items.generation.method
table A CSV of prepared items (prime-target pairs, sentences). items.path
pool A candidate word list of your own, matched over as if it were a pool. items.path, normally items.lexicon

pool_filters narrows the lexicon to the candidates a design will consider at all. Each key is a column and each value a [min, max] range for a numeric column, or a set of allowed values for a categorical one. This is a step that matters. The matcher never reads pool_filters itself, so a script that omits build_pool matches over the entire lexicon and quietly ignores the design's bands.

conditions is a list, each with a name and a define_by block that carves the condition out of the pool by the same filter syntax. Two conditions make a contrast. Four make the 2 × 2 that config/design_en_andrews_repro.yaml uses to reproduce Andrews (1989). A design may instead declare a continuous block and dispense with conditions altogether, which Matching and designs covers.

match_on lists the dimensions to equate across conditions. n_per_condition is how many items you want in each. Both are the heart of the design, and both are discussed at length in the matching guide.

matching overrides the schema defaults for this design alone. matching.method picks one of the four methods, and matching.tolerance_k sets the half-width, in standard deviations, of the tolerance window on each dimension. Overriding a single dimension is common when reproducing a published study's exact windows.

counterbalance.lists sets the number of lists, and counterbalance.optimise asks for an assignment whose lists are equated on the item dimensions, where the plain deal goes by set rank. practice and fillers each name an item table whose trials run but are not analysed. timing overrides the fixation, critical-word and inter-stimulus durations in milliseconds (fixation_ms, word_ms, isi_ms), the older *_frames forms still being accepted and converted at presentation.assumed_refresh_hz. font overrides the presentation font, which matters for a non-Latin script: config/design_zh_freqcontrast.yaml sets SimHei, because the Latin default has no glyphs for Han characters.

paradigm names one of the five registered paradigms and inherits its trial-event sequence and its counterbalancing recipe. Omitting it gives factorial. A design may instead supply an explicit events list and describe its own trial. Both routes are covered in Experiments and triggers.

The schema

config/schema.yaml is the other half, and it is worth reading once. It fixes the seed, states the column contract that a derived lexicon must satisfy (word and freq_zipf are required), declares the six lexical dimensions and how each is obtained, and sets the defaults a design may override: the matching method, the per-dimension tolerance windows, the equivalence bound and alpha, the parallel-port address and trigger timing, and the presentation fonts.

Two things in it are deliberately not configurable. Equivalence is assessed by two one-sided tests, and only the bound and alpha are settings. The tie-break order in the matcher is fixed at distance, then word bytes, then id, because that byte-order tie-break is precisely what lets the two engines agree without an RNG. Making it an option would make the guarantee an option.

A copy of the schema and of a small English, Spanish and Chinese lexicon is bundled inside the installed package, which is how the examples throughout these guides run without a clone:

from importlib.resources import files

import yaml

data = files("lexsync") / "data"
schema = yaml.safe_load((data / "schema.yaml").read_text(encoding="utf-8"))
print(sorted(p.name for p in data.iterdir()))
['en_example.csv', 'es_example.csv', 'registry.yaml', 'schema.yaml', 'zh_example.csv']

The bundled lexica are 3000-word slices meant for examples and tests. The full derived corpora live in corpora/derived/ in the repository, and that is what the worked designs read.

What a run writes

Running a design produces more than a stimulus list. Under --outdir you get the counterbalanced stimuli, the descriptives and comparisons that make up the realised-control report, a materials datasheet in both JSON and Markdown, a run log in both Markdown and JSON Lines, and the three experiment scripts with their loop tables.

# illustrative: needs a clone with config/, and writes files under output/
import lexsync

paths = lexsync.run_pipeline("config/design_en_freqcontrast.yaml")
print(paths["stimuli"])
print(paths["experiments"]["jspsych"])

The Python engine suffixes its stimuli and reports with _py, so an R run and a Python run of the same design leave both selections side by side in one directory rather than overwriting each other. That is not a filing convenience. output/stimuli/en_freqcontrast_english_stimuli_R.csv is the committed reference that the parity test regenerates the Python selection against. The experiment scripts are the exception and carry no engine suffix, because the two engines emit the same bytes there and a byte-for-byte diff of that directory is itself a continuous-integration step.