Skip to content

Commit 474747b

Browse files
authored
Merge pull request #9 from CHOP-CGTInformatics/codex/quality-report-clarifications
Update vignette, documentation, fix checkbox missingness logic
2 parents e245bb0 + e45f0ad commit 474747b

5 files changed

Lines changed: 332 additions & 105 deletions

File tree

R/quality_report.R

Lines changed: 99 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@
88
#' @param redcap_uri REDCap API URI.
99
#' @param token REDCap API token.
1010
#' @param checks Character vector of check groups to run.
11-
#' @param sparse_threshold Missingness threshold used to flag unexpectedly sparse
12-
#' fields.
13-
#' @param outlier_iqr_multiplier Multiplier used for IQR-based numeric outlier
14-
#' detection.
15-
#' @param progress Progress display mode. Use `"auto"` to show progress only in
16-
#' interactive sessions, `"none"` to suppress progress, or `"show"` to force
17-
#' progress output.
11+
#' @param sparse_threshold Number greater than 0 and less than or equal to 1.
12+
#' A field is flagged as unexpectedly sparse when its missing rate among
13+
#' applicable rows is greater than or equal to this value. The default `0.95`
14+
#' flags fields with at least 95 percent missing values.
15+
#' @param outlier_iqr_multiplier Positive number used to define IQR outlier
16+
#' bounds as Q1 minus this value times the IQR and Q3 plus this value times the
17+
#' IQR. Smaller values produce narrower bounds and more findings. The default
18+
#' is `3`.
19+
#' @param progress Progress display mode. `"auto"` enables normally throttled
20+
#' progress updates only in interactive sessions. `"none"` suppresses
21+
#' progress. `"show"` enables progress in all sessions and forces every
22+
#' update to render.
1823
#'
1924
#' @details
2025
#' Current `findings$issue` values by `findings$check`:
@@ -35,6 +40,7 @@
3540
#' - `operational`
3641
#' - `incomplete_form_status`
3742
#' - `consistency`
43+
#' - `checkbox_no_values_selected`
3844
#' - `checkbox_none_with_other`
3945
#'
4046
#' @returns A list with `findings`, `summaries`, and `metadata` elements.
@@ -52,8 +58,9 @@
5258
#' structure, and branching logic. Missingness is only assessed where the raw
5359
#' REDCap form status column `<form_name>_complete` is `1`/Unverified or
5460
#' `2`/Complete. Status `0`/Incomplete is handled by operational checks and
55-
#' does not contribute to missingness, even when REDCap exports checkbox
56-
#' choices as `0`.
61+
#' does not contribute to missingness. Checkbox parent fields are observed
62+
#' when their choice exports contain explicit values, including an all-zero
63+
#' no-selection state; only an all-missing set of choice exports is missing.
5764
#' Branching expressions that use unsupported REDCap functions or smart
5865
#' variables are treated as applicable so required values remain reviewable.
5966
#'
@@ -794,14 +801,21 @@ get_field_values <- function(project, field) {
794801
return(rep(NA_character_, nrow(project$data)))
795802
}
796803

797-
checked <- as.data.frame(map(project$data[checkbox_columns], get_is_checked))
804+
checkbox_data <- project$data[checkbox_columns]
805+
checked <- as.data.frame(map(checkbox_data, get_is_checked))
806+
missing <- as.data.frame(map(checkbox_data, get_is_missing))
798807
checked_names <- sub(paste0("^", field, "___"), "", checkbox_columns)
799808
map_chr(seq_len(nrow(checked)), \(row) {
800809
row_checked <- unlist(checked[row, ], use.names = FALSE)
801-
if (!any(row_checked, na.rm = TRUE)) {
810+
row_missing <- unlist(missing[row, ], use.names = FALSE)
811+
if (all(row_missing)) {
802812
return(NA_character_)
803813
}
804814

815+
if (!any(row_checked, na.rm = TRUE)) {
816+
return("No selections")
817+
}
818+
805819
paste(checked_names[row_checked], collapse = ", ")
806820
})
807821
}
@@ -1358,11 +1372,16 @@ get_consistency_findings <- function(project) {
13581372

13591373
bind_rows(map(names(checkbox_groups), \(field) {
13601374
columns <- checkbox_groups[[field]]
1375+
no_selection_finding <- get_checkbox_no_selection_finding(
1376+
project,
1377+
field,
1378+
columns
1379+
)
13611380
none_columns <- get_none_choice_columns(choices, field, columns)
13621381
other_columns <- setdiff(columns, none_columns)
13631382

13641383
if (length(none_columns) == 0 || length(other_columns) == 0) {
1365-
return(get_empty_findings())
1384+
return(no_selection_finding)
13661385
}
13671386

13681387
none_checked <- Reduce(
@@ -1373,40 +1392,84 @@ get_consistency_findings <- function(project) {
13731392
`|`,
13741393
lapply(project$data[other_columns], get_is_checked)
13751394
)
1376-
contradiction <- none_checked & other_checked
1395+
contradiction <- none_checked &
1396+
other_checked &
1397+
get_field_applicable_rows(project, field)
13771398

13781399
if (!any(contradiction, na.rm = TRUE)) {
1379-
return(get_empty_findings())
1400+
return(no_selection_finding)
13801401
}
13811402

13821403
metadata_row <- get_metadata_row(project, field)
1383-
tibble(
1384-
check = "consistency",
1385-
issue = "checkbox_none_with_other",
1386-
severity = "warning",
1387-
scope = "record_field",
1388-
record_id = as.character(project$data[[project$record_id_field]][
1389-
contradiction
1390-
]),
1391-
form_name = metadata_row$form_name,
1392-
event_name = get_event_values(project$data, contradiction),
1393-
repeat_instrument = get_repeat_instrument_values(
1394-
project$data,
1395-
contradiction
1396-
),
1397-
repeat_instance = get_repeat_instance_values(project$data, contradiction),
1398-
field_name = field,
1399-
value = paste(none_columns, collapse = ", "),
1400-
expected = "None option not selected with other options",
1401-
message = paste(
1402-
"Checkbox field",
1403-
field,
1404-
"has a none option selected with another option."
1404+
bind_rows(
1405+
no_selection_finding,
1406+
tibble(
1407+
check = "consistency",
1408+
issue = "checkbox_none_with_other",
1409+
severity = "warning",
1410+
scope = "record_field",
1411+
record_id = as.character(project$data[[project$record_id_field]][
1412+
contradiction
1413+
]),
1414+
form_name = metadata_row$form_name,
1415+
event_name = get_event_values(project$data, contradiction),
1416+
repeat_instrument = get_repeat_instrument_values(
1417+
project$data,
1418+
contradiction
1419+
),
1420+
repeat_instance = get_repeat_instance_values(
1421+
project$data,
1422+
contradiction
1423+
),
1424+
field_name = field,
1425+
value = paste(none_columns, collapse = ", "),
1426+
expected = "None option not selected with other options",
1427+
message = paste(
1428+
"Checkbox field",
1429+
field,
1430+
"has a none option selected with another option."
1431+
)
14051432
)
14061433
)
14071434
}))
14081435
}
14091436

1437+
get_checkbox_no_selection_finding <- function(project, field, columns) {
1438+
assessed <- get_field_applicable_rows(project, field)
1439+
explicit <- as.data.frame(map(
1440+
project$data[columns],
1441+
\(value) !get_is_missing(value)
1442+
))
1443+
checked <- as.data.frame(map(project$data[columns], get_is_checked))
1444+
assessed_explicit <- assessed & rowSums(explicit) > 0
1445+
1446+
if (
1447+
!any(assessed_explicit) ||
1448+
any(rowSums(checked[assessed_explicit, , drop = FALSE]) > 0)
1449+
) {
1450+
return(get_empty_findings())
1451+
}
1452+
1453+
metadata_row <- get_metadata_row(project, field)
1454+
tibble(
1455+
check = "consistency",
1456+
issue = "checkbox_no_values_selected",
1457+
severity = "info",
1458+
scope = "field",
1459+
record_id = NA_character_,
1460+
form_name = metadata_row$form_name,
1461+
event_name = NA_character_,
1462+
field_name = field,
1463+
value = "0 selected options",
1464+
expected = "Confirm that no selections are intentional",
1465+
message = paste(
1466+
"Checkbox field",
1467+
field,
1468+
"has no selected options among assessed rows."
1469+
)
1470+
)
1471+
}
1472+
14101473
get_none_choice_columns <- function(choices, field, columns) {
14111474
none_labels <- c("none", "no", "not applicable", "n/a", "na", "none of the above")
14121475
none_values <- choices |>

data/mock_quality_report.rda

-96 Bytes
Binary file not shown.

man/build_quality_report.Rd

Lines changed: 16 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)