--- title: "Getting Started with mighty.component" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Getting Started with mighty.component} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = rlang::is_installed(c( "admiral", "callr", "covr", "dplyr", "pharmaverseadam", "tibble", "tidyr" )) ) withr::local_options(mighty.component.verbosity_level = "quiet") ``` ```{r setup} library(mighty.component) ``` ## What is a component? A component is a reusable code template for one data transformation step. Its code takes an input data set and returns it with new or changed columns or rows. Mustache placeholders, such as `{{{domain}}}`, are filled in when the component is rendered, so the same template works across studies. Roxygen-like tags, such as `@title`, `@param` and `@depends`, document what the component does, needs and creates. Components are commonly used to build ADaM (Analysis Data Model) programs. Each component handles one derivation, and several components make up a program. In the mighty ecosystem, mighty.metadata provides study configuration (via [`mighty_study()`](https://novonordisk-opensource.github.io/mighty.metadata/reference/mighty_study.html) and `_study.yml`) that controls which components are rendered and with which parameters. ## Anatomy of a component template Below is a minimal component that creates a column as two times an existing column: ```r #' @title Double a column #' @description #' Creates a new column as two times an existing column. #' #' @param domain `character` Name of the domain #' @param output `character` Name of the new column #' @param input `character` Name of the existing column #' @type column #' @origin Derived #' @method Two times the input column #' @depends {{{domain}}} {{{input}}} #' @outputs {{{output}}} #' @code {{{domain}}} <- {{{domain}}} |> dplyr::mutate( {{{output}}} = 2 * {{{input}}} ) ``` The template syntax is described below. ```{r child = "../man/rmd/template-reference.Rmd"} ``` See `?mighty_component` for validation rules and a rendered example. ## Retrieve and inspect a component List the example components shipped with the package: ```{r list-components} path <- system.file("examples", package = "mighty.component") list_components(path) ``` Retrieve one by file path: ```{r get-ady} ady <- get_component( system.file("examples", "ady.mustache", package = "mighty.component") ) ady ``` Access individual fields: ```{r inspect-fields} ady$id ady$title ady$type ady$params ady$depends ady$outputs ady$origin ady$method ``` ## Render a component Rendering fills in the placeholders with values. `$render()` takes one named argument per `@param` tag and returns a `mighty_component_rendered` object. Every placeholder is now a name: ```{r render-ady} ady_rendered <- ady$render(domain = "ADAE", variable = "ASTDY", date = "ASTDT") ady_rendered ``` The rendered code is validated: it must be valid R, and joins must specify `by` (see [Automatic code validation]). Columns are not checked. Use `get_test_component()` (see [Testing components]) to run a component on real data. `get_rendered_component()` retrieves and renders in one step. It takes the parameters as a named `list`: ```{r shortcut} get_rendered_component( system.file("examples", "ady.mustache", package = "mighty.component"), list(domain = "ADAE", variable = "ASTDY", date = "ASTDT") ) ``` A missing parameter raises an error: ```{r render-error, error=TRUE} ady$render(domain = "ADAE") ``` ## Evaluate rendered code `$eval()` runs the code. By default it runs in the calling environment. Use the `envir` argument to choose another environment. The code assigns the result back to the domain (e.g., `ADAE <- ADAE |> ...`), so `ADAE` is updated without assigning the return value. ```{r eval-setup} ADAE <- pharmaverseadam::adae |> dplyr::select(USUBJID, ASTDT, TRTSDT) names(ADAE) ``` `ASTDY` does not exist yet. Run the rendered component: ```{r eval-run} ady_rendered$eval() names(ADAE) head(ADAE) ``` `$stream()` appends the code to an R script instead: ```{r stream-example} script_file <- tempfile(fileext = ".R") ady_rendered$stream(script_file) readLines(script_file) ``` ## Writing a custom component Write your own components as `.mustache` files. This component derives the ratio of the analysis value to baseline: ```r #' @title Ratio to baseline #' @description #' Derives the ratio of the analysis value to the baseline value. #' #' @param domain `character` Name of the domain #' @param variable `character` Name of the new ratio variable #' @type column #' @origin Derived #' @method Ratio of AVAL to BASE #' @depends {{{domain}}} AVAL #' @depends {{{domain}}} BASE #' @outputs {{{variable}}} #' @code {{{domain}}} <- {{{domain}}} |> dplyr::mutate( {{{variable}}} = dplyr::if_else(BASE != 0, AVAL / BASE, NA_real_) ) ``` ```{r custom-write, include = FALSE} r2base_file <- file.path(withr::local_tempdir(), "r2base.mustache") writeLines(c( "#' @title Ratio to baseline", "#' @description", "#' Derives the ratio of the analysis value to the baseline value.", "#'", "#' @param domain `character` Name of the domain", "#' @param variable `character` Name of the new ratio variable", "#' @type column", "#' @origin Derived", "#' @method Ratio of AVAL to BASE", "#' @depends {{{domain}}} AVAL", "#' @depends {{{domain}}} BASE", "#' @outputs {{{variable}}}", "#' @code", "{{{domain}}} <- {{{domain}}} |>", " dplyr::mutate(", " {{{variable}}} = dplyr::if_else(BASE != 0, AVAL / BASE, NA_real_)", " )" ), r2base_file) ``` Save the template as `r2base.mustache`. Then load, render and run it: ```{r custom-load} r2base <- get_component(r2base_file) r2base ``` ```{r custom-render} r2base_rendered <- r2base$render( domain = "ADLB", variable = "R2BASE" ) r2base_rendered$code ``` ```{r custom-eval} ADLB <- pharmaverseadam::adlb |> dplyr::filter(PARAMCD == "ALB") |> dplyr::select(USUBJID, PARAMCD, AVISIT, AVAL, BASE) head(ADLB) r2base_rendered$eval() ADLB |> dplyr::select(USUBJID, PARAMCD, AVISIT, AVAL, BASE, R2BASE) |> head() ``` ## Component repos A component repo is a collection of components. `get_component()`, `find_component()` and `list_components()` take a `repos` argument with one or more repo specs: - A local directory, e.g. `"inst/components"` or `"local::inst/components"`. - A GitHub repository, e.g. `"github::owner/repo/subdir@ref"`. Requires the gh and remotes packages. - Raw files under a URL, e.g. `"url::https://example.com/components"`. Requires httr2 (>= 1.2.2). A component `name` is the file `.mustache` or `.R`, either directly in the repo or in a directory `/`. See `?mighty_repo` for details. Put the `r2base` component from above in a team repo, together with a modified copy of `ady`: ```{r repos-setup} examples <- system.file("examples", package = "mighty.component") team_repo <- tempfile("team") dir.create(team_repo) invisible(file.copy(r2base_file, team_repo)) ady_template <- readLines(file.path(examples, "ady.mustache")) ady_template[[1]] <- "#' @title Analysis relative day (team version)" writeLines(ady_template, file.path(team_repo, "ady.mustache")) ``` Several repos are searched in order, and the first match wins. Here `ady` is found in the team repo: ```{r repos-priority} repos <- c(team_repo, examples) list_components(repos) get_component("ady", repos = repos)$title ``` Reverse the order to use `ady` from the examples: ```{r repos-reverse} get_component("ady", repos = rev(repos))$title ``` `find_component()` returns `NULL` if no repo contains the component. `get_component()` raises an error: ```{r repos-not-found, error = TRUE} is.null(find_component("adt", repos = repos)) get_component("adt", repos = repos) ``` `list_components(as = "tibble")` returns the metadata of each component. It requires the tibble and tidyr packages: ```{r repos-tibble} list_components(repos, as = "tibble") |> dplyr::select(id, title, type, origin) ``` A character vector is converted to repos on each call. Use `mighty_repos()` to create the repos once and reuse them. GitHub and URL repos need network access: ```{r repos-github, eval = FALSE} repos <- mighty_repos(c( "github::NovoNordisk-OpenSource/mighty.standards/components@main", paste0( "url::https://raw.githubusercontent.com/", "NovoNordisk-OpenSource/mighty.standards/main/components" ), examples )) get_component("dummy", repos = repos) ``` See `?mighty_repo_github` and `?mighty_repo_url` for authentication and retries. ## Automatic code validation Rendered code is validated. It must be valid R, and joins from dplyr, tidylog and dbplyr must specify `by`. Implicit joins are a common source of bugs when key columns differ between studies. This component fails validation: ```r #' @title Bad join example #' @description Implicit join that will fail validation. #' #' @param domain `character` Name of the domain #' @type row #' @origin Derived #' @method Implicit join on other_data #' @depends {{{domain}}} USUBJID #' @outputs MYCOL #' @code {{{domain}}} <- {{{domain}}} |> dplyr::left_join(other_data) ``` ```{r validation-fail-setup, include = FALSE} bad_template <- c( "#' @title Bad join example", "#' @description Implicit join that will fail validation.", "#'", "#' @param domain `character` Name of the domain", "#' @type row", "#' @origin Derived", "#' @method Implicit join on other_data", "#' @depends {{{domain}}} USUBJID", "#' @outputs MYCOL", "#' @code", "{{{domain}}} <- {{{domain}}} |>", " dplyr::left_join(other_data)" ) bad_file <- withr::local_tempfile(fileext = ".mustache", lines = bad_template) ``` ```{r validation-fail, error=TRUE} get_rendered_component(bad_file, list(domain = "ADAE")) ``` Specify the join key with `by` to fix it: ```r #' @title Good join example #' @description Explicit join that passes validation. #' #' @param domain `character` Name of the domain #' @type row #' @origin Derived #' @method Explicit join on other_data by USUBJID #' @depends {{{domain}}} USUBJID #' @outputs MYCOL #' @code {{{domain}}} <- {{{domain}}} |> dplyr::left_join(other_data, by = dplyr::join_by(USUBJID)) ``` ```{r validation-pass-setup, include = FALSE} good_template <- c( "#' @title Good join example", "#' @description Explicit join that passes validation.", "#'", "#' @param domain `character` Name of the domain", "#' @type row", "#' @origin Derived", "#' @method Explicit join on other_data by USUBJID", "#' @depends {{{domain}}} USUBJID", "#' @outputs MYCOL", "#' @code", "{{{domain}}} <- {{{domain}}} |>", " dplyr::left_join(other_data, by = dplyr::join_by(USUBJID))" ) good_file <- withr::local_tempfile(fileext = ".mustache", lines = good_template) ``` ```{r validation-pass} get_rendered_component(good_file, list(domain = "ADAE"))$code ``` ## Testing components `get_test_component()` creates a component that runs in a separate R session and tracks code coverage. Use it for interactive checks and for unit tests with testthat. By default (`check_coverage = TRUE`), coverage is checked when the calling environment ends, e.g. the `test_that()` block. An error is raised if any line has not run. This vignette is not a test, so it sets `check_coverage = FALSE`. ```{r test-create} ady_path <- system.file( "examples", "ady.mustache", package = "mighty.component" ) ady_test <- get_test_component( component = ady_path, params = list(domain = "ADAE", variable = "ASTDY", date = "ASTDT"), check_coverage = FALSE # keep the default TRUE in tests ) ady_test ``` Assign input data in the test session: ```{r test-assign} adae_input <- pharmaverseadam::adae |> dplyr::select(USUBJID, ASTDT, TRTSDT) ady_test$assign("ADAE", adae_input) ady_test$ls() ``` Run the component and get the result: ```{r test-eval} ady_test$eval() ady_test$get("ADAE") |> head() ``` Every line of the code has now run: ```{r test-coverage} # Print method ady_test # Percent coverage ady_test$percent_coverage # Line coverage as a data.frame ady_test$line_coverage ``` In a testthat file, create the test component inside `test_that()`, assign data, evaluate and test the results. Coverage is checked when the test ends. See `?mighty_component_test` for the workflow. ```{r cleanup, include = FALSE} ady_test$close() unlink(c(script_file, team_repo), recursive = TRUE) ```