25  Data Quality Checks

Warning

These checks are part of data_check, which has not been validated: they have unknown error rates, so evaluate their output carefully.

25.1 The basics

This chapter does not describe a module you run on its own. These data-quality checks used to be a separate module (data_validate, plus a second one called excel_check for spreadsheet formatting), but both have since been merged into data_check: there is no module_run(paper, "data_validate") anymore, because data_check now runs these checks automatically, on every column it reads. This chapter exists to document those checks in detail, separately from the rest of what data_check does. Because they are part of data_check, these checks also run wherever data_check runs, including on Metacheck’s website and in the Docker installation, which use the development version (see Data Check).

So the simplest way to see these checks run is the same call used for data_check itself, pointed at a folder with a planted problem — here a category typed two different ways:

repo <- file.path(tempdir(), "data_validate_basics_demo")
dir.create(file.path(repo, "data"), recursive = TRUE, showWarnings = FALSE)

study <- data.frame(
  id        = 1:39,
  condition = c(rep("Control", 20), rep("control", 19))  # same category, different case
)
utils::write.csv(study, file.path(repo, "data", "study.csv"), row.names = FALSE)

mo <- module_run(test_paper(), "data_check",
                 local_path = repo, local_only = TRUE)
mo$traffic_light
#> [1] "red"
cat(mo$summary_text)
#> 
#> -  We classified 1 file: 1 data.
#> -  We found 1 tabular data file and extracted 2 columns from 1 of them.
#> -  We grouped the files into 1 study group (ex1). We ran automated data-quality checks on 2 columns; 1 column has at least one potential issue (1 column with inconsistent category casing).

The result is the same kind of object data_check always returns, described in full in the Data Check chapter. The piece that matters for this chapter is mo$findings, which has one row per problem it found (as opposed to mo$table, which has one row per column):

mo$findings[, c("column", "check", "detail")] |>
  knitr::kable()
column check detail
condition Case issues Categories differing only by case: Control/control

A red or yellow light here, or a row in mo$findings, tells you a value looks suspicious by one of these general rules — for example, the same category spelled two different ways, or a column of numbers with a note typed into one cell. It does not tell you why that value is wrong, and it cannot tell whether a value that passes every check is actually correct: a value that happens to look fine will not be flagged, and a column with no findings at all can still contain mistaken or fabricated entries these checks have no way to see. Treat a flag as a place to look by hand, not a verdict, and treat a clean result as “nothing obvious was wrong” rather than “this data is correct.” The rest of this chapter describes each individual check — spreadsheet formatting, careless survey responding, personal information, and more — in detail.

25.2 What it checks

data_check runs automated data-quality checks on every column it extracts, flagging the kinds of problems that are easy to miss by eye but common in shared data. Every check is a small, dependency-free base-R function that returns a uniform list(problem, message, values), so the module can run them all over every column and collect the findings. The checks are:

  • Empty columns — a column with no observed values at all.
  • SPSS filter variables — a filter_$ variable left in the export, signalling the data are a pre-filtered subset.
  • Numeric-as-text — a mostly-numeric column contaminated by a few text entries.
  • Category casing — the same category written two ways ("Male" and "male").
  • Whitespace — leading/trailing spaces that split what should be one category.
  • Problematic column names — names carrying file-illegal characters, control characters, or excessive length that break downstream reuse.
  • Colliding column names — sibling names that become identical when special characters are stripped.
  • Mixed / legacy encoding — text whose bytes are not valid UTF-8.
  • Personal information — columns whose name or values look like data that should not be shared openly (emails, IP addresses, national IDs, credit-card numbers, geographic coordinates, free text).

For every spreadsheet file (.xlsx, legacy .xls, OpenDocument .ods/.fods), data_check also inspects the raw workbook, not just the data it extracts from it — colour-coded cells, merged ranges, empty rows and columns, and a header row that is not the first row of the sheet. And for every .csv file whose values are separated by spaces instead of commas, it reports that the file’s columns could not be checked.

Two checks that earlier versions ran are no longer part of data_check: values outside a rating scale, which has moved to Codebook Check, where the data are compared with the range a codebook documents, and constant columns, which is switched off. Both are explained below.

It also produces an inventory of demographic columns, a Qualtrics survey metadata summary, a careless-responding screen for survey data, and a combined distribution figure of the numeric columns.

Note

This chapter is offline. We build small folders with deliberately planted problems and check them with local_path.

25.3 A worked example

repo <- file.path(tempdir(), "data_validate_demo")
dir.create(file.path(repo, "data"), recursive = TRUE, showWarnings = FALSE)

study <- data.frame(
  id        = 1:40,
  age       = round(rnorm(40, 30, 5)),
  score     = c(round(rnorm(39, 50, 10), 1), "see note"),          # a note in a number column
  condition = c(rep("Control", 20), rep("control", 19), "Control "), # casing, trailing space
  notes     = NA                                                    # never filled in
)
utils::write.csv(study, file.path(repo, "data", "study.csv"), row.names = FALSE)
mo <- module_run(test_paper(), "data_check",
                 local_path = repo, local_only = TRUE)

mo$traffic_light
#> [1] "red"
cat(mo$summary_text)
#> 
#> -  We classified 1 file: 1 data.
#> -  We found 1 tabular data file and extracted 5 columns from 1 of them.
#> -  We grouped the files into 1 study group (ex1). We ran automated data-quality checks on 5 columns; 3 columns have at least one potential issue (1 column with inconsistent category casing; 1 column with no observed values (entirely empty); 1 column with numeric values stored as text; 1 column with leading/trailing whitespace). We detected demographic column for Age.

data_check reads and classifies the columns first, then applies the quality checks to the result, so a single call does both. Three of the five columns are flagged, which is more than a quarter, so the light is red (see The traffic light).

25.4 The findings table

Every flagged column appears in $findings, one row per (column, check):

mo$findings[, c("column", "check", "detail")] |>
  knitr::kable()
column check detail
score Numeric as text Column is 98% numeric but 1 value cannot be parsed: “see note”
condition Case issues Categories differing only by case: Control/control
condition Whitespace 1 value with leading/trailing whitespace: “Control”
notes Empty column Column is empty: all 40 values are missing.

The check column names the category of problem, and detail explains it in words. Below we describe each check and the function that implements it.

Note$findings and $table are different things

data_check returns both, at deliberately different grains. $table has one row per column, describing what each column is. $findings has one row per problem, describing what is wrong. They are separate because one column can raise several findings, and because a spreadsheet-formatting or careless-responding finding has no single column to attach to at all. When this chapter refers to a finding, it is in $findings.

25.5 Values outside a rating scale: moved to Codebook Check

data_check no longer looks for values outside a rating scale. That check has moved to Codebook Check, which compares each column with the value codes its codebook documents (“the codebook says 1–5, but the column contains a 6”) and reports the values outside that documented range. When data_check ran it, a codebook was in practice never available to it (codebook_check runs after data_check and reads its output), so the range was always guessed from the column’s own values; a value outside a guessed range is weak evidence, and a documented range is not guessed.

codebook_check does this with the exported helper data_check_scale_values(), which you can also call directly on any column. Given a codebook’s valid codes it uses those; called without them, it infers the range with .detect_likert_scale(), which grows a dense consecutive core outward from the modal value, bridges small interior gaps between common levels, anchors the floor to the natural scale start (0 or 1), and returns everything outside [lo, hi] as suspects. Crucially it infers the range from the dense core, not min/max, so a single stray 99 cannot destroy the scale it is meant to reveal.

Each out-of-scale value is then classified:

  • a missing-data code (a -99 / 999 from the sentinel list .data_missing_sentinels, or a codebook-declared missing code) → recode to NA;
  • a keying typo of an in-scale value (a 33 for 3, a 55 for 5) via .scale_typo_of(), which names the probable intended value;
  • otherwise unexplained.
data_check_scale_values(c(sample(1:7, 50, replace = TRUE), 99, 33))$message
[1] "2 values outside the 1-7 scale: 33 (looks like a typo of 3), 99 (looks like a missing-data code -> recode to NA)"

The sentinel list is deliberately conservative: the 9x-block (97/98/99), repeated-digit 8- and 7-families, wide placeholders, and the two attested negative codings (-99, -999). Single digits 7/8/9 are excluded (valid Likert points), and a sentinel only ever fires when it sits outside the scale, so a 99 in an age column or a 97 in a 0–100 score does not trip it.

The related helper data_check_outliers() still computes the Tukey 1.5×IQR fences and can be called directly, but data_check no longer uses it: no column is flagged for values beyond the fences, the distribution figure does not draw them, and the careless-responding screen does not use them either. On real data the fences mostly mark the long right tail of skewed columns such as reaction times, which is their expected shape, not an error.

25.6 Empty columns and SPSS filter variables

These fire for any column type:

  • data_check_empty() — an all-missing column (all NA, or for text all blank/whitespace). Always flagged: a variable that never recorded anything.
  • data_check_spss_filter() — an SPSS “Select Cases” filter variable (filter_$). Flagged whether constant or not: constant-1 means the shared file is a pre-filtered subset; varying means the reported analyses likely used only the selected rows and the filter must be re-applied.
data_check_empty(c(NA, NA, NA))$message
#> "Column is empty: all 3 values are missing."
data_check_spss_filter("filter_$", rep(1, 30))$problem
#> TRUE

Constant columns are not flagged. Earlier versions flagged a column holding a single repeated value (or 99% one value) with data_check_constant(). This check is switched off, because on real data most constant columns are constant on purpose: in a file that holds one participant’s trials, subject_ID, Age and Gender have one value because the file is about one person, and columns that record the experiment software’s settings are constant as well. data_check_constant() (and data_check_design_name(), which recognises a condition or group variable by its name) can still be called directly:

data_check_constant(rep(1, 40))$message
#> "Column is constant: every value is \"1\"."

25.7 Categorical checks

For non-numeric columns:

  • data_check_numeric_in_text() — a mostly-numeric column (≥80% coercible) stored as text, usually because a stray note or coding legend was typed into a data cell. When this fires, the case check is skipped (every distinct number would otherwise look like a spurious “level”).
  • data_check_case_issues() — categories differing only by letter case ("Male"/"male"), likely the same category entered inconsistently.
  • data_check_whitespace() — values with leading/trailing whitespace ("Male " vs "Male"), which silently split a category.
data_check_case_issues(c("Male", "male", "Female"))$message
#> "Categories differing only by case: Male/male"
data_check_numeric_in_text(c("1", "2", "3", "see note", "5"))$problem
#> TRUE

25.8 Column-name quality: data_check_colname() and collisions

Column names are checked too, because a bad name breaks downstream reuse — as a file name, in an analysis script, or on import into another package. data_check_colname() flags names carrying characters illegal in file names (< > : " / \ | ? *), control characters, or more than 64 characters (the most SPSS accepts; SAS and Stata stop at 32; Windows’ 260-character path limit also blocks the codebook’s per-variable figures for very long names). A garbled name usually means the header row was not exported or parsed as intended.

data_check_colname_collisions() flags sibling columns whose names become identical once special characters are sanitized away (e.g. t' next to a t-with-diacritic) — tools that sanitize names on import cannot tell them apart.

data_check_colname("this/is:an*illegal|name")$problem
#> TRUE

25.9 Spreadsheet formatting

Every check so far reads the data after data_check has parsed it into a data frame — but a spreadsheet file can be badly formatted in ways that are invisible once it is read that way. data_check also inspects the raw workbook directly for four kinds of non-machine-readable formatting:

  • Colour coding — cells whose fill colour is used to encode information (for example, marking excluded participants in red). Colour is invisible to any analysis script and is lost on CSV export.
  • Merged cells — merged ranges break the rectangular grid that data frames require, and typically indicate multi-level headers that a program cannot parse.
  • Empty rows / empty or unnamed columns — fully blank rows inside the data range, or columns with a blank header or no data beneath one.
  • Header not on the first row — a banner, blank, or units row sitting above the real column header, so the file does not read as a clean table (the reader takes the junk row as the header and invents names like ...1, ...4, or spreads one merged label across many columns).

This runs on every spreadsheet format data_check recognises — .xlsx, OpenDocument .ods/.fods, and legacy .xls — reading the workbook’s own XML directly for colour and merge information that a data-reading package like readxl discards entirely. It runs on the file data_check already located (via its structure table), independently of whether that file could actually be extracted into a clean tabular preview: a merged banner cell or an offset header is very often why extraction failed in the first place, so this check must not depend on a successful preview to run.

Note

This was formerly a separate module, excel_check. It no longer exists on its own — every check it performed is now part of data_check, run automatically alongside the column-level checks, on the same files.

repo_xl <- file.path(tempdir(), "spreadsheet_demo")
dir.create(file.path(repo_xl, "data"), recursive = TRUE, showWarnings = FALSE)

# a messy workbook: colour, a merge, and an empty column
wb <- openxlsx::createWorkbook()
openxlsx::addWorksheet(wb, "Data")
df <- data.frame(id = 1:4, group = c("a", "b", "a", "b"),
                 notes = rep(NA, 4), score = c(10, 20, 30, 40))
openxlsx::writeData(wb, "Data", df)
openxlsx::addStyle(wb, "Data", openxlsx::createStyle(fgFill = "#FFCC00"),
                   rows = 2, cols = 4)                 # a colour-coded cell
openxlsx::mergeCells(wb, "Data", cols = 1:2, rows = 7)  # a merged range
openxlsx::saveWorkbook(wb, file.path(repo_xl, "data", "messy.xlsx"),
                       overwrite = TRUE)

# a clean workbook, for comparison
openxlsx::write.xlsx(data.frame(id = 1:3, score = c(1.1, 2.2, 3.3)),
                     file.path(repo_xl, "data", "clean.xlsx"))
mo_xl <- module_run(test_paper(), "data_check",
                    local_path = repo_xl, local_only = TRUE)
mo_xl$findings[!is.na(mo_xl$findings$label) | mo_xl$findings$check %in%
             c("Colour coding", "Merged cells", "Empty or unnamed columns"),
           c("source_file", "label", "check", "detail")] |>
  knitr::kable()
source_file label check detail
2 messy.xlsx Data Colour coding 1 cell uses fill colour to encode information; colour is lost on CSV export.
3 messy.xlsx Data Merged cells 1 merged range (A7:B7) breaks the rectangular grid.
4 messy.xlsx Data Empty or unnamed columns 1 column is empty or has a blank header.

Only messy.xlsx is flagged; the clean workbook produces no rows. These findings carry column = NA in $findings (the issue is not about one column, it is about the sheet), and the sheet name is shown under label instead. They join the same “Issues Identified” report table as every other check, so a reviewer sees every problem with a file in one place rather than in a separate report.

Two further, related checks fire on any spreadsheet regardless of formatting:

  • “Not a rectangular dataset” — data_check itself already decided this file does not read as a usable dataset (mostly free text, or almost entirely empty); data_check adds the structural note here so the finding shows up in the same table rather than only in data_check’s own output.
  • “Un-inspectable (.xls)” — legacy binary .xls has no XML to inspect for colour or merges; it is still checked for an offset header (which does not need the XML structure), but colour/merge/empty-row-or-column checks are skipped with a note recommending conversion to .xlsx or .ods.

25.10 Space-separated .csv files

data_check works out the separator of a .csv file from the commas, semicolons, tabs and vertical bars in its first line, never from spaces (see the Data Check chapter). A .csv whose values are separated by spaces therefore reads as one column of text, and none of its columns can be checked. Without a warning, such a file could leave the light green.

So data_check looks at the first lines of every .csv file it has a copy of (up to six, skipping blank lines and lines that start with #). It reports a “Space-separated .csv” finding when none of these lines holds a comma, semicolon, tab or vertical bar, every line splits at its spaces into the same number of values (at least three), and at least one value below the first line is a number. The rule is strict on purpose, so that a one-column file of short phrases is not mistaken for a space-separated table. The file is not read again with spaces as the separator: the finding asks the authors to save it as a comma-separated .csv or a tab-separated .tsv, because a value that itself contains a space cannot be told apart from the separator.

spaced <- file.path(tempdir(), "space_csv_demo")
dir.create(file.path(spaced, "data"), recursive = TRUE, showWarnings = FALSE)
writeLines(c("id condition score",
             "1 control 4.5",
             "2 treatment 5.1",
             "3 control 3.9"),
           file.path(spaced, "data", "scores.csv"))

mo_sp <- module_run(test_paper(), "data_check",
                    local_path = spaced, local_only = TRUE)
mo_sp$traffic_light
#> [1] "yellow"
mo_sp$findings[, c("source_file", "column", "check")] |>
  knitr::kable()
source_file column check
scores.csv NA Space-separated .csv

The finding concerns the whole file, so column and label are both NA. It turns a green light yellow, but it is not counted towards the quarter-of-columns red threshold. In this example the file is also reported, in mo_sp$summary_text, as not a usable rectangular dataset, because its single column holds only text.

25.11 Mixed / legacy encoding

data_check records, per column, how many values had to be re-interpreted as Latin-1 because their bytes were not valid UTF-8 (the utf8_repaired count). data_check turns any non-zero count into a Mixed encoding finding: metacheck read the file by re-interpreting those bytes, but on another system such characters corrupt silently (é becomes é or �), so the researcher is told to re-save the file as UTF-8 (with the exact steps for Excel, R, and SPSS).

25.12 Personal-information screening

data_check screens each column for content that should not be shared openly, reported as “review before sharing” prompts that never echo the matching value. Four detectors run:

  • data_check_pii_values() — values that look like emails, IP addresses, national IDs, or credit-card numbers.
  • data_check_pii_name() — a column name such as email, firstname, address, dob, ssn.
  • data_check_pii_geo() — geographic coordinates (a latitude/longitude column name with in-range values).
  • data_check_pii_freetext() — long open typed free-text columns that may contain identifying prose (numeric columns are exempt).
pii <- file.path(tempdir(), "pii_demo")
dir.create(file.path(pii, "data"), recursive = TRUE, showWarnings = FALSE)
utils::write.csv(data.frame(
  participant = 1:3,
  email       = c("a@example.com", "b@example.com", "c@example.com"),
  full_name   = c("Ada Lovelace", "Alan Turing", "Grace Hopper")
), file.path(pii, "data", "responses.csv"), row.names = FALSE)

mo_pii <- module_run(test_paper(), "data_check",
                     local_path = pii, local_only = TRUE)
mo_pii$findings[, c("column", "check")] |>
  knitr::kable()
column check
email Personal info (values)
email Personal info (column name)
full_name Personal info (column name)

25.13 Codebook labels

No check in data_check depends on a codebook. When output from codebook_check is available, data_check uses it for two things only: the label_of() lookup adds each column’s documented label to its findings (the label column of $findings), and a careless-responding block is named after the scale codebook_check identified (“PANAS”) rather than the shared start of its column names. Usually that output is not available, because codebook_check runs after data_check and reads its results; the label is then NA and the block keeps the name taken from its columns. Comparing the data with the valid and missing values a codebook documents is done in Codebook Check.

25.14 Demographic variables

Almost every human-subjects study reports the same three demographic variables — age, gender/sex, and race/ethnicity — so data_check picks them out and lists them, reusing data_check’s precomputed concept facet (or recomputing it with data_check_demographic() against an older data_check run). Detection requires the column name and values to agree, so a condition column coded 1/2 is never mistaken for gender.

demo <- file.path(tempdir(), "demo_demo")
dir.create(file.path(demo, "data"), recursive = TRUE, showWarnings = FALSE)
utils::write.csv(data.frame(
  id        = 1:30,
  age       = sample(18:65, 30, replace = TRUE),
  gender    = sample(c("Male", "Female", "Non-binary"), 30, replace = TRUE),
  ethnicity = sample(c("Hispanic", "Non-Hispanic"), 30, replace = TRUE),
  score     = rnorm(30)
), file.path(demo, "data", "study.csv"), row.names = FALSE)

mo_demo <- module_run(test_paper(), "data_check",
                      local_path = demo, local_only = TRUE)
mo_demo$demographics |>
  knitr::kable()
source_file column demographic
study.csv age age
study.csv gender gender
study.csv ethnicity race

This is an inventory, not a problem flag — it helps a reviewer see at a glance whether the shared data documents its sample’s demographics (and, with the personal-information screen above, whether doing so raises disclosure concerns). The result is returned in $demographics.

25.15 Qualtrics survey metadata

Qualtrics exports have a fixed, recognisable shape (see the Data Check chapter). data_check summarises the metadata that is reliably extractable from any Qualtrics file with .dv_qualtrics_summary(), locating each metadata column by its semantic tag (.dv_q_col()) rather than its exact name:

  • preview / unfinished responses — rows where Status marks a preview or spam, Finished is false, or Progress is below 100, which usually need dropping before analysis;
  • the completion-time distribution — the median Duration, with a count of implausibly fast responses (under half the median and under two minutes);
  • the data-collection window — the date range from RecordedDate / StartDate (parsed tolerantly by .dv_q_datetime()); and
  • which Qualtrics personal-information fields are present (IP address, email, location, external reference, recipient name).

The substantive question columns are deliberately not interpreted here — that is the job of the scale-block detection in Codebook Check. The summary is returned in $qualtrics and rendered by .dv_qualtrics_report().

25.16 Careless responding

For survey data, data_check additionally screens for careless responding — respondents who straightline (give the same answer to almost every item of a scale). This uses the careless package’s longstring index (the longest run of identical answers), computed per scale block by .dv_careless_block(). The package’s intra-individual response variability (IRV, the standard deviation of a respondent’s answers) is computed and reported alongside, but it does not flag anyone.

Because these indices only make sense on a block of items measured on a common scale, and a finding is only actionable if you can point to which respondent, the check runs only on files with at least 30 rows and at least 5 columns, and only when both conditions hold:

  1. the file contains a block of Likert-type items — detected by .detect_scale_blocks(), a maximal run of adjacent integer columns (3–11 response options) sharing a variable-name prefix (via .scale_name_prefix()), with at least .scale_min_items (3) items; and
  2. the file has an identifier column (from data_check’s identifier role, a name-pattern fallback, or else the check is skipped).

Item blocks are split by their variable-name prefix, so panas_1 … panas_10 and rse_1 … rse_5 are treated as two separate scales even when they sit next to each other. Within a block, a respondent is flagged when their longest run of identical answers covers ≥80% of the items and is at least 5 answers long, so a block of fewer than 5 items never flags anyone. Earlier versions also flagged a respondent whose IRV lay outside the Tukey fences for the block; that rule was dropped because it flags a few percent of respondents in any block, whether or not anyone answered carelessly.

survey <- file.path(tempdir(), "careless_demo")
dir.create(file.path(survey, "data"), recursive = TRUE, showWarnings = FALSE)

set.seed(1)
n <- 60
items <- as.data.frame(matrix(sample(2:4, n * 10, replace = TRUE), nrow = n))
names(items) <- paste0("mood_", 1:10)
# add one straightliner (all 3s), who is flagged, and one erratic
# responder (1,5,1,5,...), who is not: only straightlining flags a respondent
items <- rbind(items,
               as.data.frame(matrix(rep(3, 10), nrow = 1)) |> setNames(names(items)),
               as.data.frame(matrix(rep(c(1, 5), 5), nrow = 1)) |> setNames(names(items)))
survey_df <- cbind(participant_id = seq_len(n + 2), items)
utils::write.csv(survey_df, file.path(survey, "data", "mood.csv"), row.names = FALSE)

mo_car <- module_run(test_paper(), "data_check",
                     local_path = survey, local_only = TRUE)
mo_car$careless |>
  knitr::kable()
respondent source_file n_blocks_flagged scales reasons max_longstring irv short_scale_only threshold
61 mood.csv 1 mood (1-5, 10 items) straightlining 10 0 FALSE same answer 10 times in a row, out of 10 items (flagged at 8)

The per-block findings are aggregated to one row per respondent by .dv_careless_by_respondent(), which records how many blocks flagged each person, on which scales, the longest run (max_longstring), the lowest IRV, the run that triggered the strongest flag in words (threshold), and a short_scale_only flag — TRUE when every flag is short-scale straightlining (a run of identical answers on a scale of ≤7 items), which on a short unidirectional scale is often normal coherent answering rather than carelessness. These are prompts to inspect those rows, not automatic exclusions.

Note

The careless check needs the optional careless package. If it is not installed, the check is skipped and the report notes how many survey files it would have screened.

25.16.1 Two real examples

The synthetic example above shows the mechanics. To see the check on real research data, here are two openly shared datasets. The code is shown but not executed here (the data are not redistributed with this book); run it yourself against a local copy of each repository to reproduce the output.

A long scale with a clear straightliner. The replication data of Gervais et al. (2017) — two preregistered replications of the “anticipating divine protection” hypothesis — include a 40-item scale (variable prefix dos, responses 1–5) alongside a participant id.

mo <- module_run(test_paper(), "data_check",
                 local_path = "divine-protection-replication", local_only = TRUE)
mo$careless[, c("respondent", "scales", "max_longstring", "irv")]
  respondent              scales max_longstring irv
1        129 dos (1-5, 40 items)             40   0

Respondent 129 answered all 40 items identically (max_longstring = 40, IRV = 0) — an unambiguous straightliner, and the only respondent flagged.

Several short scales, split by prefix. The “Parents’ Failure Mindsets” data of Haimovitz & Dweck (2016) contain three separate failure-mindset scales in adjacent columns, named fail_*, failbad_*, and failgood_*, all on a 1–6 metric.

mo <- module_run(test_paper(), "data_check",
                 local_path = "parents-failure-mindsets", local_only = TRUE)
unique(unlist(strsplit(mo$careless$scales, "; ")))
[1] "fail (1-6, 6 items)"     "failgood (2-6, 5 items)"
[3] "failbad (1-6, 5 items)"

Even though the three scales sit next to each other on the same response metric, the variable-name prefix splits them into three distinct blocks, and careless indices are computed within each. All 45 flagged respondents here have short_scale_only = TRUE: on scales of 5 or 6 items, answering the same way throughout is often consistent answering rather than carelessness.

25.17 Distributions in a report

When you pass plot_distributions = TRUE (and have ggplot2 installed), data_check draws a single combined figure with one small histogram per numeric column, built by data_validate_dist_facets(). Earlier versions also drew the Tukey outlier fences as dashed red lines; these are no longer drawn, because on most real columns the values beyond them are not suspect. Earlier versions drew one plot per column, which was slow and unreadable on wide files (a survey export can have hundreds of numeric columns). The combined figure renders once as a self-contained inline image (so a moved report keeps its plot) and caps the number of panels at max_facets (default 40), noting how many columns were omitted if it truncates. All columns are still checked — only the figure is limited.

module_run(test_paper(), "data_check", local_path = repo,
           local_only = TRUE, plot_distributions = TRUE, max_facets = 60)

25.18 The traffic light

Light Meaning
green columns were checked and no issues were found
yellow fewer than a quarter of columns were flagged, or only careless-responding respondents, spreadsheet-formatting issues, space-separated .csv files or R workspaces named .rds were found. The light is also yellow when tabular data files were found but none could be read as a usable table, so that no column was checked
red a quarter or more of columns were flagged
na no tabular data file was found, and there were no spreadsheet-formatting, space-separated .csv or R-workspace findings either

Spreadsheet-formatting, space-separated .csv and R-workspace (.rds) findings are file-level, not column-level, so they are never counted in the quarter-of-columns red threshold — a workbook with a colour-coded cell does not turn the whole result red the way a quarter of columns being flagged would. If no columns could be validated at all (no readable tabular data) but a spreadsheet file was found with formatting problems, or a .csv separated by spaces, the result is yellow, not na — there is a real, actionable finding to report even though nothing could be checked at the column level. The light shown is the worse of this light and the one for reading the files, described in the Data Check chapter.

25.19 Options

Earlier versions had an outlier_k argument that set the width of the Tukey fences. It has been removed, because no check uses the fences any more; passing it now stops module_run() with an “unused argument” error. The options that remain for these checks are plot_distributions and max_facets, described under Distributions in a report.

25.20 What you get back

These are elements of what data_check returns. The Data Check chapter lists the rest.

Element What it contains
$traffic_light green / yellow / red / na
$summary_text plain-text summary with a per-check breakdown
$summary_table paper_id, column_n, flagged_n, spreadsheet_file_n, spreadsheet_flagged_file_n, alongside data_file_n and empty_col_n from the file-classification pass
$findings one row per (column, check): source_file, column, label, check, detail — spreadsheet-formatting findings join the same table with column = NA and the sheet name in label; a space-separated .csv finding has both column and label NA
$table one row per column rather than per problem: what each column is, with its facets and summary statistics
$demographics one row per detected demographic column: source_file, column, demographic
$qualtrics one row per detected Qualtrics export: response count, preview/unfinished rows, median time, collection window, PII fields present
$careless one row per flagged respondent (survey data), aggregated across scale blocks
$previews the data frames themselves, as read from each file
$report formatted report with the issue table, the combined distribution figure (when plot_distributions = TRUE), Qualtrics summary, spreadsheet-formatting section, and careless-responding section

spreadsheet_file_n counts every spreadsheet file examined (.xlsx/.xls/.ods/.fods), whether or not it had any issues; spreadsheet_flagged_file_n counts how many of those had at least one formatting finding.

When no file could be read as a usable table, the column checks do not run: $table has no rows, $findings holds only the spreadsheet-formatting, space-separated .csv and R-workspace findings (if any), and $demographics, $qualtrics and $careless are not returned. The summary, report and traffic light are returned as usual.