| Title: | Quick Screening of Response Patterns in Test and Survey Items |
|---|---|
| Description: | One call screens the item responses of a test or questionnaire for patterns that deserve a closer look before modelling: flat or long-string responding, low intra-individual variability, multivariate outliers, incoherent psychometric synonyms or even-odd halves, overuse of the midpoint or of the extremes, duplicated response vectors and identical patterns that arrive in series of consecutive submissions. Indices follow Curran (2016) <doi:10.1016/j.jesp.2015.07.006>; person fit in item response theory (the standardized log-likelihood) is optional, and cut-offs can be taken from a reference group. Results are returned per person, with flags, frequency tables of response patterns and plots. Whole batches of data (a second collection phase, one classroom) can be compared with a reference group: the item correlation structure, with permutation tests that leave out flat responders and match the response level, and the Jennrich (1970) <doi:10.1080/01621459.1970.10481133> test; the similarity of consecutive rows; and near-duplicate respondents. Flagged respondents can be removed in one step and the same confirmatory or exploratory factor model can be refitted with and without them. |
| Authors: | Jose Ventura-Leon [aut, cre] (ORCID: <https://orcid.org/0000-0003-2996-4244>) |
| Maintainer: | Jose Ventura-Leon <[email protected]> |
| License: | GPL (>= 3) |
| Version: | 0.4.1 |
| Built: | 2026-11-08 20:44:24 UTC |
| Source: | https://github.com/cran/ResponseScan |
print() shows the groups compared and the tests. plot() draws one of
three views, with ggplot2:
"null"Histograms of each statistic in the reference samples, with the focal value as a vertical line. A line far to the left of the histogram means less structure than any comparable group of reference respondents. Compare the panels of the full and the level-matched reference: when only the first one is extreme, the level explains the difference.
"heatmap"Item correlations of the reference (level-matched when available) and of the focal group, side by side, with the scales marked. Genuine data show blocks along the diagonal.
"structure"Mean correlation within and between scales in each group. The gap between both bars is the structure.
## S3 method for class 'batch_comparison' print(x, digits = 3, ...) ## S3 method for class 'batch_comparison' plot( x, type = c("null", "heatmap", "structure"), lang = c("en", "es"), base_size = 11, ... )## S3 method for class 'batch_comparison' print(x, digits = 3, ...) ## S3 method for class 'batch_comparison' plot( x, type = c("null", "heatmap", "structure"), lang = c("en", "es"), base_size = 11, ... )
x |
An object returned by |
digits |
Number of decimals printed. |
... |
Not used. |
type |
One of |
lang |
Language of the labels: |
base_size |
Base font size of the theme. |
print() returns x invisibly. plot() returns a ggplot
object, printed on the current device and returned invisibly.
sc <- list(F1 = paste0("I", 1:4), F2 = paste0("I", 5:8), F3 = paste0("I", 9:12)) cb <- compare_batches(batch_sim, group = "phase", scales = sc, B = 200) print(cb, digits = 2) plot(cb, type = "structure")sc <- list(F1 = paste0("I", 1:4), F2 = paste0("I", 5:8), F3 = paste0("I", 9:12)) cb <- compare_batches(batch_sim, group = "phase", scales = sc, B = 200) print(cb, digits = 2) plot(cb, type = "structure")
Simulated answers of 260 respondents to a 12-item questionnaire (three
scales of four items, options 1 to 5), collected in two phases. Phase 1
has 200 genuine respondents, 8 of them flat. Phase 2 has 30 genuine
respondents with a higher level and a block of 30 rows filled by hand:
each row copies the previous one and changes two or three answers, and
the sex and age are often copied too. The block has the level of phase 2
but no factor structure. It is the example data of compare_batches(),
order_test() and neighbor_test(). No real person is included.
batch_simbatch_sim
A data frame with 260 rows and 17 columns:
Respondent number, in order of arrival.
Collection phase, 1 or 2.
"Female" or "Male".
Age group: "17-20", "21-25", "26-30" or "31+".
How the row was generated: "genuine", "flat" or
"hand-filled".
Item 1 (scale 1); options 1 to 5.
Item 2 (scale 1); options 1 to 5.
Item 3 (scale 1); options 1 to 5.
Item 4 (scale 1); options 1 to 5.
Item 5 (scale 2); options 1 to 5.
Item 6 (scale 2); options 1 to 5.
Item 7 (scale 2); options 1 to 5.
Item 8 (scale 2); options 1 to 5.
Item 9 (scale 3); options 1 to 5.
Item 10 (scale 3); options 1 to 5.
Item 11 (scale 3); options 1 to 5.
Item 12 (scale 3); options 1 to 5.
Simulated with the script data-raw/make_batch_sim.R of the
package source.
Asks whether a batch of respondents (a second collection phase, one classroom, the cases entered by one person) answers with the same structure as the rest. In genuine data, items of the same scale move together more than items of different scales, because the answers come from the respondents' traits. Answers filled by hand can copy the level (many 4s and 5s) but rarely reproduce that structure.
compare_batches( data, group, items = NULL, scales = NULL, focal = NULL, reference = NULL, drop_flat = TRUE, match_level = TRUE, B = 2000, min_ratio = 3 )compare_batches( data, group, items = NULL, scales = NULL, focal = NULL, reference = NULL, drop_flat = TRUE, match_level = TRUE, B = 2000, min_ratio = 3 )
data |
A data frame with one row per respondent. |
group |
The batch of each respondent: a column name of |
items |
Item columns. By default, the items in |
scales |
Optional named list of character vectors, one per scale.
Needed for the |
focal |
Group (or groups) under examination. By default, the smallest group. |
reference |
Group (or groups) used as reference. By default, every other group. |
drop_flat |
Logical; remove respondents with the same answer in every item before comparing. |
match_level |
Logical; add the comparison with reference samples paired by level with the focal batch. |
B |
Number of random samples drawn from the reference. |
min_ratio |
Minimum ratio between the reference and the focal group
for the whole batch to be compared in every sample. With a smaller
reference (even one smaller than the batch) the batch is subsampled to
|
Two statistics are computed in the focal batch: the mean inter-item
correlation and, when scales defines two or more scales, the
structure (mean correlation within scales minus mean correlation
between scales). Their null distribution comes from B random samples,
of the size of the focal batch, drawn from the reference group. The
p value is the share of samples with a value as low or lower than the
focal one, so small values mean that the batch has less structure than
any comparable group of reference respondents.
Two precautions avoid false alarms, and both changed the conclusion in the real case that motivated this function:
drop_flat: respondents with the same answer in every item
inflate the correlations of the group they belong to. They are
removed from both groups before comparing.
match_level: a batch that answers high has less variability,
and less variability lowers correlations on its own (range
restriction). Each reference sample is then built by pairing every
focal respondent with the reference respondent of the closest mean
answer, so it reproduces the whole distribution of levels of the
batch. When the difference disappears with this control, the low
correlations are explained by the level, not by the answers.
The level-matched samples need a reference clearly larger than the
batch, above all when the batch answers at an unusual level. When the
reference has fewer than min_ratio times the focal respondents,
pairing every focal respondent picks nearly the same reference rows in
every sample, the control does not vary and its p values overstate the
evidence. In the real case behind this argument (32 items, a reference
of 150 and a lower-level batch of 210), consecutive matched samples
shared 100% of their rows with the whole reference, 87% with
min_ratio = 2 and 63% with min_ratio = 3. In that case each sample takes a random
subsample of the batch, of the size of the reference divided by
min_ratio, and pairs the reference with it; the focal value reported is
then the median over the focal subsamples, so that both sides are
computed at the same size. This median is more stable than a single
focal sample, which makes the test somewhat conservative. A warning is
given when consecutive level-matched samples still share more than 80%
of their rows.
The Jennrich (1970) test of the equality of the two correlation matrices is reported as well. It is very sensitive: it also rejects when two genuine populations differ, so it says that the matrices differ, not why.
An object of class batch_comparison, a list with:
One row per group (focal, reference and, if requested, the level-matched reference): respondents, flat respondents removed, respondents used, mean answer, mean within-person standard deviation, mean inter-item correlation and the within, between and structure correlations.
One row per comparison and statistic: the focal value, the
size of the samples compared (n_sample), the median and the 2.5th
percentile of the reference samples, and the p value.
Jennrich test of the focal against each reference: chi- square, degrees of freedom and p value.
The values of the reference samples, the
correlation matrices and the arguments used, kept for the plots.
settings also keeps the focal sample size (focal_sample), whether
the batch was subsampled and the share of rows shared by consecutive
level-matched samples (matched_overlap).
Jennrich, R. I. (1970). An asymptotic chi-square test for the equality of two correlation matrices. Journal of the American Statistical Association, 65(330), 904-912. doi:10.1080/01621459.1970.10481133
order_test() and neighbor_test() for traces of copying,
and the reference argument of scan_responses() to flag single
respondents with cut-offs taken from a reference group.
sc <- list(F1 = paste0("I", 1:4), F2 = paste0("I", 5:8), F3 = paste0("I", 9:12)) cb <- compare_batches(batch_sim, group = "phase", scales = sc, B = 200) cb plot(cb) plot(cb, type = "heatmap") plot(cb, type = "structure", lang = "es")sc <- list(F1 = paste0("I", 1:4), F2 = paste0("I", 5:8), F3 = paste0("I", 9:12)) cb <- compare_batches(batch_sim, group = "phase", scales = sc, B = 200) cb plot(cb) plot(cb, type = "heatmap") plot(cb, type = "structure", lang = "es")
A sensitivity analysis in one call: the same factor model is fitted to the full sample and to the data left after each exclusion, and the fit indices, the mean standardized loading and the factor correlations are put side by side. Models are fitted with lavaan, which must be installed.
compare_fit( x, data, model = NULL, exclusions = NULL, type = c("cfa", "efa"), nfactors = 1, items = NULL, ordered = TRUE, ... )compare_fit( x, data, model = NULL, exclusions = NULL, type = c("cfa", "efa"), nfactors = 1, items = NULL, ordered = TRUE, ... )
x |
An object returned by |
data |
The data frame that was screened (same rows, same order). |
model |
For |
exclusions |
Named list of exclusions. Each element is |
type |
|
nfactors |
Number of factors for |
items |
Items analysed by the EFA. By default, every screened item. |
ordered |
Logical; treat the items as ordinal (the default, with the WLSMV estimator) or as continuous. |
... |
Further arguments passed to |
An object of class fit_comparison, a list with table (one row
per exclusion: removed and kept respondents, chi-square, degrees of
freedom, CFI, TLI, RMSEA, SRMR, mean_loading (mean over items of the
largest absolute standardized loading of each item), the
factor correlations and a note with warnings or errors; the scaled
versions of the indices are used when the estimator provides them),
fits (the lavaan objects, NULL when a model failed) and data (the
data set used in each fit). It has print() and plot() methods.
if (requireNamespace("lavaan", quietly = TRUE)) { res <- scan_responses(survey_sim, scales = list(A = paste0("A", 1:8), B = paste0("B", 1:10)), reverse = paste0("A", 1:4), time = "submitted", pattern_items = paste0("A", 1:8)) m <- "A =~ A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 B =~ B1 + B2 + B3 + B4 + B5 + B6 + B7 + B8 + B9 + B10" cf <- compare_fit(res, survey_sim, model = m, exclusions = list("Full sample" = NULL, "Without series" = "serial", "Without any flag" = "any")) cf plot(cf) }if (requireNamespace("lavaan", quietly = TRUE)) { res <- scan_responses(survey_sim, scales = list(A = paste0("A", 1:8), B = paste0("B", 1:10)), reverse = paste0("A", 1:4), time = "submitted", pattern_items = paste0("A", 1:8)) m <- "A =~ A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 B =~ B1 + B2 + B3 + B4 + B5 + B6 + B7 + B8 + B9 + B10" cf <- compare_fit(res, survey_sim, model = m, exclusions = list("Full sample" = NULL, "Without series" = "serial", "Without any flag" = "any")) cf plot(cf) }
Returns the original data without the respondents that meet the chosen
criteria, ready to be passed to any model (for example lavaan::cfa(),
lavaan::efa() or psych::fa()). Criteria can be combined: a respondent
is removed when any of them applies.
exclude_responses( x, data, flags = "serial", patterns = NULL, min_flags = NULL, rows = NULL )exclude_responses( x, data, flags = "serial", patterns = NULL, min_flags = NULL, rows = NULL )
x |
An object returned by |
data |
The data frame that was screened (same rows, same order). All its columns are kept. |
flags |
Character vector with flag names ( |
patterns |
Character vector with response patterns, as they appear in
|
min_flags |
Remove respondents with at least this number of flags. |
rows |
Additional row positions (integer) or a logical vector of
length |
A data frame with the kept rows of data (original row names are
preserved) and two attributes: excluded, the positions of the removed
rows, and rule, a text that describes the criteria used. A warning is
given when the rule removes more than half of the sample.
res <- scan_responses(survey_sim, items = paste0("A", 1:8), reverse = paste0("A", 1:4), time = "submitted") clean <- exclude_responses(res, survey_sim, flags = "serial") nrow(clean) attr(clean, "rule") # The same pattern, also when it did not arrive in series nrow(exclude_responses(res, survey_sim, flags = NULL, patterns = "11113333")) # Respondents with two or more flags nrow(exclude_responses(res, survey_sim, flags = NULL, min_flags = 2))res <- scan_responses(survey_sim, items = paste0("A", 1:8), reverse = paste0("A", 1:4), time = "submitted") clean <- exclude_responses(res, survey_sim, flags = "serial") nrow(clean) attr(clean, "rule") # The same pattern, also when it did not arrive in series nrow(exclude_responses(res, survey_sim, flags = NULL, patterns = "11113333")) # Respondents with two or more flags nrow(exclude_responses(res, survey_sim, flags = NULL, min_flags = 2))
Print and plot a fit comparison
## S3 method for class 'fit_comparison' print(x, digits = 3, ...) ## S3 method for class 'fit_comparison' plot(x, lang = c("en", "es"), base_size = 11, ...)## S3 method for class 'fit_comparison' print(x, digits = 3, ...) ## S3 method for class 'fit_comparison' plot(x, lang = c("en", "es"), base_size = 11, ...)
x |
An object returned by |
digits |
Number of decimals. |
... |
Not used. |
lang |
Language of the labels: |
base_size |
Base font size of the ggplot2 theme. |
The plot has one panel per fit index, with the usual reference values as dashed lines (CFI and TLI .95, RMSEA .06, SRMR .08), and one panel for the factor correlations, if any, with a dashed line at 1. Each row is one exclusion. If the points barely move from row to row, the decision to exclude does not change the conclusions; if a point crosses a reference line, or the factor correlation changes a lot, the decision matters and should be reported. Exclusions with an improper solution are drawn in red.
print() returns x invisibly. plot() returns a ggplot
object, printed on the current device and returned invisibly.
if (requireNamespace("lavaan", quietly = TRUE)) { res <- scan_responses(survey_sim, items = paste0("B", 1:10)) cf <- compare_fit(res, survey_sim, type = "efa", nfactors = 1) print(cf, digits = 2) plot(cf, lang = "es") }if (requireNamespace("lavaan", quietly = TRUE)) { res <- scan_responses(survey_sim, items = paste0("B", 1:10)) cf <- compare_fit(res, survey_sim, type = "efa", nfactors = 1) print(cf, digits = 2) plot(cf, lang = "es") }
Exact duplicates are easy to spot; edited copies (a row copied and then changed in two or three items) are not. For every respondent this function counts the items in which they differ from their most similar respondent (the nearest neighbour) and compares the median with two baselines:
Shuffled data: each item is shuffled on its own, which keeps the distribution of every item but breaks any link between rows. Genuine respondents are always somewhat closer than shuffled ones, because people with a high trait answer high almost everywhere, so this baseline alone is not evidence of copying.
A control (when group is given): samples of the reference
group, of the size of the focal batch and of the same level when
match_level = TRUE, go through the same comparison. The statistic is
the gap between shuffled and real distances. A focal gap well above
the control gaps means rows closer to each other than genuine data
of the same level would be.
As in compare_batches(), the control needs a reference clearly larger
than the batch. When the reference has fewer than min_ratio times the
focal respondents, each control sample is paired with a random subsample
of the batch of the same size, and p_control compares the median gap
of those focal subsamples (focal_gap_sample) with the control gaps.
neighbor_test( data, items = NULL, group = NULL, focal = NULL, reference = NULL, drop_flat = TRUE, match_level = TRUE, B = 500, top = 10, min_ratio = 3 )neighbor_test( data, items = NULL, group = NULL, focal = NULL, reference = NULL, drop_flat = TRUE, match_level = TRUE, B = 500, top = 10, min_ratio = 3 )
data |
A data frame with one row per respondent. |
items |
Item columns. By default, the items in |
group |
Optional batch of each respondent (a column name of |
focal |
Group (or groups) under examination. By default, the smallest group. |
reference |
Group (or groups) used as reference. By default, every other group. |
drop_flat |
Logical; remove respondents with the same answer in every item before comparing. |
match_level |
Logical; add the comparison with reference samples paired by level with the focal batch. |
B |
Number of shuffles and of control samples. |
top |
Number of closest pairs of the focal batch returned. |
min_ratio |
Minimum ratio between the reference and the focal group
for the whole batch to be compared in every sample. With a smaller
reference (even one smaller than the batch) the batch is subsampled to
|
An object of class neighbor_test, a list with:
Respondents, median nearest-neighbour distance (real and
shuffled), their gap and the p value against the shuffles; with
group, also the median gap of the control samples and the p value
against them (share of control gaps as large or larger). When the
batch was subsampled, focal_gap_sample is the median gap of the
focal subsamples, the value compared with the control.
One row per examined respondent: row, distance to the nearest neighbour and the row of that neighbour.
The top closest pairs of rows and the items in which they
differ.
Values of the shuffles and control samples and the arguments used, kept for the plots.
order_test() and compare_batches().
nt <- neighbor_test(batch_sim, items = paste0("I", 1:12), group = "phase", B = 100) nt plot(nt) plot(nt, type = "distance")nt <- neighbor_test(batch_sim, items = paste0("I", 1:12), group = "phase", B = 100) nt plot(nt) plot(nt, type = "distance")
print() shows the medians, the gap and the p values, and the closest
pairs. plot() draws one of two views, with ggplot2:
"null"With group: histogram of the gap (shuffled minus real)
in the control samples, with the focal gap as a vertical line.
Without group: histogram of the shuffled medians with the real
median.
"distance"Distribution of the nearest-neighbour distance in the examined group, in its shuffled version and in the control. Edited copies pile up at the left (one to three items of difference).
## S3 method for class 'neighbor_test' print(x, ...) ## S3 method for class 'neighbor_test' plot( x, type = c("null", "distance"), lang = c("en", "es"), base_size = 11, ... )## S3 method for class 'neighbor_test' print(x, ...) ## S3 method for class 'neighbor_test' plot( x, type = c("null", "distance"), lang = c("en", "es"), base_size = 11, ... )
x |
An object returned by |
... |
Not used. |
type |
|
lang |
Language of the labels: |
base_size |
Base font size of the theme. |
print() returns x invisibly. plot() returns a ggplot
object, printed on the current device and returned invisibly.
nt <- neighbor_test(batch_sim, items = paste0("I", 1:12), group = "phase", B = 100) print(nt) plot(nt, type = "distance", lang = "es")nt <- neighbor_test(batch_sim, items = paste0("I", 1:12), group = "phase", B = 100) print(nt) plot(nt, type = "distance", lang = "es")
When a block of answers is filled by hand, each row tends to be a copy of the previous one with a few changes. That leaves a trace that does not need the submission time: consecutive rows resemble each other more than rows taken in a random order. In genuine data the order of arrival is unrelated to the answers, so the observed order should not be special.
order_test( data, items = NULL, profile = NULL, group = NULL, time = NULL, B = 5000 )order_test( data, items = NULL, profile = NULL, group = NULL, time = NULL, B = 5000 )
data |
A data frame with one row per respondent, in the order of
submission (or give |
items |
Item columns. By default, every numeric column except
those named in |
profile |
Optional character vector with categorical columns (for example demographic data) whose repetition in consecutive rows is tested as well. |
group |
Optional batch of each respondent (a column name or a vector). The test is run within each batch, in its own order. |
time |
Optional submission time (column name or vector) used to sort the rows; by default the rows are taken in the order given. |
B |
Number of random orders. |
For the items, the statistic is the mean number of items in which each
row differs from the previous one (items answered in both). Its null
distribution comes from B random orders of the same rows, and the
p value is the share of orders with a mean as low or lower. For the
profile columns (for example sex, age group and cycle), the statistic
is the share of consecutive rows with exactly the same profile, and the
p value is the share of random orders with a share as high or higher.
Respondents who answer together (the same classroom, the same session) can produce a mild order effect in genuine data. Use a group of known genuine data as a control: a batch is suspicious when its order effect is much stronger than the control's.
An object of class order_test, a list with:
One row per batch and statistic: respondents, observed value, mean of the random orders, p value.
One row per respondent after the first of its batch: original row, position, items that differ from the previous row and whether the profile is the same.
Values of the random orders and arguments used, kept for the plots.
neighbor_test() for near-duplicates in any order and
compare_batches() for the correlation structure.
ot <- order_test(batch_sim, items = paste0("I", 1:12), profile = c("sex", "age"), group = "phase", B = 500) ot plot(ot) plot(ot, type = "sequence")ot <- order_test(batch_sim, items = paste0("I", 1:12), profile = c("sex", "age"), group = "phase", B = 500) ot plot(ot) plot(ot, type = "sequence")
print() shows, for each batch, the observed and the expected values and
the p value. plot() draws one of two views, with ggplot2:
"null"Histograms of the statistic in the random orders, with the observed order as a vertical line. Copying shows as a line far to the left (items) or to the right (profile) of the histogram.
"sequence"Items in which each row differs from the previous one, in order, with the value expected at random as a dashed line. A run of short bars marks a stretch of rows that resemble each other, the place to look first with the submission times.
## S3 method for class 'order_test' print(x, ...) ## S3 method for class 'order_test' plot( x, type = c("null", "sequence"), lang = c("en", "es"), base_size = 11, ... )## S3 method for class 'order_test' print(x, ...) ## S3 method for class 'order_test' plot( x, type = c("null", "sequence"), lang = c("en", "es"), base_size = 11, ... )
x |
An object returned by |
... |
Not used. |
type |
|
lang |
Language of the labels: |
base_size |
Base font size of the theme. |
print() returns x invisibly. plot() returns a ggplot
object, printed on the current device and returned invisibly.
ot <- order_test(batch_sim, items = paste0("I", 1:12), group = "phase", B = 500) print(ot) plot(ot, type = "sequence", lang = "es")ot <- order_test(batch_sim, items = paste0("I", 1:12), group = "phase", B = 500) print(ot) plot(ot, type = "sequence", lang = "es")
Ten views of the same screening, drawn with ggplot2:
"overview"Respondents flagged by each criterion. Bars do not add up: one person can cross several criteria.
"indices"Distribution of each index with its cut-off (dashed line). Look for tails or isolated bars far from the bulk.
"patterns"The most frequent response patterns; the dark part of each bar is the share that arrived in series. A very frequent pattern is suspicious only if it is implausible or arrives in series.
"heatmap"Every answer of every respondent (light = lowest option, dark = highest), with identical patterns grouped together and flagged respondents marked in red on the left. Blocks of identical rows reveal copied or repeated patterns.
"time"Minutes since the previous submission (log scale) along the collection period. Series appear as vertical piles of filled points below the dashed line.
"days"Submissions per day, split into series, other flagged respondents and the rest. Points to the session where something happened.
"overlap"How many respondents share each pair of flags (the diagonal is the total of each flag). Criteria that do not overlap detect different problems.
"profiles"Mean answer to each item (rescaled to 0-1, as answered) for series, other flagged respondents and the rest. A step between direct and reverse-worded items is the signature of a fixed pattern.
"styles"Proportion at the midpoint against proportion at the extremes, a map of response styles.
"scatter"Intra-individual variability against the Mahalanobis distance, with both cut-offs. Points in the left zone answer almost the same everywhere; points above the line answer unusually.
## S3 method for class 'response_scan' plot( x, type = c("overview", "indices", "patterns", "heatmap", "time", "days", "overlap", "profiles", "styles", "scatter"), top = 10, lang = c("en", "es"), base_size = 11, ... )## S3 method for class 'response_scan' plot( x, type = c("overview", "indices", "patterns", "heatmap", "time", "days", "overlap", "profiles", "styles", "scatter"), top = 10, lang = c("en", "es"), base_size = 11, ... )
x |
An object returned by |
type |
One of |
top |
Number of patterns shown by |
lang |
Language of the labels: |
base_size |
Base font size of the theme. |
... |
Not used. |
A ggplot object, printed on the current device and returned
invisibly, so it can be modified with further layers or saved with
ggplot2::ggsave().
res <- scan_responses(survey_sim, scales = list(A = paste0("A", 1:8), B = paste0("B", 1:10)), reverse = paste0("A", 1:4), time = "submitted", pattern_items = paste0("A", 1:8)) plot(res) plot(res, type = "indices") plot(res, type = "patterns", top = 8, lang = "es") plot(res, type = "heatmap") plot(res, type = "time") plot(res, type = "days") plot(res, type = "overlap") plot(res, type = "profiles") plot(res, type = "styles") g <- plot(res, type = "scatter") g + ggplot2::labs(title = "IRV and Mahalanobis")res <- scan_responses(survey_sim, scales = list(A = paste0("A", 1:8), B = paste0("B", 1:10)), reverse = paste0("A", 1:4), time = "submitted", pattern_items = paste0("A", 1:8)) plot(res) plot(res, type = "indices") plot(res, type = "patterns", top = 8, lang = "es") plot(res, type = "heatmap") plot(res, type = "time") plot(res, type = "days") plot(res, type = "overlap") plot(res, type = "profiles") plot(res, type = "styles") g <- plot(res, type = "scatter") g + ggplot2::labs(title = "IRV and Mahalanobis")
print() shows how many respondents each criterion flags, the most
frequent pattern and the longest series. summary() returns the main
tables. as.data.frame() returns one row per respondent with every index
and flag, ready to be merged with the original data by position.
## S3 method for class 'response_scan' print(x, top = 3, ...) ## S3 method for class 'response_scan' summary(object, top = 10, ...) ## S3 method for class 'summary.response_scan' print(x, ...) ## S3 method for class 'response_scan' as.data.frame(x, row.names = NULL, optional = FALSE, ...)## S3 method for class 'response_scan' print(x, top = 3, ...) ## S3 method for class 'response_scan' summary(object, top = 10, ...) ## S3 method for class 'summary.response_scan' print(x, ...) ## S3 method for class 'response_scan' as.data.frame(x, row.names = NULL, optional = FALSE, ...)
x, object
|
An object returned by |
top |
Number of patterns or series to show. |
... |
Not used. |
row.names, optional
|
Ignored; present for compatibility with the generic. |
print() returns x invisibly. summary() returns an object of
class summary.response_scan, a list with criteria (one row per
criterion with its rule, number and percentage of flagged respondents),
any (respondents with at least one flag), patterns (the top most
frequent patterns) and series (the top longest series).
as.data.frame() returns a data frame with the columns of x$indices
followed by the logical flags, prefixed with flag_, and n_flags.
res <- scan_responses(survey_sim, items = paste0("A", 1:8), reverse = paste0("A", 1:4), time = "submitted") summary(res) out <- cbind(survey_sim, as.data.frame(res)) table(out$group, out$n_flags > 0)res <- scan_responses(survey_sim, items = paste0("A", 1:8), reverse = paste0("A", 1:4), time = "submitted") summary(res) out <- cbind(survey_sim, as.data.frame(res)) table(out$group, out$n_flags > 0)
One call computes, for every respondent, the usual indices of careless or aberrant responding and flags the cases that cross each criterion. It also tabulates how often each exact response pattern occurs and, when the submission time is available, detects identical patterns that arrive in series of consecutive submissions (forms filled one after another, answers dictated or copied).
scan_responses( data, items = NULL, scales = NULL, reverse = NULL, range = NULL, time = NULL, pattern_items = NULL, min_run = 3, max_gap = 10, person_fit = FALSE, irv_quantile = 0.05, mahal_p = 0.001, syn_r = 0.6, syn_min_pairs = 5, dup_min_items = 15, flat_min_items = 6, eo_min_scales = 6, reference = NULL, ref_quantile = 0.05 )scan_responses( data, items = NULL, scales = NULL, reverse = NULL, range = NULL, time = NULL, pattern_items = NULL, min_run = 3, max_gap = 10, person_fit = FALSE, irv_quantile = 0.05, mahal_p = 0.001, syn_r = 0.6, syn_min_pairs = 5, dup_min_items = 15, flat_min_items = 6, eo_min_scales = 6, reference = NULL, ref_quantile = 0.05 )
data |
A data frame with one row per respondent. |
items |
Character vector with the item columns. By default, every
numeric column of |
scales |
Optional named list of character vectors, one per scale or
subscale. Flat responding is evaluated within each scale, and response
ranges are inferred per scale. By default all |
reverse |
Optional character vector with the reverse-worded items. They are reverse-scored before the indices that assume a common direction (intra-individual variability, Mahalanobis distance, psychometric synonyms, person fit). Patterns, long strings and the heat map always use the responses as given. |
range |
Optional numeric vector |
time |
Optional submission time: a column name of |
pattern_items |
Items used to define a response pattern for the
frequency table and the serial detection. By default, |
min_run |
Minimum number of consecutive submissions with the same pattern to call them a series. |
max_gap |
Maximum minutes between two consecutive submissions of a series. |
person_fit |
Logical. If |
irv_quantile |
Respondents with some variability (IRV > 0) at or
below this quantile of the non-zero values are flagged. Respondents with
IRV = 0 answered the same option everywhere and are already covered by
|
mahal_p |
Significance level for the Mahalanobis distance. |
syn_r |
Minimum inter-item correlation for two items to be treated as psychometric synonyms. |
syn_min_pairs |
Minimum number of disjoint synonym pairs (each item
in one pair only, strongest pairs first) needed to evaluate |
dup_min_items |
Minimum number of items for |
flat_min_items |
Minimum number of items for a scale to be checked for flat responding. In a short scale (three or four items) many genuine respondents choose the same option everywhere. Shorter scales are skipped; when no scale is long enough, flat responding is judged on all items together. |
eo_min_scales |
Minimum number of scales with two or more items needed to compute the even-odd consistency. Each scale is split into its odd- and even-positioned items, and the person's odd and even scale means are correlated across scales (Spearman-Brown corrected). With fewer scales the index rests on too few points and is not evaluated. |
reference |
Optional rows that form a reference group of ordinary
respondents: a logical vector of length |
ref_quantile |
Tail probability used with |
An object of class response_scan, a list with:
Data frame, one row per respondent: row (position in
data), longstring (longest run of identical consecutive answers),
irv (intra-individual standard deviation after rescaling every item to
0-1), mahal_d2 and mahal_p, psychsyn (within-person correlation
across disjoint synonym pairs), evenodd (even-odd consistency,
Spearman-Brown corrected), midpoint and extreme (proportion of answers
at the midpoint and at either end), pattern and pattern_n (the
response pattern and how many respondents share it), series (series
identifier or NA) and, if requested, zh.
Data frame of logical flags: flat, low_irv, mahal,
psychsyn, evenodd, duplicate, serial and, if requested, zh;
plus n_flags.
Data frame with every distinct pattern, its frequency, percentage and how many of its cases belong to a series, sorted by frequency.
Data frame with one row per detected series (start, end,
submissions, minutes, median gap and pattern); empty without time.
Data frame describing each criterion, its rule and the number and percentage of flagged respondents.
The item responses as given, the submission time and the arguments used, kept for the plots.
Curran, P. G. (2016). Methods for the detection of carelessly invalid responses in survey data. Journal of Experimental Social Psychology, 66, 4-19. doi:10.1016/j.jesp.2015.07.006
Drasgow, F., Levine, M. V., & Williams, E. A. (1985). Appropriateness measurement with polychotomous item response models and standardized indices. British Journal of Mathematical and Statistical Psychology, 38(1), 67-86. doi:10.1111/j.2044-8317.1985.tb00817.x
Meade, A. W., & Craig, S. B. (2012). Identifying careless responses in survey data. Psychological Methods, 17(3), 437-455. doi:10.1037/a0028085
plot.response_scan() for the plots,
as.data.frame.response_scan() to merge the indices back into the data,
and compare_batches(), order_test() and neighbor_test() to judge a
whole batch of data rather than single respondents.
res <- scan_responses(survey_sim, scales = list(A = paste0("A", 1:8), B = paste0("B", 1:10)), reverse = paste0("A", 1:4), time = "submitted", pattern_items = paste0("A", 1:8)) res plot(res, type = "patterns") plot(res, type = "heatmap") head(as.data.frame(res)) # Cut-offs taken from an earlier collection (phase 1) to judge phase 2 res2 <- scan_responses(batch_sim, items = paste0("I", 1:12), reference = batch_sim$phase == 1) res2$criteria # One by one, the hand-filled rows look ordinary: they are found by # judging the batch as a whole, with compare_batches(), order_test() # and neighbor_test() table(batch_sim$source, res2$flags$n_flags > 0)res <- scan_responses(survey_sim, scales = list(A = paste0("A", 1:8), B = paste0("B", 1:10)), reverse = paste0("A", 1:4), time = "submitted", pattern_items = paste0("A", 1:8)) res plot(res, type = "patterns") plot(res, type = "heatmap") head(as.data.frame(res)) # Cut-offs taken from an earlier collection (phase 1) to judge phase 2 res2 <- scan_responses(batch_sim, items = paste0("I", 1:12), reference = batch_sim$phase == 1) res2$criteria # One by one, the hand-filled rows look ordinary: they are found by # judging the batch as a whole, with compare_batches(), order_test() # and neighbor_test() table(batch_sim$source, res2$flags$n_flags > 0)
Simulated answers of 300 respondents to two scales, with groups of
aberrant respondents planted on purpose so that every criterion of
scan_responses() has something to find. No real person is included.
survey_simsurvey_sim
A data frame with 300 rows and 21 columns:
Respondent number, in order of submission.
Submission time (POSIXct, UTC).
Scale A, item 1; three options (1 = No, 2 = More or less, 3 = Yes), reverse-worded.
Scale A, item 2; three options (1 = No, 2 = More or less, 3 = Yes), reverse-worded.
Scale A, item 3; three options (1 = No, 2 = More or less, 3 = Yes), reverse-worded.
Scale A, item 4; three options (1 = No, 2 = More or less, 3 = Yes), reverse-worded.
Scale A, item 5; three options (1 = No, 2 = More or less, 3 = Yes).
Scale A, item 6; three options (1 = No, 2 = More or less, 3 = Yes).
Scale A, item 7; three options (1 = No, 2 = More or less, 3 = Yes).
Scale A, item 8; three options (1 = No, 2 = More or less, 3 = Yes).
Scale B, item 1; five options (1 to 5).
Scale B, item 2; five options (1 to 5).
Scale B, item 3; five options (1 to 5).
Scale B, item 4; five options (1 to 5).
Scale B, item 5; five options (1 to 5).
Scale B, item 6; five options (1 to 5).
Scale B, item 7; five options (1 to 5).
Scale B, item 8; five options (1 to 5).
Scale B, item 9; five options (1 to 5).
Scale B, item 10; five options (1 to 5).
Planted group: "typical", "flat" (same option
everywhere), "random", "duplicate" (copy of another respondent)
or "serial" (the same extreme pattern in scale A, submitted one to
two minutes apart).
Simulated with the script data-raw/make_survey_sim.R of the
package source.