---
title: "Table Properties"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Table Properties}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
library(tplyr2)
library(knitr)
```

## Introduction

In tplyr2, a table is defined by its **specification**. The `tplyr_spec()`
function captures the full configuration -- column variables, filters, treatment
groups, population data, and layers -- as a pure description of what you want.
No data processing happens until you call `tplyr_build()`. This vignette covers
the spec-level parameters that control the overall structure of your table.

Every tplyr2 workflow follows two steps: **define** a spec with `tplyr_spec()`,
then **build** the table with `tplyr_build(spec, data)`. Let's look at an
example using the included `tplyr_adsl` dataset.

```{r}
spec <- tplyr_spec(
  cols = "TRT01P",
  layers = tplyr_layers(
    group_count(target_var = "SEX")
  )
)

result <- tplyr_build(spec, tplyr_adsl)
kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))])
```

Note how the `cols` parameter defines the column structure of the output. Each
unique value of `TRT01P` becomes a result column, and the column labels
automatically include the group count as `(N=xx)`.

## Column Variables

The `cols` parameter accepts a character vector of one or more variable names
that define the columns of your output table. The most common case is a single
treatment variable:

```{r}
spec <- tplyr_spec(
  cols = "TRT01P",
  layers = tplyr_layers(
    group_count(target_var = "AGEGR1")
  )
)

result <- tplyr_build(spec, tplyr_adsl)
kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))])
```

### Multiple Column Variables

When you provide multiple variables, tplyr2 creates a cross of all combinations.
This is useful when you need columns split by treatment and another variable.

```{r}
spec <- tplyr_spec(
  cols = c("TRT01P", "SEX"),
  layers = tplyr_layers(
    group_count(target_var = "AGEGR1")
  )
)

result <- tplyr_build(spec, tplyr_adsl)
kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))])
```

Notice that the column labels use a `" | "` separator to show the cross of
treatment and sex, and each combination gets its own N.

## Table-Level Filtering with `where`

The `where` parameter applies a filter to all data before any layer processing
begins. This is useful when records should be excluded from the entire table.

```{r}
spec <- tplyr_spec(
  cols = "TRT01P",
  where = SAFFL == "Y",
  layers = tplyr_layers(
    group_count(target_var = "AGEGR1", by = "Age Group"),
    group_desc(
      target_var = "AGE",
      by = "Age (Years)",
      settings = layer_settings(
        format_strings = list(
          "n"         = f_str("xxx", "n"),
          "Mean (SD)" = f_str("xx.x (xx.xx)", "mean", "sd"),
          "Median"    = f_str("xx.x", "median"),
          "Min, Max"  = f_str("xx, xx", "min", "max")
        )
      )
    )
  )
)

result <- tplyr_build(spec, tplyr_adsl)
kable(result[, c("rowlabel1", "rowlabel2", grep("^res", names(result), value = TRUE))])
```

Both the count and descriptive statistics layers are computed on the safety
population. Individual layers can also have their own `where` filters, which
are applied in addition to the table-level filter.

## Treatment Groups

Clinical tables often need columns beyond the individual treatment arms. tplyr2
provides **total groups** and **custom groups** for this purpose.

### Total Groups

A total group creates a synthetic column that includes all subjects by
duplicating every row with the column variable set to the total group label.

```{r}
spec <- tplyr_spec(
  cols = "TRT01P",
  total_groups = list(
    total_group("TRT01P", label = "Total")
  ),
  layers = tplyr_layers(
    group_count(target_var = "SEX")
  )
)

result <- tplyr_build(spec, tplyr_adsl)
kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))])
```

The "Total" column now appears alongside the individual treatment arms, with
its N reflecting the sum of all subjects.

### Custom Groups

Custom groups combine specific treatment levels into a new group. For example,
you might pool the two active dose groups together.

```{r}
spec <- tplyr_spec(
  cols = "TRT01P",
  custom_groups = list(
    custom_group(
      "TRT01P",
      "Xanomeline" = c("Xanomeline High Dose", "Xanomeline Low Dose")
    )
  ),
  layers = tplyr_layers(
    group_count(target_var = "SEX")
  )
)

result <- tplyr_build(spec, tplyr_adsl)
kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))])
```

The "Xanomeline" column includes all subjects from both dose groups, while the
original dose-level columns are preserved.

### Combining Total and Custom Groups

You can use both together. Each synthetic column is built by duplicating the rows
it needs: a custom group copies the rows of the levels it pools, and a total group
copies the original rows. The total group deliberately ignores the custom group's
copies, so every subject is counted exactly once in the "Total" column -- its N
matches the study population, not the population plus the pooled arms.

```{r}
spec <- tplyr_spec(
  cols = "TRT01P",
  custom_groups = list(
    custom_group(
      "TRT01P",
      "Xanomeline" = c("Xanomeline High Dose", "Xanomeline Low Dose")
    )
  ),
  total_groups = list(
    total_group("TRT01P", label = "Total")
  ),
  layers = tplyr_layers(
    group_count(target_var = "SEX")
  )
)

result <- tplyr_build(spec, tplyr_adsl)
kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))])
```

## Population Data

In many clinical analyses, denominators and header Ns should come from a
different dataset than the analysis data. The classic example is an adverse
event table: `ADAE` only contains subjects who experienced events, but
percentages should reflect the full safety population from `ADSL`.

The `pop_data()` configuration specifies how the population dataset maps to the
spec. The actual data is provided at build time.

```{r}
spec <- tplyr_spec(
  cols = "TRTA",
  pop_data = pop_data(cols = c("TRTA" = "TRT01A")),
  layers = tplyr_layers(
    group_count(
      target_var = "AEBODSYS",
      settings = layer_settings(
        distinct_by = "USUBJID"
      )
    )
  )
)

result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl)
kable(head(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))], 8))
```

A few things to note:

- `cols = "TRTA"` matches the treatment variable in `ADAE`.
- `pop_data(cols = c("TRTA" = "TRT01A"))` maps `TRT01A` in the population data
  to `TRTA` in the analysis data (format: `c("analysis_name" = "pop_name")`).
- `distinct_by = "USUBJID"` counts each subject once per body system.
- Denominators and column Ns come from the full `tplyr_adsl` population.

### Extracting Header N

After building a table with population data, you can extract the header N values
using `tplyr_header_n()`:

```{r}
header_n <- tplyr_header_n(result)
kable(header_n)
```

This is useful when you need to programmatically construct column headers or
integrate with other reporting tools.

### Population Data with Filters

The population data is **not** subject to the spec-level `where` filter. It uses
its own `where` clause, specified in the `pop_data()` call:

```{r}
spec <- tplyr_spec(
  cols = "TRTA",
  pop_data = pop_data(
    cols = c("TRTA" = "TRT01A"),
    where = SAFFL == "Y"
  ),
  layers = tplyr_layers(
    group_count(
      target_var = "AEBODSYS",
      settings = layer_settings(
        distinct_by = "USUBJID"
      )
    )
  )
)

result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl)
kable(tplyr_header_n(result))
```

This separation is intentional. The table-level `where` controls which records
are summarized, while `pop_data` `where` controls which subjects contribute to
denominators. In practice these often differ -- you might filter AE records to
treatment-emergent events while basing denominators on the full safety population.

## Data Completion

When building count layers, tplyr2 automatically completes all combinations of
factor levels and cross-variables. If a treatment group has zero subjects with a
given characteristic, a `0 (0.0%)` row still appears rather than being dropped.

```{r}
spec <- tplyr_spec(
  cols = "TRT01P",
  layers = tplyr_layers(
    group_count(target_var = "RACE")
  )
)

result <- tplyr_build(spec, tplyr_adsl)
kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))])
```

Every race category appears for every treatment group, even when the count is
zero.

### Limiting Completion with `limit_data_by`

Sometimes completing all combinations is too aggressive. The `limit_data_by`
parameter in `layer_settings()` restricts the completion grid to combinations
that actually exist in the data. This is essential for AE tables where preferred
terms should only appear under their actual body system:

```{r}
spec <- tplyr_spec(
  cols = "TRTA",
  pop_data = pop_data(cols = c("TRTA" = "TRT01A")),
  layers = tplyr_layers(
    group_count(
      target_var = "AEDECOD",
      by = "AEBODSYS",
      settings = layer_settings(
        distinct_by = "USUBJID",
        limit_data_by = c("AEBODSYS", "AEDECOD")
      )
    )
  )
)

result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl)
kable(head(result[, c("rowlabel1", "rowlabel2", grep("^res", names(result), value = TRUE))], 10))
```

With `limit_data_by = c("AEBODSYS", "AEDECOD")`, tplyr2 only creates rows for
body system/preferred term combinations that exist in the data, while still
filling in zeros for treatment groups with no events for a given combination.

## Where to Go From Here

This vignette covered the table-level properties that control the overall
structure of your tplyr2 output. For details on specific layer types and
additional features, see:

- `vignette("count")` -- `group_count()` for frequency tables, including nested
  counts, distinct subject counts, population data, and missing value handling.
- `vignette("denom")` -- denominator control in depth (this vignette and the
  denominators vignette both cover `pop_data`; that one goes deeper on
  `denoms_by`, `denom_where`, and confidence intervals).
- `vignette("adverse-events")` -- a full adverse event table built end to end,
  the canonical use of population data.
- `vignette("desc")` -- `group_desc()` for summary statistics.
- `vignette("shift")` -- `group_shift()` for baseline-by-post-baseline
  cross-tabulations.
- `vignette("sort")` -- how tplyr2 orders rows.
- `vignette("options")` -- package-level options via `tplyr2_options()`.
