--- title: "Metadata and Traceability" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Metadata and Traceability} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) options(rmarkdown.html_vignette.check_title = FALSE) library(tplyr2) library(knitr) ``` ## Why Traceability Matters Clinical summary tables go through rigorous quality control. When a reviewer questions a number, the programmer needs to trace that cell back to the exact rows in the source data that produced it. Without a systematic approach, this means re-reading code, reconstructing filter logic, and manually subsetting the data -- a tedious and error-prone process. tplyr2 solves this with cell-level metadata. When you build a table with `metadata = TRUE`, the package records the filter expressions that define every cell. You can then query any cell to inspect the filters or retrieve the source data rows directly. This is valuable for regulatory review, quality control, and powering interactive drill-down interfaces in Shiny applications. ## Building with Metadata Enabling metadata is a single argument to `tplyr_build()`: ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("SEX"), group_desc("AGE", settings = layer_settings( format_strings = list( "n" = f_str("xxx", "n"), "Mean (SD)" = f_str("xx.x (xx.xx)", "mean", "sd") ) ) ) ) ) result <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) ``` The returned data frame has two additions compared to a normal build: 1. A `row_id` column containing a unique identifier for each output row. 2. A `tplyr_meta` attribute -- a named list of metadata objects keyed by `"row_id||column"`. ## Row IDs Row IDs are constructed from the layer index and row label values. For a count layer with `target_var = "SEX"` in layer 1, the IDs are `1_F` and `1_M`. For a descriptive statistics layer in layer 2, they are `2_n` and `2_Mean (SD)`. ```{r} result$row_id ``` IDs are deterministic -- building the same spec against the same data always produces the same values. The `generate_row_ids()` function creates these identifiers and can also be called on any tplyr2 output, even one built without metadata. ## Inspecting Cell Metadata Once you have a row ID and a column name, `tplyr_meta_result()` returns the metadata object for that cell: ```{r} meta <- tplyr_meta_result(result, "1_F", "res1") meta ``` The `tplyr_meta` object contains: - **names**: Variable names relevant to this cell. - **filters**: A list of R call expressions that, combined with `&`, define the data subset. - **layer_index**: Which layer this cell belongs to. - **statistic**: Which statistic the cell displays -- the `stat_columns` name (e.g. `"n (%)"` or `"E"`). This is `NULL` for ordinary layers and populated for `stat_columns` layers, where several statistics share a row and the column alone identifies which one a cell shows (see below). - **anti_join**: `NULL` for normal cells, or a `tplyr_meta_anti_join` object for missing subjects rows. If the cell does not exist, the function returns `NULL`. If you try to access metadata on a result built without it, you get a clear error: ```{r, error=TRUE} result_no_meta <- tplyr_build(spec, tplyr_adsl, metadata = FALSE) tplyr_meta_result(result_no_meta, "1_F", "res1") ``` ## Getting Source Data The real power of metadata is in `tplyr_meta_subset()`. It evaluates the stored filters against the original data, returning the rows that produced a cell: ```{r} source_rows <- tplyr_meta_subset(result, "1_F", "res1", tplyr_adsl) nrow(source_rows) all(source_rows$SEX == "F") unique(source_rows$TRT01P) ``` The number of rows returned matches the count displayed in the cell. ## Metadata for Count Layers Count cells are defined by the intersection of the column variable level and the target variable level. When a `by` variable is present, it adds an additional filter: ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("DCDECOD", by = "EOSSTT") ) ) result <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) kable(head(result[, c("rowlabel1", "rowlabel2", "res1", "res2", "res3")])) ``` ```{r} rid <- result$row_id[1] meta <- tplyr_meta_result(result, rid, "res1") meta ``` ### Total Rows When `total_row = TRUE`, the total row's metadata omits the target variable filter, leaving only the column variable and any by-variables: ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("SEX", settings = layer_settings(total_row = TRUE)) ) ) result <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) total_row <- result[result$rowlabel1 == "Total", ] meta <- tplyr_meta_result(result, total_row$row_id, "res1") meta ``` ```{r} source_rows <- tplyr_meta_subset(result, total_row$row_id, "res1", tplyr_adsl) nrow(source_rows) ``` That holds for the default `total_row_count_missings = TRUE`, where the total counts every record in the column. Set it to `FALSE` and the total row's metadata gains the matching exclusions -- `!is.na()`, plus a not-in filter for any `missing_count$missing_values` -- so the subset stays exactly the rows the total counted. ## Metadata for Descriptive Statistics Layers Each row in a desc layer represents a statistic (n, mean, etc.), not a data category. All stat rows within the same column point to the same source data -- the observations on which those statistics were computed: ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_desc("AGE", settings = layer_settings( format_strings = list( "n" = f_str("xxx", "n"), "Mean (SD)" = f_str("xx.x (xx.xx)", "mean", "sd") ) ) ) ) ) result <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) n_meta <- tplyr_meta_result(result, "1_n", "res1") n_meta ``` The `names` field includes the target variable (`AGE`). You can verify the statistics by subsetting and computing them directly: ```{r} source_rows <- tplyr_meta_subset(result, "1_n", "res1", tplyr_adsl) c(n = nrow(source_rows), mean = mean(source_rows$AGE), sd = sd(source_rows$AGE)) ``` ## Where Filters in Metadata Both spec-level and layer-level `where` filters are captured in the metadata, so you can always see the full filtering chain: ```{r} spec <- tplyr_spec( cols = "TRT01P", where = SAFFL == "Y", layers = tplyr_layers( group_count("SEX") ) ) result <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) meta <- tplyr_meta_result(result, result$row_id[1], "res1") meta ``` The filter list includes `SAFFL == "Y"` alongside the column and target variable filters. The `names` field lists every variable in the subsetting logic. ## Anti-Join Metadata for Missing Subjects Some tables include a row for subjects in the population data but not in the analysis data. The metadata for these rows uses a special `anti_join` field: ```{r} target <- data.frame( TRT = c("A", "A", "B"), USUBJID = c("S1", "S2", "S3"), VAL = c("X", "Y", "X") ) pop <- data.frame( TRT = c("A", "A", "A", "B", "B"), USUBJID = c("S1", "S2", "S4", "S3", "S5") ) spec <- tplyr_spec( cols = "TRT", pop_data = pop_data(cols = "TRT"), layers = tplyr_layers( group_count("VAL", settings = layer_settings( distinct_by = "USUBJID", missing_subjects = TRUE, missing_subjects_label = "Not in Target" ) ) ) ) result <- tplyr_build(spec, target, pop_data = pop, metadata = TRUE) kable(result[, c("rowlabel1", "res1", "res2")]) ``` ```{r} ms_row <- result[result$rowlabel1 == "Not in Target", ] meta <- tplyr_meta_result(result, ms_row$row_id, "res1") meta ``` The `anti_join` contains a `join_meta` (filters for the population side) and `on` (the join key, typically `"USUBJID"`). When calling `tplyr_meta_subset()` on such a row, you must supply `pop_data`: ```{r} missing_a <- tplyr_meta_subset(result, ms_row$row_id, "res1", target, pop_data = pop) missing_a ``` Subject S4 is in the population for `TRT = "A"` but absent from the target, so the anti-join returns that one row. Omitting `pop_data` produces a warning: ```{r, warning=TRUE} tplyr_meta_subset(result, ms_row$row_id, "res1", target) ``` ## Stat Columns and the `statistic` Field When a count layer uses `stat_columns`, one row carries several statistics, each in its own result column. Ordinary layers leave `statistic` as `NULL`, but a `stat_columns` layer populates it so a cell's metadata records *which* statistic it displays -- the column alone would be ambiguous otherwise. ```{r stat-columns-meta} sc_spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01A")), layers = tplyr_layers( group_count("AEDECOD", settings = layer_settings( distinct_by = "USUBJID", stat_columns = list( "n (%)" = f_str("xxx (xx.x%)", "distinct_n", "distinct_pct"), "E" = f_str("xxx", "n") ) ) ) ) ) sc_result <- tplyr_build(sc_spec, tplyr_adae, pop_data = tplyr_adsl, metadata = TRUE) rid <- sc_result$row_id[1] # res1 is the "n (%)" (distinct-subject) column; res2 is the "E" (event) column tplyr_meta_result(sc_result, rid, "res1")$statistic tplyr_meta_result(sc_result, rid, "res2")$statistic ``` The `statistic` field lets a downstream consumer (a QC check, a Shiny drill-down) distinguish the distinct-subject cell from the event-count cell even though they share a row. ## Practical Applications The most common use of metadata is cell verification during QC: ```{r, eval=FALSE} result <- tplyr_build(spec, data, metadata = TRUE) source <- tplyr_meta_subset(result, row_id = "1_F", column = "res2", data = data) nrow(source) ``` The metadata system also lends itself to Shiny applications where clicking a cell triggers `tplyr_meta_subset()`, displaying the source records in a detail panel: ```{r, eval=FALSE} observeEvent(input$table_cell_click, { click <- input$table_cell_click row_id <- result$row_id[click$row] col_name <- names(result)[click$col] source_data <- tplyr_meta_subset(result, row_id, col_name, original_data) output$detail_table <- renderTable(source_data) }) ``` ## Summary | Function | Purpose | |---|---| | `tplyr_build(..., metadata = TRUE)` | Build with metadata enabled | | `generate_row_ids(result)` | Create row identifiers | | `tplyr_meta_result(result, row_id, column)` | Inspect filter expressions for a cell | | `tplyr_meta_subset(result, row_id, column, data)` | Retrieve source data rows | Every cell carries its own filter expressions -- column variable, target variable, by-variable, where clauses, and anti-join logic for missing subjects. These expressions are stored at build time but only evaluated when you request a subset, keeping the metadata lightweight while providing exact reproducibility of every number in your table. ## See Also - `vignette("ard")` -- exporting the raw numeric results as Analysis Results Data. - `vignette("serialization")` -- saving the spec so a table (and its metadata) can be regenerated later. - `vignette("count")` -- the layer types whose cells the metadata traces back to source.