Skip to content

API Reference

IER: Python library for detecting Insufficient Effort Responding in survey data.

AgreementKind = Literal['overlap', 'jaccard', 'spearman'] module-attribute

EvenOddMethod = Literal['item_pairs', 'halves'] module-attribute

ItemCorrelationMode = Literal['complete', 'pairwise'] module-attribute

ResponseTimeFlagDirection = FlagDirection module-attribute

ResponseTimeMetric = Literal['mean', 'median', 'sd', 'min', 'consistency', 'mixture', 'effort'] module-attribute

__version__ module-attribute

Installed distribution version, read from package metadata on first access.

IndexOptions dataclass

Shared optional configuration for registered index scorers.

reverse_keyed_items lists 0-based reverse-worded columns. They are reverse-scored with scale_min and scale_max, inferred from the data when omitted, only for indices whose catalog entry sets uses_keyed_responses; every other index reads the responses as presented. When person_fit_ncat is set, gpoly and u3poly_fit reflect items on the declared category scale instead.

ResponseTimeArchive

Bases: TypedDict

Validated response-time scores loaded from a versioned NPZ archive.

ScoreArchive

Bases: TypedDict

Reusable registered-index scores loaded from a versioned NPZ archive.

ScreenArchive

Bases: TypedDict

Complete screening results loaded from a validated versioned NPZ archive.

index_catalog()

Return discoverable metadata for all registered orchestration indices.

load_score_archive(path)

Load reusable registered-index scores from a versioned NPZ archive.

The loader always disables pickling and validates schema version, result type, member names, registry membership, vector shape, respondent alignment, optional identifiers, and soft-failure metadata. Screen archives retaining only failed indices preserve their explicit respondent count; pass it to screen_scores() as n_respondents when reusing an empty score mapping. Full composite CLI archives must have been written with --include-components so their raw public index scores are present; compact archives from save_score_archive() are directly compatible. Complete screen archives load here as scores only; use load_screen_archive() to restore their recorded decisions.

Parameters: - path: Path to a screen or detailed composite NPZ archive.

  • A ScoreArchive containing ordered raw score vectors, result metadata, optional respondent IDs, and any recorded per-index soft failures.
Example

from ier import composite_scores, load_score_archive, save_score_archive from ier import screen_scores save_score_archive( ... "scores.npz", ... {"irv": [0.1, 0.7, 0.4], "longstring": [3.0, 8.0, 5.0]}, ... errors={"mad": "item pairs were not configured"}, ... ) saved = load_score_archive("scores.npz") list(saved["scores"]), saved["errors"] (['irv', 'longstring'], {'mad': 'item pairs were not configured'}) updated_screen = screen_scores( ... saved["scores"], percentile=99, errors=saved["errors"] ... ) updated_screen["errors"] {'mad': 'item pairs were not configured'} save_score_archive( ... "components.npz", saved["scores"], result_type="composite" ... ) saved_components = load_score_archive("components.npz") updated_composite = composite_scores( ... saved_components["scores"], weights={"irv": 2.0} ... ) updated_composite.shape (3,)

load_response_time_archive(path)

Load response-time results from a versioned, pickle-free NPZ archive.

The loader validates every schema field, the metric and suspicious-tail pairing, the 0 to 1 range of "effort" thresholds and scores, vector shape and respondent alignment, optional identifiers, and agreement between the stored flags and threshold. Flags are returned as canonical Booleans recomputed from that rule. The returned score vector can be passed directly to response_time_score_flags() to apply a new fixed or percentile cutoff without recomputing the timing metric. Fixed "effort" cutoffs from the CLI flag response time effort strictly below the threshold, like response_time_effort_flag(); compare saved effort scores with < to apply that rule to a new cutoff.

Parameters: - path: Path to a response-time NPZ archive written by the CLI.

  • A ResponseTimeArchive containing scores, flags, cutoff metadata, and optional respondent identifiers.
Example

from ier import load_response_time_archive, response_time_score_flags from ier import save_response_time_archive scores = [0.5, 1.2, 2.0, 3.1] flags = response_time_score_flags(scores, threshold=1.0) save_response_time_archive("timing.npz", scores, flags, threshold=1.0) saved = load_response_time_archive("timing.npz") saved["flags"].tolist() [True, False, False, False] strict = response_time_score_flags( ... saved["scores"], threshold=1.5, ... direction=saved["flag_direction"], ... ) strict.tolist() [True, True, False, False]

load_screen_archive(path)

Load a complete screen() result from a versioned, pickle-free NPZ archive.

Accepts archives from save_screen_archive() and ier screen --format npz. Pickling is always disabled. Beyond the schema, member shapes, registry names, identifiers, and soft failures, the loader recomputes every per-index flag from its score and recorded cutoff: fixed thresholds include ties, percentile cutoffs exclude them, and presence rules flag available scores. Flag counts, valid-index counts, consensus decisions, and summary counts, flag rates, minima, and maxima must also agree exactly. Summary means and standard deviations are rebuilt from the restored scores, so the restored summary always describes the restored scores. Flags are returned as canonical Booleans. The restored result can be passed directly to the plot_* helpers without recomputing any index.

Parameters: - path: Path to a complete screen NPZ archive.

  • A ScreenArchive with the schema version, the restored ScreenResult, and optional respondent identifiers.
Example

from ier import load_screen_archive, save_screen_archive, screen_scores original = screen_scores( ... {"irv": [0.1, 0.7, 0.4], "onset": [float("nan"), 2.0, float("nan")]}, ... thresholds={"irv": 0.4}, ... min_flags=1, ... ) save_screen_archive("screening.npz", original, respondent_ids=["a", "b", "c"]) saved = load_screen_archive("screening.npz") print(saved["respondent_ids"]) ['a', 'b', 'c'] restored = saved["result"] restored["threshold_sources"] {'irv': 'fixed', 'onset': 'presence'} restored["consensus_flags"].tolist() [True, True, True]

save_response_time_archive(path, scores, flags, *, threshold, metric='median', flag_direction='low', respondent_ids=None, compressed=False, compression_level=None)

Save reusable response-time results as a versioned, pickle-free NPZ archive.

Scores, Boolean flags, metric/direction compatibility, the finite threshold, and optional respondent identifiers are validated before the destination is opened. Flags may follow either the inclusive fixed-cutoff rule or the tie-exclusive rule of percentile cutoffs and strict response time effort cutoffs. An "effort" threshold and every available effort score must lie between 0 and 1, the range of response time effort.

Parameters: - path: Explicit destination ending in .npz. - scores: Per-respondent direct timing scores, response time effort proportions, or mixture probabilities. - flags: Aligned Boolean decisions produced from the recorded threshold. - threshold: Resolved finite cutoff in the score's units. - metric: Timing metric represented by the score vector. - flag_direction: Suspicious tail, "low" or "high". - respondent_ids: Optional aligned, unique, nonblank string identifiers, with no trailing NUL character. - compressed: Use streaming DEFLATE compression to reduce storage, at the cost of extra CPU when writing and loading. Defaults to False. - compression_level: Optional DEFLATE level from 1 (fastest) to 9 (smallest) for compressed=True. None uses level 1; any level loads the same way.

Example

from ier import response_time_score_flags, save_response_time_archive scores = [0.5, 1.2, 2.0] flags = response_time_score_flags(scores, threshold=1.0) save_response_time_archive( ... "timing.npz", scores, flags, threshold=1.0, metric="median" ... ) flags.tolist() [True, False, False]

save_score_archive(path, scores, *, result_type='screen', respondent_ids=None, errors=None, n_respondents=None, compressed=False, compression_level=None)

Save reusable registered-index scores as a versioned, pickle-free NPZ archive.

Score and metadata validation completes before the destination is opened. Compatible float64 arrays are streamed without an intermediate score matrix, and mapping insertion order is preserved. Screen archives may retain only failed indices when n_respondents is supplied explicitly. Composite archives require component scores supported by composite_scores().

Parameters: - path: Explicit destination ending in .npz. - scores: Ordered mapping of registered index names to aligned score vectors. - result_type: "screen" or "composite". - respondent_ids: Optional aligned, unique, nonblank string identifiers, with no trailing NUL character. - errors: Optional ordered mapping of failed index names to nonblank messages, with no trailing NUL character. - n_respondents: Optional positive respondent count, checked against score vectors. Required for a screen archive containing only failed indices. - compressed: Use streaming DEFLATE compression to reduce storage, at the cost of extra CPU when writing and loading. Defaults to False. - compression_level: Optional DEFLATE level from 1 (fastest) to 9 (smallest) for compressed=True. None uses level 1; any level loads the same way.

Example

from ier import load_score_archive, save_score_archive, screen_scores scores = {"irv": [0.1, 0.7, 0.4], "longstring": [3.0, 8.0, 5.0]} save_score_archive("scores.npz", scores, respondent_ids=["a", "b", "c"]) saved = load_score_archive("scores.npz") print(saved["respondent_ids"]) ['a', 'b', 'c'] updated = screen_scores(saved["scores"], percentile=95, min_flags=1) updated["consensus_flags"].tolist() [True, True, False]

save_screen_archive(path, result, *, respondent_ids=None, compressed=False, compression_level=None)

Save a complete screen() result as a versioned, pickle-free NPZ archive.

The archive uses the same schema as ier screen --format npz: raw scores, per-index flags, recorded thresholds and their sources, percentile settings, respondent counts, consensus decisions, summaries, and soft failures. Before the destination is opened, every flag, count, consensus decision, summary flag rate, and score minimum and maximum is recomputed from the scores and recorded cutoffs, so the archive always reloads through load_screen_archive(). Summary means and standard deviations are written as recomputed from the scores. Integer settings must fit in 64 bits. The write is atomic.

Reload the archive to keep the original decisions exactly. Rebuilding them with screen_scores(thresholds=...) instead treats every recorded cutoff as an inclusive fixed threshold, which flips ties at percentile cutoffs.

Parameters: - path: Explicit destination ending in .npz. - result: A ScreenResult from screen() or screen_scores(). - respondent_ids: Optional aligned, unique, nonblank string identifiers, with no trailing NUL character. - compressed: Use streaming DEFLATE compression to reduce storage, at the cost of extra CPU when writing and loading. Defaults to False. - compression_level: Optional DEFLATE level from 1 (fastest) to 9 (smallest) for compressed=True. None uses level 1; any level loads the same way.

Example

from ier import load_screen_archive, save_screen_archive, screen_scores result = screen_scores( ... {"irv": [0.1, 0.7, 0.4, 0.9], "longstring": [3.0, 8.0, 5.0, 8.0]}, ... percentile=75, ... min_flags=1, ... ) save_screen_archive("screening.npz", result, respondent_ids=["a", "b", "c", "d"]) restored = load_screen_archive("screening.npz")["result"] flags = restored["flags"] flags["longstring"].tolist() [False, False, False, False] restored["consensus_flags"].tolist() [True, False, False, False]

screen_scores(scores, *, percentile=95.0, min_flags=2, min_valid_indices=None, thresholds=None, percentiles=None, errors=None, n_respondents=None)

Apply screening decisions to already-computed registered-index scores.

This is the reusable post-scoring counterpart to :func:screen. It supports fast threshold, percentile, and consensus sensitivity analysis without calculating any index again. Score mappings preserve insertion order; each value must be a non-empty one-dimensional numeric vector with the same respondent count. Finite values and NaN are accepted, with NaN treated as an unavailable score.

Compatible float64 NumPy vectors are retained by reference. The function never mutates them, but callers should avoid changing the arrays while using the returned result.

Parameters: - scores: Mapping from registered index names to respondent score vectors. - percentile: Default tail percentile for sample-relative flagging. - min_flags: Minimum number of index flags required for consensus. - min_valid_indices: Optional minimum available-score count for consensus. - thresholds: Optional fixed per-index cutoffs. - percentiles: Optional per-index tail-percentile overrides. - errors: Optional retained per-index soft failures. Failed indices count as selected for validation but never as available respondent scores. - n_respondents: Optional positive respondent count, checked against any score vectors. Required with an empty score mapping when every selected index failed; provide the retained errors as well.

  • The same structured ScreenResult contract as :func:screen, with an errors mapping containing any supplied failure provenance.
Example

from ier import screen, screen_scores data = [ ... [1, 2, 3, 4, 5, 4], ... [3, 3, 3, 3, 3, 3], ... [5, 4, 3, 2, 1, 2], ... [2, 5, 1, 4, 3, 2], ... [4, 4, 5, 4, 4, 5], ... [1, 5, 1, 5, 1, 5], ... ] initial = screen(data, indices=["irv", "longstring"]) initial["flag_counts"].tolist() [0, 2, 0, 0, 0, 0] looser = screen_scores( ... initial["scores"], ... percentiles={"irv": 50, "longstring": 50}, ... min_flags=1, ... ) looser["consensus_flags"].tolist() [False, True, False, False, True, False]

composite_flag(x, indices=None, method='mean', threshold=None, percentile=95.0, standardize=True, *, options=None, weights=None, min_valid_indices=None, return_diagnostics=False, strict=False, workers=1)

composite_flag(x: MatrixLike, indices: list[str] | None = None, method: CompositeMethod = 'mean', threshold: float | None = None, percentile: float = 95.0, standardize: bool = True, *, options: IndexOptions | None = None, weights: Mapping[str, float] | None = None, min_valid_indices: int | None = None, return_diagnostics: Literal[False] = False, strict: bool = False, workers: int = 1) -> tuple[np.ndarray, np.ndarray]
composite_flag(x: MatrixLike, indices: list[str] | None = None, method: CompositeMethod = 'mean', threshold: float | None = None, percentile: float = 95.0, standardize: bool = True, *, options: IndexOptions | None = None, weights: Mapping[str, float] | None = None, min_valid_indices: int | None = None, return_diagnostics: Literal[True], strict: bool = False, workers: int = 1) -> tuple[np.ndarray, np.ndarray, dict[str, str]]
composite_flag(x: MatrixLike, indices: list[str] | None = None, method: CompositeMethod = 'mean', threshold: float | None = None, percentile: float = 95.0, standardize: bool = True, *, options: IndexOptions | None = None, weights: Mapping[str, float] | None = None, min_valid_indices: int | None = None, return_diagnostics: bool, strict: bool = False, workers: int = 1) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, dict[str, str]]

Calculate composite IER scores and flag potential careless responders.

Configure with options=IndexOptions(...). Missing index config soft-fails by default; set strict=True to require every selected index to succeed. Optional weights follow the same validation and combination semantics as composite(), including range errors for unrepresentable sums or maxima. Set min_valid_indices to suppress scores based on too few available indices. Set workers above 1 to score independent indices concurrently. Explicit thresholds include scores equal to the cutoff; percentile cutoffs flag only scores strictly above the sample percentile.

  • Tuple of (composite_scores, flags) where flags is True for suspected careless responders.

composite_probability(x, indices=None, method='mean', *, options=None, weights=None, min_valid_indices=None, return_diagnostics=False, strict=False, workers=1)

composite_probability(x: MatrixLike, indices: list[str] | None = None, method: CompositeMethod = 'mean', *, options: IndexOptions | None = None, weights: Mapping[str, float] | None = None, min_valid_indices: int | None = None, return_diagnostics: Literal[False] = False, strict: bool = False, workers: int = 1) -> FloatArray
composite_probability(x: MatrixLike, indices: list[str] | None = None, method: CompositeMethod = 'mean', *, options: IndexOptions | None = None, weights: Mapping[str, float] | None = None, min_valid_indices: int | None = None, return_diagnostics: Literal[True], strict: bool = False, workers: int = 1) -> tuple[FloatArray, dict[str, str]]
composite_probability(x: MatrixLike, indices: list[str] | None = None, method: CompositeMethod = 'mean', *, options: IndexOptions | None = None, weights: Mapping[str, float] | None = None, min_valid_indices: int | None = None, return_diagnostics: bool, strict: bool = False, workers: int = 1) -> FloatArray | tuple[FloatArray, dict[str, str]]

Compute an uncalibrated logistic composite IER score.

This function computes the standardized composite score and applies a logistic transformation to map it into the interval [0, 1]. The returned values are sample-relative scores, not calibrated probabilities of IER.

Configure with options=IndexOptions(...). Set strict=True to require every selected index to succeed. Set workers above 1 to score independent indices concurrently. Optional weights are applied before the logistic transform. min_valid_indices applies the same completeness rule as composite() before transformation. Set return_diagnostics=True to also receive ordered per-index soft-failure messages. Unrepresentable weighted sums or maxima raise ValueError before transformation.

composite_scores(scores, method='mean', standardize=True, *, weights=None, min_valid_indices=None, errors=None, unsupported='error')

Combine already-computed registered-index score vectors.

This reusable counterpart to :func:composite supports fast weight, standardization, completeness, and reduction-method sensitivity analysis without calculating any index again. Input scores use their original public directions; low-is-suspicious indices are reversed automatically so higher composite values consistently represent more evidence of careless responding.

Score mappings preserve insertion order. Every vector must be non-empty, one-dimensional, respondent-aligned, and contain only finite values or NaN. The function does not mutate input arrays.

Parameters: - scores: Mapping from composite-enabled registered index names to raw score vectors. - method: Reduction across available directed scores: "mean", "sum", or "max". - standardize: Standardize each index before applying direction and weights. - weights: Optional positive finite per-index weight overrides. - min_valid_indices: Optional minimum available component count per respondent. - errors: Optional retained per-index soft failures. Failed indices remain selected for weight and completeness validation, with no available scores. - unsupported: "error" (default) rejects registered indices that are not composite-enabled. "drop" omits them from scores and errors so default :func:screen results can be reused directly. Unknown index names raise in both modes.

Returns: - A respondent-aligned NumPy array of composite scores.

  • ValueError: If a final weighted sum or maximum exceeds the finite float range. Reduce weights or choose method="mean".
Example

import numpy as np from ier import composite_scores, composite_summary, screen data = [ ... [1, 2, 3, 4, 5, 4], ... [3, 3, 3, 3, 3, 3], ... [5, 4, 3, 2, 1, 2], ... [2, 5, 1, 4, 3, 2], ... [4, 4, 5, 4, 4, 5], ... [1, 5, 1, 5, 1, 5], ... ] initial = composite_summary(data, indices=["irv", "longstring"]) weighted = composite_scores( ... initial["indices"], ... weights={"irv": 2.0, "longstring": 0.5}, ... ) np.round(weighted, 2).tolist() [-0.43, 1.76, -0.43, -0.43, 0.75, -1.22] screened = screen(data) # includes screen-only u3_poly, midpoint, and acquiescence reused = composite_scores( ... screened["scores"], errors=screened["errors"], unsupported="drop" ... ) reused.shape (6,)

composite_scores_summary(scores, method='mean', standardize=True, *, weights=None, min_valid_indices=None, errors=None, unsupported='error')

Combine reusable scores with component coverage and summary statistics.

This is the post-scoring counterpart to :func:composite_summary. It uses the same directions, calibration, weights, and completeness rules as :func:composite_scores, and reports availability after calibration. Input arrays are never mutated; compatible arrays are retained in indices. Optional errors retains original soft failures without recalculating indices. Failed indices remain selected for weight and completeness validation but never contribute to coverage or scores. Set unsupported="drop" to omit registered indices that are not composite-enabled, such as screen-only defaults.

composite_summary(x, indices=None, method='mean', standardize=True, *, options=None, weights=None, min_valid_indices=None, strict=False, workers=1)

Calculate composite scores with detailed summary statistics.

Configure with options=IndexOptions(...). Set strict=True to require every selected index to succeed. Set workers above 1 to score independent indices concurrently. The returned weights mapping contains every resolved selected-index weight. valid_index_counts reports the available component count for each respondent before applying min_valid_indices. Unrepresentable weighted sums or maxima raise ValueError, as in composite().

screen_table(screen_result, *, respondent_ids=None, include_scores=True, include_flags=True)

Arrange a screening result as respondent-aligned columns.

Columns follow the CLI CSV schema without its respondent label column: flag_count, valid_index_count, consensus_eligible, and consensus_flag, then {name}_score and {name}_flag for each index in screen_result["indices_used"]. Arrays are returned by reference, never copied, so the mapping is cheap to build and should not be mutated in place.

Parameters: - screen_result: Output dict from screen() or screen_scores(). - respondent_ids: Optional identifiers, one string per respondent in respondent order, with no trailing NUL character, given as a sequence or one-dimensional array. When given, they form a leading respondent column of NumPy strings. - include_scores: Include each index's score column. - include_flags: Include each index's flag column.

  • Ordered dict of column names to one-dimensional NumPy arrays. Pass it to pandas.DataFrame(table, index=df.index) to restore a DataFrame index.
  • TypeError: If respondent_ids is a single string, is not iterable, or is a set, mapping, or iterator, none of which has a defined respondent order.
  • ValueError: If identifiers are misaligned, not strings, or end with a NUL character, a column has the wrong length, or an include option is not a boolean.
Example

from ier import screen_scores, screen_table result = screen_scores( ... {"irv": [0.1, 0.9, 1.2], "longstring": [8.0, 2.0, 3.0]}, ... thresholds={"irv": 0.5, "longstring": 5.0}, ... ) table = screen_table(result, respondent_ids=["r1", "r2", "r3"]) list(table) ['respondent', 'flag_count', 'valid_index_count', 'consensus_eligible', 'consensus_flag', 'irv_score', 'irv_flag', 'longstring_score', 'longstring_flag'] table["flag_count"].tolist() [2, 0, 0] scores = result["scores"] table["irv_score"] is scores["irv"] True

composite_table(summary, *, respondent_ids=None, include_components=True)

Arrange a detailed composite result as respondent-aligned columns.

Columns are composite_score and valid_index_count, followed by {name}_score for each successfully scored component index, matching the CLI composite CSV written with --include-components. Arrays are returned by reference, never copied.

Parameters: - summary: Output dict from composite_summary() or composite_scores_summary(). - respondent_ids: Optional identifiers, one string per respondent in respondent order, with no trailing NUL character, given as a sequence or one-dimensional array. When given, they form a leading respondent column of NumPy strings. - include_components: Include each raw component index score column.

Returns: - Ordered dict of column names to one-dimensional NumPy arrays.

  • TypeError: If respondent_ids is a single string, is not iterable, or is a set, mapping, or iterator, none of which has a defined respondent order.
  • ValueError: If identifiers are misaligned, not strings, or end with a NUL character, a column has the wrong length, or include_components is not a boolean.
Example

from ier import composite_scores_summary, composite_table summary = composite_scores_summary( ... {"irv": [0.2, 1.0, 1.4], "longstring": [6.0, 2.0, 1.0]} ... ) table = composite_table(summary) list(table) ['composite_score', 'valid_index_count', 'irv_score', 'longstring_score'] table["valid_index_count"].tolist() [2, 2, 2]

index_agreement(screen_result, kind='jaccard')

Compare screening indices by shared flags or score rankings.

Kinds: - "overlap": number of respondents flagged by both indices. The diagonal is each index's flagged count. - "jaccard": shared flags divided by respondents flagged by either index, or NaN when neither index flags anyone. - "spearman": rank correlation of suspiciousness over respondents with finite scores on both indices, using average ranks for ties. Scores of indices that flag low values, such as irv and psychsyn, are negated before ranking, so a positive correlation always means both indices rank the same respondents as more suspicious. Unregistered score names keep their raw orientation. Pairs with fewer than three shared scores or constant ranks are NaN. Presence-flagged indices such as onset are excluded because their scores are event positions.

Parameters: - screen_result: Output dict from screen() or screen_scores(). - kind: Agreement measure.

  • Tuple of (index_names, matrix) where matrix is a symmetric float array ordered like index_names.

Raises: - ValueError: If kind is not recognized.

Example

from ier import index_agreement, screen_scores result = screen_scores( ... {"irv": [0.1, 0.2, 0.9, 1.2], "longstring": [8.0, 3.0, 2.0, 7.0]}, ... thresholds={"irv": 0.5, "longstring": 5.0}, ... ) names, overlap = index_agreement(result, kind="overlap") names ['irv', 'longstring'] overlap.tolist() [[2.0, 1.0], [1.0, 2.0]] index_agreement(result)[1].round(2).tolist() [[1.0, 0.33], [0.33, 1.0]]

Low irv and high longstring scores are both suspicious, so respondents ranked similarly by the two indices give a positive correlation:

index_agreement(result, kind="spearman")[1].round(2).tolist() [[1.0, 0.4], [0.4, 1.0]]

longstring_scores(x, na_rm=True, avg=False)

Compute longest or average run-length scores directly from matrix rows.

This avoids value-collisions from string casting (e.g., 1 vs 1.0 vs 1.00) and preserves non-integer response values.

  • x: A matrix of numeric data where rows are individuals and columns are item responses.
  • na_rm: If True, removes NaN values before analysis, so a run can continue across a skipped item. R's rle() instead ends a run at NA. If False, raises an error if NaN values are present.
  • avg: If True, return the average length of uninterrupted identical runs (avgstr, the mean of rle(x)$lengths in R's careless): observed responses divided by the number of runs. If False (default), return the longest run.
  • A numpy array of run lengths per respondent. Rows without observed responses score 0 for the longest run and NaN for the average.

Raises: - ValueError: If inputs are invalid or avg is not a boolean.

Example

data = [[1, 1, 2, 2, 2, 3], [4, 4, 4, 4, 4, 4]] longstring_scores(data).tolist() [3.0, 6.0] longstring_scores(data, avg=True).tolist() [2.0, 6.0]

longstring_pattern(x, max_pattern_length=5, na_rm=True)

Detect repeating sub-patterns in numeric response sequences.

For each respondent, searches for repeating sub-patterns of length 2..k in their response vector. Returns the longest consecutive repeating pattern length found. Detects seesaw (1-2-1-2), cycling (1-2-3-1-2-3), and similar patterned responding.

  • x: A matrix of numeric data where rows are individuals and columns are item responses.
  • max_pattern_length: Maximum sub-pattern length to search for (default 5). Must be an integer of at least 2, the shortest repeating pattern.
  • na_rm: If True, removes NaN values before analysis. If False, raises error if NaN values are present.
  • A numpy array with the longest repeating pattern length per respondent. Returns 0 if no repeating pattern is found.
  • ValueError: If inputs are invalid, including a Boolean, non-integer, or smaller max_pattern_length.
Example

data = [[1, 2, 1, 2, 1, 2], [1, 2, 3, 4, 5, 6]] longstring_pattern(data).tolist() [6.0, 0.0]

mahad_summary(x, confidence=0.95, na_rm=False)

Calculate summary statistics for Mahalanobis distances.

Parameters: - x: Matrix of data where rows are observations and columns are variables - confidence: Confidence level for outlier detection - na_rm: If True, removes rows with missing data

  • Dictionary with distance summary statistics, the chi-squared outlier count (outliers), and respondent counts n_total, n_valid and n_missing. The same counts remain available as total, valid_count and missing_count for compatibility.
Example

data = [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], ... [1, 2], [2, 1], [4, 5], [5, 4], [5, 1]] summary = mahad_summary(data) summary["outliers"], summary["n_total"], summary["n_valid"], summary["n_missing"] (1, 10, 10, 0) round(summary["max"], 2) 2.52

psychant(x, critval=-0.6, diag=False, resample_na=False, random_seed=None, *, item_correlations='complete')

psychant(x: MatrixLike, critval: float = -0.6, diag: Literal[False] = False, resample_na: bool = False, random_seed: int | None = None, *, item_correlations: ItemCorrelationMode = 'complete') -> np.ndarray
psychant(x: MatrixLike, critval: float = -0.6, diag: Literal[True] = True, resample_na: bool = False, random_seed: int | None = None, *, item_correlations: ItemCorrelationMode = 'complete') -> tuple[np.ndarray, np.ndarray]
psychant(x: MatrixLike, critval: float = -0.6, diag: bool = False, resample_na: bool = False, random_seed: int | None = None, *, item_correlations: ItemCorrelationMode = 'complete') -> np.ndarray | tuple[np.ndarray, np.ndarray]

Calculate the psychometric antonym score.

Psychometric antonyms are item pairs that are highly negatively correlated across the sample. This function is a convenience wrapper around psychsyn with antonym settings.

Parameters: - x: A matrix of data where rows are individuals and columns are their item responses. - critval: Minimum magnitude of negative correlation for items to be considered antonyms. Default is -0.60. - diag: Boolean to optionally return the number of item pairs available for each observation. - resample_na: Accepted for compatibility; it has no effect on scores (see psychsyn). - random_seed: Accepted for compatibility; it has no effect on scores. - item_correlations: "complete" (default) or "pairwise" missing-data policy for pair discovery and scoring, as in psychsyn.

Returns: - A numpy array of psychometric antonym scores, or - A tuple of (scores, diagnostic_values) if diag=True.

Example

data = [ ... [1, 5, 2, 3, 5, 1], ... [1, 5, 4, 2, 1, 5], ... [5, 1, 1, 5, 2, 5], ... [2, 4, 2, 5, 5, 3], ... [1, 5, 1, 4, 5, 1], ... [2, 2, 4, 3, 5, 3], ... ] np.round(psychant(data), 2).tolist() [-0.96, -1.0, -0.97, -0.87, -0.97, 0.94]

psychsyn_flag(x, critval=0.6, threshold=None, percentile=5.0, *, resample_na=False, random_seed=None, item_correlations='complete')

Compute psychometric synonym scores and flag respondents with low consistency.

Parameters: - x: A matrix of data where rows are individuals and columns are their item responses. - critval: Minimum item correlation for synonym pairs (default 0.60). - threshold: Absolute score threshold at or below which to flag. If None, uses percentile. - percentile: Percentile below which to flag (default 5th percentile). - resample_na: Accepted for compatibility; it has no effect on scores (see psychsyn). - random_seed: Accepted for compatibility; it has no effect on scores. - item_correlations: "complete" (default) or "pairwise" missing-data policy, as in psychsyn.

  • Tuple of (scores, flags) where flags is True for flagged respondents. Unavailable (NaN) scores are never flagged.
Example

data = [ ... [1, 1, 2, 3, 5, 5], ... [1, 1, 4, 4, 1, 1], ... [5, 5, 1, 1, 2, 1], ... [2, 2, 2, 1, 5, 3], ... [1, 1, 1, 2, 5, 5], ... [2, 4, 4, 3, 5, 3], ... ] scores, flags = psychsyn_flag(data) flags.tolist() [False, False, False, False, False, True]

psychant_flag(x, critval=-0.6, threshold=None, percentile=95.0, *, resample_na=False, random_seed=None, item_correlations='complete')

Compute psychometric antonym scores and flag the highest scores.

Attentive respondents answer antonym pairs in opposite directions, giving strongly negative scores. Scores near zero or above are suspicious, so flags follow the high direction that screen() applies to psychant.

Parameters: - x: A matrix of data where rows are individuals and columns are their item responses. - critval: Maximum (negative) item correlation for antonym pairs (default -0.60). - threshold: Absolute score threshold at or above which to flag. If None, uses percentile. - percentile: Percentile above which to flag (default 95th percentile). - resample_na: Accepted for compatibility; it has no effect on scores (see psychsyn). - random_seed: Accepted for compatibility; it has no effect on scores. - item_correlations: "complete" (default) or "pairwise" missing-data policy, as in psychsyn.

  • Tuple of (scores, flags) where flags is True for flagged respondents. Unavailable (NaN) scores are never flagged.
Example

data = [ ... [1, 5, 2, 3, 5, 1], ... [1, 5, 4, 2, 1, 5], ... [5, 1, 1, 5, 2, 5], ... [2, 4, 2, 5, 5, 3], ... [1, 5, 1, 4, 5, 1], ... [2, 2, 4, 3, 5, 3], ... ] scores, flags = psychant_flag(data, threshold=0.0) np.round(scores[flags], 2).tolist() [0.94]

psychsyn_critval(x, anto=False, min_correlation=0.0, *, item_correlations='complete')

Calculate and order pairwise correlations for all items in the provided item response matrix.

This function helps identify appropriate critical values for psychsyn analysis by showing the distribution of item correlations.

Parameters: - x: A matrix of data where rows are individuals and columns are their item responses. - anto: Boolean indicating whether to order correlations by largest negative values. - min_correlation: Finite, nonnegative correlation magnitude to include in results. - item_correlations: "complete" (default) or "pairwise" missing-data policy for item correlations, as in psychsyn.

Returns: - A list of tuples containing (item_i, item_j, correlation), ordered by magnitude.

Example

data = [ ... [1, 1, 2, 3, 5, 5], ... [1, 1, 4, 4, 1, 1], ... [5, 5, 1, 1, 2, 1], ... [2, 2, 2, 1, 5, 3], ... [1, 1, 1, 2, 5, 5], ... [2, 4, 4, 3, 5, 3], ... ] pairs = psychsyn_critval(data, min_correlation=0.6) items = [(i, j) for i, j, _ in pairs] items [(0, 1), (4, 5), (2, 3), (0, 3)] np.round([correlation for , , correlation in pairs], 2).tolist() [0.88, 0.85, 0.77, -0.64]

psychsyn_summary(x, critval=0.6, anto=False, *, item_correlations='complete')

Calculate summary statistics for psychometric synonym/antonym analysis.

Parameters: - x: A matrix of data where rows are individuals and columns are their item responses. - critval: Critical value for correlation threshold. - anto: If True, analyze antonyms; if False, analyze synonyms. - item_correlations: "complete" (default) or "pairwise" missing-data policy for pair discovery and scoring, as in psychsyn.

  • Dictionary with score summary statistics (mean_score, std_score, min_score, max_score, median_score), the number of selected item_pairs, and respondent counts n_total, n_valid and n_missing, as in mahad_summary() and markov_summary(). The same counts remain available as total_individuals, valid_individuals and missing_individuals for compatibility.
Example

data = [ ... [1, 1, 2, 3, 5, 5], ... [1, 1, 4, 4, 1, 1], ... [5, 5, 1, 1, 2, 1], ... [2, 2, 2, 1, 5, 3], ... [1, 1, 1, 2, 5, 5], ... [2, 4, 4, 3, 5, 3], ... ] summary = psychsyn_summary(data) summary["item_pairs"], summary["n_total"], summary["n_valid"], summary["n_missing"] (3, 6, 6, 0) round(summary["median_score"], 2) 0.97

person_total_flag(x, threshold=None, percentile=5.0, na_rm=True)

Compute person-total correlations and flag respondents with low agreement.

Parameters: - x: A matrix of data where rows are individuals and columns are items. - threshold: Absolute correlation threshold at or below which to flag. If None, uses percentile. - percentile: Percentile below which to flag (default 5th percentile). - na_rm: If True, use pairwise complete observations for correlations.

  • Tuple of (scores, flags) where flags is True for flagged respondents. Unavailable (NaN) scores are never flagged.
Example

data = [[1, 2, 3, 4, 5], [5, 4, 3, 2, 1], [1, 2, 3, 4, 5], [1, 2, 3, 5, 4]] scores, flags = person_total_flag(data, threshold=0.0) flags.tolist() [False, True, False, False]

autocorrelation_flag(x, threshold=None, percentile=95.0, max_lag=10, min_lag=1, statistic='max_abs', na_rm=True)

Compute lagged autocorrelation scores and flag strongly repetitive respondents.

Parameters: - x: A matrix of data where rows are individuals and columns are item responses. - threshold: Absolute score at or above which to flag. If None, uses percentile. - percentile: Percentile above which to flag (default 95th percentile). - max_lag: Largest lag passed to autocorrelation() (default 10). - min_lag: Smallest lag passed to autocorrelation() (default 1). - statistic: "max_abs" (default) or "sum_abs". - na_rm: Missing-response policy passed to autocorrelation().

  • Tuple of (scores, flags) where flags is True for flagged respondents. Unavailable (NaN) scores are never flagged.
Example

cycle = [2, 3, 4, 2, 3, 4, 2, 3, 4, 2, 3, 4] attentive = [4, 2, 5, 3, 3, 1, 4, 4, 2, 5, 3, 2] scores, flags = autocorrelation_flag([cycle, attentive], threshold=0.9, max_lag=3) flags.tolist() [True, False]

missing_rate(x, item_indices=None, applicable_mask=None)

Calculate each respondent's proportion of missing item responses.

Parameters: - x: A respondent Ă— item response matrix. - item_indices: Optional 0-based subset of columns to evaluate. By default, all item columns contribute equally. - applicable_mask: Optional Boolean matrix matching x. True cells are expected responses; False cells are excluded from both the missing count and the applicable-item count.

  • A float array in [0, 1]. Zero means a complete response row and one means every selected, applicable response is missing. Rows without any applicable selected items return NaN.

Raises: - ValueError: If the matrix, item selection, or applicability mask is invalid.

Example

import numpy as np missing_rate([[1, np.nan, 3], [np.nan, np.nan, 2]]) array([0.33333333, 0.66666667])

missing_rate_flag(x, threshold=None, percentile=95.0, item_indices=None, applicable_mask=None)

Calculate missing-response rates and flag unusually incomplete rows.

An explicit threshold flags rates at or above the cutoff. Without a fixed threshold, rates strictly above the requested sample percentile are flagged.

Parameters: - x: A respondent Ă— item response matrix. - threshold: Optional fixed rate in [0, 1]. - percentile: Sample percentile in [0, 100] used when threshold is None. - item_indices: Optional 0-based subset of columns to evaluate. - applicable_mask: Optional Boolean matrix matching x. False cells do not contribute to respondent-specific missing rates.

Returns: - Tuple of (rates, flags) aligned to respondent rows.

u3_poly_flag(x, scale_min=None, scale_max=None, threshold=None, percentile=95.0)

Compute U3 extreme-response proportions and flag unusually high values.

Scale bounds come first, in the order u3_poly() takes them, so u3_poly_flag(x, 1, 5) scores on the same 1-5 scale as u3_poly(x, 1, 5).

Parameters: - x: A matrix of data where rows are individuals and columns are items. - scale_min: Minimum value of the response scale. If None, inferred from data. - scale_max: Maximum value of the response scale. If None, inferred from data. - threshold: Absolute proportion threshold at or above which to flag. If None, uses percentile. - percentile: Percentile above which to flag (default 95th percentile).

  • Tuple of (scores, flags) where flags is True for flagged respondents. Unavailable (NaN) scores are never flagged.
Example

data = [[1, 1, 5, 5, 3], [3, 3, 3, 3, 3], [1, 5, 1, 5, 1]] scores, flags = u3_poly_flag(data, 1, 5, threshold=0.8) flags.tolist() [True, False, True]

midpoint_responding(x, scale_min=None, scale_max=None, tolerance=0.0)

Calculate proportion of midpoint responses for each individual.

Excessive midpoint responding may indicate satisficing or inattentive responding.

Parameters: - x: A matrix of data where rows are individuals and columns are items. - scale_min: Minimum value of the response scale. - scale_max: Maximum value of the response scale. - tolerance: Finite, nonnegative range around midpoint to count as midpoint response.

  • A numpy array of midpoint response proportions. Rows without observed responses or unavailable scale bounds receive NaN.

Raises: - ValueError: If scale bounds are inverted or tolerance is negative or nonfinite.

Example

data = [[1, 2, 5, 4, 3], [3, 3, 3, 3, 3], [1, 5, 1, 5, 1]] midpoint_responding(data, scale_min=1, scale_max=5).tolist() # Second is all midpoint [0.2, 1.0, 0.0]

midpoint_responding_flag(x, scale_min=None, scale_max=None, tolerance=0.0, threshold=None, percentile=95.0)

Compute midpoint-response proportions and flag unusually high values.

Scorer options come first, in the order midpoint_responding() takes them, so midpoint_responding_flag(x, 1, 5) uses the same 1-5 scale as midpoint_responding(x, 1, 5).

Parameters: - x: A matrix of data where rows are individuals and columns are items. - scale_min: Minimum value of the response scale. If None, inferred from data. - scale_max: Maximum value of the response scale. If None, inferred from data. - tolerance: Finite, nonnegative range around midpoint to count as midpoint response. - threshold: Absolute proportion threshold at or above which to flag. If None, uses percentile. - percentile: Percentile above which to flag (default 95th percentile).

  • Tuple of (scores, flags) where flags is True for flagged respondents. Unavailable (NaN) scores are never flagged.
Example

data = [[1, 2, 5, 4, 3], [3, 3, 3, 3, 3], [1, 5, 1, 5, 1]] scores, flags = midpoint_responding_flag(data, 1, 5, threshold=0.5) flags.tolist() [False, True, False]

gpoly(x, ncat=None, scale_min=None, scale_max=None, normalize=True, na_rm=True)

Count polytomous Guttman errors over item steps for each respondent.

Responses on a scale with M + 1 categories are split into the item steps X_j >= h for h = 1..M. Steps are ordered by popularity, the share of respondents passing them, most popular first. Ties keep item order and then step order, like rank(ties.method = "first") in PerFit, so the steps of one item stay nested. A Guttman error is a pair of steps where the respondent fails the more popular step but passes the less popular one (Molenaar, 1991), which equals PerFit's Gpoly. With normalize=True the count is divided by the largest count any response pattern with the same total score can reach (Emons, 2008), PerFit's Gnormed.poly. For dichotomous items this is G / (r * (J - r)) with r correct answers.

  • x: A matrix of integer category responses where rows are individuals and columns are items. All items share one response scale.
  • ncat: Number of response categories per item, at least 2. If None, the categories run from scale_min to scale_max.
  • scale_min: Lowest response category, an integer. If None, inferred from scale_max and ncat or from the observed minimum. Pass it when the lowest category may be unobserved.
  • scale_max: Highest response category, an integer. If None, inferred from scale_min and ncat or from the observed maximum.
  • normalize: If True (default), divide by the maximum error count for the respondent's total score. If False, return raw counts.
  • na_rm: If True, step popularities use each item's observed responses and raw counts include only pairs of observed steps, while normalized scores of respondents with missing responses are NaN. If False, missing responses raise ValueError.
  • A numpy array of Guttman error counts or proportions. Higher values indicate less consistent responding. Normalized scores are NaN when no error is possible for the total score (for example, every step failed or passed); respondents without observed responses are NaN.
  • ValueError: If responses are not integer categories within the scale, a given scale bound is not a finite integer, ncat is not an integer of at least 2 or disagrees with both scale bounds, the scale has more than 1024 categories, or na_rm=False and responses are missing.
Example

data = [[2, 2, 1, 0], [2, 1, 1, 0], [1, 1, 0, 0], ... [2, 2, 2, 1], [2, 0, 2, 0], [0, 0, 1, 2]] gpoly(data, normalize=False).tolist() # the last respondent reverses the order [0.0, 0.0, 2.0, 0.0, 2.0, 11.0] np.round(gpoly(data), 2).tolist() [0.0, 0.0, 0.17, 0.0, 0.14, 0.85]

gpoly_flag(x, threshold=None, percentile=95.0, *, ncat=None, scale_min=None, scale_max=None, normalize=True, na_rm=True)

Compute polytomous Guttman errors and flag unusually many.

  • x: A matrix of integer category responses where rows are individuals and columns are items.
  • threshold: Absolute score at or above which to flag. If None, uses percentile.
  • percentile: Percentile above which to flag (default 95th percentile).
  • ncat: Number of response categories per item, as in :func:gpoly.
  • scale_min: Lowest response category, as in :func:gpoly.
  • scale_max: Highest response category, as in :func:gpoly.
  • normalize: If True (default), flag normalized scores; otherwise raw counts.
  • na_rm: Missing-response policy passed to :func:gpoly.
  • Tuple of (scores, flags) where flags is True for flagged respondents. Unavailable (NaN) scores are never flagged.
Example

data = [[2, 2, 1, 0], [2, 1, 1, 0], [1, 1, 0, 0], ... [2, 2, 2, 1], [2, 0, 2, 0], [0, 0, 1, 2]] scores, flags = gpoly_flag(data, threshold=0.5) flags.tolist() [False, False, False, False, False, True]

u3poly(x, ncat=None, scale_min=None, scale_max=None, na_rm=True)

Calculate the polytomous U3 person-fit statistic over item steps.

Item steps and their popularities are defined as in :func:gpoly. Each step is weighted by the log-odds of its popularity, log(p / (1 - p)), so passing popular steps adds the most. With W the summed weight of a respondent's passed steps and W_max and W_min its largest and smallest values over all nested response patterns with the same total score, U3 = (W_max - W) / (W_max - W_min) (van der Flier, 1982; Emons, 2008), as in PerFit's U3poly. A step passed by every respondent, or by none, is weighted as if half a response went the other way, which keeps its weight finite. Weights share one fixed-point scale with 61 bits for their summed magnitude, so the extremes, perfect patterns (0), and least likely patterns (1) are exact.

This is the person-fit statistic. :func:ier.u3_poly is unrelated: it measures the proportion of extreme responses.

  • x: A matrix of integer category responses where rows are individuals and columns are items. All items share one response scale.
  • ncat: Number of response categories per item, at least 2. If None, the categories run from scale_min to scale_max.
  • scale_min: Lowest response category, an integer. If None, inferred from scale_max and ncat or from the observed minimum.
  • scale_max: Highest response category, an integer. If None, inferred from scale_min and ncat or from the observed maximum.
  • na_rm: If True, step popularities use each item's observed responses and respondents with missing responses are NaN. If False, missing responses raise ValueError.
  • A numpy array of U3 values from 0 (a perfect Guttman pattern) to 1 (the lowest weight possible for the total score). Higher values indicate less consistent responding. Respondents whose total score allows only one pattern weight (for example, every step failed or passed) are NaN.
  • ValueError: If responses are not integer categories within the scale, a given scale bound is not a finite integer, ncat is not an integer of at least 2 or disagrees with both scale bounds, the scale has more than 1024 categories, or na_rm=False and responses are missing.
Example

data = [[2, 2, 1, 0], [2, 1, 1, 0], [1, 1, 0, 0], ... [2, 2, 2, 1], [2, 0, 2, 0], [0, 0, 1, 2]] np.round(u3poly(data), 2).tolist() [0.0, 0.0, 0.17, 0.0, 0.2, 0.83]

u3poly_flag(x, threshold=None, percentile=95.0, *, ncat=None, scale_min=None, scale_max=None, na_rm=True)

Compute polytomous U3 person-fit values and flag unusually high ones.

  • x: A matrix of integer category responses where rows are individuals and columns are items.
  • threshold: Absolute U3 value at or above which to flag. If None, uses percentile.
  • percentile: Percentile above which to flag (default 95th percentile).
  • ncat: Number of response categories per item, as in :func:u3poly.
  • scale_min: Lowest response category, as in :func:u3poly.
  • scale_max: Highest response category, as in :func:u3poly.
  • na_rm: Missing-response policy passed to :func:u3poly.
  • Tuple of (scores, flags) where flags is True for flagged respondents. Unavailable (NaN) scores are never flagged.
Example

data = [[2, 2, 1, 0], [2, 1, 1, 0], [1, 1, 0, 0], ... [2, 2, 2, 1], [2, 0, 2, 0], [0, 0, 1, 2]] scores, flags = u3poly_flag(data, threshold=0.5) flags.tolist() [False, False, False, False, False, True]

ht(x, na_rm=True)

Calculate the transposed scalability coefficient Ht for dichotomous items.

For respondent n, Ht sums the covariances between n's item scores and those of every other respondent, taken across items, and divides by the sum of their maxima given each pair's numbers of correct answers (Sijtsma & Meijer, 1992), as in PerFit's Ht. Respondents whose answers follow the sample's item ordering score high, up to 1; values near or below 0 indicate aberrant responding. With r the respondents' numbers of correct answers, c the item totals, and R the sum of r, the numerator is J * (x_n . c - r_n) - r_n * (R - r_n) and the denominator is J * (sum_m min(r_n, r_m) - r_n) - r_n * (R - r_n). Both are computed in exact integer arithmetic in O(N * J) time.

  • x: A matrix of dichotomous responses coded 0 and 1 where rows are individuals and columns are items.
  • na_rm: If True, respondents with missing responses are NaN and are left out of everyone else's comparisons. If False, missing responses raise ValueError.
  • A numpy array of Ht values of at most 1. Lower values indicate less consistent responding. Respondents who answer every item or no item correctly, or whose comparisons have no positive maximum covariance, are NaN.
  • ValueError: If responses are not 0 or 1, or na_rm=False and responses are missing.
Example

data = [[1, 1, 1, 0], [1, 1, 0, 0], [1, 0, 0, 0], ... [1, 1, 1, 0], [1, 1, 0, 0], [0, 0, 1, 1]] np.round(ht(data), 2).tolist() # the last respondent reverses the order [0.6, 0.43, 0.5, 0.6, 0.43, -1.0]

ht_flag(x, threshold=None, percentile=5.0, *, na_rm=True)

Compute Ht person-fit values and flag unusually low ones.

Parameters: - x: A matrix of dichotomous responses coded 0 and 1. - threshold: Absolute Ht value at or below which to flag. If None, uses percentile. - percentile: Percentile below which to flag (default 5th percentile). - na_rm: Missing-response policy passed to :func:ht.

  • Tuple of (scores, flags) where flags is True for flagged respondents. Unavailable (NaN) scores are never flagged.
Example

data = [[1, 1, 1, 0], [1, 1, 0, 0], [1, 0, 0, 0], ... [1, 1, 1, 0], [1, 1, 0, 0], [0, 0, 1, 1]] scores, flags = ht_flag(data, threshold=0.0) flags.tolist() [False, False, False, False, False, True]

individual_reliability(x, n_splits=100, random_seed=None, *, factors=None)

Calculate resampled individual reliability for each person.

Estimates how consistent each individual's responses are by repeatedly splitting items into halves and correlating the split scores. Low reliability suggests inconsistent (potentially careless) responding.

With factors (recommended), each resample randomly splits every scale into two halves of size // 2 items (Curran, 2016; Huang et al., 2012). Each respondent's two vectors of scale half means, ignoring missing responses, are correlated across scales. Each half mean is its exact mean rounded once, so a split whose half means do not vary is unusable rather than scored from rounding noise. The valid correlations are averaged over resamples and Spearman–Brown corrected as 2r / (1 + r), clamped below at -1. Scales with fewer than two items contribute no halves.

Without factors, the legacy method pairs randomly permuted items across the whole questionnaire and correlates the paired item responses. Items of one scale share a trait level, so these correlations carry little signal for attentive respondents; prefer factors when the scale structure is known.

Parameters: - x: A matrix of data where rows are individuals and columns are items. - n_splits: Number of random split-half iterations (default 100). - random_seed: Optional integer seed or np.random.Generator for an isolated reproducible random stream; a Generator is advanced by the call. Without a seed, fresh entropy is used and NumPy's global random state is neither read nor advanced. - factors: Optional positive scale lengths, in column order, summing to the number of columns. At least two scales are required.

  • A numpy array of Spearman–Brown corrected mean split correlations. Higher values indicate more consistent responding. Without factors, finite values are at most 1 and can be below -1, and respondents without valid splits or with a mean split correlation of -1 receive NaN. With factors, finite values lie in [-1, 1] and respondents without a valid split receive NaN. Constant response profiles always receive NaN.
  • ValueError: If inputs are invalid, there are too few items, or factors are invalid, fewer than two, or don't sum to the number of columns
Example

data = [[1, 2, 1, 2, 1, 2], [1, 5, 2, 4, 1, 5], [3, 3, 3, 3, 3, 3]] rel = individual_reliability(data, n_splits=50, random_seed=0) print(np.isnan(rel).tolist()) # The constant profile is undefined [False, False, True] data = [[1, 1, 3, 3, 5, 5], [1, 5, 5, 1, 2, 4], [3, 3, 3, 3, 3, 3]] rel = individual_reliability(data, n_splits=20, random_seed=0, factors=[2, 2, 2]) print(np.round(rel, 2).tolist()) [1.0, -1.0, nan]

reverse_score(x, items, scale_min=None, scale_max=None)

Reverse-score selected items, returning a recoded copy of the responses.

Each selected response v becomes scale_min + scale_max - v, so a 1 on a 1-5 scale becomes 5. Recode reverse-worded items before indices that assume all items are keyed in the same direction: evenodd, individual_reliability, guttman, lz, gpoly, u3poly_fit, and ht, the indices whose index_catalog() entry sets uses_keyed_responses. Sequence and response-style indices such as longstring, markov, irv, and acquiescence should keep the responses as presented.

Parameters: - x: A matrix of data where rows are individuals and columns are items. - items: Distinct zero-based column indices of the reverse-keyed items. - scale_min: Minimum response-scale value. If None, the observed minimum of the whole matrix is used. - scale_max: Maximum response-scale value. If None, the observed maximum of the whole matrix is used. Pass explicit bounds whenever an endpoint might be unobserved.

  • A new array with the selected columns reverse-scored; the input is never modified and missing responses stay NaN. 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. Apart from converting non-array input, the returned copy is the only full-size allocation: the selected columns are checked and recoded in bounded row batches.
  • ValueError: If inputs are invalid, items are empty, duplicated, or out of range, bounds are not finite real numbers or are reversed, or a selected response lies outside the bounds. Responses are compared with the bounds exactly, whatever their precision.
Example

data = [[1, 2, 5], [4, 5, 1], [3, 3, 2]] print(reverse_score(data, [2], scale_min=1, scale_max=5).tolist()) [[1, 2, 1], [4, 5, 5], [3, 3, 4]] print(reverse_score([[1.5, float("nan")], [4.0, 2.0]], [0, 1]).tolist()) [[4.0, nan], [1.5, 3.5]]

mad_flag(x, positive_items=None, negative_items=None, item_pairs=None, scale_max=None, threshold=None, percentile=95.0, na_rm=True, *, scale_min=None)

Calculate MAD scores and flag potential careless responders.

High MAD scores indicate careless responding (not attending to item direction).

Parameters: - x: A matrix of data where rows are individuals and columns are item responses. - positive_items: List of column indices for positively-worded items. - negative_items: List of column indices for negatively-worded items. - item_pairs: Alternative list of (positive, negative) index tuples. - scale_max: Maximum value of the response scale. - threshold: Absolute MAD threshold at or above which to flag. If None, uses percentile. - percentile: Percentile cutoff for flagging (default 95th percentile). - na_rm: Boolean indicating whether to ignore missing values. - scale_min: Minimum value of the response scale. If None, inferred from data.

Returns: - Tuple of (mad_scores, flags) where flags is True for suspected careless responders.

Example

data = [[5, 1, 4, 2], [5, 5, 5, 5], [5, 2, 4, 1]] scores, flags = mad_flag(data, positive_items=[0, 2], negative_items=[1, 3]) flags.tolist() [False, True, False]

semantic_syn(x, item_pairs, anto=False, *, scale_min=None, scale_max=None)

Calculate semantic synonym/antonym consistency scores.

Computes mean absolute differences for predefined item pairs and normalizes them by each person's response standard deviation. Synonyms are compared directly; antonyms reverse-score the second response before comparison.

Parameters: - x: A matrix of data where rows are individuals and columns are items. - item_pairs: List of (i, j) tuples specifying semantically related item pairs. Indices are 0-based. - anto: If True, reverse-score the second item in each antonym pair before comparing it with the first. If False, compare synonym pairs directly. - scale_min: Minimum response-scale value used to reverse-score antonyms. If None, inferred from the data. - scale_max: Maximum response-scale value used to reverse-score antonyms. If None, inferred from the data.

  • A numpy array of consistency scores for each individual. Higher values indicate greater consistency for both synonyms and antonyms.
  • ValueError: If item_pairs is empty, a pair does not contain two distinct integer column indices, or an index is outside the matrix bounds.
Example

data = [[1, 2, 5, 4], [1, 1, 5, 5], [3, 1, 3, 5]] pairs = [(0, 1), (2, 3)] # semantic synonym pairs scores = semantic_syn(data, pairs)

semantic_syn_flag(x, item_pairs, threshold=None, percentile=5.0)

Score semantic synonym consistency and flag unusually low values.

semantic_ant(x, item_pairs, *, scale_min=None, scale_max=None)

Calculate semantic antonym consistency scores.

Convenience wrapper for semantic_syn with anto=True.

Parameters: - x: A matrix of data where rows are individuals and columns are items. - item_pairs: List of (i, j) tuples specifying semantic antonym pairs. - scale_min: Minimum response-scale value. If None, inferred from the data. - scale_max: Maximum response-scale value. If None, inferred from the data.

Returns: - A numpy array of consistency scores for each individual.

Example

data = [[1, 5, 2, 4], [1, 5, 1, 5], [3, 3, 3, 3]] pairs = [(0, 1), (2, 3)] # semantic antonym pairs (e.g., happy/sad) scores = semantic_ant(data, pairs)

semantic_ant_flag(x, item_pairs, threshold=None, percentile=5.0, *, scale_min=None, scale_max=None)

Score semantic antonym consistency and flag unusually low values.

infrequency_flag(x, item_indices, expected_responses=None, threshold=1.0, proportion=False, missing='pass', *, acceptable_ranges=None)

Count failed attention-check items and flag respondents exceeding a threshold.

Parameters: - x: A matrix of data where rows are individuals and columns are item responses. - item_indices: Column indices (0-based) of the attention-check items. - expected_responses: Expected correct response for each attention-check item. - threshold: Failure count or proportion at or above which to flag (default 1). - proportion: If True, flag failure proportions instead of counts. - missing: Missing-response policy passed to infrequency(). - acceptable_ranges: Keyword-only inclusive (low, high) ranges passed to infrequency() instead of expected_responses.

Returns: - Tuple of (failure_scores, flags) where flags is True for flagged respondents.

Example

data = [[5, 3, 1], [5, 5, 5], [1, 3, 5]] scores, flags = infrequency_flag(data, [0, 2], [5, 1], threshold=2) print(flags) [False False True]

response_time_consistency(times)

Calculate response time consistency (coefficient of variation).

Very low consistency (uniform times) may indicate "clicking through" behavior where the person isn't reading items.

Parameters: - times: A matrix of response times.

  • A numpy array of coefficient of variation values for each individual. Lower values indicate more uniform (potentially suspicious) timing.
Example

import numpy as np times = [[2.1, 3.4, 2.8], [1.0, 1.0, 1.0], [2.5, 2.3, 2.7]] cv = response_time_consistency(times) print(np.round(cv, 2).tolist()) # Second person has very consistent (suspicious) times [0.19, 0.0, 0.07]

response_time_effort(times, thresholds=None, *, normative_fraction=0.1, max_threshold=None, return_item_flags=False)

response_time_effort(times: MatrixLike, thresholds: ArrayLike | None = None, *, normative_fraction: float = 0.1, max_threshold: float | None = None, return_item_flags: Literal[False] = False) -> np.ndarray
response_time_effort(times: MatrixLike, thresholds: ArrayLike | None = None, *, normative_fraction: float = 0.1, max_threshold: float | None = None, return_item_flags: Literal[True]) -> tuple[np.ndarray, np.ndarray]
response_time_effort(times: MatrixLike, thresholds: ArrayLike | None = None, *, normative_fraction: float = 0.1, max_threshold: float | None = None, return_item_flags: bool = False) -> np.ndarray | tuple[np.ndarray, np.ndarray]

Calculate response time effort: the share of answered items not rapidly answered.

Response time effort (RTE; Wise & Kong, 2005) compares every response time with a threshold for that item, so long and short items are judged by their own expected time. A response faster than the item threshold (strictly below it) is a rapid response. By default each item threshold is the normative NT10 threshold (Wise & Ma, 2012): 10% of the item's mean response time over the respondents who answered it, optionally capped (commonly at 10 seconds) with max_threshold.

  • times: A matrix of response times where rows are individuals and columns are items. Missing times (NaN) are unanswered items.
  • thresholds: Optional item thresholds in the units of times: one value for every item or a vector with one value per item. Missing (NaN), infinite, and nonpositive values exclude their items, but max_threshold caps positive infinity like any other value. If None, normative thresholds are used.
  • normative_fraction: Fraction of each item's mean time used as its normative threshold, greater than 0 and at most 1 (default 0.10).
  • max_threshold: Optional positive cap applied to every item threshold.
  • return_item_flags: If True, also return the Boolean matrix of rapid responses, for example for effort-moderated scoring.
  • A numpy array of RTE values in [0, 1]: the proportion of each respondent's answered items with a response time at or above the item threshold. Lower values indicate more rapid responding. Items whose threshold, after any max_threshold cap, is missing, infinite, or not positive are excluded, and respondents without an answered eligible item are unavailable (NaN). With return_item_flags, a tuple of (scores, rapid) where rapid is True for answered eligible items below their threshold.
  • ValueError: If inputs are invalid, thresholds do not match the items, or normative_fraction or max_threshold is out of range.
Example

times = [[12, 30, 18], [0.5, 1.5, 20], [14, 28, np.nan], [13.5, 20.5, 22]] np.round(response_time_effort(times), 2).tolist() # thresholds 1, 2, and 2 [1.0, 0.33, 1.0, 1.0] scores, rapid = response_time_effort(times, return_item_flags=True) rapid[1].tolist() [True, True, False] response_time_effort(times, thresholds=15).tolist() [0.6666666666666666, 0.3333333333333333, 0.5, 0.6666666666666666]

response_time_effort_flag(times, threshold=0.9, *, thresholds=None, normative_fraction=0.1, max_threshold=None)

Calculate response time effort and flag respondents below an RTE cutoff.

Parameters: - times: A matrix of response times. - threshold: RTE value between 0 and 1 below which (strictly) to flag (default 0.90, a common rapid-guessing screening rule). - thresholds: Optional item thresholds passed to response_time_effort(). - normative_fraction: Normative threshold fraction passed to response_time_effort() (default 0.10). - max_threshold: Optional item-threshold cap passed to response_time_effort().

  • Tuple of (scores, flags) where flags is True for respondents whose RTE is below threshold. Unavailable (NaN) scores are never flagged.
Example

times = [[12, 30, 18], [0.5, 1.5, 20], [14, 28, np.nan], [13.5, 20.5, 22]] scores, flags = response_time_effort_flag(times) flags.tolist() [False, True, False, False]

response_time_flag(times, threshold=None, method='median', cutoff_percentile=5.0)

Flag individuals with suspiciously fast response times.

Parameters: - times: A matrix of response times. - threshold: Absolute threshold at or below which to flag (in the same units as times). If None, uses cutoff_percentile to determine threshold. - method: Method for computing per-person response time ("mean" or "median"). - cutoff_percentile: Percentile below which to flag (default 5th percentile). Only used if threshold is None.

Returns: - Boolean array where True indicates potentially careless responding.

Example

times = [[2.1, 3.4, 2.8], [0.5, 0.4, 0.6], [2.5, 2.3, 2.7]] flags = response_time_flag(times, threshold=1.0)

response_time_mixture(times, n_components=2, log_transform=True, random_seed=None)

Fit a Gaussian mixture model to per-person response times and return the posterior probability of belonging to the fast (careless) component.

Computes per-person median response time, optionally log-transforms, then fits a k-component Gaussian mixture via EM. The component with the lowest mean is identified as the "fast" (careless) component.

  • times: A matrix of response times where rows are individuals and columns are items.
  • n_components: Integer number of mixture components, at least 2 (default 2).
  • log_transform: If True (default), log-transform median times before fitting.
  • random_seed: Optional seed for reproducibility of EM initialization.
  • A numpy array of posterior probabilities of belonging to the fast component, one per respondent. Higher values indicate greater likelihood of careless (fast) responding. Equal usable medians return 1 / n_components; the data cannot distinguish a fast group in that case. Nonpositive or nonfinite medians remain unavailable (NaN).

Raises: - ValueError: If n_components < 2 or data is insufficient.

Example

times = [[5.0, 6.0, 4.0], [0.5, 0.6, 0.4], [4.5, 5.5, 5.0]] probs = response_time_mixture(times, random_seed=42)

response_time_score_flags(scores, threshold=None, cutoff_percentile=None, direction='low')

Flag a retained one-dimensional response-time score vector.

Use low-tail flagging for direct timing summaries and consistency scores, or high-tail flagging for fast-component mixture probabilities. Fixed thresholds include equality; percentile-derived cutoffs exclude ties. When cutoff_percentile is omitted, the low tail defaults to the 5th percentile and the high tail to the 95th percentile.

Parameters: - scores: Retained per-respondent response-time scores. - threshold: Optional fixed cutoff in the score's units. - cutoff_percentile: Optional sample-relative cutoff percentile. - direction: Suspicious tail, "low" or "high".

Returns: - Boolean array where True indicates a suspicious score.

Example

times = [[2.1, 3.4, 2.8], [0.5, 0.4, 0.6], [2.5, 2.3, 2.7]] medians = response_time(times, metric="median") strict = response_time_score_flags(medians, cutoff_percentile=1) print(strict.tolist()) [False, True, False] mixture = response_time_mixture(times, random_seed=42) likely_fast = response_time_score_flags(mixture, direction="high") print(likely_fast.tolist()) [False, True, False]

plot_distributions(screen_result, figsize=None, bins=30, *, show_thresholds=True)

Plot histograms of score distributions for each index.

Each applied cutoff is drawn as a dashed line, and the tail that the index flags is shaded according to its registered direction. When per-index summaries are available, panel titles report the number of flagged respondents. Presence-flagged indices such as onset, and indices without any available scores, have no cutoff line.

Parameters: - screen_result: Output dict from screen(). - figsize: Figure size as (width, height). If None, auto-calculated. - bins: Number of histogram bins. - show_thresholds: Draw applied cutoffs and shade flagged tails.

Returns: - matplotlib Figure object.

Raises: - ValueError: If show_thresholds is not a boolean. - RuntimeError: If matplotlib is not available.

Example

from ier import plot_distributions, screen_scores result = screen_scores( ... {"irv": [0.1, 0.4, 0.9, 1.2], "longstring": [8.0, 3.0, 2.0, 7.0]}, ... thresholds={"irv": 0.5, "longstring": 5.0}, ... ) fig = plot_distributions(result) titles = [ax.get_title() for ax in fig.axes] titles ['irv (flagged: 2)', 'longstring (flagged: 2)']

plot_flag_counts(screen_result, figsize=None)

Plot a bar chart of flag counts across respondents.

X-axis is the number of flags, y-axis is the count of respondents with that many flags.

Parameters: - screen_result: Output dict from screen(). - figsize: Figure size as (width, height).

Returns: - matplotlib Figure object.

Raises: - RuntimeError: If matplotlib is not available.

Example

from ier import plot_flag_counts, screen_scores result = screen_scores( ... {"irv": [0.1, 0.4, 0.9, 1.2], "longstring": [8.0, 3.0, 2.0, 7.0]}, ... thresholds={"irv": 0.5, "longstring": 5.0}, ... ) fig = plot_flag_counts(result) heights = [int(bar.get_height()) for bar in fig.axes[0].patches] heights [1, 2, 1, 0]

plot_flagged_heatmap(screen_result, figsize=None, cmap='Reds', *, max_rows=1000, order='input')

Plot a heatmap of flag status per respondent and index.

Rows are respondents, columns are indices. Flagged and unflagged cells use the same color scale across cohorts. Unavailable scores are gray; for a presence-based index such as onset, NaN means no detected event and remains unflagged. Automatic figure dimensions are bounded for large surveys.

Surveys with more than max_rows respondents are drawn as consecutive respondent blocks, so rare flags are not lost to image downsampling. A block is flagged when any of its respondents is flagged for that index, and gray only when every respondent in it is unavailable.

Parameters: - screen_result: Output dict from screen(). - figsize: Figure size as (width, height). If None, dimensions are bounded at 18 inches wide and 12 inches high. - cmap: Matplotlib colormap name. - max_rows: Maximum number of plotted rows. Larger surveys are aggregated into blocks of ceil(n_respondents / max_rows) respondents. None plots one row per respondent. - order: "input" keeps respondent order; "flag_count" sorts respondents by descending flag count, keeping input order among ties, so flagged respondents appear at the top.

Returns: - matplotlib Figure object.

  • ValueError: If max_rows is not a positive integer or None, or order is not recognized.
  • RuntimeError: If matplotlib is not available.
Example

import numpy as np from ier import plot_flagged_heatmap, screen_scores irv = np.ones(5000) irv[1234] = 0.0 result = screen_scores({"irv": irv}, thresholds={"irv": 0.5}) fig = plot_flagged_heatmap(result) fig.axes[0].images[0].get_array().shape (1000, 1) fig.axes[0].get_ylabel() 'Respondent block (5 per row)'

plot_composite(scores, *, threshold=None, flags=None, bins=30, figsize=None)

Plot the distribution of composite scores with an optional cutoff.

Higher composite scores are more suspicious, so a cutoff shades the high tail, matching composite_flag(). The legend reports the number of flagged respondents: from flags when given, otherwise from scores at or above threshold. Flagged respondents' scores are overlaid when flags is given. Missing composite scores are omitted from the histogram.

  • scores: Composite score vector, for example from composite() or composite_flag(). May contain NaN.
  • threshold: Optional finite cutoff to draw.
  • flags: Optional boolean flags aligned with scores.
  • bins: Number of histogram bins.
  • figsize: Figure size as (width, height). Defaults to (8, 5).

Returns: - matplotlib Figure object.

  • ValueError: If scores are not a non-empty real vector of finite values or NaN, threshold is not finite, or flags are not aligned booleans.
  • RuntimeError: If matplotlib is not available.
Example

from ier import plot_composite scores = [-1.0, -0.2, 0.1, 0.4, 2.5] fig = plot_composite(scores, threshold=2.0) labels = [text.get_text() for text in fig.axes[0].get_legend().get_texts()] labels ['Cutoff (2)', 'Flagged (n=1)']

plot_index_agreement(screen_result, kind='jaccard', *, figsize=None, cmap=None, annotate=True)

Plot an index-by-index agreement matrix from index_agreement().

Color limits are fixed so plots are comparable across studies: overlap (as the share of respondents flagged by both indices) and Jaccard use [0, 1] with viridis; Spearman correlations use [-1, 1] with RdBu_r. Spearman scores are oriented by each index's flag direction, so positive (red) cells mean both indices rank the same respondents as more suspicious and negative (blue) cells mean they disagree. Undefined cells are gray. Annotations show co-flag counts for overlap and two-decimal values otherwise.

Parameters: - screen_result: Output dict from screen(). - kind: "overlap", "jaccard", or "spearman". - figsize: Figure size as (width, height). If None, auto-calculated. - cmap: Matplotlib colormap name overriding the kind's default. - annotate: Write each cell's value.

Returns: - matplotlib Figure object.

Raises: - ValueError: If kind is not recognized or annotate is not a boolean. - RuntimeError: If matplotlib is not available.

Example

from ier import plot_index_agreement, screen_scores result = screen_scores( ... {"irv": [0.1, 0.2, 0.9, 1.2], "longstring": [8.0, 3.0, 2.0, 7.0]}, ... thresholds={"irv": 0.5, "longstring": 5.0}, ... ) fig = plot_index_agreement(result) cells = [text.get_text() for text in fig.axes[0].texts] cells ['1.00', '0.33', '0.33', '1.00']

Persisting screening results

save_screen_archive() and load_screen_archive() persist a complete ScreenResult: scores, per-index flags, recorded thresholds and their sources, percentile settings, counts, consensus decisions, summaries, and soft failures. They share the schema written by ier screen --format npz, so CLI archives load the same way.

from ier import load_screen_archive, plot_flagged_heatmap, save_screen_archive, screen

result = screen(data, percentile=95, min_flags=2)
save_screen_archive("screening.npz", result, respondent_ids=ids)

saved = load_screen_archive("screening.npz")
assert saved["result"]["consensus_flags"].tolist() == result["consensus_flags"].tolist()
figure = plot_flagged_heatmap(saved["result"])

The writer validates every decision before opening the destination and replaces it atomically; the loader recomputes each flag from its score and recorded cutoff, and checks counts, consensus decisions, and summary counts, flag rates, minima, and maxima. Summary means and standard deviations are rebuilt from the restored scores. Rebuilding decisions with screen_scores(thresholds=saved["result"]["thresholds"]) is not equivalent, because explicit thresholds include ties while percentile cutoffs exclude them.

All archive writers accept compressed=True and an optional compression_level from 1 (fastest, the default) to 9 (smallest). Any level loads through the same functions.