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")Une version française de ce document est disponible : Programmer avec tabxplor.
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: 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.
gss_simple <- gss_cat_data_formatting()
tabs <- tab(gss_simple, race, marital, pct = "row", color = "difference")The quickest way to recover the underlying numbers as ordinary
numeric vectors is get_num(), which extracts whatever field
is currently displayed:
| race | Married | Separated | Divorced | Widowed | Never married | NA | Total |
|---|---|---|---|---|---|---|---|
| White | 0.507227813357731 | 0.0266544678255566 | 0.163220494053065 | 0.0899664531869472 | 0.212137846904544 | 0.000792924672156145 | 1 |
| Black | 0.277724512623841 | 0.0626398210290828 | 0.158197507190796 | 0.0837328219878555 | 0.417066155321189 | 0.000639181847235539 | 1 |
| Other | 0.475752935171006 | 0.0561510974987238 | 0.108218478815722 | 0.035732516590097 | 0.32312404287902 | 0.00102092904543134 | 1 |
| Total | 0.470930503188568 | 0.0345854861983894 | 0.157473351021738 | 0.0841130195968906 | 0.252106316622446 | 0.000791323371968533 | 1 |
To get the character strings instead (formatted, but without
colours), use format():
Individual fields are read most simply with $ on the fmt
column (see “Reading and changing fields” below):
#> [1] 0.5072278 0.2777245 0.4757529 0.4709305
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.
#> Married Separated Divorced Widowed Never married NA
#> White 8316 437 2676 1475 3478 13
#> Black 869 196 495 262 1305 2
#> Other 932 110 212 70 633 2
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():
#> n display digits wn pct mean diff ratio ctr var ci_inf
#> 1 8316 pct 0 NA 0.5072278 NA 0.036297310 1.0770757 NA NA NA
#> 2 869 pct 0 NA 0.2777245 NA -0.193205991 0.5897357 NA NA NA
#> 3 932 pct 0 NA 0.4757529 NA 0.004822432 1.0102402 NA NA NA
#> 4 10117 pct 0 NA 0.4709305 NA 0.000000000 1.0000000 NA NA NA
#> ci_sup pvalue or tot_n n_eff obs gap_se row_kind in_tottab in_refrow
#> 1 NA NA 1 16395 NA NA NA data FALSE FALSE
#> 2 NA NA 1 3129 NA NA NA data FALSE FALSE
#> 3 NA NA 1 1959 NA NA NA data FALSE FALSE
#> 4 NA NA 1 21483 NA NA NA total FALSE FALSE
Read a field with $ (the friendliest way), or
vctrs::field():
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:
#> [1] 0.4995743 0.2623114 0.4537069 0.4642615
#> [1] 0.5148780 0.2936827 0.4978939 0.4776099
Switch the displayed field with set_display():
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:
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:
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:
| marital | |||||||
|---|---|---|---|---|---|---|---|
| race | Married | Separated | Divorced | Widowed | Never married | NA | Total |
| <row% (diff)> | <row% (n)> | ||||||
| White | 51% ( +4%) | 3% (-1%) | 16% (+1%) | 9% (+1%) | 21% ( -4%) | 0% (+0%) | 100% (16 395) |
| Black | 28% (-19%) | 6% (+3%) | 16% (+0%) | 8% (+0%) | 42% (+16%) | 0% (+0%) | 100% ( 3 129) |
| Other | 48% ( +0%) | 6% (+2%) | 11% (-5%) | 4% (-5%) | 32% ( +7%) | 0% (+0%) | 100% ( 1 959) |
| Total | 47% ( 0%) | 3% ( 0%) | 16% ( 0%) | 8% ( 0%) | 25% ( 0%) | 0% ( 0%) | 100% (21 483) |
|
Percentage points (risk) difference: cell ≥ the Total row +5; +10; +20; +30 points; cell ≤ the Total row -5; -10; -20; -30 points.
|
|||||||
The rules:
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.{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."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().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."{est} ({base:1})" — which beats the column’s
digits.set_display("ci") is exactly
set_display("{ci}").tab_html(), tab_md()): the stored
fields are untouched, so you can switch back at any time.{ci} field already prints its own
[…;…] brackets, so write "{pct} {ci}" —
not "{pct} [{ci}]", which would double
them.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():
tab(gss_simple, race, marital, pct = "row") |>
mutate(across(where(is_fmt), ~ set_display(., "diff"), .names = "{.col}_diff"))| marital | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| race | Married | Separated | Divorced | Widowed | Never married | NA | Total | Married_diff | Separated_diff | Divorced_diff | Widowed_diff |
Never married_ diff |
NA_diff | Total_diff |
| <row%> | <n> | <row%-diff> | <n> | |||||||||||
| White | 51% | 3% | 16% | 9% | 21% | 0% | (16 395) | +4% | -1% | +1% | +1% | -4% | +0% | (16 395) |
| Black | 28% | 6% | 16% | 8% | 42% | 0% | ( 3 129) | -19% | +3% | +0% | +0% | +16% | +0% | ( 3 129) |
| Other | 48% | 6% | 11% | 4% | 32% | 0% | ( 1 959) | +0% | +2% | -5% | -5% | +7% | +0% | ( 1 959) |
| Total | 47% | 3% | 16% | 8% | 25% | 0% | (21 483) | ref:47% | ref:3% | ref:16% | ref:8% | ref:25% | ref:0% | (21 483) |
.names = "{.col}_diff" keeps the original percentage
columns and adds a <name>_diff twin next to each. (If
a diff column comes out empty, the source table had no
reference cell — build it with ref = / comp =,
or with color = "difference".)
The same recipe puts an adjusted prediction beside a
regression effect. Two extra calls make the copy purely
descriptive: set_color("") drops its colour (the
ladder belongs to the effect, and the same ladder twice is noise), and
set_pvalue(NA_real_) drops its stars — the stored p-value
is their only source, so erasing it is the direct way to say “this copy
tests nothing”:
Helper predicates let you act on structural parts of a table:
is_totrow() / is_tottab() /
is_refrow() (cell-level, logical vectors), and
is_totcol() / is_refcol() (column-level). They
are what keeps totals in place when you re-order or when you write
conditional formatting:
Besides its per-cell fields, each fmt column carries a
few column-level attributes, read and set with
get_* / set_* (or is_* /
as_* for the logical ones):
scale — get_scale() /
set_scale(): what the column estimates. A
level ("level_n", "level_pct",
"level_mean"), a difference ("points" for
percentage points, "mean_diff", "raw_diff" for
a regression coefficient in the outcome’s own units), a ratio
("pct_ratio", "mean_ratio",
"odds_ratio") or a link-scale coefficient
("log_coef"). It says which field holds the estimate, what
its null value is, and which colour ladder it is read on.pct_type — get_pct_type() /
set_pct_type(): for a percentage, what it is a percentage
of — "row", "col",
"all", "all_tabs", or "none" for
counts, means and coefficients.ci_method — get_ci_method(): which
interval engine built this column’s bounds ("wilson",
"newcombe", "welch", "katz", …;
"" when the column carries no interval).color — get_color() /
set_color(): the colour measure of the column —
"", "no", or one of the measure names
"difference", "ratio",
"odds_ratio", "contrib" (plus
"adjustment" / "between_groups" on a
tab_reg() table). The discipline’s acronyms
("diff", "OR", "or",
"RR", "RD") are permanent shorthands you may
type, but a built table always stores — and its legend always
names — the full word.col_var — get_col_var(): the name of the
column variable (a table can hold several).col_group — get_col_group(): which
sub-population the column’s block belongs to, after a
spread_vars / tab_spread() pivot or a
tab_reg(tab_vars =) split ("" otherwise).
Together with col_var it identifies a column
block: two blocks can show the same variable for two
sub-populations, and exports head them on two lines.comp_all — get_comp_all(): with
tab_vars, is the comparison reference the sub-table
(FALSE) or the whole table (TRUE)?totcol / refcol — is_totcol()
/ is_refcol(): is this a total column / a reference
column?Generic code cannot assume the shape of the table it is handed. Three accessors answer that, so a function can branch on facts rather than guess from column names.
tab_structure() says what the object is
— a crosstab or a regression table, merged or not, grouped or not, and
which variables sit on each axis:
#> $container
#> [1] "table"
#>
#> $kind
#> [1] "crosstab"
#>
#> $merged
#> [1] FALSE
#>
#> $grouped
#> [1] FALSE
#>
#> $row_vars
#> [1] "race"
#>
#> $tab_vars
#> character(0)
#>
#> $col_vars
#> [1] "marital"
#>
#> $same_col_vars
#> [1] TRUE
#>
#> $same_tab_vars
#> [1] TRUE
tab_columns() is the column-level view: one row per
column, with everything the exporters and the colour engine read — what
it estimates, its reference, its confidence-interval method, its
role:
#> # A tibble: 7 × 6
#> column scale pct_type ref ci_method totcol
#> <chr> <chr> <chr> <chr> <chr> <lgl>
#> 1 Married points row tot newcombe FALSE
#> 2 Separated points row tot newcombe FALSE
#> 3 Divorced points row tot newcombe FALSE
#> 4 Widowed points row tot newcombe FALSE
#> 5 Never married points row tot newcombe FALSE
#> 6 NA points row tot newcombe FALSE
#> 7 Total points row tot newcombe TRUE
And fmt_attr() reads or writes any one of those
facts by name, which is the programmatic twin of the
get_* / set_* family above — useful when the
attribute is itself a variable:
#> [1] "points"
#> [1] "newcombe"
On a regression table, reg_measures(data, outcome) plays
the same role for the model: it lists the effect ×
measure combinations that outcome can be asked for, and why
the others are refused.
fmt() constructs an fmt vector directly
from numbers — handy for tests, or to add a hand-computed column.
Everything the display and colour engine needs can be supplied:
#> <fmt-%[3]>
#> [1] 10% 20% 70%
Sometimes the data already arrives cross-tabulated —
a counts table from a report, a table(), a matrix of
frequencies. tab_counts() builds the same coloured
tabxplor table as tab(), but from those counts
instead of the raw records; every calculation runs on the counts, so the
result is identical to what tab() would have produced from
individual records.
The commonest input is a tidy counts data frame (one row per combination, the count in a column):
counts <- dplyr::count(gss_simple, marital, race)
tab_counts(counts, marital, race, counts = n, pct = "row", color = TRUE)| race | ||||
|---|---|---|---|---|
| marital | White | Black | Other | Total |
| <row%> | <row% (n)> | |||
| Married | 82% | 9% | 9% | 100% (10 117) |
| Separated | 59% | 26% | 15% | 100% ( 743) |
| Divorced | 79% | 15% | 6% | 100% ( 3 383) |
| Widowed | 82% | 14% | 4% | 100% ( 1 807) |
| Never married | 64% | 24% | 12% | 100% ( 5 416) |
| NA | 76% | 12% | 12% | 100% ( 17) |
| Total | 76% | 15% | 9% | 100% (21 483) |
|
Percentage points (risk) difference: cell ≥ the Total row +5; +10; +20; +30 points; cell ≤ the Total row -5; -10; -20; -30 points. Background colour, relative
risk (ratio): cell ≥ the Total row ×1.5;
×2;
cell ≤ the Total row ÷2;
÷4.
|
||||
It also melts a table() / xtabs() / matrix
automatically, and reads a wide frame (one column per
column-variable level) via cols = /
col_name =:
tab_counts(table(gss_simple$marital, gss_simple$race), pct = "row", color = "difference")
wide <- tidyr::pivot_wider(counts, names_from = race, values_from = n)
tab_counts(wide, row_var = marital, cols = c(White, Black, Other),
col_name = "race", pct = "row", color = "difference")Weighting works as in tab(): pass the unweighted count
in counts and the weighted count in wt_counts
— estimates use the weighted count, confidence intervals and Chi-2 use
the unweighted N. (options(tabxplor.design_effect = TRUE)
needs the individual weights, which pre-aggregated counts no longer
carry, so it does not apply here — the CIs use the unweighted N, and the
table’s footer says so rather than claiming a correction it does not
have.) When the only figures available are non-whole numbers
(percentages × a base, or weighted-only counts), CIs and the Chi-2 test
are disabled with a message.
A grouped table (built with tab_vars)
stacks one sub-table per group. tab_spread() pivots a
grouping variable’s levels into side-by-side columns —
ideal to compare one measure across groups. Select the measure column
you want first, then spread:
tab(gss_simple, relig, marital, year, pct = "row", totaltab = "no", tot = "row") |>
dplyr::select(year, relig, Married) |>
tab_spread(year)| marital | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| relig | 2000 | 2002 | 2004 | 2006 | 2008 | 2010 | 2012 | 2014 | 2000 | 2002 | 2004 | 2006 | 2008 | 2010 | 2012 | 2014 |
| <row%> | <row%> | <row%> | <row%> | <row%> | <row%> | <row%> | <row%> | <n> | <n> | <n> | <n> | <n> | <n> | <n> | <n> | |
| 1-Protestant | 47% | 48% | 54% | 50% | 51% | 47% | 50% | 49% | 1 521 | 1 460 | 1 483 | 2 328 | 1 040 | 973 | 916 | 1 125 |
| 2-Catholic | 47% | 49% | 57% | 53% | 49% | 44% | 47% | 47% | 679 | 673 | 656 | 1 114 | 470 | 482 | 444 | 606 |
| 3-Other christian | 47% | 41% | 51% | 38% | 59% | 39% | 42% | 43% | 51 | 88 | 84 | 116 | 78 | 98 | 126 | 143 |
| 4-Jewish | 49% | 44% | 53% | 51% | 44% | 54% | 64% | 50% | 63 | 48 | 55 | 78 | 39 | 37 | 28 | 40 |
| 5-Buddhist/Hinduist | 40% | 41% | 65% | 50% | 59% | 50% | 60% | 51% | 25 | 27 | 26 | 42 | 22 | 22 | 15 | 39 |
| 6-Muslim | 42% | 38% | 69% | 71% | 31% | 36% | 62% | 67% | 12 | 13 | 16 | 17 | 13 | 11 | 13 | 9 |
| 7-Other | 30% | 48% | 43% | 30% | 25% | 33% | 42% | 39% | 64 | 58 | 77 | 50 | 20 | 45 | 38 | 36 |
| 8-None | 38% | 33% | 40% | 37% | 38% | 36% | 32% | 37% | 398 | 379 | 403 | 739 | 332 | 363 | 387 | 522 |
| NA | 75% | 37% | 67% | 38% | 67% | 31% | 29% | 50% | 4 | 19 | 12 | 26 | 9 | 13 | 7 | 18 |
| Total | 45% | 46% | 53% | 48% | 48% | 44% | 46% | 46% | 2 817 | 2 765 | 2 812 | 4 510 | 2 023 | 2 044 | 1 974 | 2 538 |
tab(..., spread_vars = year) does the same in one call.
tab_reg(..., tab_vars =) produces a grouped table you can
spread the same way — see vignette("tabxplor-reg").
tab() accepts vectors of arguments to
build several differently-shaped tables in one call
(output_list = TRUE keeps them as a list rather than
merging them). For a fully data-driven batch, purrr::pmap()
over a small specification table is the idiom — one row per table, one
column per argument:
Beyond the everyday defaults (vignette("tabxplor") →
Global R options), these options() tune the
exporters and the build. HTML / tab_html():
tabxplor.tab_kable_css — inline the stylesheet with
each table (TRUE); set FALSE in a many-table
document and call tab_css() once instead (this vignette
does).tabxplor.tab_kable_tooltips — the per-cell hover
tooltips (TRUE); set FALSE once per document
to switch them off everywhere.tabxplor.tab_kable_num_font — the number CSS font stack
(everything else is tab_css()’s business).tabxplor.kable_popover — click popovers instead of
hover tooltips.Excel / tab_export("xl"):
tabxplor.xl_font_text /
tabxplor.xl_font_num /
tabxplor.xl_font_num_stars — the label, number and
starred-number fonts.tabxplor.xl_ratio_cells — how a multiplicative cell
reaches Excel: "fold" (default, the signed fold as a
number), "raw" (the untransformed ratio) or
"text" (the exact display string).Console, stats and paths:
tabxplor.console_bold — embolden reference / total /
coloured cells (auto-detected per editor).tabxplor.stars — the significance ladder, as one named
vector:
options(tabxplor.stars = c("*" = 0.05, "**" = 0.01)).tabxplor.export_dir — the default export
directory.Performance and integration:
tabxplor.parallel — build one worker per row variable
on a background pool (needs mirai);
tabxplor.parallel_min sets the smallest row-variable count
worth dispatching. Release the pool with
tab_parallel_stop().See ?tabxplor-options for the full list and every
default.
vignette("tabxplor") — cross-tables and the colour
helpers.vignette("tabxplor-reg") — regression tables.vignette("tabxplor-weights") — weighted and survey
data.?fmt and ?tabxplor-vctrs for the cell
type, ?tabxplor-options for every option, ?tab
for the arguments and the display tokens.