Screening Workflow¶
screen() runs multiple IER indices, applies flagging rules, and returns a
structured result.
Configure with a single IndexOptions object:
from ier import IndexOptions, screen
result = screen(data, options=IndexOptions(scale_min=1, scale_max=5))
print(result["indices_used"])
print(result["flag_counts"])
print(result["valid_index_counts"])
print(result["consensus_flags"])
print(result["errors"])
Result keys¶
| Key | Meaning |
|---|---|
scores |
Per-index score arrays |
flags |
Per-index boolean flags |
thresholds |
Actual per-index cutoffs (None for presence flagging) |
threshold_sources |
fixed, percentile, or presence provenance per cutoff |
percentiles |
Requested tail percentile per sample-relative cutoff; otherwise None |
flag_counts |
Total flags per respondent |
valid_index_counts |
Available index scores per respondent |
consensus_eligible |
Whether each respondent meets the optional completeness rule |
consensus_flags |
Respondents meeting the configured multi-index agreement threshold |
min_flags |
Number of per-index flags required for consensus (default: 2) |
min_valid_indices |
Required available scores for consensus, or None |
indices_used |
Successfully computed indices |
errors |
Soft failures (missing config, invalid data for an index) |
summary |
Mean/std/min/max, valid/unavailable counts, flagged count, and valid-score flag rate per index |
n_respondents / n_indices |
Size metadata |
Defaults¶
Default indices are NumPy-only and require no extra item metadata:
irv, longstring, longstring_pattern, mahad, psychsyn, person_total,
markov, u3_poly, midpoint, acquiescence, guttman
Mahalanobis distances and all direct flagging methods ("chi2", "iqr", and
"zscore") are available in the NumPy-only base install.
Config-gated indices¶
Pass configuration to include indices that need survey metadata:
from ier import IndexOptions, screen
result = screen(
data,
indices=["evenodd", "mad", "semantic_syn", "infrequency"],
options=IndexOptions(
evenodd_factors=[5, 5],
mad_positive_items=[0, 1, 2],
mad_negative_items=[3, 4, 5],
mad_scale_min=1,
mad_scale_max=5,
semantic_item_pairs=[(0, 1), (2, 3)],
infrequency_item_indices=[9],
infrequency_expected_responses=[5],
infrequency_missing="fail",
),
)
Missing required config is recorded in result["errors"] instead of aborting
the whole screening run. composite() uses the same soft-fail policy.
Consensus completeness¶
An unavailable component score is not a flag. When incomplete item data or a soft-failed index could leave a consensus decision supported by too few signals, set a minimum availability requirement:
result = screen(
data,
indices=["irv", "longstring", "infrequency"],
options=options,
min_flags=2,
min_valid_indices=3,
)
reviewable = result["consensus_eligible"]
Rows below the minimum remain unflagged even if their available signals meet
min_flags. The result always reports valid_index_counts and
consensus_eligible; with the default min_valid_indices=None, every row is
eligible and prior consensus behavior is preserved. Both counts accumulate one
index vector at a time rather than constructing a respondent-by-index matrix.
ier screen data.csv --indices irv longstring infrequency \
--infrequency-item-indices 3 --infrequency-expected-responses 5 \
--min-flags 2 --min-valid-indices 3 --format json
Attention-check missing responses¶
infrequency exposes four explicit missing-response policies through
IndexOptions.infrequency_missing, the standalone missing= argument, and CLI
--infrequency-missing:
| Policy | Behavior |
|---|---|
pass |
Treat missing checks as correct; legacy default |
fail |
Treat missing checks as failures |
omit |
Exclude missing checks from proportions; no observed checks yields NaN |
propagate |
Return NaN when any configured check is missing |
Set infrequency_proportion=True (or --infrequency-proportion) to score the
failure share instead of the count. infrequency_flag(..., proportion=True)
supports the same policy with an inclusive cutoff between zero and one.
For production batches that require every requested index to succeed, enable
strict mode. The first failed index raises a contextual ValueError:
result = screen(data, indices=["irv", "mad"], options=options, strict=True)
Parallel index scoring¶
Each selected index reads the same validated matrix independently. Larger multi-index runs can opt into standard-library worker threads:
result = screen(data, workers=4)
ier screen responses.npy --workers 4 --format npz --output screening.npz
The default is workers=1. Parallel results, soft failures, and strict failures
retain selection order. Additional workers can improve throughput when NumPy
kernels release the interpreter lock, but their temporary workspaces may overlap
and raise peak memory. Benchmark representative matrix sizes and worker counts.
Reusing computed scores¶
Threshold and consensus sensitivity checks do not need to recalculate expensive
indices. Pass the score mapping from one screening run to screen_scores():
from ier import screen, screen_scores
initial = screen(data, indices=["irv", "longstring", "mahad"])
lenient = screen_scores(initial["scores"], percentile=90, min_flags=1)
strict = screen_scores(
initial["scores"],
percentiles={"irv": 99, "longstring": 99, "mahad": 99},
min_flags=2,
)
The reusable path accepts registered index names, preserves their mapping order,
and returns the same structured result contract as screen(). All vectors must
be one-dimensional, non-empty, equally sized, and contain only finite values or
NaN. Compatible float64 arrays are retained by reference and never mutated.
Because no scorer runs, the returned errors mapping is empty; retain the first
run separately if its soft failures are part of the audit record.
On the bundled 10,000-respondent, 80-item benchmark, evaluating five tail percentiles from retained scores takes 5.1 ms and 1.6 MiB peak temporary allocation instead of 175.7 ms and 21.5 MiB for five full runs, a 34.2x speedup without another dependency.
Persist the same raw score mapping directly or through CLI NPZ output and reload it later:
from ier import load_score_archive, save_score_archive, screen_scores
save_score_archive("screening-scores.npz", result["scores"], errors=result["errors"])
saved = load_score_archive("screening.npz")
revised = screen_scores(saved["scores"], percentile=99, min_flags=3)
The public writer can also store aligned respondent IDs. The loader returns those identifiers and any recorded soft failures.
Missing responses¶
Missing-response rate is available as an opt-in registry index:
options = IndexOptions(missing_item_indices=[0, 1, 4, 5])
result = screen(
data,
indices=["missing_rate"],
options=options,
thresholds={"missing_rate": 0.2},
)
It is not a default because planned skip logic can create legitimate omissions.
Use missing_item_indices when the same item subset is required for every
respondent. The option applies through screen() and all composite helpers;
the direct equivalent is missing_rate(data, item_indices=[...]).
For respondent-specific branching, supply a Boolean matrix matching the response matrix. True cells identify expected responses and false cells are excluded from both the missing count and denominator:
import numpy as np
applicable = np.array(
[
[True, True, False, False],
[True, True, True, True],
]
)
options = IndexOptions(missing_applicable_mask=applicable)
result = screen(data, indices=["missing_rate"], options=options, min_flags=1)
Rows without any applicable selected items receive NaN and are not flagged.
missing_rate_flag() accepts the same applicable_mask argument for direct
flagging.
Flagging¶
- Most indices use percentile thresholds (
percentile=95by default). - High-direction indices flag above the percentile; low-direction indices flag
below
100 - percentile. - Override individual tail settings with
percentiles={"irv": 90, "longstring": 99}. Values use the same directional convention as the global setting, and unspecified indices retain the global percentile. - Pass fixed cutoffs with
thresholds={"irv": 0.25, "longstring": 8}. Fixed thresholds are inclusive: high-direction scores at or above the cutoff and low-direction scores at or below the cutoff are flagged. Other indices keep using the configured percentile. - An index cannot have both fixed and percentile overrides. Presence-mode indices accept neither. Results retain every cutoff's source and requested percentile.
onsetuses presence flagging: any detected changepoint is flagged.consensus_flagsmarks respondents flagged by at leastmin_flagsindices. Usescreen(..., min_flags=1)for single-index workflows.
Response times (out of band)¶
response_time* helpers take timing matrices (seconds or other duration
units), not Likert item responses. They are intentionally not registered in
screen() / composite() because mixing domains would silently mis-score
respondents. Call them directly or use the dedicated CLI command:
from ier import (
load_response_time_archive,
response_time,
response_time_flag,
response_time_score_flags,
save_response_time_archive,
)
median_rt = response_time(times, metric="median")
flags = response_time_flag(times, cutoff_percentile=5)
stricter_flags = response_time_score_flags(median_rt, cutoff_percentile=1)
saved = load_response_time_archive("timing.npz")
revised_flags = response_time_score_flags(
saved["scores"],
threshold=1.0,
direction=saved["flag_direction"],
)
save_response_time_archive(
"revised-timing.npz",
saved["scores"],
revised_flags,
threshold=1.0,
metric=saved["metric"],
flag_direction=saved["flag_direction"],
respondent_ids=saved["respondent_ids"],
)
ier response-time timings.csv --metric median --percentile 5
ier response-time timings.csv --metric consistency --threshold 0.05 --format csv
ier response-time timings.csv --metric mixture --components 2 --random-seed 42
ier response-time timings.csv --metric median --format npz --output timing.npz
Direct timing metrics and consistency scores use low-tail flagging. Mixture
probabilities use high-tail flagging. Fixed thresholds include equality; derived
percentile cutoffs exclude ties, matching the other public flagging workflows.
Retained direct scores use direction="low" by default; pass direction="high"
for mixture probabilities. This sensitivity path never recomputes row summaries
or refits the mixture. The NPZ loader also validates that archived flags agree
with their stored threshold and suspicious-tail direction before reuse. The
matching writer performs the same checks before creating a CLI-compatible
archive.
Mixture fitting excludes respondents whose median time is missing, infinite, or
non-positive. Its posterior normalization remains stable when ordinary Gaussian
density calculations underflow for an extreme valid observation.
CLI¶
Blank fields in comma-, tab-, or semicolon-delimited input are loaded as missing
values (NaN) and follow each index's documented missing-data behavior.
ier screen data.csv --scale-min 1 --scale-max 5 --indices irv longstring
ier screen data.csv --min-flags 3
ier screen data.csv --min-flags 2 --min-valid-indices 3
ier screen data.csv --threshold irv=0.25 --threshold longstring=8
ier screen data.csv --index-percentile irv=90 --index-percentile longstring=99
ier screen data.csv --indices irv mad --strict
ier screen data.csv --format json --output screen.json
ier screen data.csv --format csv --evenodd-factors 5,5 --indices evenodd irv
ier screen data.csv --indices missing_rate --missing-item-indices 0,1,4
ier screen data.csv --indices infrequency \
--infrequency-item-indices 3,7 \
--infrequency-expected-responses 5,1 --infrequency-missing fail
ier screen data.csv --id-column participant_id --item-columns q1,q2,q3,q4
ier response-time timings.csv --metric median --threshold 1.0
ier screen data.csv.gz --format json --output screening.json.gz
ier screen data.npy --indices irv longstring --format json
ier screen data.npy --indices irv longstring --format npz --output screening.npz
cat data.csv | ier screen - --indices irv longstring --format json
ier --version
--item-columns accepts comma-separated header names and may be repeated. It
lets screen and composite commands ignore unselected metadata columns while
preserving the requested item order. Any item-index options refer to that
selected order.
Header detection defaults to --header auto. Use --header present when the
first row contains ambiguous names such as numeric item codes, or --header absent
to ensure a malformed first data row is reported instead of interpreted as a
header. Named column selection cannot be combined with --header absent.
Repeat --missing-value TOKEN to treat exact, whitespace-trimmed survey-export
markers such as NA, ., or -99 as missing numeric cells. Configured markers
participate in automatic header detection, so a headerless first row containing a
missing marker is retained as data. Identifier and unselected metadata columns are
not transformed.
Uncompressed .npy input is memory-mapped read-only and must contain one
non-empty, two-dimensional, real numeric array. It has no headers, so
--id-column, --item-columns, --missing-value, and --delimiter do not
apply. Compressed .npy.gz input is not supported because it cannot be
memory-mapped.
Use - as the data path to read a forward-only standard-input stream, and use
--output - to select standard output explicitly. Input and output paths ending
in .gz are compressed or decompressed transparently with no optional package.
CSV rows and JSON respondent arrays are emitted in bounded chunks across all
three destinations.
JSON output is standards-compliant: unavailable or non-finite scores and summary
statistics are encoded as null. CSV output represents non-finite scores as empty
cells so numeric columns remain compatible with spreadsheet and statistics tools.
NPZ output preserves those values and typed flags exactly; its versioned layout is
documented in CLI output formats.