Index Catalog¶
Registry-backed indices can be selected in screen() / composite(). Response-time
helpers use a different input domain and are listed separately.
Inspect the same registry metadata programmatically or from the command line:
from ier import index_catalog
catalog = index_catalog()
print(catalog["evenodd"]["required_options"])
ier indices
ier indices --format json --output indices.json
The catalog reports flag direction and mode, screen/composite availability and
defaults, options that must be configured before an index can run, and whether
the index reads reverse-scored responses (uses_keyed_responses; see
Reverse-keyed items). Every
required_options entry must be set, and at least one option in each
alternative_options group: infrequency requires infrequency_item_indices
and either infrequency_expected_responses or infrequency_acceptable_ranges.
CLI output formats describes the ier indices
text, JSON, and CSV layouts.
Matrix indices¶
| Name | Construct | Flag when | Screen default | Composite | Extra config |
|---|---|---|---|---|---|
irv |
Intra-individual response variability | low | yes | yes | optional irv_num_split / irv_split_points |
longstring |
Max consecutive identical responses | high | yes | yes | — |
longstring_pattern |
Repeating response patterns | high | yes | yes | longstring_max_pattern_length |
mahad |
Mahalanobis distance (multivariate outlier) | high | yes | yes | — |
psychsyn |
Psychometric synonym consistency | low | yes | yes | psychsyn_critval, psychsyn_item_correlations |
psychant |
Psychometric antonym consistency | high | no | yes | psychant_critval, psychsyn_item_correlations |
person_total |
Agreement with the sample item profile | low* | yes | yes | — |
markov |
Transition entropy | low | yes | yes | — |
missing_rate |
Missing-response proportion | high | no | yes | optional item subset / applicability mask |
u3_poly |
Proportion of extreme responses (not PerFit's U3; see u3poly_fit) |
high | yes | no | scale_min / scale_max |
midpoint |
Midpoint responding | high | yes | no | scale_min / scale_max, midpoint_tolerance |
acquiescence |
Agreeing / yea-saying | high | yes | no | scale bounds; optional equal-length polarity lists |
guttman |
Guttman errors | high | yes | yes | guttman_normalize |
individual_reliability |
Split-half individual reliability | low | no | yes | reliability_n_splits, seed, optional reliability_factors |
onset |
Carelessness onset item index | present | no | no | onset_window_size, onset_min_items |
evenodd |
Even-odd consistency | low | no | yes | evenodd_factors, evenodd_method |
mad |
Mean absolute paired difference | high | no | yes | MAD item lists / optional scale bounds |
lz |
lz person-fit | low | no | yes | optional IRT params via direct API; overflow-safe logistic kernel |
semantic_syn |
Predefined synonym consistency | low | no | yes | semantic_item_pairs |
semantic_ant |
Predefined antonym consistency | low | no | yes | semantic_item_pairs, optional scale bounds |
infrequency |
Failed attention / bogus items | high | no | yes | item indices, expected responses or acceptable ranges, missing policy |
avgstr |
Mean length of identical-response runs | high | no | yes | — |
autocorrelation |
Strongest lagged autocorrelation (cyclic patterns) | high | no | yes | autocorrelation_max_lag, autocorrelation_statistic |
gpoly |
Normalized polytomous Guttman errors over item steps (PerFit Gnormed.poly) |
high | no | yes | optional person_fit_ncat, scale_min / scale_max |
u3poly_fit |
Polytomous U3 person fit over item steps (PerFit U3poly) |
high | no | yes | optional person_fit_ncat, scale_min / scale_max |
ht |
Transposed scalability Ht for 0/1 items (PerFit Ht) |
low | no | yes | dichotomous responses |
* person_total flags unusually low correlations with the sample-wide item
profile under the default low-direction percentile rule.
irv(..., split=True) averages population standard deviations across consecutive
sections, giving each section equal weight. Automatic splits distribute extra
items to the earliest sections and ignore empty sections when num_split exceeds
the item count. num_split must be a positive integer, including NumPy integer
scalars. Custom split_points must be strictly increasing integer positions
starting at zero and ending at the number of columns. With na_rm=True, missing
responses are omitted within each section; an entirely missing section leaves the
respondent's split score NaN. With na_rm=False, any missing response propagates.
Both modes accumulate in double precision, including for float32 input, and
rescale exceptional finite values to preserve very large or small deviations.
Constant finite responses, including decimal values, have exactly zero
variability; nearly constant responses retain their representable differences.
Infinite responses leave their section deviations unavailable.
Split IRV (Dunn et al., 2018; careless::irv(split = TRUE)) is also available to
screen() and composites through IndexOptions(irv_num_split=N) or
IndexOptions(irv_split_points=[...]), and on the CLI through --irv-num-split N
or --irv-split-points 0,10,20. Split points override the section count. The
default single section keeps the unsplit computation, and invalid values are
reported as index failures rather than silently ignored.
onset requires integer window_size and min_items values, including NumPy
integer scalars, with window_size >= 2 and min_items >= window_size. It returns
NaN when no changepoint is detected, too few responses or running windows are
available, or a row contains an infinite response. With na_rm=True, missing
responses are removed in sequence order and returned positions are zero-based
within that observed sequence. na_rm=False rejects missing responses, including
in rows too short for detection. Large signed and unsigned integer responses
retain their adjacent differences instead of rounding to identical values
before variability is measured.
individual_reliability never reads or advances NumPy's process-wide random
state. random_seed accepts an integer, which reproduces the established split
sequence, or a np.random.Generator, which the call advances; without a seed,
fresh entropy is used. IndexOptions.reliability_random_seed takes integers.
With factors= (IndexOptions.reliability_factors, CLI
--reliability-factors 8,8,8), each resample randomly splits every scale into
two halves of size // 2 items, as in resampled individual reliability (Curran,
2016; Huang et al., 2012). A respondent's two vectors of scale half means are
correlated across scales, valid correlations are averaged over resamples, and
the Spearman–Brown correction 2r / (1 + r) is applied and clamped below at -1.
Each half mean is its exact mean rounded once, as in evenodd, so a split whose
half means do not vary is unusable rather than scored from rounding noise.
At least two scales are required, single-item scales contribute no halves, and
respondents with fewer than two complete half-mean pairs in every resample
receive NaN. This scale-aware form is recommended whenever the scale structure
is known: in a simulated six-scale survey it separates uniform random responders
from attentive respondents with an AUC near 0.99, where the legacy form is near
chance.
Without factors, the legacy form correlates randomly paired items across the
whole questionnaire. Items of one scale share a trait level, so these
correlations carry little signal for attentive respondents. It averages valid
split correlations and applies the Spearman–Brown correction. Corrected values
can be below -1 and are at most 1. Respondents without any valid split, or with
a mean split correlation of exactly -1, receive NaN. In both forms, screening
and composites treat unavailable scores as unavailable, and the standalone
individual_reliability_flag() helper continues to flag them. n_splits must be
a positive integer, including NumPy integer scalars. Constant respondent profiles
remain unavailable, including decimal-valued profiles whose floating-point mean
requires rounding.
evenodd offers two algorithms through method= (IndexOptions.evenodd_method,
CLI --evenodd-method). method="halves" is the classic even-odd index
(Johnson, 2005; Meade & Craig, 2012; Curran, 2016) and is recommended. Each factor
is split into the mean of its odd-position items and the mean of its
even-position items, ignoring missing responses. The two vectors of factor half
means are correlated within each respondent across the factors where both halves
are available, and the correlation is Spearman–Brown corrected and clamped below
at -1. Higher scores mean more consistent responding, so the score equals
-careless::evenodd. Each half mean is its exact mean rounded once, as R's
extended-precision mean() computes it, so half means with equal exact values are
identical: a respondent whose decimal-valued half means do not vary is unavailable,
as in careless, rather than scored from rounding noise. In simulated comparisons the
only remaining differences from careless, below 1e-7, are respondents whose half
means vary only in their last binary digit; there R's long-double cor() keeps
rounding error that the correlation here avoids. At least two factors are
required. Respondents with fewer than two available factors, or whose half means
do not vary across factors, receive NaN; straight-lined profiles are therefore
unavailable here and are covered by longstring. The diag=True count reports
the factors whose two half means are both finite.
method="item_pairs" remains the default for compatibility. It pairs
alternating items within each configured factor and averages the available
respondent-level factor correlations. Factors need at least four items to
contribute two paired observations; an odd final item is unpaired. Respondents
without any valid factor correlation receive NaN, report a diagnostic count of
zero, and remain unavailable rather than being flagged. Because the items of a
factor share a trait level, these within-factor correlations are close to
uninformative for attentive respondents. In either mode, factor sizes must be
positive integers whose sum matches the response columns.
Recode reverse-worded items with reverse_score(x, items, scale_min, scale_max)
before evenodd, individual_reliability, guttman, lz, gpoly, u3poly_fit,
or ht, which assume all items are keyed in the same direction (the indices
whose uses_keyed_responses catalog field is true). It returns a copy in which
each selected response v becomes scale_min + scale_max - v, inferring missing
bounds from the observed minimum and maximum of the whole matrix. Integer and Boolean responses
with integral bounds are recoded exactly in an integer dtype: the input dtype when
it holds both bounds, otherwise the smallest integer dtype that holds the input
dtype's range and both bounds. Unsigned 64-bit responses with a negative bound have
no such dtype; they, integer responses with a non-integral bound, and other inputs
return float64 with each exact reflection rounded once, including integer responses
beyond 2**53 and bounds near the float64 limit. Responses outside the bounds
raise ValueError, which catches mistyped bounds. The comparison is exact, so a
float16 or float32 response cannot pass a bound it equals only after rounding.
Missing responses stay NaN. Apart from converting non-array input, the returned
copy is the only full-size allocation: the selected columns are checked and
recoded in row batches of a few megabytes. Keep the responses as presented for
sequence and response-style indices such as longstring, markov, irv, onset, and
acquiescence, because recoding changes the response sequence they measure.
screen() and the composite helpers apply this split for you through
IndexOptions.reverse_keyed_items, described in
Reverse-keyed items.
psychsyn and psychant discover pairs from finite item correlations. Constant
items and samples with fewer than two respondents have undefined item
correlations and cannot supply pairs, even at a zero cutoff. The
item_correlations keyword (IndexOptions.psychsyn_item_correlations, CLI
--psychsyn-item-correlations) sets the missing-data policy for psychsyn,
psychant, psychsyn_critval(), and psychsyn_summary():
"complete"(the default) followsnp.corrcoef: an item with any missing response is undefined. With scattered missingness nearly every item has a gap, so few or no pairs are found and scores are typically allNaN."pairwise"correlates each item pair over the respondents who answered both, ascareless::psychsyndoes, and scores each respondent over the selected pairs they answered. Pairs need at least three shared respondents, because two shared responses always correlate perfectly; this also holds without missing responses, so fewer than three respondents yield no pairs. Otherwise, without missing responses both modes return identical results.
Use "pairwise" for data with missing responses. At least two selected pairs
(answered pairs in pairwise mode) are needed for a respondent score; otherwise
the score is NaN and remains unavailable in screening and composites.
Diagnostics count a single selected or answered pair. resample_na and
random_seed are accepted for compatibility with careless::psychsyn but have
no effect on scores: complete discovery selects only fully observed items,
pairwise mode leaves respondents with fewer than two answered pairs unavailable,
and a respondent whose answered pairs have zero within-pair variance scores 0.0
where careless returns NA. na_rm therefore does not change psychsyn or
psychant scores in screen() and composite(). Cutoffs must be finite real
numbers; psychsyn_critval() additionally requires a nonnegative minimum magnitude.
Item discovery preserves correlations at very large or small finite scales and
for nearly constant items with representable variation. In pairwise mode this
includes extreme or outlying responses outside a pair's shared respondents.
Constant decimal-valued items remain unavailable, and an infinite response
invalidates its item. Summaries with no available scores return NaN statistics
without warnings. psychsyn_summary() reports score statistics, the number of
selected item_pairs, and n_total, n_valid, and n_missing respondent counts.
Integer item discovery preserves small differences at large baselines. Person-total
profiles retain variation between nearly identical integer item means, while
Guttman scoring preserves exact integer difficulty rankings and response categories.
psychsyn_flag(), psychant_flag(), person_total_flag(), u3_poly_flag(), and
midpoint_responding_flag() return (scores, flags) in each index's registry
flag direction (low for psychsyn and person_total, high for psychant and
the response-style indices). Attentive respondents give strongly negative
antonym correlations, so psychant flags scores near zero or above.
Their default tail percentiles (5 for low, 95 for high) reproduce the per-index
flags of screen() at its default percentile=95. Fixed thresholds include
ties, percentile cutoffs exclude them, and unavailable scores are never flagged.
guttman orders items easiest first, by descending sample item mean, and keeps
items with tied means in column order. An error is an item pair where the
respondent scores strictly higher on the harder (less endorsed) item than on the
easier one, so a perfect cumulative pattern scores 0 and its reversal scores the
maximum. Item means always use each item's available responses, whatever
na_rm is, and items without any observed response are ordered last. Pairs
involving a missing response never count as errors, so raw counts do not depend
on na_rm; it only selects the normalization denominator. With na_rm=True,
normalized scores divide by the respondent's available pairs; with
na_rm=False, the denominator counts every item pair. guttman_flag() flags
normalized scores strictly above a finite threshold (default 0.5); like the
other flag helpers, it converts threshold with float(), so numeric strings
are accepted while True and False are rejected.
mahad flags only unusually large distances. method="chi2" compares squared
distances with the confidence quantile of a chi-squared distribution with one
degree of freedom per item, "iqr" uses the upper fence Q3 + 1.5 * IQR, and
"zscore" flags standardized distances above the two-sided normal critical value
for confidence (1.96 at 0.95). confidence must be a finite real number from 0
to 1: Python and NumPy scalars, 0-d arrays, Decimal, and Fraction values are
accepted, while Booleans and strings are rejected. mahad_summary() reports
distance statistics, the chi-squared outlier count, and n_total, n_valid, and
n_missing respondent counts.
acquiescence uses the normalized respondent mean by default. For a balanced
instrument, pair positively and negatively worded items in order with
IndexOptions.acquiescence_positive_items and
IndexOptions.acquiescence_negative_items, or the matching CLI options. Lists
must be nonempty and equal in length so every configured item participates;
indices are 0-based positions in the scored matrix. Both item polarities use raw
agreement responses, without reversing negative items. Pair means are normalized
using scale_min and scale_max, so agreement with every item scores 1 and
disagreement with every item scores 0.
Response-style indices infer omitted scale bounds from observed responses and
require scale_min <= scale_max. Explicit bounds accept Python and NumPy real
scalars, zero-dimensional real arrays, Decimal, and Fraction; they reject
Booleans, text, nonscalar arrays, NaN, and finite values outside float range.
Integer bounds retain their exact values, and infinite bounds remain available
for response-style comparisons with open or unreachable endpoints. Reverse
scoring requires finite bounds. If the data cannot supply an omitted bound,
u3_poly, midpoint_responding, and acquiescence return unavailable (NaN)
scores. Missing respondents remain unavailable even on a constant scale, where
acquiescence assigns 0.5 to respondents with a usable mean. Balanced-pair mode
requires both responses in a pair; na_rm=False propagates missing responses.
Acquiescence preserves normalization for very wide, subnormal, and nearly
constant finite scales, including adjacent large integer responses.
Midpoint tolerance must be finite and nonnegative.
Integer midpoint intervals retain adjacent categories even above the exact
integer range of double precision. Explicit response-scale bounds retain their
precision during comparisons with integer and single-precision responses.
response_pattern() calculates extreme-response and midpoint proportions, the
raw respondent mean, and population standard deviation in bounded batches,
sharing observed-response counts across all four summaries.
Large integer row means retain exact totals before final rounding, and
variability preserves small differences between adjacent large observations.
The registry's longstring index uses longstring_scores() for numeric response
matrices. The standalone longstring() helper analyzes text strings only and
rejects numeric or multidimensional arrays.
The opt-in avgstr index scores the average length of uninterrupted identical
runs, longstring_scores(x, avg=True): observed responses divided by the number
of runs, matching mean(rle(x)$lengths) from careless::longstring(avg = TRUE).
Curran (2016) recommends it beside longstring because repeated short runs raise
the average even when no single run is long. Higher values are flagged.
Rows without observed responses are unavailable (NaN). With na_rm=True,
missing responses are removed before runs are counted, so a run can continue
across a skipped item; R's rle() instead ends a run at NA. na_rm=False
rejects missing responses.
longstring_pattern() and IndexOptions.longstring_max_pattern_length require an
integer max_pattern_length of at least 2, the shortest repeating pattern.
Booleans, fractional values, and smaller lengths raise ValueError instead of
silently scoring zero; the CLI rejects --longstring-max-pattern-length values
below 2.
autocorrelation (opt-in) correlates each respondent's responses with lagged
copies of themselves (Gottfried et al., 2022; responsePatterns::rp.acors). For
each lag k from min_lag (default 1) to max_lag, the responses r[0:n-k] are
correlated (Pearson) with r[k:n]. The score is the largest absolute lag
correlation (statistic="max_abs", the default) or the sum of absolute lag
correlations ("sum_abs"), and higher values are flagged. Zigzags
(1-2-3-4-5-4-3-2), seesaws, and short cycles correlate strongly at their period
even when some responses are perturbed, which exact pattern matching
(longstring_pattern) misses and transition entropy (markov) tolerates less
well as noise grows. Items must be in presentation order and on one response
scale; keep reverse-worded items as presented.
The default max_lag=10 deliberately departs from the R default of J - 3
lags: long lags pair few responses, and their noisy correlations dilute the
signal of short cycles. In a simulated 40-item survey with 1,000 attentive and
150 careless zigzag, seesaw, and cycle responders whose responses were 20%
randomly perturbed, max_abs separated the groups with an AUC of 0.96 at
max_lag=10 but 0.75 at J - 3. Keep max_lag well below the item count on
short questionnaires; max_lag=None restores the R default for each respondent.
Each lag needs at least three response pairs, so a respondent's usable lags stop
at their number of observed responses minus 3, and respondents with fewer than
min_lag + 3 observed responses are unavailable (NaN). Following the
zero-variance rule of rp.acors, a lag whose window r[0:n-k] or r[k:n] does
not vary scores 1, a perfectly repetitive pattern. Respondents whose responses
are all identical therefore score 1 at every usable lag, and a straight-liner
whose first or last response differs still scores 1 at every lag; a respondent
who answers a few items and then straight-lines scores 1 at the lags whose
second window falls within the run. Straight-lining thus overlaps with
longstring. With na_rm=True, missing responses are removed in sequence order;
na_rm=False leaves respondents with any missing response unavailable. Rows
containing infinite responses are unavailable. return_lags=True also returns
the lag with the largest absolute correlation, with ties going to the smallest
lag. Exact ties can differ by floating-point rounding, so correlations within a
relative 2**-42 of 1 score exactly 1 and lags within that tolerance of the
largest correlation count as tied: a perfect 1-2-3-4-5-4-3-2 zigzag reports lag
4, not 8. Registry scoring uses IndexOptions.autocorrelation_max_lag and
IndexOptions.autocorrelation_statistic, and the CLI accepts
--autocorrelation-max-lag (none for the R default) and
--autocorrelation-statistic. autocorrelation_flag() flags the high tail.
Window moments come from prefix sums of centered responses in bounded batches.
Nearly constant windows are recomputed from their own centered responses, so
very large offsets, tiny or huge response scales, and large integers keep
accurate correlations.
gpoly, u3poly_fit, and ht (all opt-in) are the nonparametric person-fit
statistics of the R package PerFit (Tendeiro et al., 2016), available directly as
gpoly(), u3poly(), and ht(). They ask whether a respondent's answers follow
the item ordering of the whole sample. Each item with M + 1 ordered categories
is split into M item steps X_j >= h, and a step's popularity is the share of
respondents who pass it. Steps are ordered from most to least popular; ties keep
item order and then step order, like PerFit's rank(ties.method = "first"), so
the steps of one item stay nested. Niessen, Meijer, and Tendeiro (2016) found
polytomous Guttman errors effective for detecting careless respondents, and Ht
was among the best-performing statistics in Karabatsos (2003).
gpoly()counts polytomous Guttman errors (Molenaar, 1991): pairs of steps where the respondent fails the more popular step but passes the less popular one, PerFit'sGpoly.normalize=True(the default, and the registry score) divides by the largest count any response pattern with the same total score can reach (Emons, 2008), PerFit'sGnormed.poly; for dichotomous items this isG / (r * (J - r)). Unlikeguttman(normalize=True), which divides by every item pair, the normalized value is not confounded with the total score. Counts and maxima are exact integers. Each maximum passes every step of some items and part of at most one more, so all maxima takeO(J * M)work.u3poly()weights each step by the log-odds of its popularity and scores(W_max - W) / (W_max - W_min), whereWsums the weights of the passed steps andW_maxandW_minare its extremes over all nested patterns with the same total score (van der Flier, 1982; Emons, 2008), PerFit'sU3poly. Scores run from 0 for a perfect Guttman pattern to 1. A step passed by every respondent, or by none, is weighted as if half a response went the other way. Weights share one integer fixed-point scale, so the extremes, perfect patterns, and the lowest-weight patterns are exact.ht()is the transposed scalability coefficient (Sijtsma & Meijer, 1992), PerFit'sHt, for responses coded 0 and 1; other values raiseValueError. It divides the summed covariances, across items, between a respondent's answers and everyone else's by the sum of their maxima given each pair's numbers of correct answers. Lower values are flagged. It is computed in exact integers inO(N * J)time, without theN-by-Ncovariance matrix.
Responses must be integer categories on one scale shared by all items. The
scale runs from scale_min to scale_max (IndexOptions.scale_min /
scale_max), inferred from the observed minimum and maximum when omitted, and
ncat (IndexOptions.person_fit_ncat, CLI --person-fit-ncat) sets the number
of categories from either endpoint, or from the observed minimum when neither is
given. Pass scale_min when the lowest category may be unobserved, since PerFit
codes categories from 0. Fractional responses, including extended-precision
values within rounding of an integer, responses outside the scale, and scales
with more than 1024 categories raise ValueError. Like guttman, these
statistics assume every item is keyed in one direction; recode reverse-worded
items with reverse_score() first, or set IndexOptions.reverse_keyed_items so
the registry recodes them for these indices. With person_fit_ncat set, the
registry reflects the items for gpoly and u3poly_fit on the scale that ncat
declares, so a category nobody chose still sets the reflection. ht and the
other keyed indices keep scale_min and scale_max, inferred when omitted, and
read a second recoded copy unless both endpoints are given.
Scores are NaN when the total score allows no misfit: every step failed or
passed for gpoly and u3poly, and every or no item answered correctly for
ht. PerFit's Gnormed.poly and U3poly score those all-minimum and
all-maximum response vectors 0 instead. Step popularities use each item's
available responses. With na_rm=True, raw gpoly(normalize=False) counts only
pairs of observed steps, while normalized gpoly, u3poly, and ht leave
respondents with missing responses unavailable (NaN); ht also leaves them out
of everyone else's comparisons. na_rm=False raises ValueError for missing
responses. PerFit's default NA.method = "Pairwise" instead scores incomplete
respondents from their observed items, and only its Hotdeck, NPModel, and
PModel methods impute missing responses, so incomplete respondents differ.
gpoly_flag() and u3poly_flag() flag the high tail and ht_flag() the low
tail (default 5th percentile).
In five simulated samples of 1,000 graded-response-model respondents and 100
uniform random responders on 30 five-category items, normalized gpoly
separated the groups with AUCs of 0.93–0.99, raw counts 0.97–0.99, and u3poly
0.92–0.98. On 20 two-parameter logistic items, ht reached 0.98–1.00, beside
0.98–1.00 for lz.
u3_poly is unrelated to PerFit's U3 despite its name: it is the proportion of
extreme (endpoint) responses, a response-style index. Use u3poly() or the
registry's u3poly_fit for the person-fit statistic.
semantic_ant reverse-scores the second item in each configured pair before
computing consistency. Pass scale_min and scale_max through IndexOptions
when the matrix does not contain both response-scale endpoints; otherwise the
bounds are inferred from the observed data.
Explicit item_pairs for MAD and semantic consistency must contain exactly two
integer column indices per entry. Fractional values, Boolean values, and
out-of-range positions are rejected. Semantic pairs must refer to distinct
items; repeated pairs and ordering are preserved.
mad also reverse-scores the second item in each pair. Provide
mad_scale_min and mad_scale_max when observed responses may omit a scale
endpoint or use fractional endpoints. Higher MAD values mean greater paired
inconsistency. Positive and negative item lists must contain the same number of
items; mismatched lists are rejected rather than truncated. The standalone
semantic_syn_flag() and semantic_ant_flag()
helpers flag unusually low consistency scores.
Pair scoring avoids overflowing reverse-score endpoint sums and intermediate
differences for large finite responses. Semantic consistency can retain a finite
normalized score even when the unnormalized mean difference exceeds float range.
Large integer pairs preserve differences before floating-point conversion.
Floating pairs preserve the rounding residual of reverse scoring, so swapping
pair endpoints does not erase a small difference. Semantic normalization happens
before exceptionally small means receive their final rounding.
When row variation approaches underflow, bounded power-of-two scaling preserves
the ratio of paired differences to standard deviation, including inferred
antonym bounds. Constant and unavailable rows retain their existing behavior.
missing_rate is opt-in because planned skip logic and matrix preprocessing can
create legitimate omissions. Use IndexOptions.missing_item_indices to restrict
registry scoring to a fixed required-item subset, or pass the same subset as
item_indices to the standalone helper. For respondent-specific skip logic,
provide a Boolean missing_applicable_mask through IndexOptions or
applicable_mask directly. False cells are excluded from both the numerator and
denominator; rows without applicable selected items return NaN and are not
flagged. The CLI exposes fixed subsets through --missing-item-indices and
respondent-specific masks through --missing-applicable-mask PATH.
infrequency preserves its historical missing-response behavior with
missing="pass": unanswered checks do not count as failures and remain in a
proportion denominator. Choose "fail" for conservative scoring, "omit" for
available-case proportions, or "propagate" to require complete attention-check
data. Configure registry scoring with IndexOptions.infrequency_missing and the
CLI with --infrequency-missing. The standalone infrequency_flag() can flag
either counts or proportions.
Instructed items ("select Strongly Agree") have one correct answer, supplied
through expected_responses. Bogus and infrequency items (Meade & Craig, 2012)
usually accept several answers: both disagreement categories
are correct for "I have been to every country in the world", and only agreement
indicates inattention. Self-reported diligence or "use my data" items likewise
accept a band of ratings. Supply one inclusive (low, high) range per item
through keyword-only acceptable_ranges instead, with -inf or inf for an
open end:
from ier import infrequency
# Item 3: bogus item on a 1-5 scale; disagreement (1-2) is correct.
# Item 7: diligence self-report on a 1-7 scale; ratings of 5 or more pass.
failures = infrequency(
responses, item_indices=[3, 7], acceptable_ranges=[(1, 2), (5, float("inf"))]
)
Exactly one of expected_responses or acceptable_ranges must be given. Range
bounds must be real numbers with low <= high; NaN bounds are rejected.
Integer responses compare against the integer categories inside each range, so
fractional bounds round inward and large integer bounds stay exact; a range
containing no category fails every observed response. Single-precision responses
are compared with double-precision bounds. Missing responses follow the
configured missing policy. Registry scoring uses
IndexOptions.infrequency_acceptable_ranges, which can replace
infrequency_expected_responses (the catalog lists the two as an
alternative_options group), and the CLI accepts
--infrequency-acceptable-ranges '1:2,5:', with an empty side for an open end.
The two answer options cannot be combined. The command line can mistake a
separate argument that starts with - for another option, so write an open
lower end as :-1 and attach values that begin with a minus sign with =:
--infrequency-acceptable-ranges=-3:-1 or
--infrequency-expected-responses=-1,2.
Reverse-keyed items¶
Consistency and person-fit indices assume that every item is keyed in the same
direction, so reverse-worded items must be recoded before scoring. Sequence and
response-style indices measure the responses as presented, and recoding would
distort them: after recoding, a straight-liner no longer gives identical answers
and a respondent who agrees with every item no longer looks acquiescent. In a
simulated survey of six eight-item scales with a third of the items
reverse-worded, the classic even-odd index (method="halves") separated 100
uniform random responders from 1,000 attentive respondents with an AUC of 0.62
on raw data and 0.96 on recoded data, and scale-aware individual_reliability
with 0.54 and 0.99. Against 100 straight-liners, even-odd was unavailable for
every straight-liner on raw data and reached an AUC of 1.00 on recoded data,
while longstring reached 1.00 on raw data and 0.17 on recoded data. No single
matrix serves both groups.
Set IndexOptions.reverse_keyed_items to the 0-based columns of the
reverse-worded items, and screen(), composite(), and the other composite
helpers recode them once with reverse_score() for the indices that need keyed
responses, while every other index reads the matrix as given:
from ier import IndexOptions, screen
options = IndexOptions(
scale_min=1,
scale_max=5,
evenodd_factors=[8, 8, 8],
evenodd_method="halves",
reverse_keyed_items=[1, 4, 7, 9, 12, 15, 17, 20, 23],
)
result = screen(data, indices=["evenodd", "guttman", "longstring", "irv"], options=options)
The CLI accepts the same list with --reverse-keyed-items 1,4,7 on ier screen
and ier composite, and a configuration file accepts
reverse_keyed_items = [1, 4, 7].
| Responses | Indices | Reason |
|---|---|---|
| Recoded | evenodd, individual_reliability |
Split halves of a scale agree only when all of its items point the same way |
| Recoded | guttman, lz, gpoly, u3poly_fit, ht |
Cumulative, item-step, and item response models assume every item increases with one trait |
| As presented | irv, longstring, longstring_pattern, avgstr, markov, autocorrelation, onset |
They measure the response sequence as given |
| As presented | acquiescence, u3_poly, midpoint |
Agreement uses raw responses; extreme and midpoint categories are unchanged by reflection |
| As presented | mad, semantic_ant |
Each pair already reverse-scores its second item |
| As presented | psychsyn, psychant, semantic_syn |
Pairs are discovered or configured on the presented items; antonym pairs come from reverse-worded items |
| As presented | person_total |
The reference profile is the sample's own item means |
| As presented | mahad |
Reverse scoring is affine per item, so distances do not change |
| As presented | infrequency, missing_rate |
Expected answers refer to presented responses, and recoding preserves missingness |
index_catalog() and ier indices report the same split as
uses_keyed_responses. Bounds come from scale_min and scale_max and are
inferred from the whole matrix when omitted, so pass them whenever an endpoint
might be unobserved. Items must be distinct in-range columns and every recoded
response must lie within the bounds. Otherwise each selected keyed index fails
with a reverse_keyed_items could not be applied message, recorded in the
result's errors like other configuration failures or raised with strict=True,
while the other indices are still scored. The recoded copy is made once per run
and shared by the keyed indices, including with workers above 1, and
reverse_score() works through the selected columns in row batches of a few
megabytes, so peak memory grows by about one recoded matrix only when the option
is set and a keyed index is selected. A second copy is made only when
person_fit_ncat is set without both scale bounds and gpoly or u3poly_fit
is selected alongside another keyed index, because the person-fit indices then
reflect items on their declared category scale. That matrix has the dtype reverse_score()
returns: float64 for floating responses, twice the size of a float32 input, and
usually the input dtype for integer responses. On a 50,000 × 60 float64 survey
(24 MB) with a third of the items reversed, screen() with evenodd peaked
24 MB higher with the option than without it. Without a keyed index the option
is ignored, like other index-specific options.
Response-time indices (standalone — not in the registry)¶
These helpers take timing matrices (durations), not item-response matrices.
They are intentionally excluded from screen() / composite() so item scores
and timestamps are never mixed by accident. Compute them separately and merge
flags in your analysis code if needed.
| Function | Signal | Typical flag |
|---|---|---|
response_time |
Central tendency of RT | low (too fast) |
response_time_consistency |
RT coefficient of variation | low (too uniform) |
response_time_flag |
Percentile / threshold flagging | low |
response_time_mixture |
Stable mixture P(fast component) | high |
response_time_score_flags |
Reflag retained direct or mixture scores | low or high |
response_time_effort |
Share of answered items at or above item thresholds (RTE) | low (rapid responding) |
response_time_effort_flag |
Fixed RTE cutoff flagging (default below 0.90) | low |
Response-time medians omit missing items and return NaN for entirely missing
respondents. Middle pairs preserve finite medians at very large or small scales;
float32 pairs are averaged in double precision and integer pairs are averaged
before rounding the result to a float.
Response-time coefficients of variation preserve dimensionless ratios even for varying subnormal observations, avoiding a zero or undefined result caused only by separately rounding the mean and standard deviation. Positive constant rows still have zero variation; zero means and unavailable observations retain their documented arithmetic behavior.
response_time_mixture() uses positive, finite respondent medians and leaves
other respondents' probabilities NaN. Its n_components must be an integer
of at least two, including NumPy integer scalars, and cannot exceed the number
of usable medians.
Constant usable medians return 1 / n_components, since the timings do not
distinguish a fast group. Fits preserve finite probabilities at very large timing
scales, including with log_transform=False. The variance floor remains 1e-10
in the fitted units (log timing by default, raw timing otherwise), so changing raw
timing units can affect fits whose variation is near that floor. Components with
negligible fitted mass cannot be selected as the fast group.
response_time_effort() computes response time effort (RTE; Wise & Kong, 2005):
the proportion of a respondent's answered items whose response time is at or
above that item's threshold. The person-level summaries above reduce each row to
one number first, so item length and difficulty confound them; RTE judges each
response against its own item, so a respondent who skims long items while
answering short ones at a normal pace still loses effort. Without explicit
thresholds, every item uses the normative NT10 threshold (Wise & Ma, 2012):
normative_fraction (default 0.10) times the item's exact mean time over the
respondents who answered it. max_threshold caps every threshold, for example
at the 10 seconds Wise and Ma recommend. Pass thresholds as one shared value or
one value per item, in the units of the timing matrix, to apply another rule.
A response is rapid when its time is strictly below the threshold. Missing
times are unanswered items. Items whose threshold, after any max_threshold
cap, is missing, infinite, or not positive, including items nobody answered, are
excluded from every respondent's denominator, and respondents without an
answered eligible item are unavailable (NaN). This applies to explicit
thresholds as well as normative ones, so NaN, inf, and -inf thresholds
exclude their items, except that max_threshold caps inf like any other
threshold. Integer and float32 times are compared with double-precision
thresholds. return_item_flags=True also returns the Boolean matrix of rapid
responses for effort-moderated scoring. response_time_effort_flag() flags RTE
strictly below threshold (default 0.90) and never flags unavailable
respondents. Like the other timing helpers, these functions are not registry
indices.
from ier import response_time_effort, response_time_effort_flag
effort, rapid = response_time_effort(times, max_threshold=10.0, return_item_flags=True)
scores, low_effort = response_time_effort_flag(times, threshold=0.90, max_threshold=10.0)
ier response-time --metric effort scores RTE from a timing file.
--effort-fraction (default 0.10) and --effort-max-threshold set the
normative thresholds; --effort-threshold instead applies one shared item
threshold, in the timing units, and cannot be combined with either.
Effort follows Wise and Kong's absolute rule rather than a sample percentile:
without a cutoff option it flags RTE strictly below 0.90, matching
response_time_effort_flag(). RTE is a proportion of answered items, so many
respondents share exact values such as 1 or 9/10; a sample percentile would move
with those ties and with how many respondents rushed, while a fixed cutoff keeps
one meaning across samples. --threshold replaces the cutoff with another RTE
value between 0 and 1, still flagged strictly below, and --percentile applies
a tie-exclusive sample cutoff. NPZ output records the metric and its decision,
and ier response-time-scores reflags saved effort scores with the same strict
rule.
ier response-time timings.csv --metric effort --effort-max-threshold 10
ier response-time timings.csv --metric effort --effort-threshold 2 --threshold 0.8
ier response-time timings.csv --metric effort --format npz --output effort.npz
ier response-time-scores effort.npz --threshold 0.95 --format csv
Persisting screening results¶
save_screen_archive() stores a complete screen() or screen_scores() result
in the same versioned, pickle-free NPZ schema as ier screen --format npz.
load_screen_archive() restores the ScreenResult from either source, so a
saved run can be inspected or plotted without the original responses:
from ier import load_screen_archive, plot_flag_counts, save_screen_archive, screen
result = screen(data, indices=["irv", "longstring", "onset"], percentile=95)
save_screen_archive("screening.npz", result, respondent_ids=ids, compressed=True)
saved = load_screen_archive("screening.npz")
restored = saved["result"]
print(restored["threshold_sources"], saved["respondent_ids"][:3])
figure = plot_flag_counts(restored)
The loader recomputes each index's flags from its scores and recorded cutoff
(fixed thresholds include ties, percentile cutoffs exclude them, presence rules
flag available scores) and rejects archives whose flags, counts, consensus
decisions, summary counts, flag rates, or score minima and maxima disagree.
Summary means and standard deviations are rebuilt from the restored scores, so
a loaded summary always describes the loaded scores. Reload the archive to keep
the original decisions: passing restored["thresholds"] back to
screen_scores(thresholds=...) turns percentile cutoffs into inclusive fixed
thresholds and flags tied scores.
Use load_score_archive() when only the raw scores are needed for new cutoffs.
Plot helpers¶
Requires insufficient-effort[plot]:
plot_distributions(screen_result)plot_flag_counts(screen_result)plot_flagged_heatmap(screen_result)plot_index_agreement(screen_result, kind="jaccard")plot_composite(scores, threshold=None, flags=None)mahad_qqplot(...)
Distribution panels draw each applied cutoff as a dashed line and shade the tail
that index flags: left of the cutoff for low-direction indices and right of it
for high-direction indices. Panel titles report flagged counts. Presence-flagged
onset and indices without any available scores have no cutoff line, and
unregistered score names get a line without shading. Pass
show_thresholds=False for plain histograms.
Heatmaps use a fixed flag color scale, so fully flagged and wholly unflagged
cohorts remain visually distinct. Unavailable scores appear gray with a legend.
For onset, an absent detection remains unflagged because NaN represents no
detected event. Automatic heatmap dimensions are capped at 18 × 12 inches; pass
figsize to choose different dimensions. A figure has only about a thousand
pixel rows, so drawing every respondent of a large survey would silently drop
most rare flags. Surveys with more than max_rows=1000 respondents are instead
drawn as consecutive respondent blocks of ceil(n_respondents / max_rows). A
block is flagged when any of its respondents is flagged and gray only when all
of them are unavailable, so a single flag among 100,000 respondents stays
visible. Pass max_rows=None for one row per respondent, or order="flag_count"
to sort respondents by descending flag count first so flagged respondents
cluster at the top. In a local Agg comparison, a 1,000,000-respondent,
11-index heatmap rendered in 0.15–0.42 s instead of 1.7–2.2 s, and showed
610–660 flagged pixel rows per index instead of 0–4. Flag-count charts aggregate
respondents in a single pass and retain bins for every selected index.
plot_index_agreement() draws index_agreement() with fixed color limits:
[0, 1] (viridis) for Jaccard similarity and for overlap, shown as the share
of respondents flagged by both indices, and [-1, 1] (RdBu_r) for Spearman
correlations. Spearman scores are oriented by flag direction, so red cells mean
both indices rank the same respondents as more suspicious and blue cells mean
they disagree. Undefined cells are gray. plot_composite() draws a composite
score histogram with an optional cutoff and shaded high tail, matching
composite_flag(). Its legend counts supplied flags, or scores at or above the
cutoff when no flags are given.