The full HiTOP-SR asks 405 items and returns 76 scale scores, which is more than many studies need and more than many participants will sit through. A module is a chosen set of the instrument’s scales, administered and scored on its own.
This article walks the whole module workflow end to end: choosing which scales to include, describing the module, generating the files you will actually field, and scoring the data that comes back. For scoring a complete administration, see Scoring the HiTOP-SR instead.
Choosing Scales
Start from the menu. available_scales() lists every
scale the instrument offers, along with the camelCase stem that will
name its scored output column, the number of items it contributes, and a
brief clinician-facing definition (Brief, printed here
truncated; hitopsr_definitions carries it in full).
scale_menu <- available_scales("hitopsr")
scale_menu
#> # A tibble: 76 × 4
#> Scale camelCase nItems Brief
#> <chr> <chr> <int> <chr>
#> 1 Agoraphobia agoraphobia 5 Fear and avoidance of situat…
#> 2 Antisocial Behavior antisocialBehavior 8 Behavior that goes against s…
#> 3 Appearance Focus appearanceFocus 5 Excessive focus on appearanc…
#> 4 Appetite Loss appetiteLoss 3 Decreased appetite and food …
#> 5 Binge Eating bingeEating 3 Eating unusually large amoun…
#> 6 Bodily Distress bodilyDistress 6 Physical symptoms like pain,…
#> 7 Body Dissatisfaction bodyDissatisfaction 4 Persistent dissatisfaction w…
#> 8 Callousness callousness 6 Disregard for others' feelin…
#> 9 Checking checking 5 Repetitive urges to check ta…
#> 10 Cleaning cleaning 6 Fear of dirt and germs, lead…
#> # ℹ 66 more rowsBoth name columns are accepted when you build a module, so you can
write scale names the way they are printed on the form
("Antisocial Behavior") or the way they appear in scored
output ("antisocialBehavior").
A module is built from these 76 scales only. The instrument also
defines 17 subscales (see hitopsr_subscales), but a
subscale is not a unit you can select here — hitop_module()
rejects a subscale name as unknown. Ask for the scale that contains it
instead.
Because this is an ordinary tibble, you can also use it to plan the length of your module. For example, to see the shortest scales:
scale_menu[order(scale_menu$nItems), ][1:5, ]
#> # A tibble: 5 × 4
#> Scale camelCase nItems Brief
#> <chr> <chr> <int> <chr>
#> 1 Appetite Loss appetiteLoss 3 "Decreased app…
#> 2 Binge Eating bingeEating 3 "Eating unusua…
#> 3 Difficulties Reaching Orgasm difficultiesReachingOrgasm 3 "Trouble reach…
#> 4 Excoriation excoriation 3 "Repetitive sk…
#> 5 Low Sexual Arousal lowSexualArousal 3 "Problems with…Building the Module
hitop_module() turns a set of scale names into a
validated description of the module. It resolves the names, works out
which items those scales need, and records the reverse-keying flags that
scoring will require.
four_scale <- hitop_module(
instrument = "hitopsr",
scales = c(
"Agoraphobia", "Appetite Loss",
"Antisocial Behavior", "Romantic Disinterest"
)
)
four_scale
#> <hitop_module> hitopsr: 21 items from 4 scales
#> * Agoraphobia
#> * Antisocial Behavior
#> * Appetite Loss
#> * Romantic DisinterestThe printed summary reports how many items the module needs. The description itself is a plain list, so you can look inside it:
four_scale$camelCase
#> [1] "agoraphobia" "antisocialBehavior" "appetiteLoss"
#> [4] "romanticDisinterest"
four_scale$nItems
#> [1] 21
four_scale$items
#> [1] 42 66 68 109 118 144 152 156 167 185 187 202 239 260 268 274 291 310 338
#> [20] 389 390Note that $items holds the original
HiTOP-SR item numbers. That is true of the module file whatever a
generator later prints: item 42 of the full instrument is item 42 here,
which is what lets data collected with a module be scored against the
full instrument’s key.
Generating the Instrument
Each of the three HiTOP-SR generators takes a module
argument. Pass the description and you get a form containing only that
module’s items.
The three do not number those items alike, on purpose. The Word form
is what a participant fills in on paper, so
generate_docx_hitopsr() numbers it 1 to
n down the page – a four-scale module reads 1, 2, 3 rather
than 7, 42, 213. Pass renumber = FALSE to keep the original
numbers instead. In the Qualtrics and REDCap exports an item number
names a collected data column, so those keep the original HiTOP-SR
numbers always; renumbering them would rename variables in dictionaries
already in the field.
A Word form also says on its face that it is a module.
generate_docx_hitopsr() heads a module form
HiTOP-SR Module (v1.0) where it heads a full-instrument
form HiTOP-SR (v1.0). Pass title = to print
something else — a study name, say — and that string is used verbatim on
either kind of form.
With include_subscales = TRUE, a module form adds to its
scoring page the subscales of the scales it holds, and no others. Every
item of a subscale is an item of its parent scale, so each subscale row
lists only items the form contains.
generate_docx_hitopsr() also takes
randomize = TRUE, which prints the items in a random order.
A renumbered module form — the default — is still numbered
1 to n and carries a crosswalk from each
printed number back to its original HiTOP-SR number, printed whether or
not include_scoring appends the key, so it can be scored
from the paper alone. That is the only case the crosswalk is printed in.
Under renumber = FALSE there is none, because the printed
numbers already are the original ones; and shuffling the full instrument
prints none either, since 405 pairs would fill a page. In both of those
cases read the order from the item_order attribute of the
returned path, or save it with descriptor = below. Call
set.seed() beforehand to make an order reproducible.
One thing to watch: by default score_hitopsr() expects a
module’s items in ascending original order, not the
order a shuffled form prints them in. Data entered straight off a
shuffled form has its columns in the order the form printed: the first
column holds the answer to the paper’s item 1, the second the answer to
item 2, and so on. Score such columns with
layout = "printed" and a module that carries the printed
order on its item_order attribute, which is what the module
file saved with descriptor = below gives you. The function
then puts the columns back into instrument order itself. Scoring
printed-order columns under the default layout returns wrong scale
scores with no error raised, and scoring columns already in instrument
order under layout = "printed" scrambles what was
right.
Here we write all three formats into a temporary folder; in your own work you would give a real path or let the default filename land in your working directory.
outdir <- tempdir()
docx_file <- generate_docx_hitopsr(
file = file.path(outdir, "hitopsr_module.docx"),
module = four_scale
)
qualtrics_file <- generate_qualtrics_hitopsr(
file = file.path(outdir, "hitopsr_module.txt"),
module = four_scale
)
redcap_file <- generate_redcap_hitopsr(
file = file.path(outdir, "hitopsr_module.zip"),
module = four_scale
)
basename(c(docx_file, qualtrics_file, redcap_file))
#> [1] "hitopsr_module.docx" "hitopsr_module.txt" "hitopsr_module.zip"The DOCX file is a paper form ready to print, the .txt
file imports into Qualtrics as an advanced-format question block, and
the .zip file imports into REDCap as an instrument. See Importing Instruments into Qualtrics and
REDCap for the import steps themselves.
Selecting the Collected Columns
Once the data comes back, it has only the module’s item columns — plus whatever else your platform recorded, such as participant identifiers and demographics. Both scoring functions need you to name those item columns.
We will stand in for module-collected data using
ku_hitopsr, the package’s real example dataset. Its first
two columns are participant and biosex, and
its 405 item columns are named hsr_001 through
hsr_405:
data("ku_hitopsr")
names(ku_hitopsr)[1:5]
#> [1] "participant" "biosex" "hsr_001" "hsr_002" "hsr_003"Select the item columns by name, never by position.
The numbers in four_scale$items are item numbers, not
column numbers. They coincide only in a data frame that is exactly the
405 items in order — and this one is not, because two other columns come
first. Build the column names instead:
item_cols <- sprintf("hsr_%03d", four_scale$items)
collected <- ku_hitopsr[item_cols]
ncol(collected)
#> [1] 21That width matches the module’s own item count, which is the check worth running before you score anything:
ncol(collected) == four_scale$nItems
#> [1] TRUEIndexing by four_scale$items directly would have
selected columns 42, 66, 68, and so on by position, which in
this frame are the wrong items — shifted by the two leading columns:
names(ku_hitopsr[four_scale$items])[1:4]
#> [1] "hsr_040" "hsr_064" "hsr_066" "hsr_107"Those are items 40, 64, 66, and 107 masquerading as the module’s
items. Nothing would have failed loudly; the scores would simply have
been wrong. If your item columns carry other names,
rename_hitopsr_items() can rename them to standard item
numbers first; its prefix argument sets the stem those
names are built from.
Scoring and Reliability
Hand the same description back through the module
argument. Without it, score_hitopsr() expects a full
administration and stops on the item count (“Expected 405 items but got
21”); with it, the items argument names the columns you
actually have, in instrument order, and module tells the
function which scales they belong to.
module_scores <- score_hitopsr(
data = collected,
items = names(collected),
module = four_scale,
append = FALSE
)
module_scores
#> # A tibble: 411 × 4
#> hsr_agoraphobia hsr_antisocialBehavior hsr_appetiteLoss
#> <dbl> <dbl> <dbl>
#> 1 1 1.5 1
#> 2 1.4 1 1.67
#> 3 2 1.12 1
#> 4 1.2 1 1.33
#> 5 1.6 1 2.67
#> 6 1 1 1.33
#> 7 1 1 1
#> 8 1 1 1.33
#> 9 2 1 1
#> 10 1 1 1
#> # ℹ 401 more rows
#> # ℹ 1 more variable: hsr_romanticDisinterest <dbl>Only the module’s scales come back, in the order they appear in
hitopsr_scales. The values are exactly what a full
administration would have produced for those scales — a scale score
depends only on its own items, so dropping the other 72 scales’ columns
cannot move it:
full_scores <- score_hitopsr(
data = ku_hitopsr,
items = sprintf("hsr_%03d", 1:405),
append = FALSE
)
all.equal(module_scores, full_scores[names(module_scores)])
#> [1] TRUEreliability_hitopsr() takes the same argument and
returns one row per module scale.
reliability_hitopsr(
data = collected,
items = names(collected),
module = four_scale,
omega = FALSE
)
#> # A tibble: 4 × 4
#> Scale camelCase nItems alpha
#> <chr> <chr> <int> <dbl>
#> 1 Agoraphobia agoraphobia 5 0.820
#> 2 Antisocial Behavior antisocialBehavior 8 0.351
#> 3 Appetite Loss appetiteLoss 3 0.762
#> 4 Romantic Disinterest romanticDisinterest 5 0.821Reliability is worth checking on your own module rather than assumed.
In this sample the four coefficients run from modest down to negative:
Romantic Disinterest returns a negative α, which is not a small number
but a sign that its items covary near zero or oppositely in these data,
so a mean of them is not measuring one thing. Short scales make α less
stable, but shortness alone does not produce a negative value. Set
omega = FALSE to skip McDonald’s ω, which needs the
lavaan package.
The srange, prefix, missing,
calc_se, and append arguments all behave
exactly as they do for a full administration; see Scoring the HiTOP-SR for those.
calc_se is deprecated there and here alike
— it, and the _se columns it adds, will be removed in a
future release, and interval_hitopsr() replaces it.
Saving the Module Beside the Form
Everything above depends on still having four_scale when
the data comes back. Months later, in a fresh session, rebuilding it
means retyping every scale name — and a typo scores the wrong scales
while a forgotten scale scores none.
write_module() saves the description to a small JSON
file you can keep beside the forms you generated:
descriptor <- write_module(four_scale, file.path(outdir, "hitopsr_module.json"))
cat(readLines(descriptor), sep = "\n")
#> {
#> "format": "1.0",
#> "package": "hitop",
#> "packageVersion": "0.2.0",
#> "buildDate": "2026-10-07",
#> "instrument": "hitopsr",
#> "scales": ["Agoraphobia", "Antisocial Behavior", "Appetite Loss", "Romantic Disinterest"],
#> "items": [42, 66, 68, 109, 118, 144, 152, 156, 167, 185, 187, 202, 239, 260, 268, 274, 291, 310, 338, 389, 390],
#> "nItems": 21
#> }You do not have to remember to call it. Each of the three generators
takes a descriptor argument, so one call writes both the
form and the file that scores it:
generate_docx_hitopsr(
file = file.path(outdir, "hitopsr_module2.docx"),
module = four_scale,
descriptor = file.path(outdir, "hitopsr_module2.json")
)A call that passes no module writes a module file naming
every scale, so a full administration is described too. And on a
shuffled Word form the module file also records the printed order, which
is the one thing a whole-instrument form gives you nowhere else — no
crosswalk is printed for one:
set.seed(42)
shuffled_descriptor <- file.path(outdir, "hitopsr_shuffled.json")
generate_docx_hitopsr(
file = file.path(outdir, "hitopsr_shuffled.docx"),
module = four_scale,
randomize = TRUE,
descriptor = shuffled_descriptor
)
shuffled <- read_module(shuffled_descriptor)
attr(shuffled, "item_order")
#> [1] 291 118 42 185 109 66 310 338 156 152 390 167 389 144 187 274 239 68 202
#> [20] 260 268Those are the original HiTOP-SR item numbers in the order the page
printed them. Responses entered off that form have their columns in the
order the form printed, the first column holding the paper’s item 1.
Score them with layout = "printed" and the module that
carries the order:
score_hitopsr(collected, items = seq_along(collected), module = shuffled,
layout = "printed")The scoring function puts the columns back into instrument order
itself. Columns already in instrument order take the default,
layout = "instrument".
The file is plain text, so you can read it, edit it, and send it to a
collaborator. read_module() turns it back into a
module:
reloaded <- read_module(descriptor)
reloaded
#> <hitop_module> hitopsr: 21 items from 4 scales
#> * Agoraphobia
#> * Antisocial Behavior
#> * Appetite Loss
#> * Romantic DisinterestWhat comes back is not simply what the file said. The file records
scale names; the items and their reverse-keying flags are
rebuilt from this package’s own tables, so a module file can never
introduce a scoring key of its own. The items the file
records are checked against that rebuild, and a disagreement stops with
an error rather than scoring quietly — which is what you want if the
file was written by an older version of the package whose tables have
since changed.
Hand the reloaded module to score_hitopsr() exactly as
you would the original:
identical(
score_hitopsr(collected, items = names(collected), module = reloaded, append = FALSE),
module_scores
)
#> [1] TRUEScoring a REDCap or Qualtrics export without naming the columns
A module file written by generate_redcap_hitopsr() or
generate_qualtrics_hitopsr() also records the names that
the generated file assigns to the items. For REDCap these are the data
dictionary’s field names. For Qualtrics they are the questions’
[[ID:]] values, such as HSR_066. A data export
with the item variable names as column headers uses these names.
read_module() returns them on the module’s
columns attribute. The REDCap case looks like this:
redcap_descriptor <- file.path(outdir, "hitopsr_redcap.json")
generate_redcap_hitopsr(
file = file.path(outdir, "hitopsr_module.zip"),
module = four_scale,
descriptor = redcap_descriptor
)
from_redcap <- read_module(redcap_descriptor)
head(attr(from_redcap, "columns"))
#> [1] "hsr_042" "hsr_066" "hsr_068" "hsr_109" "hsr_118" "hsr_144"With that module, leave out items. The scoring functions
take the names from the module:
identical(
score_hitopsr(collected, module = from_redcap, append = FALSE),
module_scores
)
#> [1] TRUEreliability_hitopsr() does the same. Export Qualtrics
data as numeric values, not choice text, so there are numbers to score.
Pass items for data whose columns carry other names, such
as a Qualtrics export made with “Use internal IDs in header”, or a
question renamed after import. A supplied items is always
used. A module with no columns, such as one read from a
Word form’s module file, still needs items.
Collecting Responses Online with hitop-form
The Word, Qualtrics and REDCap files above each need a platform of their own. hitop-form is a third route that needs none: an online form that shows a HiTOP-SR module in the browser, or any of five forms in full: the PID-5, PID-5-SF, PID-5-BF, HiTOP-SR and HiTOP-BR. A study link tells it where each participant’s answers go: a file saved on the participant’s own device, which they send to you, or a Google Sheet, another web address or a Supabase table the link names. In the Study Link Builder, “A Google Sheet” is its own choice, with the sheet’s setup steps, and you download the sheet as one CSV file. The Collecting Responses Online article walks the Google Sheet route end to end, from deploying the sheet’s script to scoring.
The online form reads the same module file
write_module() writes. Open the Study Link
Builder and choose “HiTOP-SR module (scales you choose)” as the
instrument. In that row, paste the module file in or choose it as a
file. Name the study, choose where the responses go, and press “Make the
link”. The link shows under “Your study link”, ready to send to
participants. A module file from a shuffled Word form carries that
form’s order, and the online form shows the items in it. The “Show the
items in a random order” box, in the closed “Item order” section, asks
for a new order per participant instead; the module file’s order is then
not followed.
Whichever route the answers take, the CSV file has five lead columns
(study, participant, instrument,
form_build, submitted) and then one column per
item, named by the instrument’s file stem and the item number and in the
order the page showed them. Under the random order, a sixth lead column,
item_order, records the order shown, and the item columns
are in the order the module file’s items field lists them,
which write_module() writes ascending. A link that recruits
through Prolific adds two more lead columns, prolific_study
and prolific_session. A file the page saved holds one
participant’s row; a download from the destination holds one row per
participant.
read_form_responses() reads those files. Give it the
folder the files are in, or a vector of their paths. The file below is
one the page saved from a two-scale module with a shuffled order. The
package installs it as an example, and system.file() gives
its path:
path <- system.file("examples", "responses-module-shuffled.csv", package = "hitop")
responses <- read_form_responses(path)
responses
#> # A tibble: 1 × 29
#> study participant instrument form_build submitted item_order
#> <chr> <chr> <chr> <chr> <dttm> <chr>
#> 1 fixture p001 hitopsr 2026-09-20 2026-09-20 21:20:44 NA
#> # ℹ 23 more variables: prolific_study <chr>, prolific_session <chr>,
#> # hitopsr_233 <int>, hitopsr_194 <int>, hitopsr_170 <int>, hitopsr_064 <int>,
#> # hitopsr_365 <int>, hitopsr_011 <int>, hitopsr_020 <int>, hitopsr_300 <int>,
#> # hitopsr_109 <int>, hitopsr_118 <int>, hitopsr_260 <int>, hitopsr_394 <int>,
#> # hitopsr_224 <int>, hitopsr_291 <int>, hitopsr_304 <int>, hitopsr_367 <int>,
#> # hitopsr_343 <int>, hitopsr_066 <int>, hitopsr_100 <int>, hitopsr_386 <int>,
#> # hitopsr_380 <int>Each row of each file is a row of the result, the lead columns typed
(form_build is character, the page’s build date as the file
holds it, and submitted a UTC date-time), and every item
column an integer. A folder of many files reads the same way, file by
file in path order. Every file must hold the same item columns in the
same order; a folder mixing two forms stops with an error that names the
files that differ, so read each form’s files in a call of their own.
A file can also hold a module beside other instruments, one group of
item columns per instrument, as the page saves it for a study link that
lists several instruments. The instrument cell then names
the stems in the order of the groups, joined by single spaces
(hitopsr pid5bf), and form_build holds one
date per instrument in the same order. Under the random order,
item_order holds one group per instrument, joined by
|. Choose the module’s columns by the hitopsr_
stem and score them as below. The Collecting
Responses Online article describes this file shape.
Score the item columns through the module the page showed. Without
the random order, the columns are in the order the page showed the
items, so this is layout = "printed" with the module file
that recorded that order, as in the shuffled Word form above. (A file
saved under the random order has its columns in instrument order, so it
takes the default layout, as the first scoring call in this article
does.) The item column names carry the item numbers in the order the
page showed them, which is the module’s item_order, so they
pass through items as they are, with no warning.
form_module <- read_module(
system.file("examples", "module-shuffled.json", package = "hitop")
)
item_cols <- grep("^hitopsr_", names(responses), value = TRUE)
score_hitopsr(
responses,
items = item_cols,
module = form_module,
layout = "printed",
append = FALSE
)
#> # A tibble: 1 × 2
#> hsr_agoraphobia hsr_distressDysphoria
#> <dbl> <dbl>
#> 1 3 2.44A full HiTOP-SR or HiTOP-BR saved by the page reads the same way and
scores with score_hitopsr() or score_hitopbr()
alone, with items naming the item columns and no module at
all.
