Extract the typed (structured) view of a spicy table
Source:R/regression_dispatch.R
as_structured.Rdspicy's tables return a display representation by default – a
character data.frame with stars suffixes, en-dash for reference
rows, bracketed "[L, U]" confidence intervals, and APA padding on
p-values. This accessor returns the typed view that the output
engines (Excel, gt, tinytable, flextable, clipboard) consume
internally: a fully numeric body with CI pre-split into LL / UL
columns, NAs for non-applicable / reference cells, plus per-cell
markers and a format specification.
Arguments
- x
A spicy table built with
output = "default":table_regression(),table_categorical(),table_continuous(),table_continuous_lm(),table_outcome(),table_multiresponse()or the survey twinstable_categorical_svy()andtable_continuous_svy(). All return the same schema.
Details
This is the right entry point for users who want to:
Filter coefficients programmatically, e.g.
as_structured(tbl)$body[which(as_structured(tbl)$body$p < 0.05), ](which()drops the structurally empty rows – factor headers, reference levels – whosepisNA; a bare logical index would keep them as all-NArows).Aggregate raw values across rows, e.g.
mean(as_structured(tbl)$body[["B"]], na.rm = TRUE).Build a custom downstream renderer that consumes the same structured contract as spicy's built-in engines.
Schema
version– integer contract version (see Versioning below).body–data.framewith aVariablecharacter column, one or more numeric value columns, and four dot-prefixed identity columns at the end. Confidence intervals are split into a lower and an upper column, named as each family names them:"95% CI: LL"/"95% CI: UL"in a regression table (prefixed with the model label in multi-model output),"95% CI LL"/"95% CI UL"intable_continuous(),table_continuous_lm(),table_outcome()andtable_continuous_svy(),"CI lower"/"CI upper"for the association measure oftable_categorical(), and"<group> % CI lower"/"<group> % CI upper"intable_categorical_svy(). A cell with no value (reference level, term absent from a model, factor header) isNA.body$.variable– source variable of the row: the factor name on a factor header / level / reference row, the term otherwise, and the fit-statistic token ("nobs","r2","fixed_effects", ...) on a fit-statistic row.body$.level– the factor level (or the absorbed factor of a fixed-effects block, or the grouping factor of an"N (...)"row);NAoutside a factor.body$.row_role– what the row is:"coef","factor_header","level","reference","fit_stat","outcome","vc"(variance component) in a regression table, plus"summary"(a row summarizing one variable),"group"(a row keyed by one level ofby),"missing"(a row keyed by the missing value),"none"(the respondents who chose no item of a multiple-response question) and"total"(the Total row oftable_categorical()undertotal_row = TRUE) in the descriptive ones. The role is the key a consumer matches on:"(Missing)"is a display label – translatable, and relabeled throughoptions(spicy.labels)– and the role survives both.body$.indent– display indent depth of the label (0or1). Renderers indent.indent > 0rows; the label text itself is already indented in the character body only.cell_status– per-cell semantics, keyed by column name, one character vector as long asbodyper column that needs one:"reference"(reference level, in this estimate block and this model),"undefined"(the statistic applies to the row but no number expresses it – an unavailable variance-component standard error, a fit statistic undefined for one model's class),""otherwise. Both marked statuses display as an en-dash. A cell whose value isNAwith no status is absent and displays blank. Columns with no marked cell are omitted.outcome_labels_by_col– for the outcome row (explicitoutcome_labelswith two or more models), the display label keyed by each model's first structured column name.col_meta– per-column metadata keyed by structured column name (token, model_id, precision, p-style, below-threshold, CI pair / role / label). A column whose cells cannot be reconstructed from one number – the"events/N"counts ofshow_columns = "n_events"– also carriesdisplay_cells, a character vector as long asbodyholding the display string of each cell (NAwhere the number formats normally). A renderer must prefer it over the numeric value.stars–NULLunlessstarswas requested, otherwise a list withthresholds(symbol to p cutoff) andmarkers(per-cell marker strings, keyed by column name,""where a cell takes none).spanners– named list mapping a grouping label to its column indices inbody: the model labels of a multi-model regression table, thebylevels of a categorical one (each spanning itsn/%pair).ci_pairs– list of(label, cols)entries describing each CI pair inbody.format_spec– global format defaults (decimal mark, digits, p-style, CI level, etc.).
Descriptive tables
The descriptive tables return the same schema; only the col_meta
tokens and the row roles they emit differ. table_outcome() has the
columns of table_continuous() and the rows of table_categorical():
a "summary" row for the whole sample, then a "factor_header" row
per grouping and its "level" (or "missing") rows. The survey twins
follow the table they twin, table_categorical_svy() with the
interval of each percentage (token "prop_ci") and no association
measure.
table_categorical()– one"factor_header"row per variable then one"level"(or"missing") row per category, and undertotal_row = TRUEthe"total"row that closes each block, exactly as the console lays them out. Tokens:"n","pct","p","assoc"(the association measure),"assoc_ci"(its bounds) and"smd"(the standardized mean difference, whensmd = TRUE). In abytable each group owns ann/%pair with its own spanner, and the margin column is flaggedcol_meta$<col>$total, never matched on the label"Total".table_continuous()– one"summary"row per variable, or one"group"row per level ofby("missing"for the missing-bygroup), with the level in.level. Tokens are theshow_columnsvocabulary itself ("m","sd","med","iqr","med_iqr","q1","q3","min","max","ci","med_ci","n","weighted_n") plus"statistic","p","es"and"smd"for the group comparison – the last four arecol_metatokens without beingshow_columnsvalues, because they belong to a comparison BETWEEN groups rather than to any one group. A statistic another variable displays is absent (NA, no status); one that applies but has no value is"undefined".table_continuous_lm()– one"summary"row per outcome. Tokens:"emmean"(a marginal mean; the column says which level incol_meta$<col>$level, also itsdisplay_label: the table heads each mean by its level under the mean symbol),"delta"(the contrast),"b"(a numeric predictor's slope),"ci","statistic","p","r2","es","n","weighted_n".table_multiresponse()– one"summary"row per item, then the rows ofnone_row: a"none"row (.variable == "none") for the respondents of the base with no item chosen and a"missing"row (.variable == "no_data") for those with no data for the question, then one"fit_stat"row (.variable == "nobs") holding the respondents of each group. Tokens:"n","pct"(percentage of cases),"pct_responses"(percentage of responses),"statistic"and"p". Each group owns its columns under its own spanner, and the margin is flaggedcol_meta$<col>$total.
Cells the console builds from more than one number – a
"Mdn [Q1, Q3]", a test gloss, an effect size with its interval –
keep their numeric anchor in body and their exact printed string
in col_meta$<col>$display_cells. So does a cell of a variable that a
named digits gives decimals other than the column's precision,
which stays the table's digits. stars is always NULL:
descriptive tables carry no significance markers.
Under p_adjust, the p-value column holds the adjusted values and its
col_meta adds p_adjust (the method), p_adjust_m (the number of
tests adjusted together) and p_unadjusted (the raw p-value of each
cell, as long as body). In table_categorical() and
table_multiresponse(), the p-value column's col_meta also carries
test_type, the test of each cell, as long as body
("chi_squared", "n1_chi_squared", "fisher_exact",
"fisher_midp", "fisher_exact_mc", NA where the cell holds no
p-value).
Versioning
version says which contract an object carries. Version 3 moved
row identity out of index vectors and into the body itself:
reference_rowsandreference_models_by_rowbecomecell_status, which marks the reference cell instead of the whole row – the row-scoped flag was blanking estimate blocks that have no per-level reference at all.factor_header_rowsbecomes.row_role == "factor_header".fit_stat_rowsbecomes.row_role == "fit_stat".level_rowsbecomes.indent > 0.outcome_rowbecomes.row_role == "outcome".
Index vectors are the structure that corrupts as soon as two bodies are stacked or merged, so they were removed rather than kept alongside. An object carrying an older contract (or none) is refused with the correspondence above; rebuild it with the function that produced it. An object carrying a newer contract than the spicy reading it is refused as well.
Version 3 also opened the accessor to the descriptive families,
which had no typed view before: .row_role gained "summary",
"group" and "missing", then "none" with table_multiresponse()
and "total" with the total_row of table_categorical().
The vocabulary is extended by addition – an existing role never
changes meaning.
Version 4 renames one component: in the format_spec of
table_categorical(), the number of decimals of the association
measure and the SMD is effect_size_digits, the key of the other
tables, where it was v_digits (the argument was renamed alike). An
object carrying version 3 is refused as an older contract.
See also
table_regression(), table_categorical(),
table_continuous(), table_continuous_lm() for the
user-facing entry points.
Examples
fit <- lm(mpg ~ wt + factor(cyl), data = mtcars)
tbl <- table_regression(fit)
s <- as_structured(tbl)
s$body # raw numeric body
#> Variable B SE 95% CI: LL 95% CI: UL p
#> 1 (Intercept) 33.9907940 1.8877934 30.123824 37.857764 6.257246e-17
#> 2 wt -3.2056133 0.7538957 -4.749898 -1.661328 2.130435e-04
#> 3 factor(cyl): NA NA NA NA NA
#> 4 4 (ref.) NA NA NA NA NA
#> 5 6 -4.2555824 1.3860728 -7.094824 -1.416341 4.717834e-03
#> 6 8 -6.0708597 1.6522878 -9.455418 -2.686301 9.991893e-04
#> 7 n 32.0000000 NA NA NA NA
#> 8 R² 0.8374325 NA NA NA NA
#> 9 Adj. R² 0.8200146 NA NA NA NA
#> .variable .level .row_role .indent
#> 1 (Intercept) <NA> coef 0
#> 2 wt <NA> coef 0
#> 3 factor(cyl) <NA> factor_header 0
#> 4 factor(cyl) 4 reference 1
#> 5 factor(cyl) 6 level 1
#> 6 factor(cyl) 8 level 1
#> 7 nobs <NA> fit_stat 0
#> 8 r2 <NA> fit_stat 0
#> 9 adj_r2 <NA> fit_stat 0
s$body[which(s$body$p < 0.05), ] # filter significant rows
#> Variable B SE 95% CI: LL 95% CI: UL p
#> 1 (Intercept) 33.990794 1.8877934 30.123824 37.857764 6.257246e-17
#> 2 wt -3.205613 0.7538957 -4.749898 -1.661328 2.130435e-04
#> 5 6 -4.255582 1.3860728 -7.094824 -1.416341 4.717834e-03
#> 6 8 -6.070860 1.6522878 -9.455418 -2.686301 9.991893e-04
#> .variable .level .row_role .indent
#> 1 (Intercept) <NA> coef 0
#> 2 wt <NA> coef 0
#> 5 factor(cyl) 6 level 1
#> 6 factor(cyl) 8 level 1
# which() drops the structural NA rows (headers, reference levels)
s$body$.row_role # what each row is
#> [1] "coef" "coef" "factor_header" "reference"
#> [5] "level" "level" "fit_stat" "fit_stat"
#> [9] "fit_stat"
s$body[s$body$.variable == "wt", ] # address a row by its variable
#> Variable B SE 95% CI: LL 95% CI: UL p .variable
#> 2 wt -3.205613 0.7538957 -4.749898 -1.661328 0.0002130435 wt
#> .level .row_role .indent
#> 2 <NA> coef 0
s$col_meta$B # column metadata for B
#> $token
#> [1] "b"
#>
#> $model_id
#> [1] "M1"
#>
#> $source_field
#> [1] "estimate"
#>
#> $precision
#> [1] 2
#>
#> $p_style
#> NULL
#>
#> $threshold
#> NULL
#>
#> $signif
#> NULL
#>
#> $ci_role
#> NULL
#>
#> $ci_pair
#> NULL
#>
#> $ci_label
#> NULL
#>
#> $is_df
#> [1] FALSE
#>
#> $display_label
#> [1] "B"
#>
#> $fit_stat_overrides
#> $fit_stat_overrides[[1]]
#> $fit_stat_overrides[[1]]$fit_stat
#> [1] "nobs"
#>
#> $fit_stat_overrides[[1]]$precision
#> [1] 0
#>
#> $fit_stat_overrides[[1]]$p_style
#> NULL
#>
#> $fit_stat_overrides[[1]]$threshold
#> NULL
#>
#> $fit_stat_overrides[[1]]$row
#> [1] 7
#>
#>
#> $fit_stat_overrides[[2]]
#> $fit_stat_overrides[[2]]$fit_stat
#> [1] "r2"
#>
#> $fit_stat_overrides[[2]]$precision
#> [1] 2
#>
#> $fit_stat_overrides[[2]]$p_style
#> NULL
#>
#> $fit_stat_overrides[[2]]$threshold
#> NULL
#>
#> $fit_stat_overrides[[2]]$row
#> [1] 8
#>
#>
#> $fit_stat_overrides[[3]]
#> $fit_stat_overrides[[3]]$fit_stat
#> [1] "adj_r2"
#>
#> $fit_stat_overrides[[3]]$precision
#> [1] 2
#>
#> $fit_stat_overrides[[3]]$p_style
#> NULL
#>
#> $fit_stat_overrides[[3]]$threshold
#> NULL
#>
#> $fit_stat_overrides[[3]]$row
#> [1] 9
#>
#>
#>
# The same schema on a descriptive table.
ct <- table_categorical(mtcars, c(cyl, gear), by = am)
sc <- as_structured(ct)
sc$body[, c("Variable", ".variable", ".level", ".row_role")]
#> Variable .variable .level .row_role
#> 1 cyl cyl <NA> factor_header
#> 2 4 cyl 4 level
#> 3 6 cyl 6 level
#> 4 8 cyl 8 level
#> 5 gear gear <NA> factor_header
#> 6 3 gear 3 level
#> 7 4 gear 4 level
#> 8 5 gear 5 level
sc$spanners # one per `by` group
#> $`0`
#> [1] 2 3
#>
#> $`1`
#> [1] 4 5
#>
#> $Total
#> [1] 6 7
#>