
Understanding REDCap Quality Reports
Source:vignettes/articles/quality-report.Rmd
quality-report.Rmdbuild_quality_report() retrieves REDCap records and
project structure, applies general data quality checks, and organizes
the results for review. It helps data managers and analysts identify
records, fields, and metadata that warrant attention without modifying
the REDCap project.
This article explains how to build, review, and interpret a quality
report. All evaluated examples use mock_quality_report, a
synthetic report included with the package, so they do not require
REDCap credentials.
How to Build a Report
Generate a report object
Store the REDCap API URI and token outside version-controlled code,
such as in environment variables, and pass them to
build_quality_report():
report <- build_quality_report(
redcap_uri = Sys.getenv("REDCAP_URI"),
token = Sys.getenv("REDCAP_TOKEN")
)Choose checks
The default call runs all five check groups:
missingness, metadata, outliers,
operational, and consistency. Use
checks for a focused review:
report <- build_quality_report(
redcap_uri = Sys.getenv("REDCAP_URI"),
token = Sys.getenv("REDCAP_TOKEN"),
checks = c("missingness", "metadata")
)Summaries and standardized metadata are always returned. Project- and
record-level finding_count values reflect only the selected
check groups. mock_quality_report was built with all
default checks and thresholds.
Set heuristic thresholds
Two arguments control heuristic findings. They do not change the underlying REDCap data or missing-rate calculations.
| Argument | Default | Finding affected | Lower values | Higher values |
|---|---|---|---|---|
sparse_threshold |
0.95 |
unexpected_sparse_field |
Flag more fields | Flag fewer fields |
outlier_iqr_multiplier |
3 |
numeric_iqr_outlier |
Flag more values | Flag fewer values |
For each field, sparse_threshold is compared with
missing_count / record_count across rows eligible for
missingness assessment. A field is flagged when its missing rate is
greater than or equal to the threshold. At the default
0.95, a field with 19 missing values among 20 applicable
rows is flagged.
For each numeric, non-choice field other than the record identifier,
outlier_iqr_multiplier defines the heuristic range:
lower bound = Q1 - outlier_iqr_multiplier * IQR
upper bound = Q3 + outlier_iqr_multiplier * IQR
Values strictly outside the range are flagged. The default multiplier
of 3 uses wider bounds than the conventional
1.5 * IQR rule and is less sensitive to moderate extremes.
The check requires at least four observed values and a nonzero IQR. It
does not affect metadata-defined validation ranges or future-date
findings.
How to Review a Report
A practical review usually follows four steps:
- Inspect the project overview.
- Count findings by check, issue, and severity.
- Filter record-field findings into a review queue.
- Join field metadata when labels or field types add context.
Start with the report overview
Printing the report gives a short project-level overview. The object
is a list with findings, summaries, and
metadata elements.
mock_quality_report
#> <REDCap quality report>
#> Records: 3
#> Fields: 11
#> Findings: 7
names(mock_quality_report)
#> [1] "findings" "summaries" "metadata"
names(mock_quality_report$summaries)
#> [1] "project" "forms" "fields" "records" "events"
names(mock_quality_report$metadata)
#> [1] "fields" "forms" "events"
#> [4] "event_instruments" "instruments" "repeating_instruments"
#> [7] "choices" "validation" "branching"
#> [10] "record_id_field" "source"Begin with the project summary and a count of findings. This shows project size and structure alongside the types of review items found.
mock_quality_report$summaries$project
#> # A tibble: 1 × 8
#> record_count raw_row_count field_count form_count event_count
#> <int> <int> <int> <int> <int>
#> 1 3 10 11 3 2
#> # ℹ 3 more variables: repeating_enabled <lgl>, missing_rate <dbl>,
#> # finding_count <int>
mock_quality_report$findings |>
dplyr::count(check, issue, severity, sort = TRUE)
#> # A tibble: 4 × 4
#> check issue severity n
#> <chr> <chr> <chr> <int>
#> 1 operational incomplete_form_status info 4
#> 2 metadata high_risk_free_text info 1
#> 3 outliers future_date info 1
#> 4 outliers outside_validation_range warning 1Review actionable findings
Use scope to separate record-field findings from
field-level metadata and aggregate findings. Event and repeat columns
preserve context for longitudinal and repeating projects.
record_findings <- mock_quality_report$findings |>
dplyr::filter(scope == "record_field") |>
dplyr::select(
finding_id,
severity,
issue,
record_id,
event_name,
repeat_instrument,
repeat_instance,
field_name,
value,
expected
)
record_findings
#> # A tibble: 6 × 10
#> finding_id severity issue record_id event_name repeat_instrument
#> <int> <chr> <chr> <chr> <chr> <chr>
#> 1 2 warning outside_validation… C002 baseline_… NA
#> 2 3 info future_date C002 baseline_… NA
#> 3 4 info incomplete_form_st… C0003 baseline_… NA
#> 4 5 info incomplete_form_st… C002 month_1_a… NA
#> 5 6 info incomplete_form_st… C002 month_1_a… adverse_events
#> 6 7 info incomplete_form_st… C0003 month_1_a… adverse_events
#> # ℹ 4 more variables: repeat_instance <chr>, field_name <chr>, value <chr>,
#> # expected <chr>Filter by record, issue, form, or severity to create a review queue. Severity is a package-provided prioritization aid, not a clinical or regulatory classification.
mock_quality_report$findings |>
dplyr::filter(record_id == "C002") |>
dplyr::select(
finding_id,
severity,
issue,
form_name,
field_name,
value,
expected,
message
)
#> # A tibble: 4 × 8
#> finding_id severity issue form_name field_name value expected message
#> <int> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 2 warning outside_valid… demograp… age 999 Between… Field …
#> 2 3 info future_date demograp… enrollmen… 2999… On or b… Date f…
#> 3 5 info incomplete_fo… follow_up follow_up… Inco… Complete Comple…
#> 4 6 info incomplete_fo… adverse_… adverse_e… Unve… Complete Comple…Join findings to field metadata when labels and field types are useful during review:
field_reference <- mock_quality_report$metadata$fields |>
dplyr::select(field_name, field_label, field_type, required_field)
mock_quality_report$findings |>
dplyr::filter(!is.na(field_name)) |>
dplyr::inner_join(field_reference, by = "field_name") |>
dplyr::select(
finding_id,
issue,
record_id,
field_name,
field_label,
field_type,
required_field
)
#> # A tibble: 3 × 7
#> finding_id issue record_id field_name field_label field_type required_field
#> <int> <chr> <chr> <chr> <chr> <chr> <lgl>
#> 1 1 high_ri… NA visit_not… Visit notes notes FALSE
#> 2 2 outside… C002 age Age at enr… text TRUE
#> 3 3 future_… C002 enrollmen… Enrollment… text TRUEQuality Report Structure
Findings
findings is a standardized tibble with one row per
detected issue. A report with no findings returns an empty tibble with
the same columns.
| Column | Meaning |
|---|---|
finding_id |
Sequential identifier assigned within the report. |
check |
Check group that produced the finding. |
issue |
Stable issue name suitable for filtering and counting. |
severity |
Package-assigned warning or info review
priority. |
scope |
record_field for record-field findings or
field for metadata and aggregate findings. |
record_id |
REDCap record identifier when the finding is record-specific. |
form_name |
REDCap instrument associated with the finding. |
event_name |
Unique REDCap event name when available. |
repeat_instrument |
Repeating instrument name when available. |
repeat_instance |
Repeating instance number when available. |
field_name |
REDCap field associated with the finding. |
value |
Observed or aggregate value that triggered the finding. |
expected |
Concise description of the expected state or range. |
message |
Human-readable explanation of the finding. |
The current findings are grouped below by check.
Missingness
These checks describe absent data only where a field is eligible for assessment. They identify required values that are missing on specific rows and fields whose eligible values are almost entirely missing.
| Issue | What it identifies | Severity and scope |
|---|---|---|
required_field_missing |
A required field is blank where form status, project structure, and supported branching logic permit assessment. | Warning, record_field
|
unexpected_sparse_field |
A field’s missing rate meets or exceeds
sparse_threshold among applicable rows. |
Info, field
|
Metadata
Metadata checks inspect project design rather than the validity of individual record values. They highlight labels, choice definitions, and free-text fields that may need review by a project designer or data manager.
| Issue | What it identifies | Severity and scope |
|---|---|---|
duplicate_field_label |
The same nonblank label is used by multiple field names. | Info, field
|
missing_field_label |
A field has no label. | Info, field
|
missing_choice_definition |
A radio, dropdown, or checkbox field has no explicit choice definition. | Warning, field
|
high_risk_free_text |
A text or notes field name or label contains terms associated with notes, comments, other details, or similar free text. | Info, field
|
Yes/No and True/False fields use implicit REDCap choices, so they do not require explicit choice definitions.
Outliers
Outlier checks combine rules configured in REDCap with general review heuristics. They identify values outside metadata bounds, unusually distant numeric values, and dates after the report is built.
| Issue | What it identifies | Severity and scope |
|---|---|---|
outside_validation_range |
A valid numeric, date, datetime, or time value is outside
text_validation_min or
text_validation_max. |
Warning, record_field
|
numeric_iqr_outlier |
A numeric, non-choice value is outside the configured IQR heuristic. | Info, record_field
|
future_date |
A valid date or datetime has a calendar date after the system date captured when the report is built. | Info, record_field
|
Choice fields are excluded from IQR outlier detection even when their stored codes are numeric, because those codes represent categories rather than quantities.
Operational
Operational checks review REDCap workflow state rather than field values. The current check identifies applicable forms that have not been marked Complete.
| Issue | What it identifies | Severity and scope |
|---|---|---|
incomplete_form_status |
An applicable form status is missing or is not Complete. | Info, record_field
|
Consistency
Consistency checks compare stored values with their field definitions and identify contradictory checkbox selections. These findings are tied to either a specific record-field value or an aggregate checkbox pattern.
| Issue | What it identifies | Severity and scope |
|---|---|---|
invalid_choice_value |
A raw radio, dropdown, Yes/No, or True/False code is absent from the field’s choices. | Warning, record_field
|
invalid_validation_format |
A text value does not satisfy its configured REDCap validation. | Warning, record_field
|
checkbox_no_values_selected |
No options are selected across assessed rows with explicit checkbox exports. | Info, field
|
checkbox_none_with_other |
A none-like option is selected with another checkbox option. | Warning, record_field
|
Summaries
summaries provides five views of the normalized project
and report findings.
Project
summaries$project is a one-row overview:
-
record_count: number of distinct record identifiers. -
raw_row_count: number of rows returned by the records API, including event and repeating rows. -
field_count,form_count, andevent_count: project structure counts. -
repeating_enabled: whether repeating configuration was returned by REDCap. -
missing_rate: weighted missing fraction across applicable field-row cells. -
finding_count: total findings from the selected check groups.
mock_quality_report$summaries$project
#> # A tibble: 1 × 8
#> record_count raw_row_count field_count form_count event_count
#> <int> <int> <int> <int> <int>
#> 1 3 10 11 3 2
#> # ℹ 3 more variables: repeating_enabled <lgl>, missing_rate <dbl>,
#> # finding_count <int>Forms and fields
summaries$forms reports field counts, required-field
counts, and weighted missing rates by form.
summaries$fields provides the detailed missingness
profile:
-
record_countis the number of applicable rows assessed for the field. It is not necessarily the number of distinct records because events and repeating instruments can contribute multiple rows per record. -
missing_countandobserved_countpartition applicable rows. -
missing_rateismissing_count / record_count, orNAwhen no rows are applicable. -
distinct_countcounts unique nonmissing values.
mock_quality_report$summaries$forms
#> # A tibble: 3 × 4
#> form_name field_count required_field_count missing_rate
#> <chr> <int> <int> <dbl>
#> 1 adverse_events 3 2 0
#> 2 demographics 4 3 0
#> 3 follow_up 4 1 0
mock_quality_report$summaries$fields |>
dplyr::arrange(dplyr::desc(missing_rate), field_name) |>
dplyr::select(
field_name,
form_name,
required_field,
record_count,
missing_count,
missing_rate,
distinct_count
)
#> # A tibble: 11 × 7
#> field_name form_name required_field record_count missing_count missing_rate
#> <chr> <chr> <lgl> <int> <int> <dbl>
#> 1 ae_grade adverse_… TRUE 4 0 0
#> 2 ae_related adverse_… FALSE 4 0 0
#> 3 ae_term adverse_… TRUE 4 0 0
#> 4 age demograp… TRUE 2 0 0
#> 5 enrollment_… demograp… TRUE 2 0 0
#> 6 record_id demograp… TRUE 2 0 0
#> 7 response_sc… follow_up FALSE 1 0 0
#> 8 sex demograp… FALSE 2 0 0
#> 9 visit_date follow_up TRUE 1 0 0
#> 10 visit_notes follow_up FALSE 1 0 0
#> 11 symptoms follow_up FALSE 0 0 NA
#> # ℹ 1 more variable: distinct_count <int>Project and form missing rates are weighted across applicable field-row cells; they are not simple averages of field-level percentages.
Records and events
summaries$records contains one row per record.
missing_field_count sums missing applicable field values
across that record’s events and repeat instances.
finding_count counts record-specific findings; field-level
findings do not belong to an individual record.
summaries$events counts raw API rows by unique event
name. It is empty for nonlongitudinal projects.
mock_quality_report$summaries$records |>
dplyr::arrange(dplyr::desc(finding_count), record_id)
#> # A tibble: 3 × 3
#> record_id missing_field_count finding_count
#> <chr> <dbl> <int>
#> 1 C002 0 4
#> 2 C0003 0 2
#> 3 C001 0 0
mock_quality_report$summaries$events
#> # A tibble: 2 × 2
#> event_name row_count
#> <chr> <int>
#> 1 baseline_arm_1 3
#> 2 month_1_arm_1 7Metadata
metadata preserves the standardized project structure
used by the checks.
| Element | Contents |
|---|---|
fields |
Standardized REDCap field metadata used by the report. |
forms |
One row per form with field, required-field, and choice-field counts. |
events |
Standardized longitudinal event metadata. |
event_instruments |
Standardized event-to-form mapping. |
instruments |
Instrument names and labels returned by REDCap. |
repeating_instruments |
Repeating event and instrument configuration returned by REDCap. |
choices |
Parsed explicit choices and implicit Yes/No and True/False choices. |
validation |
Fields with validation types or minimum/maximum bounds. |
branching |
Fields with branching logic and parsed referenced field names. |
record_id_field |
Field name used as the project record identifier. |
source |
Source of the normalized project; API reports use
"api". |
These tables support project-level review and help interpret findings:
mock_quality_report$metadata$repeating_instruments
#> # A tibble: 1 × 6
#> event_name arm_num unique_event_name form_name custom_form_label
#> <chr> <int> <chr> <chr> <chr>
#> 1 month_1_arm_1 1 month_1_arm_1 adverse_events NA
#> # ℹ 1 more variable: redcap_event_name <chr>
mock_quality_report$metadata$validation
#> # A tibble: 5 × 5
#> field_name form_name text_validation_type_or_s…¹ text_validation_min
#> <chr> <chr> <chr> <dbl>
#> 1 age demographics integer 0
#> 2 enrollment_date demographics date_ymd NA
#> 3 visit_date follow_up date_ymd NA
#> 4 response_score follow_up integer 0
#> 5 ae_grade adverse_events integer 1
#> # ℹ abbreviated name: ¹text_validation_type_or_show_slider_number
#> # ℹ 1 more variable: text_validation_max <dbl>
mock_quality_report$metadata$branching
#> # A tibble: 1 × 4
#> field_name form_name branching_logic referenced_fields
#> <chr> <chr> <chr> <chr>
#> 1 symptoms follow_up [response_score] < '10' response_scoreHow Checks Work
Different check groups intentionally evaluate different sets of rows. In this table, form status determines whether the row can enter the check, branching logic determines whether a currently hidden field is excluded, and blank values describes what happens after a row and field are eligible.
| Evaluation | Form status | Branching logic | Blank values |
|---|---|---|---|
| Missingness | Only Unverified or Complete forms are evaluated. | A field is evaluated only where supported logic indicates it is shown. | A blank eligible value is counted as missing. |
| Choice and text validation | Stored values are evaluated for any form status, including Incomplete. | A stored value is evaluated even if branching currently hides the field. | Blank values do not produce choice or format findings. Eligible blanks are handled separately by missingness checks. |
| Checkbox consistency | Only Unverified or Complete forms are evaluated. | A checkbox is evaluated only where supported logic indicates it is shown. | Explicit 0 values are meaningful; rows where every
option is blank are skipped. |
| Metadata | Record rows and form status are not used. | Branching logic does not control metadata checks. | A blank label or choice definition can trigger a metadata finding; blank record values are not examined. |
“Supported logic” refers to the subset of REDCap branching expressions the package can evaluate, described under Core Assumptions. Event-to-form mappings and repeating configuration are respected whenever a check evaluates record data. For example, a blank field on a Complete form counts as missing only when supported branching logic shows that field. By contrast, a nonblank malformed value on an Incomplete form can still produce a validation finding, even when branching currently hides the field.
Missingness
Missingness is evaluated in two stages. The report first determines whether a field should be assessed on an exported row, then determines whether the field has an observed value. This prevents structural blanks from events, repeating instruments, branching logic, and unfinished forms from being treated as missing data.
Form status
Raw REDCap form status determines whether a row contributes to missingness:
| Form status | Included in missingness? |
incomplete_form_status? |
|---|---|---|
Incomplete (0) |
No | Yes |
Unverified (1) |
Yes | Yes |
Complete (2) |
Yes | No |
| Blank value | No | Yes, when the form is otherwise applicable |
| Status column unavailable | No | No status finding can be generated |
If a form has no exported <form_name>_complete
column, its fields have no assessable rows and their missing rates are
NA.
Field applicability
Among rows with an assessable status, applicability accounts for:
- Event-to-form mappings in longitudinal projects.
- Repeating event and instrument configuration.
- The repeating instrument associated with each exported row.
- Supported branching logic that determines whether the field is shown.
For applicable non-checkbox fields, NA and blank or
whitespace-only values are missing. A missing required field produces
required_field_missing. Any required or optional field can
produce unexpected_sparse_field when its missing rate meets
sparse_threshold.
Checkbox behavior
REDCap exports one 0/1 column per checkbox
choice. The report evaluates missingness at the parent-field level:
- One or more checked (
1) choices are observed. - All choices explicitly unchecked (
0) are also observed. This is a meaningful no-selection state. - The parent field is missing only when every exported choice value is
NAor blank on an otherwise applicable row.
On an Incomplete form, checkbox exports are ignored for missingness. On an Unverified or Complete form, an all-zero state is observed, while an all-missing set can contribute to sparse-field and required-field findings.
Checkbox consistency findings do not redefine the all-zero state as missing:
-
checkbox_no_values_selectedis one field-level prompt when no option is selected across assessed rows with explicit checkbox exports. -
checkbox_none_with_otheris a record-field warning when an option labeled “None”, “No”, “Not applicable”, “N/A”, “NA”, or “None of the above” is selected with another option.
Metadata-defined validation
REDCap metadata records the validation selected by a project designer
in the REDCap field configuration UI. For text fields, the selected
validation type and optional minimum or maximum are exported in the
text_validation_* columns. Choice definitions configured in
REDCap are similarly exported as field metadata.
The report compares nonblank raw values with these user-configured rules. It inspects structurally applicable rows regardless of whether a form is Incomplete, Unverified, or Complete. Branching logic does not suppress a stored value that violates its metadata rule.
Choice values
invalid_choice_value identifies raw codes absent from
metadata choices for radio, dropdown, Yes/No, and True/False fields.
Yes/No and True/False use their implicit REDCap
0/1 definitions.
Checkbox fields are excluded. Their metadata codes identify separate
option columns, while each exported cell contains a 0 or
1 selection state.
Why the raw export is used
REDCap choice fields have a stored code and a displayed label.
Validation must compare the stored code with the codes defined in
metadata, so the report requests raw values rather than labels. For
example, a dropdown choice stored as code 1 remains
"1" instead of being replaced by its display label.
Automatic column type guessing is also disabled. This prevents a
column of 0 and 1 codes from being converted
to logical FALSE and TRUE, which would no
longer match the REDCap choice definitions. Empty API cells are imported
as missing, while a literal code such as "NA" is preserved
as an actual value.
Fields without explicit radio, dropdown, or checkbox definitions
produce missing_choice_definition instead of an
invalid-choice finding.
Text formats and bounds
invalid_validation_format applies only to
text fields with a supported
text_validation_type_or_show_slider_number. The report does
not infer validation from R classes, field names, labels, or observed
values.
| Family | Metadata examples | Validation behavior |
|---|---|---|
| Integer | integer |
Integer syntax; scientific notation is rejected. |
| Number |
number, number_1dp through
number_4dp, comma-decimal variants |
Configured decimal mark and precision; a leading zero is optional. |
| Date |
date_dmy, date_mdy,
date_ymd
|
Canonical API value YYYY-MM-DD; impossible dates are
rejected. |
| Datetime | DMY, MDY, and YMD datetime_* types |
Canonical YMD export with minutes or seconds, according to the validation type. |
| Time |
time, time_hh_mm_ss,
time_mm_ss
|
Configured format and clock boundaries. |
email |
Structural email format, not deliverability. | |
| Phone |
phone, phone_australia
|
Common punctuation and regional digit structure. |
The DMY, MDY, and YMD suffix controls REDCap’s data-entry display.
API exports still use canonical YMD date and datetime values. Number
validations accept values with or without a leading zero, such as
0.850 and .850.
When provided, text_validation_min and
text_validation_max produce
outside_validation_range for valid numeric, date, datetime,
and time values outside the configured bounds. Either bound can be used
alone. Malformed values produce invalid_validation_format
but not range or future-date findings.
Use issue names to review metadata-defined validation findings together. The mock report contains a range violation but no malformed formats or invalid choice codes.
validation_issues <- c(
"invalid_choice_value",
"invalid_validation_format",
"outside_validation_range"
)
mock_quality_report$findings |>
dplyr::filter(issue %in% validation_issues) |>
dplyr::select(record_id, field_name, issue, value, expected)
#> # A tibble: 1 × 5
#> record_id field_name issue value expected
#> <chr> <chr> <chr> <chr> <chr>
#> 1 C002 age outside_validation_range 999 Between 0 and 120Distribution and future-date checks
numeric_iqr_outlier is a distribution-based heuristic,
not a REDCap metadata rule. Its behavior depends on sample size, field
distribution, and outlier_iqr_multiplier.
future_date applies to valid date and datetime fields.
It compares the exported calendar date with Sys.Date()
captured when the report runs. A date after that system date is flagged;
a datetime later on the same calendar day is not. Malformed temporal
values are handled by invalid_validation_format
instead.
Core Assumptions
Findings are review prompts
The checks identify items worth reviewing. They do not establish
clinical correctness, protocol compliance, or regulatory compliance. An
info finding can still be important, and a report with no
findings does not prove that the data are correct.
finding_id is assigned when a report is built and is not
persistent across runs. Use the issue and REDCap context columns when
tracking findings externally.
Branching logic support is intentionally bounded
The branching evaluator supports ordinary field comparisons, checkbox
choice references, parentheses, and and/or
combinations. A valid expression that evaluates to NA is
treated as not shown for that row.
REDCap functions, smart variables, and expressions outside the supported subset are treated as applicable rather than used to hide a potentially required value. The report does not produce findings for apparently missing branching references because event-qualified fields, cross-form fields, and smart variables cannot be classified reliably from bracketed tokens alone.
Counts reflect REDCap’s row structure
A REDCap record can produce multiple API rows because of events and
repeating instances. Project record_count counts distinct
records, raw_row_count counts exported rows, field
record_count counts applicable rows, and event
row_count counts exported rows per event. Choose the
measure that matches the question being asked.
Results reflect the current project export
Descriptive fields are removed before analysis because they do not store record values. Structural metadata retrieval failures are reported as errors rather than silently converted to empty project structure. Results reflect the records and metadata available when the report is built, including the token’s project access and export configuration.