--- title: "Programming with tabxplor" description: > Programming with the fmt cell — know what a table is, reach any field or attribute, and write your own pipelines. output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Programming with tabxplor} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} # Messages and warnings are off for every chunk: the teaching notes tabxplor prints (an # auto-detected family, an over-dispersion caveat) are explained in the prose where they # matter, and repeated under every table they only clutter it. Re-enable one with # `message = TRUE` on the chunk that needs it. knitr::opts_chunk$set(collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE) ``` ```{r setup} library(tabxplor) # Pin the legend language: it defaults to "auto" = the ambient locale, so building this English # vignette on a French machine silently renders French legends and captions (the -fr articles pin # "fr" for the same reason). Output must not depend on where it is built. options(tabxplor.lang = "en") # The shape table a continuous predictor draws under the footer is not this vignette's subject. options(tabxplor.shape_table = "no") Sys.setenv(LANGUAGE = "en") # the test-summary / model-fit row labels go through gettext, not this option library(dplyr) # Tables render as tabxplor's real html tables (the recommended everyday setting); the shared # stylesheet is emitted once by tab_css() below, and the hover tooltips are kept off here. options(tabxplor.print = "html") options(tabxplor.tab_kable_css = FALSE) options(tabxplor.tab_kable_tooltips = FALSE) # Console outputs (vectors, fields...) keep their terminal colors, turned to html by fansi. options(cli.num_colors = 256) set_color_palette(theme = "light") ``` ```{r, echo = FALSE, results = "asis"} # The website carries a light/dark switch and tab_css("auto") follows it; a shipped vignette # is always read on a light page, so there it stays light. cat(tab_css(theme = if (Sys.getenv("IN_PKGDOWN") == "true") "auto" else "light")) ``` ```{r, echo = FALSE, include = FALSE} # Colour the console outputs (ANSI -> html, via fansi), but hand as-is results (the html tables, # marked by knitr with an ASIS token) back to knitr's default hook untouched. # Escape the three HTML specials before fansi turns the ANSI codes into markup. esc_html <- function(x) gsub(">", ">", gsub("<", "<", gsub("&", "&", x, fixed = TRUE), fixed = TRUE), fixed = TRUE) # fansi is Suggests-only, so the ANSI -> html step degrades: without it the escape codes are # stripped and the output is handed on uncoloured, which is what a check run with no Suggests gets. ansi_html <- if (requireNamespace("fansi", quietly = TRUE)) { function(x) fansi::sgr_to_html(x = esc_html(x), warn = FALSE) } else { function(x) esc_html(gsub("\033\\[[0-9;]*m", "", x)) } default_output_hook <- knitr::knit_hooks$get("output") knitr::knit_hooks$set(output = function(x, options) { if (grepl("KNITR_ASIS_OUTPUT_TOKEN", x, fixed = TRUE)) return(default_output_hook(x, options)) paste0('
',
ansi_html(x),
'')
})
# A cli message or warning is its own kind of condition, so knitr routes each through its own hook,
# not `output`: without these two it would land in the collapsed source block, ANSI codes and all.
for (hook in c("message", "warning")) {
knitr::knit_hooks$set(stats::setNames(list(function(x, options) {
paste0('',
ansi_html(x),
'')
}), hook))
}
```
*Une version française de ce document est disponible : [Programmer avec tabxplor](https://bricenocenti.github.io/tabxplor/articles/tabxplor-programming-fr.html).*
This vignette is for users who want to **program with** `tabxplor` — write their own helpers, pull the underlying numbers out, or reshape a table cell by cell. If you only want to make and read tables, `vignette("tabxplor")` is the place to start.
Every numeric column of a `tabxplor` table is a single vector of class **`tabxplor_fmt`** ("formatted number"). It is a [`vctrs` record](https://vctrs.r-lib.org/reference/new_rcrd.html): behind the one value you see printed, each cell stores **all the data needed to compute the displayed number, its format and its colour** — counts, percentages, differences, relative risks, confidence-interval bounds, odds-ratios, and so on. Because it is a proper vector, it survives every `dplyr` verb, and you can read or rewrite any of its fields.
```{r}
gss_simple <- gss_cat_data_formatting()
tabs <- tab(gss_simple, race, marital, pct = "row", color = "difference")
```
## Getting the plain numbers out
The quickest way to recover the underlying numbers as ordinary numeric vectors is `get_num()`, which extracts whatever field is currently displayed:
```{r}
tabs |> mutate(across(where(is_fmt), get_num))
```
To get the character strings instead (formatted, but without colours), use `format()`:
```{r, eval = FALSE}
tabs |> mutate(across(where(is_fmt), format))
```
Individual fields are read most simply with `$` on the fmt column (see "Reading and changing fields" below):
```{r}
tabs$Married$pct
```
To hand the whole table to base R --- a correspondence analysis, `chisq.test()`, `mosaicplot()` --- `as.matrix()` and `as.table()` do it in one call. They keep only the data cells: the total row, the total columns and the display-time rows (the base count, `add_pct`, the p-values) go, because an analysis run on a table's own margins is wrong. Pass `totals = TRUE` to keep them.
```{r}
as.matrix(tab(gss_simple, race, marital))
```
## The fields of a cell
A `tabxplor_fmt` cell carries **21 fields** (many are `NA` when the relevant quantity was not requested). The user-facing ones are:
| Field | Meaning |
|:------|:--------|
| `n` | unweighted count (integer) |
| `wn` | weighted count |
| `pct` | percentage |
| `mean` | mean (numeric column variables) |
| `diff` | difference from the total / reference cell |
| `ratio` | ratio to the reference (relative risk, or a ratio of means) |
| `ci_inf`, `ci_sup` | confidence-interval bounds |
| `pvalue` | per-cell significance p-value (feeds the stars) |
| `or` | odds ratio / relative-risk ratio |
| `ctr` | contribution to the chi-squared (`color = "contrib"`) |
| `var` | variance (numeric columns; chi-squared variance with `pct`) |
| `tot_n` | the cell's own base — the count its percentage is computed on |
| `n_eff` | effective sample size used for the CI, with `options(tabxplor.design_effect = TRUE)` or a `survey` design |
| `obs` | `tab_reg()` only: the observed (crude) effect the modelled one is compared to |
| `gap_se` | `tab_reg()` only: the standard error of the gap between the estimate and `obs` |
| `digits` | number of decimals to display (per cell) |
| `display` | which field is shown (per cell) |
| `row_kind` | what kind of row the cell sits in: `"data"`, `"total"`, or one of the synthetic display rows `"n"` / `"pct"` / `"pvalue"` / `"gof"` / `"blank"` |
| `in_tottab`, `in_refrow` | is the cell in a total table / a reference row (logical) |
(`row_kind` replaced a logical `in_totrow` field in 2.0.0. `x$in_totrow` still returns that logical, and `is_totrow()` / `as_totrow()` are unchanged.)
See the whole underlying data frame of a column with `vctrs::vec_data()`:
```{r}
vctrs::vec_data(tabs$Married)
```
## Reading and changing fields
Read a field with `$` (the friendliest way), or `vctrs::field()`:
```{r, eval = FALSE}
tabs$Married$pct
tabs |> mutate(across(where(is_fmt), ~ .$pct))
tabs |> mutate(across(where(is_fmt), ~ vctrs::field(., "pct")))
```
Confidence intervals are stored as their two bounds, the `ci_inf` and `ci_sup` fields:
```{r}
ci_tab <- tab(gss_simple, race, marital, pct = "row", ci = "cell")
ci_tab$Married$ci_inf
ci_tab$Married$ci_sup
```
Switch the displayed field with `set_display()`:
```{r, eval = FALSE}
tabs |> set_display("diff")
tabs |> mutate(across(where(is_fmt), ~ set_display(., "diff")))
```
To change a field, the easiest route is `dplyr::mutate()` **on the fmt vector itself** — a `tabxplor_fmt` behaves like a little data frame of its fields. For example, round every cell to two decimals:
```{r, eval = FALSE}
tabs |> mutate(across(where(is_fmt), ~ mutate(., digits = 2L)))
```
A fuller example: turn a means table's variance into a standard deviation and add it as a new, un-coloured column shown to one decimal:
```{r, eval = FALSE}
tab(gss_simple, race, c(age, tvhours), digits = 1L) |>
mutate(across(
c(age, tvhours),
~ mutate(., var = sqrt(var), display = "var", digits = 1L) |> set_color("no"),
.names = "{.col}_sd"
))
```
## Composite display: combining fields
`set_display()` chooses *one* field to show. To show **several fields in one cell**, give it a `{}` **template** instead of a bare field name — the same template you can pass to `tab(display =)` when you build the table. The template is plain text with `{field}` tokens; each token is replaced by that field, formatted as usual:
```{r}
tabs |> set_display("{pct} ({diff})")
```
The rules:
- **Valid tokens** are `pct`, `n`, `wn`, `mean`, `est`, `base`, `diff`, `ratio`, `ci`, `moe`, `or`, `ctr`, `var`, `sd`, `cv`, `coef`, `resid`, `obs`, `gap` — the same set as the single-field displays. Most name a stored field; a few are **derived** and so read-only — `resid` (from the p-value and the sign of `ctr`), `sd` and `cv` from `var`, `gap` from `est` and `obs`.
- Two of them are **scale-relative**, and are what makes one template work on every table: `{est}` is whatever the column estimates (a percentage, a difference, an odds ratio) and `{base}` the level it sits on (a percentage, a mean, a count). `"{est} ({base})"` reads the same on a crosstab and on a regression.
- Common layouts have **names**: `"est"`, `"est_ci"`, `"est_base"`, `"base_est"`, `"base"`, `"base_ci"`, `"base_moe"`, `"mean_sd"`, `"mean_cv"`. They are the same names in `tab()`, in `tab_reg()` and in `set_display()`.
- The first token **outside brackets** is the *primary* one: it is what `get_num()` returns, the value Excel keeps, and the only part the colours paint. A template with no token outside brackets has no primary, and renders whole as an aside.
- A token may carry **its own precision** — `"{est} ({base:1})"` — which beats the column's `digits`.
- A **bare field name** is shorthand for its own template, so `set_display("ci")` is exactly `set_display("{ci}")`.
- It is a **display overlay** for text output (the console, `tab_html()`, `tab_md()`): the stored fields are untouched, so you can switch back at any time.
- The `{ci}` field already prints its own `[…;…]` brackets, so write `"{pct} {ci}"` — **not** `"{pct} [{ci}]"`, which would double them.
## Creating a column from another field
Because switching the display recomputes nothing — every field is already stored in the cell — you can spin off a **new column that shows a different field**. A percentage table has a reference by default (the Total row), so its `diff` field is already filled; a difference twin of every percentage column is then one `mutate()`:
```{r}
tab(gss_simple, race, marital, pct = "row") |>
mutate(across(where(is_fmt), ~ set_display(., "diff"), .names = "{.col}_diff"))
```
`.names = "{.col}_diff"` keeps the original percentage columns and adds a `