Architecture¶
This note explains how IER orchestrates indices, handles missing data, and flags respondents. It is aimed at contributors and methods-curious users.
Layers¶
CLI / examples
│
screen() / composite() ← public orchestration
screen_scores() / composite_scores() / response_time_score_flags()
← reusable decision layer
│
IndexOptions + registry ← shared config + index catalog
│
per-index modules ← irv, longstring, mahad, …
│
_validation / _flagging ← shared input checks and threshold helpers
- NumPy-first core. Base installs depend only on NumPy. Statistical routines, including chi-square flagging, response-time mixtures, and IRT theta estimation, are implemented locally. Matplotlib is only required for plotting helpers.
- Registry.
src/ier/_registry.pymaps string names to scorers, default screen/composite membership, flag direction, and requiredIndexOptionsfields (e.g.evenodd_factors). Missing required fields become soft errors whose messages derive from that metadata, so the published catalog and the enforced configuration cannot drift; infrequency keeps a custom check because acceptable ranges can replace expected responses. Per-index thresholds, percentiles, and weights share one override validator. - Keyed input. Specs with
keyed_input=True(evenodd,individual_reliability,guttman,lz,gpoly,u3poly_fit,ht) assume every item is keyed in one direction, and the catalog publishes the flag asuses_keyed_responses. WhenIndexOptions.reverse_keyed_itemsis set and such an index is selected,score_registered_indices()builds one reverse-scored copy before sequential scoring or thread submission and passes it only to those scorers;reverse_score()checks and recodes the selected columns in bounded row batches, so that copy is its only full-size allocation. Other indices read the validated input without a copy, and a failed recoding is a soft error of each keyed index, like other configuration errors. - Soft per-index errors.
score_registered_indices()catches validation / runtime failures per index and returns them in anerrorsdict instead of aborting the whole screen. Public orchestration APIs also acceptstrict=Trueto raise a contextual error as soon as a selected index fails. Composite callers still raise if no index succeeds under the default soft policy. - Opt-in parallelism. Registry orchestration evaluates indices sequentially
by default.
workers>1uses a lazily imported standard-library thread pool, then records scores and failures in selection order. This keeps default import cost and resource use stable while allowing NumPy-heavy scorers to overlap. NumPy wheels bundle a multithreaded OpenBLAS, and every concurrently scored index can start its own full set of BLAS threads. Default BLAS threading did not slow single calls on a quiet machine, butscreen(workers=4)was about twice as slow as with single-threaded BLAS. SetOPENBLAS_NUM_THREADS=1(andOMP_NUM_THREADS=1for OpenMP builds) when usingworkers>1. Benchmark headers print both variables so recorded timings remain reproducible. - Bounded reductions. Screening flag counts, valid-score counts, and summary coverage share one index-at-a-time pass; composite mean, sum, and maximum values use the same bounded approach. Orchestration retains its documented per-index result vectors but does not construct another complete respondent-by-index matrix for final reductions.
- Composite reductions. Component calibration and all three reduction methods
share
_composite_reductions. Ordinary weighted means and sums normalize safe common weight units. Instability triggers bounded respondent batches that scale numerator products and, for means, available weight totals independently. Maximum repairs compare product signs, exponents, and mantissas before converting the winner, allowing oversized negative contributions to be discarded. Exact scalar arithmetic repairs severe cancellation and subnormal rounding. Final sums and maxima outside the finite float range raise a contextual error; respondents below the requested coverage minimum retainNaN. Calibrations retain only scalar metadata between components, allowing repair without another complete respondent-by-index matrix. Single-component reductions own their output buffer, and means omit the cancelling weight. Unweighted mean summaries reuse coverage counts as their denominator. Ordinary component calibration also returns its availability mask, using a scalar complete-coverage marker to enable unmasked accumulation and scalar count increments. Sum presence vectors are released once a complete component supplies every respondent. Cancellation checks use boolean comparisons rather than a full absolute-value float buffer. - Score statistics. Shared summaries reuse stable row means, deviations, and medians on observed score vectors. Screening and composite summaries reuse complete vectors directly. Composite standardization owns a scaled observation buffer, shifting nearby values before centering to preserve small differences from a common baseline and leaving caller-owned arrays untouched.
- Paired-item validation. Balanced acquiescence and MAD share one ordered-pair boundary for integer conversion, matrix bounds, and equal cardinality. Their bounded numeric kernels therefore receive exact pairs and never discard a trailing configured item.
- Correlation workspaces. Row correlations used by even–odd, person-total, psychometric consistency, and individual reliability scoring use bounded respondent batches. Two paired observations use their relative order directly; longer vectors use centered reductions with separate norm divisions and rescaling for extreme magnitudes. Missing pairs and zero-variance policies remain distinct. Complete person-total batches reuse a single centered item-mean profile and its norm instead of allocating the same profile for every respondent. Exceptional floating profiles are shifted and scaled before averaging; severe cancellation uses exact Decimal means. Missing respondents that select a narrowly separated part of a wider profile receive a separately repaired profile with the original item observation counts. Identical missing masks reuse a capped cache, and all response workspaces remain bounded.
- Reusable decisions.
screen_scores()reapplies direction-aware flagging, completeness, consensus, and summaries to retained registered score vectors.composite_scores()reuses raw composite-enabled vectors for alternative weights, standardization, completeness, and reductions.response_time_score_flags()reapplies low- or high-tail cutoffs to retained direct timing scores or mixture probabilities. All three support sensitivity analysis without rerunning scorers.
Command-line boundaries¶
The command-line path is split by responsibility:
cli.pydefines arguments, converts index options, and registers one handler per command withset_defaults(handler=...). Handlers share input loading, cutoff flagging, and a single text, JSON, CSV, or NPZ output dispatch._cli_config.pyconverts--configTOML tables into arguments for the running command, rejects two keys that set one option, validates them with that command's own argparse actions and the list and table parserscli.pysupplies, and fills only the options the command line left unset._cli_streams.pyowns suffix-selected, standard-library text compression._cli_input.pyowns forward-only delimited input, preamble removal, named-column selection, and memory-mapped NumPy input. An optional report records delimiter, header, and column decisions outside the row loop._cli_inspect.pysummarizes those decisions, missing cells, and observed values forier inspectin bounded row blocks._cli_composite.pydefines the composite and response-time reports that every serializer receives, validating respondent alignment and flag metadata once._cli_output.pyrenders text plus bounded strict JSON and CSV results._cli_npz.pyassembles composite result payloads for the low-level typed, pickle-free NumPy writer and delegates screen and response-time results to the public archive writers._atomic_output.pystages regular result files for replacement after every stream closes, sharing permission and symbolic-link handling between text and archive writers. Special-file destinations retain direct streaming.archive.pyowns the shared archive stream writer and public validated save/load boundaries for reusable registered score vectors, complete screen results, and response-time results._archive_input.pychecks bounded NPY headers, shape ranges, and actual payload sizes before allocating arrays. Stored ZIP members use their physical byte boundaries; compressed members decode once in bounded chunks and must produce exactly the declared payload size.
Screen and composite commands carry the registry's ordered soft-failure map through text, JSON, and NPZ serializers and mirror failures to standard error for every format. Score computation still runs once, and CSV remains a compact respondent table.
Detailed composite output is explicit through --include-components. The CLI
reuses composite_summary() so scoring still runs once, JSON wraps each
component in the bounded array writer, CSV converts bounded row chunks, and NPZ
writes separate typed members without stacking another respondent-by-index
matrix. The default aggregate-only path does not allocate or serialize these
details.
Composite standardization is resolved before either aggregate-only or detailed scoring begins. The same boolean is passed to the public scoring API and written to text, JSON, and NPZ metadata, avoiding a second calculation or inference from the resulting values. CSV intentionally remains a respondent-only table.
Optional composite flagging runs after aggregate scoring and reuses that score vector. Cutoff resolution and boolean comparison use the same shared helpers as the public flagging APIs. JSON writes the flag vector in bounded chunks, CSV streams it in row chunks, and NPZ stores it as a boolean member; no component is rescored and the score-only path allocates no flag vector.
Keeping parsing, matrix construction, serialization, and orchestration separate makes format-specific changes independently testable without adding runtime packages or coupling them to statistical index implementations.
IndexOptions¶
Orchestration APIs accept configuration only via
options=IndexOptions(...). That keeps screen() / composite() signatures
stable as indices grow. Per-function kwargs remain available when calling an
index module directly (e.g. mahad(x, method="iqr")).
Flagging policy¶
screen() turns scores into boolean flags using registry metadata:
| Mode | Typical indices | Rule |
|---|---|---|
| Low / high percentile | IRV, longstring, mahad, … | Extreme tail relative to the sample |
| Fixed low / high threshold | Any percentile-mode index | At or beyond a validated cutoff |
| Presence | onset |
Any detected changepoint is flagged |
Percentile defaults are sample-relative heuristics, not calibrated
diagnostic cutoffs. screen(thresholds=...) accepts fixed cutoffs when a survey
or validation study provides them, while screen(percentiles=...) tunes
sample-relative tail sensitivity by index. Results retain the actual cutoff,
its fixed/percentile/presence source, and the requested tail percentile. See
Threshold Guidance.
Sample cutoffs use linear interpolation at rank (n - 1) * percentile / 100
among available scores. The shared selector partitions an owned observation
buffer without reordering retained scores. Interpolation uses double precision,
with exact scalar repairs for overflowing spans, subnormal differences, and
integer endpoints. Percentiles 0 and 100 use the observed minimum and maximum;
an entirely unavailable sample retains cutoff zero and produces no flags.
Integer score comparisons preserve exact values, and single-precision arrays
compare against the double-precision cutoff without narrowing it.
Missing data¶
Most indices honor IndexOptions.na_rm (default True) and document
row-wise vs listwise behavior in their docstrings. Policies are intentionally
not identical across indices: IRV can use nanstd, Mahalanobis may drop
incomplete rows, Markov may require complete sequences depending on na_rm.
When comparing to R packages, align NA policy first.
The opt-in missing_rate index separates observed missingness from survey
applicability. A fixed missing_item_indices subset works through registry and
command-line scoring. Python callers can additionally provide a Boolean
respondent-by-item applicability mask; its false cells do not contribute to
either the missing count or denominator. A zero denominator yields NaN, which
the shared flagging policy leaves unflagged.
Attention-check scoring keeps its legacy missing-as-pass behavior by default but
also supports missing-as-failure, available-case omission, and strict propagation.
The policy is carried through IndexOptions; unavailable scores reuse the same
flagging and composite-coverage rules as every other registry index.
Missing-response and attention-check scoring share ordered item-selection
validation. Both select columns and reduce responses in bounded respondent
batches; missing-response scoring selects applicability cells within the same
batch instead of copying the entire selected mask. Attention checks compare
selected items together and write directly into a single result vector.
Expected attention-check answers are prepared once in the response dtype for
integer inputs, retaining adjacent categories near signed and unsigned 64-bit
limits. Fractional and out-of-range answers cannot match integer responses;
integer answers that the floating comparison dtype cannot represent cannot
match rounded neighboring responses. Integer and boolean response batches
skip missing-value scans and use complete-check denominators for every policy.
Complex expected answers are rejected before conversion can discard their
imaginary part.
Screening counts available scores alongside flags without stacking either set of
vectors. An optional min_valid_indices rule marks rows with insufficient score
coverage as ineligible for consensus while retaining the individual scores,
flags, counts, and eligibility decision for audit.
Composite scores¶
composite() z-combines selected indices with direction multipliers so that
higher composite values mean more evidence of careless responding.
composite(), composite_flag(), composite_summary(), and
composite_probability() share one validation, scoring, and reduction
pipeline, and the precomputed composite_scores*() helpers reuse its
reduction step, so every entry point applies identical directions, weights, and
completeness rules.
Optional positive weights are applied after direction correction and
standardization. Weighted means renormalize over available scores per
respondent, so an unavailable index does not silently dilute the remaining
evidence.
Direction multipliers and weights are applied one vector at a time during the
reduction. Raw component vectors remain available for detailed summaries, while
ordinary and reusable composite paths avoid retaining a second mapping of
direction-corrected arrays.
An optional minimum valid-index rule masks under-supported respondent scores
after reduction. Equal-weight means reuse their existing denominator counts, so
the rule needs no additional respondent-sized workspace on that path; other
methods allocate one integer count vector only when the rule is enabled.
Mixed finite and infinite component observations have an unavailable calibration
and contribute no coverage. Single-component reductions count the calibrated
values consistently with multiple-component reductions. Exceptional reduction
batches separate already infinite scores from finite scaled arithmetic, then
repair rows in their original order. Sums and maxima report unrepresentable
eligible results with the respondent index; negative infinite maximum candidates
can lose to finite winners, and raw means retain IEEE infinite-value behavior.
composite_probability() applies a logistic transform for convenience — it is
not a calibrated probability of carelessness. Do not treat it as a
posterior or diagnostic probability without your own validation study. Its
shared piecewise NumPy kernel evaluates positive and negative values separately,
avoiding overflow while preserving finite-tail precision and exact infinite
endpoints. The lz theta and likelihood paths reuse the same kernel for both
complete and missing-response batches. Each bounded batch excludes missing
items from the ability score equation, information, and likelihood reductions
when na_rm=True; entirely missing rows remain unavailable. With na_rm=False,
missing responses propagate through ability estimation. This shares one solver
and likelihood implementation across both input paths without per-respondent
Python loops. The safeguarded Newton solver drops each row from its workspace
once its score or step converges, so the few roots beyond the [-4, 4] bracket,
which bisect toward a bound for dozens of iterations, no longer keep the whole
batch iterating. Row reductions are independent and each iterate keeps the
precision of its starting estimate, so every estimate is unchanged, including
those for float32 and float16 binary inputs.
Binary response matrices are reused without modification; polytomous inputs
allocate a converted matrix and fill it in bounded blocks. Difficulty estimation
shares the bounded item-mean reduction used by Guttman and person-total scoring,
and discrimination estimation uses bounded row totals. Discrimination estimates
reduce item means, item-specific total-score means, and centered cross-products
in bounded respondent batches, replacing per-item sorting and correlation
matrices. The binary-item variance uses n * p * (1 - p); total-score variance
uses centered observations to preserve constant-score fallbacks even when
different items have different observed respondents. Both centered buffers
share the element budget. These reductions avoid full-matrix NaN replacement
copies. Items with no observed responses have unavailable difficulty estimates
and are omitted when na_rm=True.
The CLI computes this transform from the final aggregate vector only when
--include-probability is requested. JSON and CSV then serialize it
forward-only, while NPZ stores one additional typed vector; index scoring is not
repeated and default output schemas remain unchanged.
Response-time helpers¶
Timing matrices are a different data modality than item responses. Helpers in
response_time.py are public but intentionally outside the screen /
composite registry so they are not mixed into item-response pipelines by
accident. Gaussian-mixture fitting reuses responsibility and scratch buffers
throughout EM, with contiguous component columns and a separate returned probability
vector so other component buffers can be released. Its expectation step follows a
fast probability-space path for ordinary observations and normalizes subnormal or
overflowed densities in log space. Very narrow components also use log densities
so their large normalization factor cannot amplify an underflowed exponential.
Only unrepresentable or nearly tied extreme log
densities need Decimal arithmetic. Large timing magnitudes are scaled by powers
of two, nearly constant data are shifted before fitting, and component standard
deviations retain the variance floor in the original fitting units. Weighted
deviations rescale exceptional residuals before squaring. Empty components receive
zero weight and cannot supply the fast component; constant medians return equal
component probabilities without iterating EM.
Retained summary vectors and mixture probabilities pass through the same shared
single-vector validation and threshold boundary, so cutoff sensitivity analysis
does not recalculate row summaries or refit EM.
Markov transition entropy discovers and encodes categories once per bounded
sequence block, reusing the encoded values for transition counting. Dense tables
contain only the block's observed states, up to 64, and serve blocks whose state
square stays within a multiple of k log2 k for rows of k items, the cost of
sorting a row. The multiple depends on the NumPy major version: 0.5 with NumPy
2's vectorized sorts and 1.5 with NumPy 1.x, chosen so the sorted kernel is used
only where it measured at least as fast; long rows therefore keep dense tables.
Other blocks sort each row's source states and transition pairs and sum
c log2 c over runs of equal values with unmasked pairwise summation, so their
workspace grows with the items rather than the states and long rows keep full
precision.
Integral labels retain their original precision, including adjacent 64-bit
integers beyond floating-point precision. Both paths evaluate the equivalent
count form of conditional entropy.
Longstring and repeating-pattern scoring share bounded sequence preparation for
complete and missing-response inputs. Batches with missing responses compact
observed values in their original order; NaN padding and observed lengths exclude
artificial runs and overlong candidate patterns. Both indices reuse a cumulative
run-length kernel that counts consecutive matches without per-column Python loops
or scalar fallbacks. Markov scoring reuses the same sequence preparation, masks
padded transitions, and gives padding distinct sentinels in its sorted kernel.
All-missing rows still return zero for longstring indices and NaN for Markov
entropy, and na_rm=False still rejects missing responses. These paths use NumPy
kernels without requiring a native extension or compiler.
For a local benchmark against commit 7143d28, a 100,000-by-80 complete-response
matrix produced the following median times (five runs after one warmup, seed
20260927, Python 3.14.7, NumPy 2.3.5, OPENBLAS_NUM_THREADS=1):
| Operation | Before | Cumulative runs | Speedup |
|---|---|---|---|
| Longstring | 73.3 ms | 27.6 ms | 2.7× |
| Repeating patterns | 494.3 ms | 143.0 ms | 3.5× |
| Default screening | 1144.2 ms | 652.3 ms | 1.8× |
| Default composite | 282.7 ms | 200.0 ms | 1.4× |
Reproduce this workload with benchmarks/bench_sequence_scoring.py --respondents
100000 --missing-rate 0; results depend on the machine and response distribution.
Peak traced allocation fell from 22.89 to 2.07 MiB for repeating patterns and
from 3.15 to 1.54 MiB for longstring. Full screening remained at 82.45 MiB because
other indices determine its peak. Smaller inputs can use more temporary memory:
complete 10,000-by-80 longstring scoring increased from 0.32 to 0.85 MiB. The
shared row budget bounds these workspaces independently of respondent count.
For the Markov refactor against commit 1e76814, the same 100,000-by-80 workload
gave these medians with five alternating before/after runs following one warmup
per implementation (same seed, Python, NumPy, and BLAS settings as above):
| Operation | Missing responses | Before | After | Peak allocation before → after |
|---|---|---|---|---|
| Markov | 0% | 130.9 ms | 66.0 ms | 77.86 → 5.41 MiB |
| Markov | 10% | 153.1 ms | 135.3 ms | 9.22 → 9.22 MiB |
| Default screening | 0% | 789.0 ms | 725.0 ms | 82.45 → 70.39 MiB |
Use benchmarks/bench_sequence_scoring.py --respondents 100000 --missing-rate 0
--operations markov screen to reproduce the complete-response workload, and
--missing-rate 0.1 for omissions. Whole-workflow gains depend on the other
indices: missing-data screening timing varied between runs, with this alternating
comparison measuring 876.4 ms before and 919.2 ms after and unchanged 63.40 MiB
peak allocation. Small response scales benefit most; 64- and 65-state inputs
showed little timing change. Allocation tracing remains separate from timing.
Even–odd consistency reduces factor correlations directly into respondent-level
sums and valid-factor counts. Correlation kernels use centered row workspaces and
contraction reductions, so peak allocation does not grow with the factor count.
Split IRV similarly accumulates section deviations inside bounded respondent
blocks without retaining a score vector for every section. Row-contiguous inputs
reduce adjacent equal-width sections together through array views; column-contiguous
inputs reduce one section at a time. Both reuse the last-axis mean/deviation kernel,
and singleton sections require only finiteness checks. Empty automatic sections are
excluded before allocation, and standalone row deviations no longer retain an
unused full-length mean vector.
Psychometric synonym and antonym scoring reuse the same row-correlation kernel
in bounded respondent batches, centering owned pair-selection buffers in place.
Complete and missing inputs share this path. Finiteness checks inspect selected
items once per block, and rows with unavailable selected responses skip correlation
work. Missing responses therefore do not trigger a complete respondent-by-pair
contribution matrix. The compatibility resample_na and random_seed parameters
do not enter scoring.
Undefined item correlations never become candidate pairs; fewer than two selected
pairs leave respondent scores unavailable. Common summary reductions filter missing
scores once and return unavailable statistics when no observed scores remain.
Item correlations for synonym, antonym, and cutoff discovery share a centered
cross-product reduction over bounded row blocks. Items with unavailable means
are excluded from multiplication and restored as undefined correlations; the
default item_correlations="complete" mode thereby preserves column-wise
missing-value propagation. Constant items and samples with fewer than two
respondents also have undefined correlations. The opt-in "pairwise" mode
introduces pairwise deletion only when a floating input actually contains a
missing response; otherwise it reuses the complete reduction unchanged, after
leaving every pair undefined when there are fewer than three respondents. Each
row block then contributes four item-by-item matrix products: shared counts,
sums, squares, and cross-products over the rows where both items are observed.
Items are scaled by a power of two and shifted by a median observation from the
first block that observes them, so moments cannot overflow and integer-valued
responses accumulate exactly. A sparsely observed first block can still supply
an outlying shift, and one-pass moments then lose about eps * squares /
variance of their relative precision. Pairs whose variance falls below 1e-2
of their squares, whose correlation is nonfinite, or whose variance nears the
underflow range for an item with responses far below its largest one are
recomputed exactly from their shared rows with the complete reduction, which
bounds the remaining error near eps / 1e-2. Ordinary responses take none of
these repairs. Pairs need three shared respondents. The moments are finished in
place: peak workspace is the four item-by-item accumulators, Boolean pair masks
of one byte per pair, and bounded blocks. Transposed finishing steps run in
row-batch slices, and each block's cross-product update allocates at most four
row-batch budgets (8 MiB by default), so a full item-by-item product is formed
only for about 1,000 items or fewer, where NumPy's symmetric block.T @ block
product is fastest. The cross-product accumulator becomes the returned matrix
when every item is usable. Pairwise respondent scoring reuses the
row-correlation kernel over each respondent's answered pairs. Centering no
longer copies the full response matrix, while the item-by-item output still
needs quadratic space in item count.
Cross-products are normalized directly, avoiding the covariance divisor that
cancels in a correlation. Constant candidates are verified against their original
observations and left undefined without repeating the full matrix calculation.
Other overflow, underflow, and variance very small relative to an item's mean
trigger bounded power-of-two rescaling. That path subtracts a sample observation
before accumulating the mean, preserving small differences. Real and complex inputs
share this policy; complex component scales avoid overflowing their magnitude.
Shared item means also accumulate in double precision, skip masked reductions for
complete blocks, and repair overflowing finite means using bounded rescaling. This keeps
person-total profiles and Guttman item ordering available at large finite scales.
Integer item means use native totals when their range permits; exceptional means
accumulate high and low integer parts within each respondent block. Person-total
profiles subtract a common exact total before final conversion and division,
retaining differences that would disappear in raw floating-point means. Guttman
orders exact integer totals and retains integer response categories. Integer item
correlations subtract each item's exact minimum before floating-point scaling,
so discovery preserves small differences on large baselines.
Predefined semantic pairs and MAD item pairs share a bounded absolute-difference
reducer. Pair selection, optional reverse scoring, and missing-aware means stay
within the common element budget instead of materializing complete pair matrices.
Finite reverse scoring retains the reflection's rounded remainder and uses
compensated subtraction to preserve small responses when the reflected value
rounds. Corrections that themselves cancel receive exact row repairs.
For small integral reflections, a compact int32 equality probe proves that
floating responses permit exact native reflection subtraction. The cast only
checks equality and never supplies arithmetic values. Fractional responses use
compensated subtraction, while responses outside that probe's range retain the
wider float64 integrality proof. Scalar missing-aware range reductions select
the proof without allocating range masks or attempting casts of wide responses.
Overflowing reflections subtract each scale endpoint separately, matching
ordered responses to ordered endpoints. Overflowing differences recover their
original responses and reduce in power-of-two-scaled units. A finite reflection
sets the effective scale without retaining cancelled large endpoints; the two
largest terms combine first to preserve a small residual. Rounded reflection
remainders, exceptional cancellation, and subnormal or overflowing normalized
ratios receive exact scalar repairs before final conversion. Semantic consistency
batches row deviations alongside those pair reductions and can divide before
restoring units, preserving finite ratios when the raw difference mean exceeds
float range. Native subnormal means and exceptional normalized ratios also
receive exact row repairs before final rounding. MAD restores original units,
including infinity for an unrepresentable mean difference.
Large integer pairs subtract before floating-point conversion; reverse scoring
retains exact scale bounds, including fractional endpoints. Only the bounded
exceptional path uses Python integer arithmetic, and its means are rounded after
averaging and optional normalization.
Ordinary integer categories use native signed differences when safe. Selected
pair buffers are released before the next block, avoiding overlapping allocations.
Mahalanobis scoring uses the same row budget for missing-value detection,
complete-case means, centered covariance accumulation, and quadratic-form
evaluation. Complete-case selection happens inside each block, so enabling
na_rm does not copy the selected response matrix. Covariance decomposition
uses its real symmetric structure while retaining the inverse/pseudo-inverse
cutoff policy. Matrix workspaces depend on the row budget and square item count;
only the output and complete-case mask scale with the respondent count.
Guttman scoring likewise batches item means, difficulty-ordered selection,
valid-response counts, and error accumulation by respondent. Small categorical
scales use cumulative category counts within each batch, while high-cardinality
data use the same row bound with direct item-pair comparisons.
Split-half individual reliability generates the established seeded item splits
once, then reuses each bounded respondent block across them. The shared correlation
kernel centers owned selection buffers in place for both complete and missing
responses. Exceptional rows recover their original paired values from the input
block before rescaling, keeping source data unchanged. Missing-value detection
runs once per input block, and profiles with constant observed responses are
excluded before evaluating splits. Seeded split generation remains unchanged;
paired reductions retain each respondent's valid-split count.
Undefined Spearman–Brown corrections remain unavailable instead of
propagating infinity into screening or composites.
For exactly two observed pairs, including wider rows with missing responses,
the kernel obtains exact correlations from the relative ordering of each pair.
This avoids feeding rounded near-perfect negative correlations into the
Spearman–Brown correction.
Wider large-integer profiles shift by their observed minimum before conversion
to double precision. Unsigned differences preserve the full signed and unsigned
64-bit range; missing partners are excluded when choosing that minimum. Even–odd
scoring retains the original integer matrix until its bounded correlation work.
Half-scale even–odd scoring and scale-aware split-half reliability share one
bounded kernel. Equally sized half-scale groups are gathered item position first,
so a single exact row-mean reduction covers every group of a respondent batch
with long vector operations instead of one narrow reduction per factor. The
resulting factor half means feed the shared correlation kernel. Constant profiles
are excluded first, because rounded half means of a constant decimal profile can
differ by one unit in the last place.
Complete-response onset detection derives stable sliding-window variability
from rolling means and bounded deviation buffers. Windows of at least 16 items
use cumulative first and second moments for integer-valued responses only when
all sums, squares, and products remain exact in double precision. The variance
numerator is formed before division, preserving constant-window variability
without cancellation. Other values retain the direct deviation calculation.
Large integer responses shift before floating-point conversion, and integer
blocks skip missing-value and infinity scans. Its changepoint test retains
only prefix and candidate-position workspaces instead of complete centered and
test-statistic matrices. Missing-response blocks compress rows into equal
retained-length groups and reuse the same bounded complete-response kernel.
Missing-value detection and eligibility checks share each input block, and the
rolling batch size accounts for its multiple workspaces. The changepoint series
is centered before accumulating moments to avoid cancellation from a common
baseline. Exceptionally large finite responses are scaled by powers of two;
logarithmic statistic comparisons retain the variance floor in the original
response units without overflowing or losing candidate ordering.
Person–total correlation calculates item-profile means and respondent
correlations in bounded batches as well. The shared kernel accepts the index's
undefined-correlation policy, so constant person or item profiles remain
unavailable rather than being assigned a synthetic score.
Strict person-total missing checks also use bounded batches and skip integer
inputs, which cannot contain missing values.
IRV, acquiescence, and response-style summaries share bounded row mean and
population-standard-deviation reductions. Both missing-value policies accumulate
in double precision and reuse the mean when centering. Complete blocks skip
masked reductions; blocks with mostly unavailable means center only the
remaining rows. Incomplete blocks retain observed counts, so an entirely
unavailable row returns an unavailable score without constructing a complete
boolean or centered workspace for the input. Exceptional finite rows use
power-of-two rescaling to recover overflowing means and overflowing or
underflowing variances. It also detects deviations small relative to their row
mean, where subtracting a rounded baseline can distort the remaining variation.
The repair first identifies constant observations, then shifts nearby values
by an observed value before calculating their centered deviations at a safe
scale. Wider ranges retain unshifted mean reductions to preserve cancellation
between positive and negative responses. Constant finite rows therefore have
exactly zero variability, while
nearly constant rows retain their representable differences. Infinite
observations retain unavailable deviations. The exceptional-moment detector and
the shifted scaling strategy are shared with row correlations, which preserve
each caller's zero-variance policy. Split IRV also
rescales overflowing section-score totals in bounded respondent blocks before
averaging them.
Integer row reductions skip missing masks. Ordinary integer totals accumulate
in native integer arithmetic when the complete total fits double precision
exactly. Exceptional 64-bit means sum high and low 32-bit parts separately,
combine only their reduced totals as Python integers, and round after division.
Their deviations use exact unsigned distances from each row's minimum before
conversion, preserving adjacent observations at large baselines without storing
a full object-valued response matrix.
Acquiescence normalizes observations before averaging when finite scale widths
overflow, are subnormal, or are very small relative to the baseline. Large integer
observations subtract an exact integer endpoint before conversion to float.
Balanced scoring selects both item polarities together and excludes incomplete
pairs from both halves; overflowing pair sums fall back to averaging the original
endpoints. Exceptionally narrow explicit bounds can make out-of-range response
proportions overflow; only those rows use decimal arithmetic to retain cancellation
before clipping the normalized mean.
Response-style endpoint comparisons resolve representable bounds once per call.
Integer midpoint intervals use exact rational arithmetic followed by integer
ceiling and floor, so fractional tolerances do not collapse adjacent categories.
Floating midpoint calculation adds before halving unless the sum overflows,
retaining constant subnormal scales. Explicit bounds are compared in double
precision even with single-precision response arrays. Integer proportions skip
missing scans, and the combined summary reuses these prepared comparisons.
Response-time summaries use the same reductions. Row medians own one bounded
buffer: incomplete rows are sorted with missing values last, while wide complete
rows use one partition and, for even lengths, the maximum of the lower half.
Strict reductions exclude incomplete rows before copying responses. Midpoint
arithmetic preserves large finite and subnormal results, promotes single-precision
endpoints, and averages large integer endpoints exactly before final rounding.
Median-based mixture preprocessing therefore does not duplicate the complete
timing matrix before fitting its respondent-level model, and log transformation
reuses the selected respondent medians.
For response checks against commit d61be58, a local 100,000-by-80 float64 matrix
with 10% missing responses and 40 selected items gave these medians (nine
alternating before/after runs after one warmup each, seed 20260927, Python 3.14.7,
NumPy 2.3.5, row-contiguous input):
| Operation | Before | After | Peak allocation before → after |
|---|---|---|---|
| Attention checks, missing-as-pass proportions | 48.0 ms | 16.3 ms | 1.05 → 3.76 MiB |
| Missing rates, all items | 8.2 ms | 6.1 ms | 8.46 → 1.26 MiB |
| Missing rates, selected items with applicability | 33.0 ms | 21.4 ms | 40.66 → 3.56 MiB |
Reproduce the workload with benchmarks/bench_response_checks.py --checks 40.
Timing excludes allocation tracing. Attention checks trade additional bounded
workspace for throughput; missing-rate scoring reduces temporary allocation.
Results depend on layout and selection: column-contiguous all-item missing rates
were about 7% slower in the local comparison, while still using less memory.
Use --order F and --checks 2 to explore these cases.
Optional dependencies¶
All statistical functionality is available in the NumPy-only base install.
Chi-square quantiles use direct normal/exponential special cases for one or two
degrees of freedom, with the two-degree array path evaluated by NumPy. General
extreme lower tails are solved in logarithmic coordinates using the shared
gamma series, preserving probabilities and quantiles below the normal floating-point
range. These cases follow the NIST gamma identities;
regression tests include independently calculated high-precision lower-tail values.
Array quantiles solve the remaining probabilities with shared safeguarded Newton
iterations in bounded batches. A batch with fewer than 192 such probabilities
uses the scalar solver instead, which is faster at that size; both paths give
results identical to scalar quantiles.
Plotting remains optional and reports a centralized install hint from
_optional_imports.py: pip install 'insufficient-effort[plot]'.
Parity and simulation¶
Performance scripts use benchmarks/_measurement.py for untraced timed repeats
and a separate peak-allocation call. Paired comparisons alternate operation order
and retain the last timed results for correctness checks. See
Contributing
for measurement details and historical-comparison guidance.
- Hand-locked regression fixtures live in
tests/test_golden_parity.pyand JSON undertests/fixtures/parity/. - Detection-rate simulation:
benchmarks/bench_detection.py. - Throughput microbench:
benchmarks/bench_screen.py. - Multi-factor even–odd throughput and memory:
benchmarks/bench_evenodd.py. - Psychometric synonym missing-data throughput and memory:
benchmarks/bench_psychsyn.py. - Predefined semantic/MAD pair throughput and memory:
benchmarks/bench_pair_differences.py. - Mahalanobis covariance and distance throughput and memory:
benchmarks/bench_mahad.py. - Guttman error-scoring throughput and memory:
benchmarks/bench_guttman.py. - Item-step person-fit (
gpoly,u3poly,ht) throughput and memory beside Guttman errors:benchmarks/bench_person_fit.py. - Split-half reliability throughput and memory:
benchmarks/bench_reliability.py. - Carelessness-onset throughput and memory:
benchmarks/bench_onset.py. - Person–total correlation throughput and memory:
benchmarks/bench_person_total.py. - Row-wise response reduction throughput and memory:
benchmarks/bench_row_reductions.py. - Missing-response and attention-check throughput and memory, including item
subsets and applicability masks:
benchmarks/bench_response_checks.py. - Lz person-fit throughput and memory:
benchmarks/bench_lz.py. - Markov transition-entropy throughput and memory:
benchmarks/bench_markov.py. - Sequence indices and screening/composite workflows with configurable missingness:
benchmarks/bench_sequence_scoring.py. Timing excludes allocation tracing; peak allocation is measured separately. - Response-time mixture EM, scoring, and reusable cutoff sensitivity:
benchmarks/bench_response_time.py. - Screen/composite reduction memory:
benchmarks/bench_orchestration.py. - Reusable composite sensitivity analysis:
benchmarks/bench_composite.py. - Validated and atomic score and response-time archive loading and saving:
benchmarks/bench_archive.py. - Shared fixed and percentile flagging throughput and memory:
benchmarks/bench_flagging.py. - CLI JSON, CSV, and NPZ serialization:
benchmarks/bench_cli_output.py. The same benchmark accepts--compression gzip|bzip2|xzfor text formats. - Delimited parsing with and without preamble rows:
benchmarks/bench_cli_input.py.