Skip to content

API Reference

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

ResponseTimeFlagDirection = Literal['high', 'low'] module-attribute

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

__version__ = version('insufficient-effort') module-attribute

IndexOptions dataclass

Shared optional configuration for registered index scorers.

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.

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 are reusable directly. 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.

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, screen_scores saved = load_score_archive("screening.npz") updated_screen = screen_scores(saved["scores"], percentile=99) saved_components = load_score_archive("composite.npz") updated_composite = composite_scores( ... saved_components["scores"], ... weights={"irv": 2.0}, ... )

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, vector shape and respondent alignment, optional identifiers, and agreement between the stored flags and threshold. 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.

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 saved = load_response_time_archive("timing.npz") strict = response_time_score_flags( ... saved["scores"], cutoff_percentile=1, ... direction=saved["flag_direction"], ... )

save_response_time_archive(path, scores, flags, *, threshold, metric='median', flag_direction='low', respondent_ids=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 percentile rule.

Parameters: - path: Explicit destination ending in .npz. - scores: Per-respondent direct timing scores 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.

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" ... )

save_score_archive(path, scores, *, result_type='screen', respondent_ids=None, errors=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. Composite archives accept only indices 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. - errors: Optional ordered mapping of failed index names to nonblank messages.

Example

from ier import load_score_archive, save_score_archive, screen_scores scores = {"irv": [0.1, 0.7], "longstring": [3.0, 8.0]} save_score_archive("scores.npz", scores, respondent_ids=["a", "b"]) saved = load_score_archive("scores.npz") updated = screen_scores(saved["scores"], percentile=95)

screen_scores(scores, *, percentile=95.0, min_flags=2, min_valid_indices=None, thresholds=None, percentiles=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.

  • The same structured ScreenResult contract as :func:screen, with an empty errors mapping because no index calculation is attempted.
Example

from ier import screen, screen_scores initial = screen(data, indices=["irv", "longstring"]) stricter = screen_scores( ... initial["scores"], ... percentiles={"irv": 99, "longstring": 99}, ... )

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)

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(). 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) -> np.ndarray
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[np.ndarray, 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.

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

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.

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

Example

from ier import composite_scores, composite_summary initial = composite_summary(data, indices=["irv", "longstring"]) weighted = composite_scores( ... initial["indices"], ... weights={"irv": 2.0, "longstring": 0.5}, ... )

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.

longstring_scores(x, na_rm=True)

Compute longest 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.

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).
  • 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.

Raises: - ValueError: If inputs are invalid.

Example

data = [[1, 2, 1, 2, 1, 2], [1, 2, 3, 4, 5, 6]] longstring_pattern(data) array([6., 0.])

psychant(x, critval=-0.6, diag=False, resample_na=False, random_seed=None)

psychant(x: MatrixLike, critval: float = -0.6, diag: Literal[False] = False, resample_na: bool = False, random_seed: int | None = None) -> np.ndarray
psychant(x: MatrixLike, critval: float = -0.6, diag: Literal[True] = True, resample_na: bool = False, random_seed: int | None = None) -> 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: Boolean to indicate resampling when encountering NA for a respondent. - random_seed: Optional seed for random number generation when resample_na=True.

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

Example

data = [[1, 2, 3, 4, 5, 6], [2, 3, 4, 5, 6, 7], [1, 1, 1, 4, 5, 6]] scores = psychant(data, critval=-0.5) print(scores) [0.23, 0.18, 0.45]

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.

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: Range around midpoint to count as midpoint response.

Returns: - A numpy array of midpoint response proportions.

Example

data = [[1, 2, 5, 4, 3], [3, 3, 3, 3, 3], [1, 5, 1, 5, 1]] mid = midpoint_responding(data, scale_min=1, scale_max=5) print(mid) # Second person has all midpoint responses

individual_reliability(x, n_splits=100, random_seed=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.

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 seed for an isolated reproducible random stream.

  • A numpy array of reliability estimates for each individual. Values range from -1 to 1, with higher values indicating more consistent responding.

Raises: - ValueError: If inputs are invalid or too few items

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) print(rel) # First person: high, second: variable, third: undefined

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]) print(flags) [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.

Raises: - ValueError: If inputs are invalid or item_pairs is empty

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, threshold=1.0, proportion=False, missing='pass')

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().

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

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(cv) # Second person has very consistent (suspicious) times

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: Number of mixture components (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.

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

medians = response_time(times, metric="median") strict = response_time_score_flags(medians, cutoff_percentile=1) mixture = response_time_mixture(times, random_seed=42) likely_fast = response_time_score_flags(mixture, direction="high")

plot_distributions(screen_result, figsize=None, bins=30)

Plot histograms of score distributions for each index.

Parameters: - screen_result: Output dict from screen(). - figsize: Figure size as (width, height). If None, auto-calculated. - bins: Number of histogram bins.

Returns: - matplotlib Figure object.

Raises: - RuntimeError: If matplotlib is not available.

Example

result = screen(data) fig = plot_distributions(result) fig.savefig("distributions.png")

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

result = screen(data) fig = plot_flag_counts(result)

plot_flagged_heatmap(screen_result, figsize=None, cmap='Reds')

Plot a heatmap of flag status per respondent and index.

Rows are respondents, columns are indices. Colored cells indicate flagged.

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

Returns: - matplotlib Figure object.

Raises: - RuntimeError: If matplotlib is not available.

Example

result = screen(data) fig = plot_flagged_heatmap(result)