Skip to content

Commit

Permalink
Merge branch 'release/0.1.0.9001'
Browse files Browse the repository at this point in the history
  • Loading branch information
pbchase committed Jan 30, 2025
2 parents c5b4875 + b8f7cf6 commit 82f2527
Show file tree
Hide file tree
Showing 26 changed files with 532 additions and 46 deletions.
4 changes: 3 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: redcapfiller
Type: Package
Title: Fill a REDCap Project with Generated Data
Version: 0.1.0
Version: 0.1.0.9001
Authors@R: c(
person("Philip", "Chase",
email = "[email protected]",
Expand Down Expand Up @@ -37,6 +37,8 @@ Suggests:
Config/testthat/edition: 3
Imports:
dplyr,
lorem,
purrr,
rlang,
tidyr
RoxygenNote: 7.3.2
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@

export(get_long_categorical_field_response_values)
export(get_long_categorical_field_responses)
export(get_long_text_field_values)
export(get_long_text_fields)
export(get_one_rectangle_of_values)
importFrom(rlang,.data)
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# redcapfiller (0.1.0.9001)

* Supports filling of checkbox, dropdown, radio, yes/no, True/False and unvalidated text fields on classic projects.
* Provides a proof_of_concept.R script with write-back to a demonstration project and properly managed secrets.

9 changes: 9 additions & 0 deletions R/get_long_categorical_field_response_values.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@
#' }
get_long_categorical_field_response_values <- function(long_categorical_field_responses) {
single_value_responses <- long_categorical_field_responses |>
# Filter for any categorical field_type
dplyr::filter(.data$field_type %in% c(
"checkbox",
"dropdown",
"radio",
"yesno"
)) |>
# Filter for anything but checkbox fields
dplyr::filter(.data$field_type != "checkbox") |>
dplyr::group_by(.data$field_name) |>
dplyr::slice_sample(n = 1, weight_by = .data$weight) |>
dplyr::ungroup()

multi_value_responses <-
long_categorical_field_responses |>
# Filter for checkbox fields
dplyr::filter(.data$field_type == "checkbox") |>
dplyr::group_by(.data$field_group) |>
dplyr::slice_sample(prop = 0.5, weight_by = .data$weight) |>
Expand Down
7 changes: 6 additions & 1 deletion R/get_long_categorical_field_responses.R
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@ get_long_categorical_field_responses <- function(metadata) {
balanced_responses <-
metadata |>
# include only categorical field types
dplyr::filter(.data$field_type %in% c("checkbox", "radio", "dropdown")) |>
dplyr::filter(.data$field_type %in% c("checkbox", "radio", "dropdown", "truefalse", "yesno")) |>
# excluding anything displayed by branching logic
dplyr::filter(is.na(.data$branching_logic)) |>
# narrow our focus to the required columns
dplyr::select(c("field_name", "form_name", "field_type", "select_choices_or_calculations")) |>
dplyr::mutate(select_choices_or_calculations = dplyr::case_when(
.data$field_type == "truefalse" ~ "1, True|0, False",
.data$field_type == "yesno" ~ "1, Yes|0, No",
TRUE ~ .data$select_choices_or_calculations)
) |>
# separate responses
tidyr::separate_longer_delim("select_choices_or_calculations", delim = stringr::regex("\\s?\\|\\s?")) |>
# separate response_codes from response_labels
Expand Down
48 changes: 48 additions & 0 deletions R/get_long_text_field_values.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#' @title generate text field values
#' @description
#' Provide a set of values for each field in the output of
#' `get_long_text_fields`
#'
#' @param long_text_fields a long data set of text
#' fields, their parameters, and weights.
#'
#' @return a long dataframe of text field values with one row for each value set.
#' @export
#'
#' @examples
#' \dontrun{
#' get_long_text_field_values(long_text_fields)
#' }
get_long_text_field_values <- function(long_text_fields) {
tvt_na <- function(df) {
df |>
dplyr::filter(.data$tvt == "tvt_na") |>
dplyr::mutate(value = replicate(length(.data$field_name), lorem::ipsum_words(round(stats::rnorm(mean = .data$mean, sd = .data$sd, n = 1)))))
# |>
# dplyr::slice_sample(prop = 0.5, weight_by = .data$weight)
}

tvt_types <- c(
"tvt_na"
)

process_one_text_validation_type <- function(my_tvt, df) {
# exec (run) the function named in `alert` on the rows of data that
# have an alert_type of `alert`
result <- rlang::exec(my_tvt, df |>
dplyr::filter(.data$tvt == my_tvt))
return(result)
}

text_field_values <-
purrr::map(tvt_types,
process_one_text_validation_type,
long_text_fields |> dplyr::filter(.data$field_type == "text")
) |>
dplyr::bind_rows()

result <- text_field_values |>
dplyr::select("field_name", "value")

return(result)
}
85 changes: 85 additions & 0 deletions R/get_long_text_fields.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#' @title Get every text field response from a REDCap data dictionary
#'
#' @description
#' Given a REDCap data dictionary, enumerate every text field in that data dictionary and return a dataset with default weights
#'
#' @param metadata A REDCap data dictionary
#'
#' @returns a dataframe with these columns
#' \describe{
#' \item{field_name}{REDCap field name}
#' \item{form_name}{REDCap form name}
#' \item{field_type}{REDCap field type}
#' \item{text_validation_type}{REDCap text validation type}
#' \item{text_validation_min}{REDCap text validation min}
#' \item{text_validation_max}{REDCap text validation max}
#' \item{tvt}{text validation type function name}
#' \item{weight}{a default weight for the field}
#' \item{mean}{mean of data to be generated}
#' \item{sd}{standard deviation of data to be generated}
#' }
#' @export
#'
#' @examples
#' \dontrun{
#' long_text_fields <-
#' get_long_text_fields(metadata_to_populate)
#' }
get_long_text_fields <- function(metadata) {
text_fields <-
metadata |>
# include only categorical field types
dplyr::filter(.data$field_type %in% c("text")) |>
# excluding anything displayed by branching logic
dplyr::filter(is.na(.data$branching_logic)) |>
# narrow our focus to the required columns
dplyr::select(c(
"field_name",
"form_name",
"field_type",
"text_validation_type_or_show_slider_number",
"text_validation_min",
"text_validation_max"
)) |>
dplyr::rename(text_validation_type = "text_validation_type_or_show_slider_number") |>
dplyr::mutate(tvt = dplyr::case_when(
is.na(.data$text_validation_type) ~ "tvt_na",
TRUE ~ "tvt_unsupported"
)) |>
# set weights for each response
dplyr::mutate(weight = 100)

tvt_na <- function(text_fields) {
result <-
text_fields |>
dplyr::filter(is.na(.data$text_validation_type)) |>
dplyr::mutate(mean = 1.5, sd = 0.8)
return(result)
}

tvt_unsupported <- function(text_fields) {
result <-
text_fields |>
dplyr::filter(F)
return(result)
}

tvt_types <- c(
"tvt_na",
"tvt_unsupported"
)

process_one_text_validation_type <- function(my_tvt, df) {
# exec (run) the function named in `alert` on the rows of data that
# have an alert_type of `alert`
result <- rlang::exec(my_tvt, df |>
dplyr::filter(.data$tvt == my_tvt))
return(result)
}

text_fields_and_weights <-
purrr::map(tvt_types, process_one_text_validation_type, text_fields) |>
dplyr::bind_rows()

return(text_fields_and_weights)
}
26 changes: 19 additions & 7 deletions R/get_one_rectangle_of_values.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
#' @param one_record_id a single record_id
#' @param record_id_name the column name the record_id should be returned in
#' @param forms_to_fill the forms to fill for this rectangle
#' @param long_categorical_field_responses the output of `get_long_categorical_field_responses()`
#' @param long_fields_and_responses the output of `get_long_*_fields` and
#' `get_long_categorical_field_responses_responses` functions
#'
#' @returns a rectangle of data with appropriate REDCap identifiers ready to write to REDCap
#' @export
Expand All @@ -17,7 +18,7 @@ get_one_rectangle_of_values <- function(
one_record_id = 1,
record_id_name,
forms_to_fill,
long_categorical_field_responses) {
long_fields_and_responses) {
# Build tibble of static REDCap identifiers
redcap_identifiers <- dplyr::tibble(
record_id = one_record_id
Expand All @@ -26,14 +27,25 @@ get_one_rectangle_of_values <- function(
# fix the first column name
names(redcap_identifiers) <- record_id_name

value_getter_functions <- c(
"get_long_categorical_field_response_values",
"get_long_text_field_values"
)

process_one_value_getter <- function(value_getter, df) {
rlang::exec(value_getter, df)
}

# pick values for one record on one event
# ...by binding the output of each field_type / field_validation function
all_responses <- dplyr::bind_rows(
get_long_categorical_field_response_values(
long_categorical_field_responses |>
all_responses <-
purrr::map(
value_getter_functions,
process_one_value_getter,
long_fields_and_responses |>
dplyr::filter(.data$form_name %in% forms_to_fill)
)
)
) |>
dplyr::bind_rows()

# prefix responses with redcap fields
long_result <- dplyr::bind_cols(
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Copy [proof_of_concept.R](https://github.com/ctsit/redcapfiller/blob/main/proof_

### Limitations

REDCap Filler does not yet understand all the dimensions of a modern REDCap project. It can fill the categorical fields checkbox, dropdown, and radio. It ignores all other field types and will not attempt to fill them. It doesn't even know how to fill a yesno field. :-( Filler only knows how to fill classic projects without repeating forms or events. It does not honor form display logic and ignores all fields governed by branching logic.
REDCap Filler does not yet understand all the dimensions of a modern REDCap project. It can fill the categorical fields. It can fill unvalidated text fields. It ignores all other field types and will not attempt to fill them. It doesn't even know how to fill a date field. :-( Filler only knows how to fill classic projects without repeating forms or events. It does not honor form display logic and ignores all fields governed by branching logic.

Focusing more on what Filler _can_ do, the first release milestone will support these features:

Expand Down
2 changes: 1 addition & 1 deletion example.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
filler_demo_pid=15321
path_credential = ~/credentials.csv
path_credential=~/credentials.csv
24 changes: 24 additions & 0 deletions man/get_long_text_field_values.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions man/get_long_text_fields.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions man/get_one_rectangle_of_values.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 82f2527

Please sign in to comment.