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")
Sys.setenv(LANGUAGE = "en") # the test-summary / model-fit row labels go through gettext, not this option
library(dplyr)
# As in the introduction vignette, tables render as tabxplor's real html tables (the recommended
# everyday setting); the shared stylesheet is emitted once by tab_css() below, tooltips kept off.
options(tabxplor.print = "html")
options(tabxplor.tab_kable_css = FALSE)
options(tabxplor.tab_kable_tooltips = FALSE)
# The shape table a continuous predictor draws under the footer is the subject of one
# section below, and noise everywhere else: it is switched on there and off here.
options(tabxplor.shape_table = "no")
options(cli.num_colors = 256)
set_color_palette(theme = "light")Une version française de ce document est disponible : Tableaux de régression avec tab_reg().
For the most common regression models, tab_reg() builds
a regression table that looks and behaves like a
tabxplor cross-table: one row per predictor level,
significance stars, colours that grey out non-significant effects, and
the same Excel, html or markdown exports. You give it a data frame, an
outcome and some predictors, and it
works out the right kind of model from the outcome’s type. Its
distinctive feature is empirical = TRUE, which shows the
observed / crude / empirical effect right next to the model’s
adjusted one, so you can see what “controlling for the other
variables” actually changed.
There are two articles on regression, and this is the
reference one. It is organised by feature: one section per
family, the full grid of what each model can report, weighted data,
interactions, model checks and plots. If you are learning rather than
looking something up, start with
vignette("tabxplor-reading-a-regression"), which walks a
single analysis from a first cross-table to a finished sentence and
sends you back here for the details.
Without writing R code, the same tables are a point-and-click Regressions analysis in the tabxplor module for jamovi; its options carry the names of the arguments below, so this reads as its manual too.
We use a formatted version of the forcats::gss_cat data,
from the US General Social Survey, and — for the summed-score model —
facto_tea (from the FactoMineR package,
with thanks; see ?facto_tea) with its six “where do you
drink tea?” items reduced to one score (see Batteries of yes/no
items in vignette("tabxplor") for that
preparation):
gss_simple <- gss_cat_data_formatting()
tea_where_vars <- c("home", "work", "tearoom", "friends", "resto", "pub")
tea <- facto_tea |> score_from_lv1("tea_where", vars_list = tea_where_vars)1. What a regression table is
The outcome’s type chooses the model and the observed quantity to compare with
You rarely set family by hand — tab_reg()
mostly detects it:
| Outcome | Detected model | The measure that model works in |
|---|---|---|
| 2-level factor | binomial (logistic) | odds ratio (OR) |
| numeric (continuous) | gaussian (linear) | mean difference (diff) |
| count | poisson | incidence-rate ratio (IRR) |
| 3+ level unordered factor | multinomial | one OR column per category vs. the
reference |
| 3+ level ordered factor | ordinal (proportional odds) | cumulative OR, or Somers’
D
|
That last column is the measure the model estimates,
not a ceiling: any other measure can still be reported from the
same fit, and measure = is how you ask. Four arguments
chain — family says what kind of number the outcome is,
link which measure the model estimates,
measure which one is reported, effect where
that number comes from — and each follows from the one before unless you
say otherwise. The full grid is in What a
model column reports below.
With empirical = TRUE, each model column is joined by a
crude/observed companion column showing the observed, unadjusted
(univariable) effect — the effect you would see with no controls at
all, “all things being unequal”, for that predictor. The two columns are
the same column twice, one estimand computed with one predictor and with
all of them: same colour ladder, same layout, one legend. Each cell
prints the effect with the level it sits on beside it — the observed
percentage or mean on the crude side, the
model-adjusted prediction on the model side — so the
two effects sit next to each other in the middle and the comparison is
read straight across.
One rule covers every case: the observed effect is the effect
of that predictor alone. With a categorical predictor that is
exactly the observed contrast between levels (a percentage difference,
an odds ratio computed from the raw percentages). With a
continuous predictor there is no such shortcut, so it is the
univariable slope — which assumes the effect is linear on the model’s
scale, something worth checking (with cut(), or splines)
before trusting it.
- 2 levels logistic (binomial) → observed %, and observed odd-ratios
- 3+ levels logistic (multinomial) → the observed ORs are shown as a tooltip on the model cells in html exports
- gaussian (linear) → group means and their difference
- poisson (counts) → observed rate and observed rate ratio
A continuous predictor has no levels, so its cell shows the effect alone — there is no observed percentage or mean to put beside it — and its distribution (mean, standard deviation, and the mean in each outcome group) is shown in the html tooltip instead.
On weighted data, one thing is worth knowing from
the start: an observed column is always measured exactly like the model
column beside it, so its confidence interval accounts for the weighting
(and, under a survey design, for the whole design). A
tab() cross-table does not, by default — add
design_effect = TRUE there if you want its percentages
directly comparable with these. See Weighted and survey data below.
Every outcome has an observed counterpart
empirical = TRUE answers one question — “how
much of this association survives once I adjust for the rest?”
— and it works for every kind of outcome. The rule behind it is a single
sentence:
The observed effect is the model’s own effect, fitted with one predictor.
That is worth saying plainly, because it is what makes the comparison fair. The crude number is not “a percentage” and the modelled one “a coefficient” — they are the same quantity, computed the same way, on the same people (the same complete cases), and shown on the same scale. Only the list of predictors differs. So the distance between them measures adjustment, and nothing else.
The “same people” part is the default, not a hope:
na = "drop_by_outcome" gives every model of an outcome one
complete-case population, so a compared model cannot be fitted on rows
the observed columns do not cover. If you ask for
na = "drop_by_model" instead, a model on a different
population gets no observed effect at all — better an
empty cell than a “gap” that is really listwise deletion.
A word on how to read it. The colour says how much two numbers differ, and the greying whether that difference exceeds noise. It does not say “this is a confounder”: the 10% first threshold is a convention, not a decision rule, and part of an odds-ratio gap is arithmetic rather than confounding (see One warning about odds ratios, below). “This effect was attenuated by adjustment” is the safe reading; “explained by” is not.
An ordered outcome: what changes when you adjust
Income here is an ordered factor, so it is fit as a
proportional-odds model. Obs_cumOR is the same model with
one predictor at a time:
| rincome | ||||
|---|---|---|---|---|
| levels | n | Obs_cumOR | Model_cumOR | |
| <n> | <OR> | <OR> | ||
| Constant | Reference profile | 4 692 | ||
| race | White | 9 863 | 1 | |
| Black | 1 866 | 1/1.54*** | 1/1.51*** | |
| Other | 1 256 | 1/1.51*** | 1/1.60*** | |
| relig | 1-Protestant | 6 282 | 1 | |
| 2-Catholic | 3 127 | 1.09* | 1.09* | |
| 3-Other christian | 508 | 1/1.10 | 1/1.09 | |
| 4-Jewish | 223 | 2.29*** | 2.09*** | |
| 5-Buddhist/Hinduist | 144 | 1.93*** | 2.32*** | |
| 6-Muslim | 56 | 1.01 | 1.22 | |
| 7-Other | 267 | 1/1.06 | 1/1.04 | |
| 8-None | 2 378 | 1/1.05 | 1/1.08* | |
| Model fit | N | 12 985 | ||
| Proportionality (Brant) | 8.87% | |||
| Dispersion (robust/model SE) | 1.09 | |||
| Collinearity (max VIF) | 1.07 | |||
| Influence (max dfbetas) | 0.22 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.006 | |||
| AIC | 29 559 | |||
| BIC | 29 649 | |||
|
Model: ordinal logistic regression; cumOR: cumulative odds ratio
(proportional-odds model); sup%: probability of superiority, 50 % being
a coin flip.
rincome, from low to high: 1-Lt $10000 (17%) < 2-$10000 to 14999 (9%) < 3-$15000 to 24999 (18%) < 4-$25000 or more (57%). Obs_cumOR — cumOR ≥ 1.2; 1.5; 2; 4; cumOR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Background colour: the observed effect (the reference for the adjustment). Uncoloured: not significantly different from the reference category (Wald interval on the log scale, 95% confidence) or under the first colour threshold (×1.2). Part of an odds-ratio gap is non-collapsibility, not confounding: a risk ratio or a marginal effect is the collapsible comparison. Model_cumOR — cumOR ≥ 1.2; 1.5; 2; 4; cumOR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Background colour: cumOR further from no effect (1) than the observed column, by ×1.1; ×1.25; ×1.5; ×2; cumOR closer to no effect (1) than the observed column (or inversed effect), by ÷1.1; ÷1.25; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log scale, 95% confidence) or under the first colour threshold (×1.2). Part of an odds-ratio gap is non-collapsibility, not confounding: a risk ratio or a marginal effect is the collapsible comparison. ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Read a row left to right: the observed cumulative odds ratio, then
the modelled one, with the background colouring the gap
between them. Being Black is associated with lower income
(1/1.54 observed), and adjusting for religion barely moves
it (1/1.51) — the association is not explained by religion.
Compare Buddhist/Hinduist, where the effect grows once race and
the other groups are held constant: adjustment can strengthen an
association as easily as it can wash one away.
On this scale the background colour is not a test:
an odds ratio moves when any strong predictor is added, which is
arithmetic rather than confounding. Ask for
measure = "difference" and the same comparison is read on
Somers’ D, where it is one — the crude value is
then the cross-table’s own superiority probability, computed from the
counts with no model at all, so the distance to the modelled one
measures adjustment and nothing else:
tab_reg(gss_simple, "rincome", c("race", "relig"), measure = "difference",
empirical = TRUE, display = "est_base", color = c(TRUE, "adjustment"))| rincome | ||||
|---|---|---|---|---|
| levels | n | Obs_D | Model_mD | |
| <n> | <(sup%) diff> | <diff (sup%)> | ||
| Constant | Population average | 12 985 | ||
| race | White | 9 863 | (50%) 0% | 0% (50%) |
| Black | 1 866 | (44%) -12.1%*** | -11.2%*** (44%) | |
| Other | 1 256 | (44%) -11.2%*** | -12.9%*** (44%) | |
| relig | 1-Protestant | 6 282 | (50%) 0% | 0% (50%) |
| 2-Catholic | 3 127 | (51%) +2.2%* | +2.2%* (51%) | |
| 3-Other christian | 508 | (49%) -2.7% | -2.3% (49%) | |
| 4-Jewish | 223 | (60%) +19.1%*** | +17.4%*** (59%) | |
| 5-Buddhist/Hinduist | 144 | (58%) +15.6%*** | +19.5%*** (60%) | |
| 6-Muslim | 56 | (50%) +0.2% | +5.3% (53%) | |
| 7-Other | 267 | (49%) -1.6% | -1.2% (49%) | |
| 8-None | 2 378 | (49%) -1.4% | -2.1%* (49%) | |
| Model fit | N | 12 985 | ||
| Proportionality (Brant) | 8.87% | |||
| Dispersion (robust/model SE) | 1.09 | |||
| Collinearity (max VIF) | 1.07 | |||
| Influence (max dfbetas) | 0.22 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.006 | |||
| AIC | 29 559 | |||
| BIC | 29 649 | |||
|
Model: ordinal logistic regression; mD: marginal Somers’ D (how often
someone from this group ends up higher than someone from the reference
group, as a difference in percentage points, wins minus losses;
sample-averaged); sup%: probability of superiority, 50 % being a coin
flip.
rincome, from low to high: 1-Lt $10000 (17%) < 2-$10000 to 14999 (9%) < 3-$15000 to 24999 (18%) < 4-$25000 or more (57%). Obs_D — D ≥ +5; +10; +20; +30 points; D ≤ -5; -10; -20; -30 points. Background colour: the observed effect (the reference for the adjustment). Uncoloured: not significantly different from the reference category (Wald interval, 95% confidence) or under the first colour threshold (±5 points). Model_mD — D ≥ +5; +10; +20; +30 points; D ≤ -5; -10; -20; -30 points. Background colour: D further from no effect (0) than the observed column, by +2; +5; +10; +20 points; D closer to no effect (0) than the observed column (or inversed effect), by -2; -5; -10; -20 points. Uncoloured: not significantly different from the reference category (Wald interval, 95% confidence) or under the first colour threshold (±5 points). Some rows carry no test and are left uncoloured. Background: the same rule, applied to the gap with the observed effect (z test on the difference between two estimates fitted on the same sample, 95% confidence). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
A nominal outcome: the crude number rides in the cell
A multinomial model already spends one column per outcome category, so a second set of columns would double the table. The observed effect is printed in the cell instead, in parentheses:
tab_reg(gss_simple, "party3", c("race", "relig"), family = "multinomial",
empirical = TRUE, color = c(TRUE, "adjustment"))| party3: OR | ||||
|---|---|---|---|---|
| levels | n |
2-Independent, other vs 1-Democrat |
3-Republican vs 1-Democrat |
|
| <n> | <OR (adj%)> | |||
| Constant | Reference profile | 8 157 | 1/1.98*** (34%) | 1.47*** (60%) |
| race | White | 16 253 | 1 (21%) | 1 (40%) |
| Black | 3 080 | 1/2.46*** (16%) | 1/12.48*** ( 7%) | |
| Other | 1 921 | 1.28*** (31%) | 1/2.29*** (22%) | |
| relig | 1-Protestant | 10 794 | 1 (18%) | 1 (42%) |
| 2-Catholic | 5 090 | 1/1.09* (20%) | 1/1.71*** (31%) | |
| 3-Other christian | 778 | 1.34*** (24%) | 1/1.23** (35%) | |
| 4-Jewish | 388 | 1/2.86*** (12%) | 1/4.76*** (17%) | |
| 5-Buddhist/Hinduist | 212 | 1/1.11 (24%) | 1/3.59*** (18%) | |
| 6-Muslim | 99 | 1/1.27 (21%) | 1/3.21*** (20%) | |
| 7-Other | 384 | 1.29** (28%) | 1/2.18*** (24%) | |
| 8-None | 3 509 | 1.35*** (31%) | 1/3.08*** (18%) | |
| Model fit | N | 21 254 | ||
| Dispersion (robust/model SE) | 1.07 | |||
| Influence (max dfbetas) | 0.31 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.069 | |||
| AIC | 41 721 | |||
| BIC | 41 880 | |||
|
Model: multinomial logistic regression; OR: odds ratio (each category vs
the reference); adj%: adjusted/predicted proportion.
OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Background colour: OR further from no effect (1) than the observed column, by ×1.1; ×1.25; ×1.5; ×2; OR closer to no effect (1) than the observed column (or inversed effect), by ÷1.1; ÷1.25; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence) or under the first colour threshold (×1.2). Part of an odds-ratio gap is non-collapsibility, not confounding: a risk ratio or a marginal effect is the collapsible comparison. ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
1/2.46 (1/2.43) means “modelled 1/2.46,
observed 1/2.43” — nothing to see.
1/12.48 (1/10.17) means adjustment pushed the effect a
little further from 1. The footer says which number is which, and
hovering a cell (in an HTML export) adds the crude percentages behind
it.
A summed score
With trials =, the crude column is the odds ratio of the
summed items, Obs_OR, with the observed mean
score beside it — the average number of places out of six. The
model column shows the same two quantities adjusted, so the pair reads
across.
tab_reg(tea, "tea_where", c("sex", "SPC", "Sport"),
family = "binomial", trials = length(tea_where_vars),
empirical = TRUE, color = c(TRUE, "adjustment"))| tea_where | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs mean) OR> | <OR (adj mean)> | ||
| Constant | Reference profile | 16 | 1/1.22 (2.7) | |
| sex | F | 178 | (2.8) 1 | 1 (2.8) |
| M | 122 | (2.3) 1/1.36*** | 1/1.44*** (2.3) | |
| SPC | employee | 59 | (2.5) 1 | 1 (2.5) |
| middle | 40 | (2.5) 1/1.04 | 1.01 (2.5) | |
| non-worker | 64 | (2.4) 1/1.12 | 1/1.14 (2.3) | |
| other worker | 20 | (2.9) 1.29 | 1.31 (2.9) | |
| senior | 35 | (2.9) 1.25 | 1.44** (3.0) | |
| student | 70 | (2.7) 1.14 | 1.11 (2.6) | |
| workman | 12 | (2.2) 1/1.21 | 1/1.08 (2.4) | |
| Sport | sportsman | 179 | (2.6) 1 | 1 (2.6) |
| Not sportsman | 121 | (2.6) 1.02 | 1.02 (2.6) | |
| Model fit | N | 300 | ||
| Dispersion (robust/model SE) | 1.04 | |||
| Pearson dispersion (phi) | 1.02 | |||
| Collinearity (max VIF) | 1.13 | |||
| Influence (max dfbetas) | 0.46 | |||
| LR vs null | 0.99% | |||
| McFadden R2 | 0.021 | |||
| AIC | 971 | |||
| BIC | 1 004 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Background colour: the observed effect (the reference for the adjustment). Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; Woolf closed form) or under the first colour threshold (×1.2). Part of an odds-ratio gap is non-collapsibility, not confounding: a risk ratio or a marginal effect is the collapsible comparison. Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Background colour: OR further from no effect (1) than the observed column, by ×1.1; ×1.25; ×1.5; ×2; OR closer to no effect (1) than the observed column (or inversed effect), by ÷1.1; ÷1.25; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence) or under the first colour threshold (×1.2). Part of an odds-ratio gap is non-collapsibility, not confounding: a risk ratio or a marginal effect is the collapsible comparison. ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
How to read it — and how not to
A gap is a clue, not a verdict: it holds for these
covariates, measured this way, and both numbers are estimates,
so a crude one based on few people wanders and the gap wanders with it.
vignette("tabxplor-reading-a-regression") is a whole
article on reading that distance.
One habit is specific to the scale, and it decides what the table will let you do. An odds ratio changes when you add a predictor even if that predictor has nothing to do with the exposure — non-collapsibility — so part of every gap on an odds-ratio column is arithmetic rather than confounding, and the colours there stay purely descriptive. For a gap you can test, ask for a collapsible measure: percentage points, or a risk ratio.
tab_reg(gss_simple, "party3", c("race", "relig"), family = "multinomial",
measure = "difference", empirical = TRUE, color = c(TRUE, "adjustment"),
color_signif = "grey_non_signif")| party3: mRD | |||||
|---|---|---|---|---|---|
| levels | n | 1-Democrat |
2-Independent, other |
3-Republican | |
| <n> | <diff (adj%)> | ||||
| Constant | Population average | 21 254 | 45.4% | 21.1% | 33.5% |
| race | White | 16 253 | 0% (39%) | 0% (21%) | 0% (40%) |
| Black | 3 080 | +37.8%*** (77%) | -4.1%*** (16%) | -33.6%*** ( 7%) | |
| Other | 1 921 | +7.7%*** (47%) | +10.8%*** (31%) | -18.5%*** (22%) | |
| relig | 1-Protestant | 10 794 | 0% (41%) | 0% (18%) | 0% (42%) |
| 2-Catholic | 5 090 | +8.4%*** (49%) | +2.5%*** (20%) | -10.9%*** (31%) | |
| 3-Other christian | 778 | +0.4% (41%) | +6.4%*** (24%) | -6.8%*** (35%) | |
| 4-Jewish | 388 | +30.7%*** (71%) | -6.2%*** (12%) | -24.5%*** (17%) | |
| 5-Buddhist/Hinduist | 212 | +17.5%*** (58%) | +6.0%** (24%) | -23.5%*** (18%) | |
| 6-Muslim | 99 | +18.0%*** (59%) | +3.2% (21%) | -21.2%*** (20%) | |
| 7-Other | 384 | +7.5%*** (48%) | +10.1%*** (28%) | -17.6%*** (24%) | |
| 8-None | 3 509 | +10.1%*** (51%) | +13.3%*** (31%) | -23.4%*** (18%) | |
| Model fit | N | 21 254 | |||
| Dispersion (robust/model SE) | 1.07 | ||||
| Influence (max dfbetas) | 0.31 | ||||
| LR vs null | <0.01% | ||||
| McFadden R2 | 0.069 | ||||
| AIC | 41 721 | ||||
| BIC | 41 880 | ||||
|
Model: multinomial logistic regression; mRD: marginal risk difference
(on adjusted proportions, in percentage points, sample-averaged); adj%:
adjusted/predicted proportion.
RD ≥ +5; +10; +20; +30 points; RD ≤ -5; -10; -20; -30 points. Background colour: RD further from no effect (0) than the observed column, by +2; +5; +10; +20 points; RD closer to no effect (0) than the observed column (or inversed effect), by -2; -5; -10; -20 points. Uncoloured: not significantly different from the reference category (Wald interval, 95% confidence) or under the first colour threshold (±5 points). Some rows carry no test and are left uncoloured. Background: the same rule, applied to the gap with the observed effect (z test on the difference between two estimates fitted on the same sample, 95% confidence). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
|||||
The cell now reads “modelled effect in percentage points, observed one in parentheses”, and a background that stays grey means the two are not distinguishable from noise.
2. The four arguments: family, link,
measure, effect
Four arguments decide what a model column contains, and they
chain — each follows from the one before unless you say
otherwise, which is what "auto" means on all four:
outcome ──auto──▶ family ──auto──▶ link ──auto──▶ measure ──auto──▶ effect
One rule underpins them, and it is what differs from
tab(). Only link
changes the model that is fitted. measure and
effect change what is read off it, and
display only changes what the cell shows.
In a cross-table every measure comes from the same counts, so asking for
a ratio rather than a difference is a display choice; in a regression it
is a step of computation — the model works your measure out from its
predictions — but it is still the same fit. So a risk ratio and
an odds ratio can be two readings of one logistic model, and a
different model is something you ask for by name.
The four arguments, in plain words
| argument | the question it answers |
"auto" gives you |
who sets it |
|---|---|---|---|
family |
what kind of number is the outcome | detected from the outcome | you, on a numeric outcome |
link |
which measure the model estimates | the family’s own | rarely — experts |
measure |
which measure is reported | the link’s | the one you set |
effect |
where that number is taken from | a coefficient if there is one | rarely; for an ideal type |
effect’s three values name three quantities:
effect |
the question it answers | its name in the literature |
|---|---|---|
"conditional" |
two people alike on all other predictors: what does this do? | conditional effect (the coef.) |
"marginal" |
if everyone switched it, how far would the outcome move? | average marginal effect (AME) |
"at_reference" |
the same, but for one person at the reference profile | effect at the reference profile |
By hand you would get them with coef(glm(...))
(exponentiated when the measure is a ratio), with
marginaleffects::avg_comparisons(model), and with the same
call on a one-row profile where every other predictor sits at its
reference level or its mean. You will rarely type any of them:
"auto" picks "conditional" whenever the
reported measure is the model’s own — which is exactly when a
coefficient exists — and "marginal" otherwise.
"conditional" earns its keep as an assertion: ask for it
where no coefficient can carry your measure and the table says so,
naming the two cures, instead of quietly giving you something else.
⚠ One clause qualifies "auto": it never lands on
a predicted odds ratio. A marginal odds ratio is a
specialist quantity (Karlson & Jann 2023), so on a prediction route
"auto" steps back to the outcome’s own measure — “x times
as likely” for a percentage. Name measure = "odds_ratio" to
get it.
effect = "at_reference" evaluates the
effect at the reference profile — the abstract individual combining the
reference level of every predictor — instead of averaging it over the
sample. For a multinomial model it also unlocks a contrast the other two
do not have: the odds ratio of each outcome category versus all the
others at that profile.
Which model an outcome can be fitted with
link is the value set of models. Its words are
measure’s own words, because a link is a
measure — the one the model estimates directly — so the statistician’s
vocabulary never surfaces. † marks the family’s own link, which is what
link = "auto" resolves to.
| outcome kind | family |
a cell is… | link = |
the model that fits | its coef. |
|---|---|---|---|---|---|
| factor, 2 lvl | binomial |
percentage |
"odds_ratio" † |
logistic regression | OR |
"ratio" |
modified Poisson, robust SE | RR |
|||
"difference" |
identity-link binomial | RD |
|||
| numeric, score |
binomial+trials |
percentage | the same three | the same, per item |
OR/RR/RD
|
| numeric | gaussian |
a mean |
"difference" † |
linear regression | diff |
"ratio" |
log-link Poisson pseudo-ML | RoM |
|||
| numeric, count | poisson |
a count |
"ratio" † |
Poisson, quasi-Poisson SEs | IRR |
| factor, 3+ un. | multinomial |
percentage |
"odds_ratio" † |
multinomial logit | OR |
| factor, ord. | ordinal |
a rank |
"odds_ratio" † |
proportional-odds model | cumOR |
family = "quasipoisson" has exactly the row of
poisson — it changes the variance assumption, not what is
estimated.
What each measure reports, from any of them
Which measures exist is decided by what a cell is: a
percentage has an identity, a log and a logit, so all three; a mean or a
count has no odds, so no odds ratio; and a rank — an
ordered outcome’s cell — is compared by pairs of people rather than by
shares, which is what lets an ordinal model report in one column. Every
cell below is the acronym the combination puts in the column header,
after the constant Model_ prefix.
| a cell is… | measure = |
"conditional" * |
"marginal" |
"at_reference" |
|---|---|---|---|---|
| percentage | "odds_ratio" |
OR |
mOR ¹ |
refOR ¹ |
| percentage | "ratio" |
RR |
mRR |
refRR |
| percentage | "difference" |
RD |
mRD |
refRD |
| a mean | "difference" |
diff |
mdiff |
refdiff |
| a mean | "ratio" |
RoM |
mRoM |
refRoM |
| a mean | "odds_ratio" |
not defined | not defined | not defined |
| a count | "ratio" |
IRR |
mIRR |
refIRR |
| a count | "difference" |
— | mdiff |
refdiff |
| a count | "odds_ratio" |
not defined | not defined | not defined |
| a rank | "difference" |
— | mD |
not offered ² |
| a rank | "ratio" |
— | mWR |
not offered ² |
| a rank | "odds_ratio" |
cumOR |
not offered ¹ | not offered ¹ |
The "conditional" column is the one
reg_measures() shows per link; the other
two are the same for every link the family fits, which is why it lists
them once.
* a coefficient exists only where that measure is the
link — see the previous table. Ask for one that is
not, and the call is refused naming its two cures: drop
effect, or fit the model that estimates it.
¹ a predicted odds ratio needs a percentage and its
complement, so a 3+ category outcome has to be asked “versus
what?” first: on a multinomial outcome it exists only as the versus
the rest contrast at effect = "at_reference", and on
an ordinal one not at all.
² a rank’s measures compare two people drawn from the population, and one profile holds only one.
reg_measures(data, outcome) prints this grid for
your outcome, and prints only what can be built: a
not defined combination has no row, and the message above the
table says why. Its two refusals are not the same one — a quantity has
no odds to take a ratio of, whatever anyone implements, while not
offered means tabxplor does not build it and the error lists what
this outcome does offer, at this model and at the others.
A further state exists only at run time: a link that does not converge on your data. The call says so, and for the risk difference it falls back to the linear probability model.
measure = "raw_coefficient" is not a fourth column: it
is the model’s own coefficient, the estimand shown
un-transformed. Where the reported measure is multiplicative that is its
log, and the header says which one it logs (Model_log(OR),
Model_log(IRR), Model_log(RoM)); where the
model is already additive there is nothing to un-exponentiate, and the
coefficient IS the additive estimate the column already shows. So it
answers for every family — which is what lets a table mixing a logistic
outcome and a linear one be asked for its coefficients at all. Being the
fit’s own number it is always the conditional one; ask it with
effect = "marginal" and the call says so and names the
cure. ("coefficient", "coef",
"log", "log_odds", "log_risk" and
"log_rate" are accepted spellings; the last three pin which
base.)
What the header words mean
A header names the measure; the contrast is a marker on it. So there is one acronym to look up per quantity, and one marker to read on top of it.
| acronym | the quantity | how you would get it by hand |
|---|---|---|
OR |
odds ratio | exp(coef(glm(y ~ ., binomial))) |
cumOR |
cumulative odds ratio |
exp(coef(MASS::polr(...))), the proportional-odds
model |
RR |
risk ratio | modified Poisson, robust SE (Zou 2004) |
RD |
risk difference, in points |
glm(y ~ ., binomial("identity")), robust SE |
IRR |
incidence-rate ratio | exp(coef(glm(y ~ ., poisson))) |
RoM |
ratio of means | log-link pseudo-Poisson, robust SE (Santos Silva & Tenreyro 2006) |
diff |
mean difference | coef(lm(...)) |
| marker | reads | example |
|---|---|---|
| (none) | conditional — holding the other predictors fixed | Model_OR |
m prefix |
marginal — averaged over the sample | Model_mRR |
ref prefix |
at the reference profile | Model_refRD |
log(…) |
the same estimand, un-exponentiated | Model_log(OR) |
The observed companion carries the measure without a
marker (Obs_RR beside Model_mRR): it is a
univariable effect, and where its levels come from the counts themselves
a marginal and a conditional contrast are the same number.
The one argument that changes the model: link
Everything else changes what is reported. link changes
what is fitted, and that is the whole of the difference — so it is the
one argument that can put an assumption in your table you did not
intend. Each link buys a directly readable coefficient at the price of
one:
family and link
|
what is fitted | what the coefficient assumes |
|---|---|---|
binomial · "odds_ratio" (default)
|
logistic regression | a constant odds ratio across profiles |
binomial · "ratio"
|
modified Poisson, robust SE | a constant risk ratio |
binomial · "difference"
|
identity-link binomial, robust SE | a constant risk difference |
gaussian · "difference" (default)
|
linear regression | a constant mean difference |
gaussian · "ratio"
|
log-link Poisson pseudo-ML, robust SE | E(y) = exp(xβ), so a constant ratio of means |
poisson · "ratio" (default)
|
Poisson, quasi-Poisson SEs | a constant rate ratio |
ordinal · "odds_ratio" (default)
|
proportional-odds model | one odds ratio, shared by every cut |
multinomial · "odds_ratio" (default)
|
multinomial logit | a constant odds ratio per category |
What the literature says about the three non-default links:
-
The risk ratio is a well-established route, not a
workaround.
link = "ratio"on a binary outcome fits Zou’s modified Poisson, which exists precisely to avoid the convergence failures of a log-binomial fit and is standard practice in epidemiology. Its own limits are different in kind from the risk difference’s: fitted risks are not bounded above by 1, and in small or sparse samples both the estimate and the sandwich standard error are biased — penalised variants exist for that case. - The risk difference is the fragile one. An identity link is bounded on neither side, so it can predict impossible risks and can simply fail to converge. When it does, tabxplor fits the linear probability model instead and the footer says so — the two target the same quantity, but they are different estimators and agree only if the model is right.
- The ratio of means is consistent whenever the mean function is right. Poisson pseudo-likelihood does not claim your outcome is a count: it is a device for the log link, and its robust standard error does not require the Poisson variance to be correct. Nothing, however, rescues a wrong mean function — and the outcome must be non-negative.
- The default is not assumption-free either. A logistic coefficient assumes a constant odds ratio, an ordinal one assumes the same odds ratio at every cut (the footer’s Brant test checks it), and the odds ratio is non-collapsible, so it moves when a covariate is added even where there is nothing to confound.
Which route to take. If a risk ratio or a risk
difference is what you want to report, the safer default is to
leave the model on its family’s own scale and name the measure with
measure =. Three reasons, none of them about taste: the
logit always converges and can never predict a probability outside 0–100
%, while the log and identity links can do both; a marginal effect
imposes no constant-effect assumption on the scale you report, since it
averages the effect the fitted model actually implies at each
respondent’s own profile; and every measure runs on the
same fit, so changing which measure you show never changes what was
estimated.
That said, the two are different questions rather than two spellings
of one. A coefficient is a conditional effect — “two
people alike on every other predictor” — and a marginal effect a
population average. Take the link route when the
conditional quantity is what you want: to line up with a published
conditional estimate, or when “the effect is the same size everywhere on
this scale” is the claim you actually mean to make.
reg_formulas() tells you which model ran.
Caveats worth knowing
-
A binary outcome with
family = "poisson"is not a count model, and is refused rather than quietly rewritten.familysays what kind of number the outcome is and never picks a link behind your back; the message names the two things that spelling could have meant —link = "ratio"(the modified Poisson’s conditional risk ratio) andmeasure = "ratio"(the marginal one). -
link = "ratio"on a numeric outcome stops on a negative outcome. A ratio of means is not defined there; the call suggests modellinglog()of a positive outcome, or leavinglinkalone. -
Model_RDandModel_mRDare both percentage points, and are not the same number.Model_RDis a conditional risk difference from an identity-link fit (link = "difference"),Model_mRDa marginal one standardised over the logistic fit (measure = "difference") — which is what themmarker says. -
Conditional odds ratios are non-collapsible, so
color = "adjustment"colours the gap on such a column but never tests it (see How to read it — and how not to, above). Ask formeasure = "difference"ormeasure = "ratio"— or, if you must publish odds ratios,measure = "odds_ratio", effect = "marginal", whose marginal odds ratio is collapsible and whose gap the table does test. -
One combination has no observed companion. At
effect = "at_reference"the model is conditional on one profile while the observed columns stay marginal over the whole sample, so the two are shown but not compared —color = "adjustment"and{obs}stay empty. -
What needs an extra package, and what has no
method.
effect = "at_reference"goes through marginaleffects; a survey-weighted multinomial model has no marginal-effects method at all, so there it can only be read on its coefficients — keep the model’s own measure and the call goes through. A survey-weighted ordinal model is the exception: its rank measures (measure = "difference"/"ratio") run on tabxplor’s own g-computation and take the design-based variance straight from the fit, so they work under any design — only the crude/adjusted gap test is unavailable there, andcolor = "adjustment"then colours without testing. (displayitself reaches every column, marginal or not.)
Asking your own outcome
reg_measures() prints this grid for one outcome of your
own data — it reads the very table tab_reg() resolves
against, so what it prints is what the function does:
reg_measures(gss_simple, "married")#> # A tibble: 4 × 5
#> link measure effect header reads_as
#> <chr> <chr> <chr> <chr> <chr>
#> 1 odds_ratio odds_ratio conditional Model_OR odds ratio
#> 2 (any) difference marginal|at_reference Model_mRD marginal risk difference
#> 3 (any) ratio marginal|at_reference Model_mRR marginal risk ratio
#> 4 (any) odds_ratio marginal|at_reference Model_mOR marginal odds ratio
It comes in two blocks. The first is the model
itself: its link, and the measure that model’s own
coefficients carry. The second is what is read off the model’s
predictions — the same whichever link you fit, which is why
those rows say link = "(any)". So what link
changes is only which measure has a coefficient; every
measure stays reportable from any of them.
By default the first block holds each family’s own
model alone. link = "all" adds every other link it can be
fitted at — specialist choices — and marks the family’s own with
base_link:
reg_measures(gss_simple, "married", link = "all")#> # A tibble: 6 × 6
#> link base_link measure effect header reads_as
#> <chr> <lgl> <chr> <chr> <chr> <chr>
#> 1 odds_ratio TRUE odds_ratio conditional Model_OR odds ratio
#> 2 ratio FALSE ratio conditional Model_RR risk ratio
#> 3 difference FALSE difference conditional Model_RD risk differen…
#> 4 (any) NA difference marginal|at_reference Model_mRD marginal risk…
#> 5 (any) NA ratio marginal|at_reference Model_mRR marginal risk…
#> 6 (any) NA odds_ratio marginal|at_reference Model_mOR marginal odds…
Name a link to read the table at that model alone, and a family to read it as that kind of outcome:
reg_measures(gss_simple, "married", link = "ratio")#> # A tibble: 4 × 5
#> link measure effect header reads_as
#> <chr> <chr> <chr> <chr> <chr>
#> 1 ratio ratio conditional Model_RR risk ratio
#> 2 (any) difference marginal|at_reference Model_mRD marginal risk difference
#> 3 (any) ratio marginal|at_reference Model_mRR marginal risk ratio
#> 4 (any) odds_ratio marginal|at_reference Model_mOR marginal odds ratio
The same list is in ?tab_reg, section “Which models
each outcome offers, and which measures”, generated from the same
source.
3. One part per kind of outcome
Logistic regression (a binary factor)
When the outcome is a two-level factor, here “married” versus “not
married”, tab_reg() chooses a binomial
family to fit a logistic regression and reports odds
ratios (the reference level of each predictor shows the neutral
value 1).
- Like in every regression model, the effect of a predictor level on the dependent variable reads “all the others chosen predictors being equals”.
- Colors read like any
tabxplortable: an odds ratio above 1 (blue) means more likely to be married than the reference level, below 1 (red) means less likely; stars and colors both flag significance.
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1/1.19*** (46%) | ||
| race | White | 9 846 | (52%) 1 | 1 (51%) |
| Black | 1 860 | (31%) 1/2.43*** | 1/2.40*** (32%) | |
| Other | 1 254 | (49%) 1/1.12* | 1.11 (54%) | |
| age | per 26.9 (2SD), at 42.4 (mean) | 2.12*** | 1.83*** | |
| rincome | 1-Lt $10000 | 2 142 | (37%) 1 | 1 (39%) |
| 2-$10000 to 14999 | 1 164 | (41%) 1.20** | 1.14* (42%) | |
| 3-$15000 to 24999 | 2 322 | (43%) 1.32*** | 1.27*** (45%) | |
| 4-$25000 or more | 7 332 | (55%) 2.12*** | 1.86*** (54%) | |
| relig | 1-Protestant | 6 269 | (52%) 1 | 1 (53%) |
| 2-Catholic | 3 121 | (52%) 1/1.03 | 1/1.17*** (49%) | |
| 3-Other christian | 507 | (42%) 1/1.53*** | 1/1.43*** (45%) | |
| 4-Jewish | 222 | (55%) 1.10 | 1/1.29* (47%) | |
| 5-Buddhist/Hinduist | 144 | (50%) 1/1.09 | 1/1.36* (46%) | |
| 6-Muslim | 56 | (48%) 1/1.17 | 1.05 (54%) | |
| 7-Other | 267 | (35%) 1/2.04*** | 1/2.04*** (37%) | |
| 8-None | 2 374 | (38%) 1/1.76*** | 1/1.79*** (39%) | |
| Model fit | N | 12 960 | ||
| Dispersion (robust/model SE) | 1.03 | |||
| Collinearity (max VIF) | 1.08 | |||
| Influence (max dfbetas) | 0.19 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.057 | |||
| AIC | 16 960 | |||
| BIC | 17 064 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
For a simple logistic regression, empirical = TRUE adds
the crude unmodelised odds ratios, each with the
raw percentage it is computed from.
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1/1.19*** (46%) | ||
| race | White | 9 846 | (52%) 1 | 1 (51%) |
| Black | 1 860 | (31%) 1/2.43*** | 1/2.40*** (32%) | |
| Other | 1 254 | (49%) 1/1.12* | 1.11 (54%) | |
| age | per 26.9 (2SD), at 42.4 (mean) | 2.12*** | 1.83*** | |
| rincome | 1-Lt $10000 | 2 142 | (37%) 1 | 1 (39%) |
| 2-$10000 to 14999 | 1 164 | (41%) 1.20** | 1.14* (42%) | |
| 3-$15000 to 24999 | 2 322 | (43%) 1.32*** | 1.27*** (45%) | |
| 4-$25000 or more | 7 332 | (55%) 2.12*** | 1.86*** (54%) | |
| relig | 1-Protestant | 6 269 | (52%) 1 | 1 (53%) |
| 2-Catholic | 3 121 | (52%) 1/1.03 | 1/1.17*** (49%) | |
| 3-Other christian | 507 | (42%) 1/1.53*** | 1/1.43*** (45%) | |
| 4-Jewish | 222 | (55%) 1.10 | 1/1.29* (47%) | |
| 5-Buddhist/Hinduist | 144 | (50%) 1/1.09 | 1/1.36* (46%) | |
| 6-Muslim | 56 | (48%) 1/1.17 | 1.05 (54%) | |
| 7-Other | 267 | (35%) 1/2.04*** | 1/2.04*** (37%) | |
| 8-None | 2 374 | (38%) 1/1.76*** | 1/1.79*** (39%) | |
| Model fit | N | 12 960 | ||
| Dispersion (robust/model SE) | 1.03 | |||
| Collinearity (max VIF) | 1.08 | |||
| Influence (max dfbetas) | 0.19 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.057 | |||
| AIC | 16 960 | |||
| BIC | 17 064 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
The raw percentages in Obs_OR’s brackets are the
observed results everything in the model is derived from: “31% of black
americans are married, compared to 52% for white americans”.
- The plain difference from reference is colored (31% - 52% = -21 points).
- Cells that are non-significantly different from the reference are greyed out (based on a Newcombe confidence interval for differences of proportions).
The modelised odds-ratios Model_OR
directly compared to the observed odds-ratios
Obs_OR :
- Comparing the two tells you what adjustment the model did: if a predictor’s model OR is much closer to 1 than its crude OR, the raw association was largely explained by the other predictors.
- Here, “all things being unequal”, black americans have
1/2.43the odds of being married of white americans. “All things being equal” (more precisely : income, age and religion being equal), they still have1/2.40. The result holds, it’s not explained by income differences or religious differences.
The observed odds-ratios Obs_OR are the
same as those you can calculate from percentages only in a crosstable
:
- Colors and significance stars use a Woolf OR confidence interval that matches what is done in the regression model.
- The population of the table must match the complete-cases population
of the model, filtering out indivivuals with
NAat any involved variable.
gss_simple |>
dplyr::filter(dplyr::if_all(all_of(c("race", "age", "rincome", "relig")), ~ !is.na(.) )) |>
tab(race, married, pct = "row", na = "drop",
display = "{or}", ref = "first", color = "odds_ratio", color_signif = "grey_non_signif"
)| married | |||
|---|---|---|---|
| race | 01-Married | 02-Not married | Total |
| <row%-OR> | <n> | ||
| White | 1 | 1 | ( 9 846) |
| Black | 1/2.43 | 2.43 | ( 1 860) |
| Other | 1/1.12 | 1.12 | ( 1 254) |
| Total | 1/1.14 | 1.14 | (12 960) |
|
Odds ratio: OR ≥ 1.2;
1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly
different from the reference category (Wald interval on the log
odds-ratio, 95% confidence; Woolf closed form) or under the first colour
threshold (×1.2).
|
|||
Reading the same model in another measure
A logistic model works in odds ratios, but an odds ratio is a poor
thing to say out loud. You do not have to leave the model to get
something better — measure = asks for another one, and one
rule covers every case:
If the measure you ask for is the one the model already works in, you read the model’s own coefficient. If it is not, the model works your measure out from its predictions — for every person in the file — and averages them.
That second operation is what statisticians call a
marginal effect, and the column header says which you
got: an unmarked Model_OR is a coefficient, the little
m of Model_mRD means
worked out and averaged. Since measure left alone
gives the model’s own coefficient, every other measure you ask for is a
marginal effect.
Percentage points: measure = "difference"
The friendliest of the three, because points are the unit people
already think in. Model_mRD is the average marginal
effect: the model’s predicted probability computed for every
respondent as if they were at the level, then at the reference, and the
two averages subtracted.
tab_reg(gss_simple, "married", c("race", "age", "rincome", "relig"),
measure = "difference", empirical = TRUE)| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_RD | Model_mRD | |
| <n> | <(obs%) diff> | <diff (adj%)> | ||
| Constant | Population average | 12 960 | 48.7% | |
| race | White | 9 846 | (52%) 0% | 0% (51%) |
| Black | 1 860 | (31%) -21.2%*** | -19.8%*** (32%) | |
| Other | 1 254 | (49%) -2.7%* | +2.4% (54%) | |
| age | per 26.9 (2SD), at 42.4 (mean) | +17.6%*** | +13.6%*** | |
| rincome | 1-Lt $10000 | 2 142 | (37%) 0% | 0% (39%) |
| 2-$10000 to 14999 | 1 164 | (41%) +4.3%** | +3.0%* (42%) | |
| 3-$15000 to 24999 | 2 322 | (43%) +6.7%*** | +5.5%*** (45%) | |
| 4-$25000 or more | 7 332 | (55%) +18.5%*** | +14.5%*** (54%) | |
| relig | 1-Protestant | 6 269 | (52%) 0% | 0% (53%) |
| 2-Catholic | 3 121 | (52%) -0.6% | -3.7%*** (49%) | |
| 3-Other christian | 507 | (42%) -10.6%*** | -8.3%*** (45%) | |
| 4-Jewish | 222 | (55%) +2.3% | -6.0%* (47%) | |
| 5-Buddhist/Hinduist | 144 | (50%) -2.2% | -7.2%* (46%) | |
| 6-Muslim | 56 | (48%) -4.0% | +1.2% (54%) | |
| 7-Other | 267 | (35%) -17.4%*** | -16.4%*** (37%) | |
| 8-None | 2 374 | (38%) -14.0%*** | -13.5%*** (39%) | |
| Model fit | N | 12 960 | ||
| Dispersion (robust/model SE) | 1.03 | |||
| Collinearity (max VIF) | 1.08 | |||
| Influence (max dfbetas) | 0.19 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.057 | |||
| AIC | 16 960 | |||
| BIC | 17 064 | |||
|
Model: logistic regression; mRD: marginal risk difference (on adjusted
proportions, in percentage points, sample-averaged); obs%: observed
proportion; adj%: adjusted/predicted proportion.
Obs_RD, Model_mRD — RD ≥ +5; +10; +20; +30 points; RD ≤ -5; -10; -20; -30 points. Uncoloured: not significantly different from the reference category (Wald interval, 95% confidence) or under the first colour threshold (±5 points). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Read the race block: “comparing Black and White
respondents who are alike in income, age and religion, being Black is
associated with a marriage rate 19.8 points lower, on
average”. The crude Obs_RD beside it is the plain observed
difference of percentages, -21.2 — very close, so little of
that gap is explained by age, income or religion. The adjusted
percentages in the brackets say it the other way round: Black
respondents marry at 30.9% observed, and standardising
their income, age and religion to the population mix moves that only to
31.5%, against 51.3% if everyone were
White.
“How many times as likely?”: measure = "ratio"
An odds ratio is not a “times more likely”. The two
are close only when the outcome is rare; above roughly
10 %, which covers most survey questions, the odds ratio sits much
further from 1 than the ratio of probabilities. And an odds ratio is
non-collapsible: it moves when you add a covariate even if that
covariate is not a confounder, so comparing Model_OR across
nested models is not valid. Risk ratios do not have that problem.
tab_reg(gss_simple, "married", c("race", "age", "rincome", "relig"),
measure = "ratio", empirical = TRUE)| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_RR | Model_mRR | |
| <n> | <(obs%) ratio> | <ratio (adj%)> | ||
| Constant | Population average | 12 960 | 49% | |
| race | White | 9 846 | (52%) 1 | 1 (51%) |
| Black | 1 860 | (31%) ÷1.69*** | ÷1.63*** (32%) | |
| Other | 1 254 | (49%) ÷1.06* | ×1.05* (54%) | |
| age | per 26.9 (2SD), at 42.4 (mean) | ×1.36*** | ×1.28*** | |
| rincome | 1-Lt $10000 | 2 142 | (37%) 1 | 1 (39%) |
| 2-$10000 to 14999 | 1 164 | (41%) ×1.12** | ×1.08* (42%) | |
| 3-$15000 to 24999 | 2 322 | (43%) ×1.18*** | ×1.14*** (45%) | |
| 4-$25000 or more | 7 332 | (55%) ×1.50*** | ×1.37*** (54%) | |
| relig | 1-Protestant | 6 269 | (52%) 1 | 1 (53%) |
| 2-Catholic | 3 121 | (52%) ÷1.01 | ÷1.07*** (49%) | |
| 3-Other christian | 507 | (42%) ÷1.25*** | ÷1.19*** (45%) | |
| 4-Jewish | 222 | (55%) ×1.04 | ÷1.13* (47%) | |
| 5-Buddhist/Hinduist | 144 | (50%) ÷1.04 | ÷1.16 (46%) | |
| 6-Muslim | 56 | (48%) ÷1.08 | ×1.02 (54%) | |
| 7-Other | 267 | (35%) ÷1.50*** | ÷1.45*** (37%) | |
| 8-None | 2 374 | (38%) ÷1.37*** | ÷1.34*** (39%) | |
| Model fit | N | 12 960 | ||
| Dispersion (robust/model SE) | 1.03 | |||
| Collinearity (max VIF) | 1.08 | |||
| Influence (max dfbetas) | 0.19 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.057 | |||
| AIC | 16 960 | |||
| BIC | 17 064 | |||
|
Model: logistic regression; mRR: marginal risk ratio (the ratio of
adjusted proportions, sample-averaged); obs%: observed proportion; adj%:
adjusted/predicted proportion.
Obs_RR, Model_mRR — RR ≥ ×1.1; ×1.2; ×1.5; ×2; RR ≤ ÷1.1; ÷1.25; ÷2; ÷4. Uncoloured: not significantly different from the reference category (Wald interval on the log risk-ratio, 95% confidence; matching Katz interval on the observed column) or under the first colour threshold (×1.1). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Model_mRR reads plainly: Black respondents alike in
income, age and religion are about 1.6 times less
likely to be married. The cell is coherent by construction —
the reference probability divided by the risk ratio gives the level’s
probability (51% ÷ 1.6 ≈ 32%), the multiplicative
counterpart of the additive identity above.
The odds ratio, made comparable: measure = "odds_ratio"
on a prediction route
If your field publishes odds ratios you are not stuck with the
conditional one. Ask for the odds ratio of the two adjusted
predictions and you get a marginal odds ratio
(Model_mOR), which keeps the relative-effect reading while
behaving like a marginal effect across models (Karlson & Jann
2023):
tab_reg(gss_simple, "married", c("race", "age", "rincome", "relig"),
measure = "odds_ratio", effect = "marginal", empirical = TRUE)| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_mOR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Population average | 12 960 | 1/1.05 (49%) | |
| race | White | 9 846 | (52%) 1 | 1 (51%) |
| Black | 1 860 | (31%) 1/2.43*** | 1/2.29*** (32%) | |
| Other | 1 254 | (49%) 1/1.12* | 1.10 (54%) | |
| age | per 26.9 (2SD), at 42.4 (mean) | 2.07*** | 1.74*** | |
| rincome | 1-Lt $10000 | 2 142 | (37%) 1 | 1 (39%) |
| 2-$10000 to 14999 | 1 164 | (41%) 1.20** | 1.13* (42%) | |
| 3-$15000 to 24999 | 2 322 | (43%) 1.32*** | 1.26*** (45%) | |
| 4-$25000 or more | 7 332 | (55%) 2.12*** | 1.80*** (54%) | |
| relig | 1-Protestant | 6 269 | (52%) 1 | 1 (53%) |
| 2-Catholic | 3 121 | (52%) 1/1.03 | 1/1.16*** (49%) | |
| 3-Other christian | 507 | (42%) 1/1.53*** | 1/1.40*** (45%) | |
| 4-Jewish | 222 | (55%) 1.10 | 1/1.27* (47%) | |
| 5-Buddhist/Hinduist | 144 | (50%) 1/1.09 | 1/1.33* (46%) | |
| 6-Muslim | 56 | (48%) 1/1.17 | 1.05 (54%) | |
| 7-Other | 267 | (35%) 1/2.04*** | 1/1.95*** (37%) | |
| 8-None | 2 374 | (38%) 1/1.76*** | 1/1.73*** (39%) | |
| Model fit | N | 12 960 | ||
| Dispersion (robust/model SE) | 1.03 | |||
| Collinearity (max VIF) | 1.08 | |||
| Influence (max dfbetas) | 0.19 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.057 | |||
| AIC | 16 960 | |||
| BIC | 17 064 | |||
|
Model: logistic regression; mOR: marginal odds ratio (the odds ratio of
adjusted proportions, sample-averaged); obs%: observed proportion; adj%:
adjusted/predicted proportion.
Obs_OR, Model_mOR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
1/2.29 against the conditional 1/2.40 above
— a little closer to 1, which is the non-collapsibility showing. It
matters for one thing in particular: because it is collapsible,
color = "adjustment" tests the
crude-to-adjusted gap on this column, where on a conditional odds ratio
it can only colour it (see One warning about odds ratios
below). It is available on a binary outcome and on a summed score; a 3+
category outcome has to be asked “versus what?” first, so there it
exists only at effect = "at_reference".
⚠ You have to name it: "auto" never lands on a
predicted odds ratio, because it is a specialist quantity that
should be asked for rather than arrived at by accident.
The other risk ratio: link = "ratio"
Everything above changed what was reported, never what was fitted. One argument changes the model itself, and it gives the other risk ratio — the one epidemiologists mean:
tab_reg(gss_simple, "married", c("race", "age", "rincome", "relig"),
link = "ratio", empirical = TRUE)| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_RR | Model_RR | |
| <n> | <(obs%) ratio> | <ratio (adj%)> | ||
| Constant | Reference profile | 44% | ||
| race | White | 9 846 | (52%) 1 | 1 (51%) |
| Black | 1 860 | (31%) ÷1.69*** | ÷1.63*** (31%) | |
| Other | 1 254 | (49%) ÷1.06* | ×1.06* (54%) | |
| age | per 26.9 (2SD), at 42.4 (mean) | ×1.44*** | ×1.34*** | |
| rincome | 1-Lt $10000 | 2 142 | (37%) 1 | 1 (39%) |
| 2-$10000 to 14999 | 1 164 | (41%) ×1.12** | ×1.09* (42%) | |
| 3-$15000 to 24999 | 2 322 | (43%) ×1.18*** | ×1.16*** (45%) | |
| 4-$25000 or more | 7 332 | (55%) ×1.50*** | ×1.40*** (54%) | |
| relig | 1-Protestant | 6 269 | (52%) 1 | 1 (53%) |
| 2-Catholic | 3 121 | (52%) ÷1.01 | ÷1.07*** (49%) | |
| 3-Other christian | 507 | (42%) ÷1.25*** | ÷1.19*** (44%) | |
| 4-Jewish | 222 | (55%) ×1.04 | ÷1.13** (47%) | |
| 5-Buddhist/Hinduist | 144 | (50%) ÷1.04 | ÷1.16* (46%) | |
| 6-Muslim | 56 | (48%) ÷1.08 | ×1.03 (54%) | |
| 7-Other | 267 | (35%) ÷1.50*** | ÷1.46*** (36%) | |
| 8-None | 2 374 | (38%) ÷1.37*** | ÷1.35*** (39%) | |
| Model fit | N | 12 960 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.07 | |||
| Influence (max dfbetas) | 0.20 | |||
| Wald vs null | <0.01% | |||
|
Model: modified Poisson regression; RR: risk ratio (vs the reference
category); obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_RR, Model_RR — RR ≥ ×1.1; ×1.2; ×1.5; ×2; RR ≤ ÷1.1; ÷1.25; ÷2; ÷4. Uncoloured: not significantly different from the reference category (Wald interval on the log risk-ratio, 95% confidence; matching Katz interval on the observed column) or under the first colour threshold (×1.1). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Model_RR is unmarked, so it is a
coefficient: this is the modified Poisson regression (Zou
2004), whose coefficients are risk ratios directly. Compare it with the
Model_mRR above — they nearly agree here — and check which
model actually ran:
reg_formulas(tab_reg(gss_simple, "married", c("race", "age"), measure = "ratio"))#> # A tibble: 1 × 6
#> model outcome family link fit formula
#> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 01-Married: mRR married binomial odds_ratio "glm(binomial(\"logit\"))" marrie…
reg_formulas(tab_reg(gss_simple, "married", c("race", "age"), link = "ratio"))#> # A tibble: 1 × 6
#> model outcome family link fit formula
#> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 01-Married: RR married binomial ratio "svyglm(quasipoisson(\"log\"))" married…
Two columns answer it. link is the measure the model
estimates — odds_ratio for the first, so the ratio is
worked out from its predictions; ratio for the second,
which estimates it directly — and that word is what you type into
link =. fit is the R call:
glm(binomial("logit")) against
svyglm(quasipoisson("log")), a different model altogether.
A svyglm() row also means robust (Huber-White) standard
errors, which is exactly why a Poisson likelihood may be put on a 0/1
outcome.
Which to use. Prefer
measure = "ratio", the marginal one, in
almost every case: the logistic model always converges, can never
predict a probability above 100%, and the number it gives describes the
people you surveyed. Reach for
link = "ratio" when you need the
conditional risk ratio — the one that assumes the same ratio
holds for everybody, which is what a reader coming from epidemiology
expects, and what you must use to line up with a published
modified-Poisson estimate. The two are different estimands, not two
spellings of one.
For readers who want the technical detail: standard errors are
handled consistently in both cases. Fitting a Poisson likelihood to a
0/1 outcome is a deliberate misspecification, so the naive standard
errors would be too wide; tabxplor replaces them with the robust
Huber–White sandwich, via survey::svyglm()
— which means the design-based variance when you supply survey weights,
and the equivalent of HC0 otherwise. The observed companion
follows the same estimand throughout: Obs_RR is the crude
risk ratio with a Katz interval, never the crude odds ratio. The
modified Poisson needs a reasonable sample (n of at least about 100),
and since a quasi-likelihood has no true likelihood its footer reports N
and a Wald-vs-null test instead of AIC/BIC.
Linear regression (a numeric outcome)
A continuous outcome gives plain linear regression coefficients (here
we set family explicitly, because an integer like
age is ambiguous — it could also be modelled as a
count):
| age | ||||
|---|---|---|---|---|
| levels | n | Obs_diff | Model_diff | |
| <n> | <(obs mean) diff> | <diff (adj mean)> | ||
| Constant | Reference profile | 346 | 43.8 | |
| race | White | 9 846 | (43.3) 0 | 0 (42.8) |
| Black | 1 860 | (40.3) -3.0*** | -1.0*** (41.8) | |
| Other | 1 252 | (38.1) -5.3*** | -3.3*** (39.5) | |
| marital | Married | 6 315 | (44.9) 0 | 0 (44.5) |
| Separated | 446 | (41.9) -3.0*** | -2.0*** (42.5) | |
| Divorced | 2 152 | (47.6) +2.7*** | +2.9*** (47.4) | |
| Widowed | 401 | (59.9) +15.0*** | +15.2*** (59.7) | |
| Never married | 3 644 | (33.0) -11.9*** | -10.7*** (33.8) | |
| relig | 1-Protestant | 6 268 | (44.1) 0 | 0 (43.3) |
| 2-Catholic | 3 121 | (42.0) -2.2*** | -1.0*** (42.3) | |
| 3-Other christian | 507 | (39.2) -4.9*** | -3.2*** (40.0) | |
| 4-Jewish | 222 | (48.0) +3.9*** | +3.4*** (46.7) | |
| 5-Buddhist/Hinduist | 143 | (41.4) -2.7** | +0.5 (43.8) | |
| 6-Muslim | 56 | (38.7) -5.4*** | -2.9* (40.4) | |
| 7-Other | 267 | (38.9) -5.2*** | -3.5*** (39.8) | |
| 8-None | 2 374 | (38.9) -5.2*** | -2.8*** (40.5) | |
| rincome | 1-Lt $10000 | 2 142 | (38.5) 0 | 0 (40.3) |
| 2-$10000 to 14999 | 1 163 | (41.1) +2.6*** | +1.2*** (41.5) | |
| 3-$15000 to 24999 | 2 321 | (40.8) +2.3*** | +1.0*** (41.3) | |
| 4-$25000 or more | 7 332 | (44.2) +5.7*** | +3.2*** (43.5) | |
| Model fit | N | 12 958 | ||
| Dispersion (robust/model SE) | 1.18 | |||
| Collinearity (max VIF) | 1.09 | |||
| Influence (max dfbetas) | 0.27 | |||
| F | <0.01% | |||
| R2 | 0.256 | |||
| Adjusted R2 | 0.255 | |||
| Residual SD | 11.62 | |||
|
Model: linear regression; diff: mean difference (vs the reference
category); obs mean: observed mean; adj mean: adjusted/predicted
mean.
Obs_diff, Model_diff — diff ≥ +0.1; +0.2; +0.4; +0.8 SD; diff ≤ -0.1; -0.2; -0.4; -0.8 SD. Uncoloured: not significantly different from the reference category (Wald interval, 95% confidence; matching pooled OLS interval on the observed column) or under the first colour threshold (±0.1 SD). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
In the case of a linear regression, the empirical/observed counterpart of the model coefficient for a categorical predictor is simply the difference of means : here, the difference of mean age, per level of the predictor, compared to the reference level.
tab_reg(gss_simple, "age", c("race", "marital", "relig", "rincome"), family = "gaussian", empirical = TRUE)| age | ||||
|---|---|---|---|---|
| levels | n | Obs_diff | Model_diff | |
| <n> | <(obs mean) diff> | <diff (adj mean)> | ||
| Constant | Reference profile | 346 | 43.8 | |
| race | White | 9 846 | (43.3) 0 | 0 (42.8) |
| Black | 1 860 | (40.3) -3.0*** | -1.0*** (41.8) | |
| Other | 1 252 | (38.1) -5.3*** | -3.3*** (39.5) | |
| marital | Married | 6 315 | (44.9) 0 | 0 (44.5) |
| Separated | 446 | (41.9) -3.0*** | -2.0*** (42.5) | |
| Divorced | 2 152 | (47.6) +2.7*** | +2.9*** (47.4) | |
| Widowed | 401 | (59.9) +15.0*** | +15.2*** (59.7) | |
| Never married | 3 644 | (33.0) -11.9*** | -10.7*** (33.8) | |
| relig | 1-Protestant | 6 268 | (44.1) 0 | 0 (43.3) |
| 2-Catholic | 3 121 | (42.0) -2.2*** | -1.0*** (42.3) | |
| 3-Other christian | 507 | (39.2) -4.9*** | -3.2*** (40.0) | |
| 4-Jewish | 222 | (48.0) +3.9*** | +3.4*** (46.7) | |
| 5-Buddhist/Hinduist | 143 | (41.4) -2.7** | +0.5 (43.8) | |
| 6-Muslim | 56 | (38.7) -5.4*** | -2.9* (40.4) | |
| 7-Other | 267 | (38.9) -5.2*** | -3.5*** (39.8) | |
| 8-None | 2 374 | (38.9) -5.2*** | -2.8*** (40.5) | |
| rincome | 1-Lt $10000 | 2 142 | (38.5) 0 | 0 (40.3) |
| 2-$10000 to 14999 | 1 163 | (41.1) +2.6*** | +1.2*** (41.5) | |
| 3-$15000 to 24999 | 2 321 | (40.8) +2.3*** | +1.0*** (41.3) | |
| 4-$25000 or more | 7 332 | (44.2) +5.7*** | +3.2*** (43.5) | |
| Model fit | N | 12 958 | ||
| Dispersion (robust/model SE) | 1.18 | |||
| Collinearity (max VIF) | 1.09 | |||
| Influence (max dfbetas) | 0.27 | |||
| F | <0.01% | |||
| R2 | 0.256 | |||
| Adjusted R2 | 0.255 | |||
| Residual SD | 11.62 | |||
|
Model: linear regression; diff: mean difference (vs the reference
category); obs mean: observed mean; adj mean: adjusted/predicted
mean.
Obs_diff, Model_diff — diff ≥ +0.1; +0.2; +0.4; +0.8 SD; diff ≤ -0.1; -0.2; -0.4; -0.8 SD. Uncoloured: not significantly different from the reference category (Wald interval, 95% confidence; matching pooled OLS interval on the observed column) or under the first colour threshold (±0.1 SD). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
The empirical mean ages, and differences of mean ages from reference, can be computed in a simple table with :
tab(gss_simple, "race", "age", pct = "row", digits = 2, na = "drop",
color = "difference", ref = 1, ci_method = c(mean_diff = "ols")
) |>
mutate(diff = set_display(age, "diff"))| age | ||
|---|---|---|
| race | mean | diff |
| <mean (cv)> | <mean-diff> | |
| White | 48.72 (cv 36%) | ref:48.72 |
| Black | 43.90 (cv 37%) | -4.83 |
| Other | 39.48 (cv 36%) | -9.24 |
| Total | 47.18 (cv 37%) | -1.54 |
|
Standardized mean difference: cell ≥ the reference category (in bold)
+0.1; +0.2; +0.4; +0.8 SD; cell ≤ ref -0.1; -0.2; -0.4; -0.8 SD.
|
||
# ols : the variance pooled over every level of the variable, so the intervals are exactly those a
# linear regression puts on its coefficients ("student" pools the two compared groups only).Poisson regression (a count outcome)
| tvhours | ||||
|---|---|---|---|---|
| levels | n | Obs_IRR | Model_IRR | |
| <n> | <(obs mean) ratio> | <ratio (adj mean)> | ||
| Constant | Reference profile | 180 | 2.8 | |
| race | White | 5 138 | (2.4) 1 | 1 (2.4) |
| Black | 1 009 | (3.7) ×1.55*** | ×1.47*** (3.5) | |
| Other | 664 | (2.4) ×1.03 | ×1.01 (2.4) | |
| marital | Married | 3 230 | (2.4) 1 | 1 (2.5) |
| Separated | 230 | (2.9) ×1.21*** | ×1.06 (2.6) | |
| Divorced | 1 104 | (2.5) ×1.08*** | ×1.05* (2.6) | |
| Widowed | 222 | (2.9) ×1.24*** | ×1.11** (2.7) | |
| Never married | 2 025 | (2.8) ×1.19*** | ×1.07*** (2.7) | |
| relig | 1-Protestant | 3 237 | (2.7) 1 | 1 (2.6) |
| 2-Catholic | 1 618 | (2.5) ÷1.09*** | ×1.00 (2.7) | |
| 3-Other christian | 272 | (2.3) ÷1.18*** | ÷1.18*** (2.2) | |
| 4-Jewish | 109 | (2.1) ÷1.27*** | ÷1.11 (2.4) | |
| 5-Buddhist/Hinduist | 77 | (1.9) ÷1.40*** | ÷1.26** (2.1) | |
| 6-Muslim | 35 | (1.9) ÷1.44** | ÷1.58*** (1.7) | |
| 7-Other | 140 | (2.6) ÷1.03 | ÷1.02 (2.6) | |
| 8-None | 1 323 | (2.4) ÷1.15*** | ÷1.11*** (2.4) | |
| rincome | 1-Lt $10000 | 1 152 | (3.1) 1 | 1 (3.0) |
| 2-$10000 to 14999 | 663 | (3.0) ÷1.03 | ÷1.04 (2.9) | |
| 3-$15000 to 24999 | 1 158 | (2.8) ÷1.09*** | ÷1.10*** (2.8) | |
| 4-$25000 or more | 3 838 | (2.2) ÷1.39*** | ÷1.34*** (2.3) | |
| Model fit | N | 6 811 | ||
| Dispersion (robust/model SE) | 1.38 | |||
| Pearson dispersion (phi) | 1.45 | |||
| Collinearity (max VIF) | 1.11 | |||
| Influence (max dfbetas) | 0.68 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.033 | |||
| AIC | 26 179 | |||
| BIC | 26 295 | |||
|
Model: Poisson regression; IRR: incidence-rate ratio (vs the reference
category); obs mean: observed mean; adj mean: adjusted/predicted
mean.
Obs_IRR, Model_IRR — IRR ≥ ×1.1; ×1.2; ×1.5; ×2; IRR ≤ ÷1.1; ÷1.2; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log rate-ratio, 95% confidence; matching quasi-Poisson interval on the observed column) or under the first colour threshold (×1.1). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
An incidence-rate ratio (IRR) of 1.5 means “50% more
hours of TV per day”. Unweighted Poisson models automatically use
dispersion-scaled (quasi-Poisson) standard errors, so over-dispersed
counts get honest, wider intervals. Concretely: with an over-dispersed
outcome, family = "poisson" returns CIs and p-values
identical to family = "quasipoisson" and
emits a warning saying so (the footer reports the dispersion); at
equidispersion (≈ 1) the scaling is a no-op and the result matches a
plain glm(family = poisson) — so a comparison to a hand-fit
Poisson glm never surprises you.
In the case of a poisson regression, the empirical/observed counterpart of the model’s exponentiated coefficient for a categorical predictor is the ratio of means : here, the ratio of average TV hours compared to the reference level.
tab_reg(gss_simple, "tvhours", c("race", "marital", "relig", "rincome"), family = "poisson", empirical = TRUE)| tvhours | ||||
|---|---|---|---|---|
| levels | n | Obs_IRR | Model_IRR | |
| <n> | <(obs mean) ratio> | <ratio (adj mean)> | ||
| Constant | Reference profile | 180 | 2.8 | |
| race | White | 5 138 | (2.4) 1 | 1 (2.4) |
| Black | 1 009 | (3.7) ×1.55*** | ×1.47*** (3.5) | |
| Other | 664 | (2.4) ×1.03 | ×1.01 (2.4) | |
| marital | Married | 3 230 | (2.4) 1 | 1 (2.5) |
| Separated | 230 | (2.9) ×1.21*** | ×1.06 (2.6) | |
| Divorced | 1 104 | (2.5) ×1.08*** | ×1.05* (2.6) | |
| Widowed | 222 | (2.9) ×1.24*** | ×1.11** (2.7) | |
| Never married | 2 025 | (2.8) ×1.19*** | ×1.07*** (2.7) | |
| relig | 1-Protestant | 3 237 | (2.7) 1 | 1 (2.6) |
| 2-Catholic | 1 618 | (2.5) ÷1.09*** | ×1.00 (2.7) | |
| 3-Other christian | 272 | (2.3) ÷1.18*** | ÷1.18*** (2.2) | |
| 4-Jewish | 109 | (2.1) ÷1.27*** | ÷1.11 (2.4) | |
| 5-Buddhist/Hinduist | 77 | (1.9) ÷1.40*** | ÷1.26** (2.1) | |
| 6-Muslim | 35 | (1.9) ÷1.44** | ÷1.58*** (1.7) | |
| 7-Other | 140 | (2.6) ÷1.03 | ÷1.02 (2.6) | |
| 8-None | 1 323 | (2.4) ÷1.15*** | ÷1.11*** (2.4) | |
| rincome | 1-Lt $10000 | 1 152 | (3.1) 1 | 1 (3.0) |
| 2-$10000 to 14999 | 663 | (3.0) ÷1.03 | ÷1.04 (2.9) | |
| 3-$15000 to 24999 | 1 158 | (2.8) ÷1.09*** | ÷1.10*** (2.8) | |
| 4-$25000 or more | 3 838 | (2.2) ÷1.39*** | ÷1.34*** (2.3) | |
| Model fit | N | 6 811 | ||
| Dispersion (robust/model SE) | 1.38 | |||
| Pearson dispersion (phi) | 1.45 | |||
| Collinearity (max VIF) | 1.11 | |||
| Influence (max dfbetas) | 0.68 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.033 | |||
| AIC | 26 179 | |||
| BIC | 26 295 | |||
|
Model: Poisson regression; IRR: incidence-rate ratio (vs the reference
category); obs mean: observed mean; adj mean: adjusted/predicted
mean.
Obs_IRR, Model_IRR — IRR ≥ ×1.1; ×1.2; ×1.5; ×2; IRR ≤ ÷1.1; ÷1.2; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log rate-ratio, 95% confidence; matching quasi-Poisson interval on the observed column) or under the first colour threshold (×1.1). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
The empirical average TV hours per day, and differences of rate of television watching from reference, can be computed in a simple table with :
tab(gss_simple, "race", "tvhours", pct = "row", digits = 2, na = "drop",
color = "ratio", ref = 1, ci_method = c(mean_ratio = "quasipoisson")
) |>
mutate(IRR = set_display(tvhours, "ratio"))| tvhours | ||
|---|---|---|
| race | mean | IRR |
| <mean (cv)> | <mean-ratio> | |
| White | 2.77 (cv 84%) | 1 |
| Black | 4.18 (cv 84%) | ×1.51 |
| Other | 2.76 (cv 87%) | ×1.00 |
| Total | 2.98 (cv 87%) | ×1.08 |
|
Ratio of means: cell ≥ the reference category (in bold) ×1.1; ×1.2; ×1.5; ×2; cell ≤ ref ÷1.1; ÷1.2; ÷1.5; ÷2.
|
||
# the default method is a robust ratio of means (each group's own variance) ;
# we use "quasipoisson" to match those computed by quasi-poisson regression -- one dispersion
# estimated over every level (assumption : variance is proportional to mean). Grouped-binomial outcomes (a summed score)
When the outcome is a summed score — how many of
several yes/no items a respondent answered one way — you model the
number of “successes” out of a fixed number of items with
trials =. tab_reg() then fits
cbind(score, trials - score) as a binomial, so the odds
ratios read on the per-item probability.
This is the natural model for a multiple-answer survey
question — here the tea data prepared above, its
six “where do you drink tea?” items summed into one score by
score_from_lv1().
tab_reg(tea, "tea_where", c("sex", "SPC", "Sport"),
family = "binomial", trials = length(tea_where_vars))| tea_where | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs mean) OR> | <OR (adj mean)> | ||
| Constant | Reference profile | 16 | 1/1.22 (2.7) | |
| sex | F | 178 | (2.8) 1 | 1 (2.8) |
| M | 122 | (2.3) 1/1.36*** | 1/1.44*** (2.3) | |
| SPC | employee | 59 | (2.5) 1 | 1 (2.5) |
| middle | 40 | (2.5) 1/1.04 | 1.01 (2.5) | |
| non-worker | 64 | (2.4) 1/1.12 | 1/1.14 (2.3) | |
| other worker | 20 | (2.9) 1.29 | 1.31 (2.9) | |
| senior | 35 | (2.9) 1.25 | 1.44** (3.0) | |
| student | 70 | (2.7) 1.14 | 1.11 (2.6) | |
| workman | 12 | (2.2) 1/1.21 | 1/1.08 (2.4) | |
| Sport | sportsman | 179 | (2.6) 1 | 1 (2.6) |
| Not sportsman | 121 | (2.6) 1.02 | 1.02 (2.6) | |
| Model fit | N | 300 | ||
| Dispersion (robust/model SE) | 1.04 | |||
| Pearson dispersion (phi) | 1.02 | |||
| Collinearity (max VIF) | 1.13 | |||
| Influence (max dfbetas) | 0.46 | |||
| LR vs null | 0.99% | |||
| McFadden R2 | 0.021 | |||
| AIC | 971 | |||
| BIC | 1 004 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Each odds ratio now reads for any one place:
1/1.44 for men means that, for any of the six places, men
are about 1.44 times less likely than women to drink tea there. The
model treats the six items as interchangeable trials, so one odds ratio
covers them all — which is exactly the assumption to keep in mind before
using it.
Grouped-binomial (like Poisson) models report a Pearson dispersion check in the footer, flagging over-dispersed counts.
Ordinal and nominal outcomes (a factor with 3+ levels)
An ordered factor outcome is fit as a
proportional-odds (cumulative) logistic model, and it reports in
one column: its whole claim is that one number per
predictor level is enough, so it never spends a column per outcome
category. By default that number is the cumulative odds ratio
cumOR — the model’s own coefficient, and the one measure
the proportional-odds assumption makes the same at every cut:
| rincome | ||||
|---|---|---|---|---|
| levels | n | Obs_cumOR | Model_cumOR | |
| <n> | <OR> | <OR> | ||
| Constant | Reference profile | |||
| race | White | 9 846 | 1 | |
| Black | 1 860 | 1/1.55*** | 1/1.40*** | |
| Other | 1 254 | 1/1.51*** | 1/1.45*** | |
| age | per 34.6 (2SD), at 47.2 (mean) | 2.38*** | 2.28*** | |
| relig | 1-Protestant | 6 269 | 1 | |
| 2-Catholic | 3 121 | 1.09** | 1.15*** | |
| 3-Other christian | 507 | 1/1.10 | 1.02 | |
| 4-Jewish | 222 | 2.33*** | 2.00*** | |
| 5-Buddhist/Hinduist | 144 | 1.93*** | 2.41*** | |
| 6-Muslim | 56 | 1.01 | 1.32 | |
| 7-Other | 267 | 1/1.06 | 1.09 | |
| 8-None | 2 374 | 1/1.05 | 1.05 | |
| Model fit | N | 12 960 | ||
| Proportionality (Brant) | 0.89% | |||
| Dispersion (robust/model SE) | 1.16 | |||
| Collinearity (max VIF) | 1.07 | |||
| Influence (max dfbetas) | 0.23 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.017 | |||
| AIC | 29 193 | |||
| BIC | 29 290 | |||
|
Model: ordinal logistic regression; cumOR: cumulative odds ratio
(proportional-odds model); sup%: probability of superiority, 50 % being
a coin flip.
rincome, from low to high: 1-Lt $10000 (17%) < 2-$10000 to 14999 (9%) < 3-$15000 to 24999 (18%) < 4-$25000 or more (57%). Obs_cumOR, Model_cumOR — cumOR ≥ 1.2; 1.5; 2; 4; cumOR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log scale, 95% confidence) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
A cumulative odds ratio is hard to say out loud, so ask for another
measure of deviation and the same fit is read as a
probability. measure = "difference" gives Somers’
D — of two people, one from this group and one from the
reference group, how often does the one from this group end up higher on
the scale? — with that probability itself in brackets, 50 % being a
coin flip:
tab_reg(gss_simple, "rincome", c("race", "age", "relig"),
measure = "difference", display = "est_base")| rincome | ||||
|---|---|---|---|---|
| levels | n | Obs_D | Model_mD | |
| <n> | <(sup%) diff> | <diff (sup%)> | ||
| Constant | Population average | 12 960 | ||
| race | White | 9 846 | (50%) 0% | 0% (50%) |
| Black | 1 860 | (44%) -12.2%*** | -9.1%*** (45%) | |
| Other | 1 254 | (44%) -11.2%*** | -10.0%*** (45%) | |
| age | per 34.6 (2SD), at 47.2 (mean) | +19.6%*** | +18.7%*** | |
| relig | 1-Protestant | 6 269 | (50%) 0% | 0% (50%) |
| 2-Catholic | 3 121 | (51%) +2.3%** | +3.6%*** (52%) | |
| 3-Other christian | 507 | (49%) -2.7% | +0.5% (50%) | |
| 4-Jewish | 222 | (60%) +19.3%*** | +16.5%*** (58%) | |
| 5-Buddhist/Hinduist | 144 | (58%) +15.6%*** | +20.2%*** (60%) | |
| 6-Muslim | 56 | (50%) +0.3% | +7.1% (54%) | |
| 7-Other | 267 | (49%) -1.6% | +2.2% (51%) | |
| 8-None | 2 374 | (49%) -1.4% | +1.3% (51%) | |
| Model fit | N | 12 960 | ||
| Proportionality (Brant) | 0.89% | |||
| Dispersion (robust/model SE) | 1.16 | |||
| Collinearity (max VIF) | 1.07 | |||
| Influence (max dfbetas) | 0.23 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.017 | |||
| AIC | 29 193 | |||
| BIC | 29 290 | |||
|
Model: ordinal logistic regression; mD: marginal Somers’ D (how often
someone from this group ends up higher than someone from the reference
group, as a difference in percentage points, wins minus losses;
sample-averaged); sup%: probability of superiority, 50 % being a coin
flip.
rincome, from low to high: 1-Lt $10000 (17%) < 2-$10000 to 14999 (9%) < 3-$15000 to 24999 (18%) < 4-$25000 or more (57%). Obs_D, Model_mD — D ≥ +5; +10; +20; +30 points; D ≤ -5; -10; -20; -30 points. Uncoloured: not significantly different from the reference category (Wald interval, 95% confidence) or under the first colour threshold (±5 points). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
measure = "ratio" reads the same pair multiplicatively,
as a win ratio (wins to losses). Both still take one
column, because both read the whole predicted distribution rather than
one slice of it — and both are robust where the cumulative odds ratio is
not: they barely move when the proportional-odds assumption fails, and,
unlike an odds ratio, they do not drift under adjustment when there is
nothing to adjust for. The footer names the scale from low to high,
since a one-column table shows the outcome’s name and none of its
categories.
If you want a number per outcome category — a
percentage-point effect on each income band — that is a question the
ordering does not help with, and family = "multinomial"
answers it without assuming proportional odds.
A nominal outcome with three or more unordered levels is fit as one multinomial logistic model, giving one odds-ratio column per outcome category versus the reference category (also called relative risks ratios) :
| party3: OR | ||||
|---|---|---|---|---|
| levels | n |
2-Independent, other vs 1-Democrat |
3-Republican vs 1-Democrat |
|
| <n> | <OR (adj%)> | |||
| Constant | Reference profile | 1/1.52*** (40%) | 1.53*** (61%) | |
| race | White | 9 821 | 1 (21%) | 1 (41%) |
| Black | 1 850 | 1/3.03*** (14%) | 1/12.89*** ( 7%) | |
| Other | 1 243 | 1.05 (27%) | 1/2.37*** (23%) | |
| age | per 26.9 (2SD), at 42.4 (mean) | 1/1.37*** | 1/1.14*** | |
| rincome | 1-Lt $10000 | 2 128 | 1 (25%) | 1 (31%) |
| 2-$10000 to 14999 | 1 156 | 1.01 (26%) | 1/1.21** (28%) | |
| 3-$15000 to 24999 | 2 317 | 1.00 (25%) | 1/1.08 (30%) | |
| 4-$25000 or more | 7 313 | 1/1.53*** (17%) | 1.17** (37%) | |
| relig | 1-Protestant | 6 253 | 1 (17%) | 1 (44%) |
| 2-Catholic | 3 105 | 1/1.11 (20%) | 1/1.78*** (32%) | |
| 3-Other christian | 506 | 1.13 (21%) | 1/1.27** (37%) | |
| 4-Jewish | 222 | 1/2.96*** (11%) | 1/4.88*** (18%) | |
| 5-Buddhist/Hinduist | 140 | 1/1.33 (22%) | 1/4.83*** (16%) | |
| 6-Muslim | 55 | 1/2.36** (14%) | 1/5.96*** (15%) | |
| 7-Other | 265 | 1.10 (25%) | 1/2.56*** (24%) | |
| 8-None | 2 368 | 1.22*** (29%) | 1/3.54*** (18%) | |
| Model fit | N | 12 914 | ||
| Dispersion (robust/model SE) | 1.05 | |||
| Influence (max dfbetas) | 0.47 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.083 | |||
| AIC | 24 939 | |||
| BIC | 25 148 | |||
|
Model: multinomial logistic regression; OR: odds ratio (each category vs
the reference); adj%: adjusted/predicted proportion.
OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Relative risk ratios may be quite difficult to read because they are relative to two reference levels : not only the reference chosen for the predictor, but also the reference level chosen for the dependent variable. It’s specially difficult when it’s difficult to find a good reference level corresponding to the most common situation (like “married” for matrimonial status).
Most of the time, asking for percentage points instead is easier to
interpret, because it makes the second reference level disappear and
modelises, for each level of the outcome, the difference of percentages
of each predictor level compared to its reference (a less abstract
quantity than odds ratios). It is measure = "difference",
exactly as on a binary outcome, and the resulting mRD
columns are average marginal effects:
tab_reg(gss_simple, "party3", c("race", "age", "rincome", "relig"), measure = "difference", empirical = TRUE) # |> tab_export()| party3: mRD | |||||
|---|---|---|---|---|---|
| levels | n | 1-Democrat |
2-Independent, other |
3-Republican | |
| <n> | <diff (adj%)> | ||||
| Constant | Population average | 12 914 | 45.3% | 20.4% | 34.3% |
| race | White | 9 821 | 0% (38%) | 0% (21%) | 0% (41%) |
| Black | 1 850 | +40.6%*** (79%) | -6.6%*** (14%) | -34.0%*** ( 7%) | |
| Other | 1 243 | +11.0%*** (49%) | +6.8%*** (27%) | -17.8%*** (23%) | |
| age | per 26.9 (2SD), at 42.4 (mean) | +4.5%*** | -3.8%*** | -0.7% | |
| rincome | 1-Lt $10000 | 2 128 | 0% (44%) | 0% (25%) | 0% (31%) |
| 2-$10000 to 14999 | 1 156 | +2.0% (46%) | +1.6% (26%) | -3.6%** (28%) | |
| 3-$15000 to 24999 | 2 317 | +0.9% (45%) | +0.5% (25%) | -1.4% (30%) | |
| 4-$25000 or more | 7 313 | +1.8% (46%) | -7.7%*** (17%) | +5.9%*** (37%) | |
| relig | 1-Protestant | 6 253 | 0% (39%) | 0% (17%) | 0% (44%) |
| 2-Catholic | 3 105 | +9.2%*** (48%) | +2.4%*** (20%) | -11.5%*** (32%) | |
| 3-Other christian | 506 | +2.4% (42%) | +3.7%** (21%) | -6.1%*** (37%) | |
| 4-Jewish | 222 | +31.4%*** (71%) | -6.0%*** (11%) | -25.4%*** (18%) | |
| 5-Buddhist/Hinduist | 140 | +23.1%*** (62%) | +4.4% (22%) | -27.5%*** (16%) | |
| 6-Muslim | 55 | +31.6%*** (71%) | -3.1% (14%) | -28.5%*** (15%) | |
| 7-Other | 265 | +11.7%*** (51%) | +8.0%*** (25%) | -19.6%*** (24%) | |
| 8-None | 2 368 | +13.4%*** (53%) | +11.9%*** (29%) | -25.4%*** (18%) | |
| Model fit | N | 12 914 | |||
| Dispersion (robust/model SE) | 1.05 | ||||
| Influence (max dfbetas) | 0.47 | ||||
| LR vs null | <0.01% | ||||
| McFadden R2 | 0.083 | ||||
| AIC | 24 939 | ||||
| BIC | 25 148 | ||||
|
Model: multinomial logistic regression; mRD: marginal risk difference
(on adjusted proportions, in percentage points, sample-averaged); adj%:
adjusted/predicted proportion.
RD ≥ +5; +10; +20; +30 points; RD ≤ -5; -10; -20; -30 points. Uncoloured: not significantly different from the reference category (Wald interval, 95% confidence) or under the first colour threshold (±5 points). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
|||||
A 3+ level outcome would need one crude column per outcome category,
so empirical = TRUE folds the crude effect
into the model cell instead —
+40.6% (+38.6%), modelled then observed — and the crude
percentages and differences appear in html tooltips, at mouse hover of a
cell. Ask for the columns anyway with
empirical = "column".
4. Reading what adjustment did
Colouring what adjustment did
empirical = TRUE puts the crude effect next to the
modelled one, but comparing them cell by cell is tedious on a real
table. color = "adjustment" colours the
gap between the two, so a whole table of “what did
adjusting change?” reads at a glance. Put it on the background
channel and the text keeps showing the effect size, so one look answers
both questions:
tab_reg(gss_simple, "married", c("race", "rincome", "relig"),
empirical = TRUE, color = c(TRUE, "adjustment"))| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 715 | 1/1.20*** (45%) | |
| race | White | 9 863 | (52%) 1 | 1 (52%) |
| Black | 1 866 | (31%) 1/2.44*** | 1/2.54*** (30%) | |
| Other | 1 256 | (49%) 1/1.11* | 1.01 (52%) | |
| rincome | 1-Lt $10000 | 2 146 | (37%) 1 | 1 (37%) |
| 2-$10000 to 14999 | 1 164 | (41%) 1.20** | 1.19** (42%) | |
| 3-$15000 to 24999 | 2 328 | (43%) 1.32*** | 1.32*** (44%) | |
| 4-$25000 or more | 7 347 | (55%) 2.12*** | 2.05*** (55%) | |
| relig | 1-Protestant | 6 282 | (52%) 1 | 1 (54%) |
| 2-Catholic | 3 127 | (52%) 1/1.03 | 1/1.22*** (49%) | |
| 3-Other christian | 508 | (42%) 1/1.53*** | 1/1.58*** (43%) | |
| 4-Jewish | 223 | (54%) 1.09 | 1/1.23 (49%) | |
| 5-Buddhist/Hinduist | 144 | (50%) 1/1.09 | 1/1.40* (46%) | |
| 6-Muslim | 56 | (48%) 1/1.17 | 1/1.02 (53%) | |
| 7-Other | 267 | (35%) 1/2.04*** | 1/2.26*** (35%) | |
| 8-None | 2 378 | (38%) 1/1.76*** | 1/1.99*** (38%) | |
| Model fit | N | 12 985 | ||
| Dispersion (robust/model SE) | 1.02 | |||
| Collinearity (max VIF) | 1.07 | |||
| Influence (max dfbetas) | 0.20 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.043 | |||
| AIC | 17 245 | |||
| BIC | 17 343 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Background colour: the observed effect (the reference for the adjustment). Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; Woolf closed form) or under the first colour threshold (×1.2). Part of an odds-ratio gap is non-collapsibility, not confounding: a risk ratio or a marginal effect is the collapsible comparison. Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Background colour: OR further from no effect (1) than the observed column, by ×1.1; ×1.25; ×1.5; ×2; OR closer to no effect (1) than the observed column (or inversed effect), by ÷1.1; ÷1.25; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence) or under the first colour threshold (×1.2). Part of an odds-ratio gap is non-collapsibility, not confounding: a risk ratio or a marginal effect is the collapsible comparison. ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Text colour = how strong the adjusted odds ratio is. Background = how
far it sits from the observed one: one pole means adjustment
strengthened the effect (it moved further from “no
effect”), the other that it attenuated it (it moved
closer). An effect that changed direction under adjustment —
crude above 1, modelled below it — counts as attenuated, and strongly
so: whatever the raw association said, the model says it is not that.
You can see the reversal itself in the pair of cells, whose
× and ÷ face opposite ways. The first
threshold is ×1.1, the classic “a 10% change in the estimate is worth
noticing” convention; the rest are ×1.25, ×1.5 and ×2 (change them with
set_color_breaks(adj_ratio = ...)). An additive
effect gets an additive ladder instead: percentage points for a marginal
effect on a probability (±2 / ±5 / ±10 / ±20), and standard
deviations of the outcome for a linear mean difference
(±0.05 / ±0.1 / ±0.2 / ±0.4) — so the same model reads the
same way whether the outcome is recorded in hours, minutes or days. You
do not have to ask for empirical separately — the colour
needs the observed effect, so it turns it on for you.
The direction is measured from the null, not up or down. A protective effect (OR below 1) that adjustment pulls toward 1 gets the same colour as a risky effect pulled toward 1, which is what you want to read: “the other variables explained part of this association” is one statement, whichever side of 1 the effect is on.
The same gap is readable as a number when you want the exact values,
in the cell or on hover. display = writes the template at
build time (part 5, Fine-tuning the display); on a finished
table set_display() does the same — the columns that have
no observed counterpart simply keep their plain display:
tab_reg(gss_simple, "married", c("race", "rincome"), empirical = TRUE) |>
set_display("{est} (obs {obs})")| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <OR (obs)> | <OR (obs)> | ||
| Constant | Reference profile | 1 506 | 1/1.51*** | |
| race | White | 9 879 | 1 | 1 (obs 1) |
| Black | 1 873 | 1/2.45*** | 1/2.33*** (obs 1/2.45) | |
| Other | 1 263 | 1/1.11* | 1/1.04 (obs 1/1.11) | |
| rincome | 1-Lt $10000 | 2 153 | 1 | 1 (obs 1) |
| 2-$10000 to 14999 | 1 168 | 1.21** | 1.21*** (obs 1.21) | |
| 3-$15000 to 24999 | 2 331 | 1.33*** | 1.33*** (obs 1.33) | |
| 4-$25000 or more | 7 363 | 2.13*** | 2.07*** (obs 2.13) | |
| Model fit | N | 13 015 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.00 | |||
| Influence (max dfbetas) | 0.04 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.031 | |||
| AIC | 17 483 | |||
| BIC | 17 528 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
One warning about odds ratios
An odds ratio changes when you adjust it even when there is nothing to adjust for. This is called non-collapsibility: it is a property of the odds ratio itself, not a sign of confounding. In a simulation where the extra variable is independent of the exposure — so there is strictly no confounding — the crude odds ratio still moved by about 8% once adjusted, while the risk ratio moved by 0.3% and the marginal effect by essentially nothing. Eight percent is about the size of the first colour step, so on the odds-ratio scale a faint background colour may be arithmetic rather than confounding. The legend of an exported table says so.
The comparisons that are clean are the ones the previous
sections introduced: measure = "difference" (percentage
points), measure = "ratio" (risk ratios),
link = "ratio" (the modified Poisson’s own risk ratio), the
mean difference of a linear regression — and, if you must publish odds
ratios, measure = "odds_ratio", effect = "marginal", whose
marginal odds ratio is collapsible. On those scales the gap is
confounding by the variables you added — nothing else. If reading
confounding is the point of your table, prefer one of them:
tab_reg(gss_simple, "married", c("race", "rincome", "relig"),
measure = "ratio", empirical = TRUE, color = c(TRUE, "adjustment"))| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_RR | Model_mRR | |
| <n> | <(obs%) ratio> | <ratio (adj%)> | ||
| Constant | Population average | 12 985 | 49% | |
| race | White | 9 863 | (52%) 1 | 1 (52%) |
| Black | 1 866 | (31%) ÷1.69*** | ÷1.71*** (30%) | |
| Other | 1 256 | (49%) ÷1.05* | ×1.00 (52%) | |
| rincome | 1-Lt $10000 | 2 146 | (37%) 1 | 1 (37%) |
| 2-$10000 to 14999 | 1 164 | (41%) ×1.12** | ×1.11** (42%) | |
| 3-$15000 to 24999 | 2 328 | (43%) ×1.18*** | ×1.17*** (44%) | |
| 4-$25000 or more | 7 347 | (55%) ×1.50*** | ×1.45*** (55%) | |
| relig | 1-Protestant | 6 282 | (52%) 1 | 1 (54%) |
| 2-Catholic | 3 127 | (52%) ÷1.01 | ÷1.10*** (49%) | |
| 3-Other christian | 508 | (42%) ÷1.26*** | ÷1.25*** (43%) | |
| 4-Jewish | 223 | (54%) ×1.04 | ÷1.10 (49%) | |
| 5-Buddhist/Hinduist | 144 | (50%) ÷1.04 | ÷1.17* (46%) | |
| 6-Muslim | 56 | (48%) ÷1.08 | ÷1.01 (53%) | |
| 7-Other | 267 | (35%) ÷1.50*** | ÷1.55*** (35%) | |
| 8-None | 2 378 | (38%) ÷1.37*** | ÷1.43*** (38%) | |
| Model fit | N | 12 985 | ||
| Dispersion (robust/model SE) | 1.02 | |||
| Collinearity (max VIF) | 1.07 | |||
| Influence (max dfbetas) | 0.20 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.043 | |||
| AIC | 17 245 | |||
| BIC | 17 343 | |||
|
Model: logistic regression; mRR: marginal risk ratio (the ratio of
adjusted proportions, sample-averaged); obs%: observed proportion; adj%:
adjusted/predicted proportion.
Obs_RR — RR ≥ ×1.1; ×1.2; ×1.5; ×2; RR ≤ ÷1.1; ÷1.25; ÷2; ÷4. Background colour: the observed effect (the reference for the adjustment). Uncoloured: not significantly different from the reference category (Wald interval on the log risk-ratio, 95% confidence; Katz closed form) or under the first colour threshold (×1.1). Model_mRR — RR ≥ ×1.1; ×1.2; ×1.5; ×2; RR ≤ ÷1.1; ÷1.25; ÷2; ÷4. Background colour: RR further from no effect (1) than the observed column, by ×1.1; ×1.25; ×1.5; ×2; RR closer to no effect (1) than the observed column (or inversed effect), by ÷1.1; ÷1.25; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log risk-ratio, 95% confidence) or under the first colour threshold (×1.1). Some rows carry no test and are left uncoloured. Background: the same rule, applied to the gap with the observed effect (z test on the difference between two estimates fitted on the same sample, 95% confidence). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Is the gap bigger than noise?
A background colour tells you the model moved an effect. It
does not, on its own, tell you the move is real: with 20 000 respondents
a tiny shift is solid, with 300 a large one may be luck.
color_signif answers that, exactly as it does everywhere
else in tabxplor — add "grey_non_signif" and a
coloured background now means “the model really changed this
effect”:
tab_reg(gss_simple, "married", c("race", "rincome", "relig"),
link = "ratio", empirical = TRUE,
color = c(TRUE, "adjustment"), color_signif = "grey_non_signif")| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_RR | Model_RR | |
| <n> | <(obs%) ratio> | <ratio (adj%)> | ||
| Constant | Reference profile | 715 | 44% | |
| race | White | 9 863 | (52%) 1 | 1 (52%) |
| Black | 1 866 | (31%) ÷1.69*** | ÷1.70*** (30%) | |
| Other | 1 256 | (49%) ÷1.05* | ×1.01 (52%) | |
| rincome | 1-Lt $10000 | 2 146 | (37%) 1 | 1 (37%) |
| 2-$10000 to 14999 | 1 164 | (41%) ×1.12** | ×1.11** (42%) | |
| 3-$15000 to 24999 | 2 328 | (43%) ×1.18*** | ×1.17*** (44%) | |
| 4-$25000 or more | 7 347 | (55%) ×1.50*** | ×1.46*** (54%) | |
| relig | 1-Protestant | 6 282 | (52%) 1 | 1 (54%) |
| 2-Catholic | 3 127 | (52%) ÷1.01 | ÷1.10*** (49%) | |
| 3-Other christian | 508 | (42%) ÷1.26*** | ÷1.26*** (43%) | |
| 4-Jewish | 223 | (54%) ×1.04 | ÷1.10 (49%) | |
| 5-Buddhist/Hinduist | 144 | (50%) ÷1.04 | ÷1.17* (46%) | |
| 6-Muslim | 56 | (48%) ÷1.08 | ÷1.01 (53%) | |
| 7-Other | 267 | (35%) ÷1.50*** | ÷1.55*** (35%) | |
| 8-None | 2 378 | (38%) ÷1.37*** | ÷1.43*** (38%) | |
| Model fit | N | 12 985 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.06 | |||
| Influence (max dfbetas) | 0.20 | |||
| Wald vs null | <0.01% | |||
|
Model: modified Poisson regression; RR: risk ratio (vs the reference
category); obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_RR — RR ≥ ×1.1; ×1.2; ×1.5; ×2; RR ≤ ÷1.1; ÷1.25; ÷2; ÷4. Background colour: the observed effect (the reference for the adjustment). Uncoloured: not significantly different from the reference category (Wald interval on the log risk-ratio, 95% confidence; Katz closed form) or under the first colour threshold (×1.1). Model_RR — RR ≥ ×1.1; ×1.2; ×1.5; ×2; RR ≤ ÷1.1; ÷1.25; ÷2; ÷4. Background colour: RR further from no effect (1) than the observed column, by ×1.1; ×1.25; ×1.5; ×2; RR closer to no effect (1) than the observed column (or inversed effect), by ÷1.1; ÷1.25; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log risk-ratio, 95% confidence) or under the first colour threshold (×1.1). Some rows carry no test and are left uncoloured. Background: the same rule, applied to the gap with the observed effect (z test on the difference between two estimates fitted on the same sample, 95% confidence). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Here link = "ratio" gives the modified Poisson’s own
risk ratios, one of the clean scales from the previous
section. Text colour = how strong the adjusted risk ratio is. Background
= how far it sits from the observed one, greyed out when that distance
could be chance. Hovering a cell in the html table gives the exact
numbers: the observed effect, the size of the gap, its confidence
interval and its p-value.
One example is worth the whole explanation. Read the
race block of that table, comparing each group with white
respondents, before and after holding income and religion equal:
| scale | Black vs White | Other vs White |
|---|---|---|
| odds ratio, crude → model |
1/2.44 → 1/2.54
|
1/1.11 → 1.01
|
| risk ratio, crude → model |
÷1.7 → ÷1.7
|
÷1.1 → ×1.0
|
| did the risk ratio really move? | no (p = 0.55) | yes (p < 0.001) |
Two different stories, and the test separates them. For Black respondents nothing moves: their much lower marriage rate is not explained by income or religion — it survives the adjustment untouched. For the “Other” group the crude 5% deficit disappears once income and religion are held equal, and the gap between the two numbers is far too big to be luck: that difference was explained by them.
Notice that the odds ratios move too — including for Black
respondents, where the risk ratio says nothing happened. That is the
non-collapsibility of the previous section, showing up as a phantom
“change”. It is why color_signif is not applied to
plain odds ratios: tab_reg() leaves those colours
descriptive and says so once, pointing at
measure = "difference", measure = "ratio" or
link = "ratio".
The greying earns its keep on smaller samples. With 20 000 respondents almost every visible move is real; on 2 000 the same table keeps one background colour out of three:
set.seed(1)
small <- gss_simple[sample(nrow(gss_simple), 2000), ]
tab_reg(small, "married", c("race", "rincome", "relig"),
link = "ratio", empirical = TRUE,
color = c(TRUE, "adjustment"), color_signif = "grey_non_signif")| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_RR | Model_RR | |
| <n> | <(obs%) ratio> | <ratio (adj%)> | ||
| Constant | Reference profile | 73 | 47% | |
| race | White | 936 | (52%) 1 | 1 (52%) |
| Black | 166 | (34%) ÷1.52*** | ÷1.53*** (34%) | |
| Other | 112 | (54%) ×1.04 | ×1.05 (55%) | |
| rincome | 1-Lt $10000 | 201 | (41%) 1 | 1 (42%) |
| 2-$10000 to 14999 | 106 | (41%) ÷1.02 | ×1.00 (42%) | |
| 3-$15000 to 24999 | 237 | (45%) ×1.09 | ×1.09 (45%) | |
| 4-$25000 or more | 670 | (56%) ×1.36*** | ×1.33*** (55%) | |
| relig | 1-Protestant | 584 | (52%) 1 | 1 (54%) |
| 2-Catholic | 275 | (57%) ×1.08 | ÷1.01 (54%) | |
| 3-Other christian | 52 | (38%) ÷1.36* | ÷1.38* (39%) | |
| 4-Jewish | 18 | (61%) ×1.17 | ÷1.02 (53%) | |
| 5-Buddhist/Hinduist | 18 | (61%) ×1.17 | ×1.03 (56%) | |
| 6-Muslim | 5 | (60%) ×1.15 | ×1.04 (56%) | |
| 7-Other | 24 | (46%) ÷1.14 | ÷1.22 (45%) | |
| 8-None | 238 | (38%) ÷1.39*** | ÷1.46*** (37%) | |
| Model fit | N | 1 214 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.11 | |||
| Influence (max dfbetas) | 0.70 | |||
| Wald vs null | <0.01% | |||
|
Model: modified Poisson regression; RR: risk ratio (vs the reference
category); obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_RR — RR ≥ ×1.1; ×1.2; ×1.5; ×2; RR ≤ ÷1.1; ÷1.25; ÷2; ÷4. Background colour: the observed effect (the reference for the adjustment). Uncoloured: not significantly different from the reference category (Wald interval on the log risk-ratio, 95% confidence; Katz closed form) or under the first colour threshold (×1.1). Model_RR — RR ≥ ×1.1; ×1.2; ×1.5; ×2; RR ≤ ÷1.1; ÷1.25; ÷2; ÷4. Background colour: RR further from no effect (1) than the observed column, by ×1.1; ×1.25; ×1.5; ×2; RR closer to no effect (1) than the observed column (or inversed effect), by ÷1.1; ÷1.25; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log risk-ratio, 95% confidence) or under the first colour threshold (×1.1). Some rows carry no test and are left uncoloured. Background: the same rule, applied to the gap with the observed effect (z test on the difference between two estimates fitted on the same sample, 95% confidence). ***: significantly different from the reference category (in bold) at the 99% confidence level; **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
For the record: what exactly is tested
The "adjustment" colour always shows the
size of the gap. Whether it also tests it
depends on one property of the measure, and the table decides for
you:
| what you asked for | gap tested? |
|---|---|
measure = "difference" — any outcome |
yes |
measure = "ratio", or link = "ratio" (risk
ratios) |
yes |
measure = "odds_ratio", effect = "marginal" (binary or
summed score) |
yes |
| Poisson incidence-rate ratios, a linear mean difference | yes |
| conditional odds ratios — binomial, multinomial, ordinal cumulative | no (non-collapsible) |
a compound formula =
|
no |
| a weighted 3+ level outcome | it can only be read on its coefficients |
Where there is no test, color_signif is ignored for that
channel and the colours read descriptively — the table never pretends to
a significance it does not have.
The null is that the modelled and observed effects are equal, on the effect’s own scale: the log-ratio for a risk ratio, an odds ratio or an incidence-rate ratio, the plain difference for a mean difference or a marginal effect in points. That is the scale the colour already folds around, so the test and the colour cannot drift apart.
The two estimates come from the same rows, so they are correlated and
their difference has a smaller standard error than either alone. The
correct variance is the sampling variance of the difference of their
influence functions — what Stata calls seemingly-unrelated
estimation (Weesie 1999; Mize, Doan & Long 2019 is the sociological
statement). Two things make it exact rather than approximate here: every
observed effect tabxplor prints is the coefficient
of a saturated one-predictor model, so its influence function is a
closed-form expression whose standard error is the interval already
shown; and with survey weights or a design the variance goes through
survey’s own linearization, so strata, clusters and
finite-population corrections are respected. The reference distribution
is the normal (z), which is mildly conservative in small samples.
The three policies read that same interval:
color_signif |
what the background shows |
|---|---|
"ignore" (default in tab()) |
the size of the gap, coloured whether or not it is significant |
"grey_non_signif" (default in
tab_reg()) |
the same, greyed unless the gap’s interval excludes “no change” |
"guaranteed_effect" |
the floor of the gap: “adjustment moved this effect by at least ×1.1” (on a difference scale, “by at least 2 points”) |
Stars and colour do not say the same thing, and the combination is the useful part. The stars keep reading each estimate’s own p-value — “is this effect different from 1?” — while the colour reads the gap’s. All four combinations occur in one small table:
| level | stars | p (effect) | p (gap) | coloured |
|---|---|---|---|---|
| Black | *** | < 0.0001 | 0.55 | no — the gap is far below the first threshold |
| Jewish | — | 0.12 | < 0.0001 | yes |
| Muslim | — | 0.92 | 0.12 | no |
| Buddhist/Hinduist | * | 0.057 | < 0.0001 | yes |
Read starred and uncoloured as a robust effect — adjustment barely touched it — and unstarred and coloured as an effect that only the crude table showed. Jewish is the instructive row: no effect worth reporting, but adjustment moved it a lot, and significantly.
Two more details. The direction is measured from the null, so a
protective effect pulled toward 1 colours like a risky one pulled toward
1. And the test needs both estimates computed on the same people: at
effect = "at_reference", or under
na = "drop_by_model" when a compared model drops different
missing rows than the observed columns, the comparison is not made at
all. (By default it always is — see just below.)
5. Shaping the table
Fine-tuning the display, and scaling a predictor
A few arguments change what each cell shows, or how a predictor is scaled — without changing the model itself.
display chooses the cell layout — the
same named layouts tab() offers, in the same
{} grammar. "est_ci" shows the confidence
interval beside every estimate (any family):
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <OR ci> | <OR ci> | ||
| Constant | Reference profile | 1.02 [1/1.01;1.05] | ||
| race | White | 16 341 | 1 | 1 |
| Black | 3 115 | 1/2.68*** [1/2.91;1/2.46] | 1/2.58*** [1/2.81;1/2.37] | |
| Other | 1 951 | 1/1.14*** [1/1.25;1/1.04] | 1/1.05 [1/1.16;1.04] | |
| age | per 34.6 (2SD), at 47.2 (mean) | 1.40*** [1.33;1.48] | 1.34*** [1.27;1.41] | |
| Model fit | N | 21 407 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.03 | |||
| Influence (max dfbetas) | 0.03 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.023 | |||
| AIC | 28 933 | |||
| BIC | 28 965 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference
category).
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
"est_base" folds the model-adjusted
prediction into the estimate cell — an adjusted probability on
a logistic model, an adjusted mean on a linear or a count one, each
column answering with its own quantity. "base_est" swaps
them, so the table reads as adjusted predictions graded by the effect,
and "base" shows the predictions alone:
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1.02 (50%) | ||
| race | White | 16 341 | (51%) 1 | 1 (50%) |
| Black | 3 115 | (28%) 1/2.68*** | 1/2.58*** (28%) | |
| Other | 1 951 | (48%) 1/1.14*** | 1/1.05 (49%) | |
| age | per 34.6 (2SD), at 47.2 (mean) | 1.40*** | 1.34*** | |
| Model fit | N | 21 407 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.03 | |||
| Influence (max dfbetas) | 0.03 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.023 | |||
| AIC | 28 933 | |||
| BIC | 28 965 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Note the difference: display = "est_base" keeps
the odds-ratio column and adds the adjusted probability beside it,
whereas measure = "difference" (seen above) turns the
whole column into a marginal effect in points.
multiplier chooses the
unit a continuous predictor’s effect is reported per.
One unit is rarely a readable amount: a one-year change in
age barely moves the odds, so its odds ratio sits near 1,
never crosses a colour threshold, and the row reads as “no effect” —
while a whole standard deviation of age multiplies the odds by about
0.66. So the default is per two standard deviations,
and the row says which (per 34.6 (2SD)) — the variable is
named in the column beside it. Two standard deviations is roughly the
span of a binary predictor, which is the point: it puts a continuous row
and a two-level row on the same footing, so a whole table can be read
down its column:
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1.02 (50%) | ||
| race | White | 16 341 | (51%) 1 | 1 (50%) |
| Black | 3 115 | (28%) 1/2.68*** | 1/2.58*** (28%) | |
| Other | 1 951 | (48%) 1/1.14*** | 1/1.05 (49%) | |
| age | per 34.6 (2SD), at 47.2 (mean) | 1.40*** | 1.34*** | |
| Model fit | N | 21 407 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.03 | |||
| Influence (max dfbetas) | 0.03 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.023 | |||
| AIC | 28 933 | |||
| BIC | 28 965 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Pass a single value to change it for every continuous predictor, or a
named vector to override chosen ones — "sd",
"2sd" (roughly bottom to top of the distribution), or a
number of units. multiplier = 1 gives the raw per-one-unit
effect:
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1.02 (50%) | ||
| race | White | 16 341 | (51%) 1 | 1 (50%) |
| Black | 3 115 | (28%) 1/2.68*** | 1/2.58*** (28%) | |
| Other | 1 951 | (48%) 1/1.14*** | 1/1.05 (49%) | |
| age | per 10, at 47.2 (mean) | 1.10*** | 1.09*** | |
| Model fit | N | 21 407 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.03 | |||
| Influence (max dfbetas) | 0.03 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.023 | |||
| AIC | 28 933 | |||
| BIC | 28 965 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Everything scales together — the estimate, its interval, the observed
Obs_* companion and the model-versus-observed comparison —
and the p-value never changes. Two things worth knowing: because the
default is not 1, a continuous predictor’s Model_OR will
not match exp(coef(glm(...))) unless you
ask for multiplier = 1; and the standard deviation is
measured once, on the predictors’ complete cases, so the same variable
keeps the same unit across several outcomes, compared models and
tab_vars groups.
The reference a predictor’s effect is measured from
ref names the reference — for a factor,
the level the others are compared against; for a continuous predictor,
the value it is anchored at:
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1/2.69*** (27%) | ||
| race | Black | 3 115 | (28%) 1 | 1 (28%) |
| White | 16 341 | (51%) 2.68*** | 2.58*** (50%) | |
| Other | 1 951 | (48%) 2.35*** | 2.45*** (49%) | |
| age | per 34.6 (2SD), at 40 | 1.40*** | 1.34*** | |
| Model fit | N | 21 407 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.03 | |||
| Influence (max dfbetas) | 0.03 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.023 | |||
| AIC | 28 933 | |||
| BIC | 28 965 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
The two halves are one idea, and they share one grammar with
multiplier and shape: a value written
without a variable name is the default for every
predictor it can apply to, a named one overrides that variable. So
shape = "quintiles" cuts every continuous predictor,
multiplier = c("2sd", age = 10) reads “per two standard
deviations, except age, per decade”, and
ref = c("median", "last", race = "Black") sets both
defaults at once — the value itself says which kind of predictor it is
for (a number or "mean" / "median" /
"min" / "max" for a continuous one,
"first" / "last" for a factor).
Anchoring a continuous predictor does not change its own
effect: a slope is the same wherever you start reading it from.
The predictor’s row says where its anchor sits, right beside the unit
its effect is per — per 34.6 (2SD), at 47.2 (mean), or
at 0 (min) — so the Constant row’s profile is readable from
the table itself. What it changes is the Constant row,
and any term the predictor interacts with. That is why the default is
the mean rather than zero: nobody is 0 years old, so the intercept of a
raw fit describes nobody. Read the Constant row as the baseline the rest
of the column moves away from — a baseline odds on an odds-ratio column,
a baseline probability or mean on an additive one — and its label says
where that baseline sits (Reference profile, or
Population average on a marginal column). If a predictor’s
zero is meaningful, say so:
ref = c(tvhours = 0).
Order matters in one place, and it is stated rather than guessed:
shape recodes the column first (it defines what the model’s
variable is), then the anchor applies to the result. So
ref on a "log"-shaped predictor anchors the
log, and a "quartiles"-shaped one has become a factor and
takes a level name.
Interactions: one predictor’s effect depending on another
The section above compares sub-populations — one
model per group. An interaction asks the same question inside
one model, where a third variable can be adjusted for across
the whole sample. Write it in predictors with R’s own
*, bare or quoted:
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 2 648 | 1/1.04 (49%) | |
| race*party3 | White · 1-Democrat | 6 369 | (45%) 1 | 1 |
| Black · 1-Democrat | 2 338 | (29%) 1/2.07*** | 1/2.25*** | |
| Other · 1-Democrat | 942 | (44%) 1/1.06 | 1/1.06 | |
| White · 2-Independent, other | 3 347 | (46%) 1.01 | 1.04 | |
| Black · 2-Independent, other | 506 | (25%) 1/2.45*** | 1/2.52*** | |
| Other · 2-Independent, other | 624 | (50%) 1.18** | 1.19** | |
| White · 3-Republican | 6 537 | (59%) 1.71*** | 1.61*** | |
| Black · 3-Republican | 236 | (27%) 1/2.23*** | 1/2.38*** | |
| Other · 3-Republican | 355 | (54%) 1.42*** | 1.36*** | |
| relig | 1-Protestant | 10 794 | (50%) 1 | 1 |
| 2-Catholic | 5 090 | (50%) 1.00 | 1/1.11*** | |
| 3-Other christian | 778 | (44%) 1/1.24*** | 1/1.25*** | |
| 4-Jewish | 388 | (51%) 1.04 | 1/1.02 | |
| 5-Buddhist/Hinduist | 212 | (51%) 1.07 | 1.07 | |
| 6-Muslim | 99 | (54%) 1.17 | 1.53** | |
| 7-Other | 384 | (38%) 1/1.62*** | 1/1.63*** | |
| 8-None | 3 509 | (37%) 1/1.70*** | 1/1.70*** | |
| Model fit | N | 21 254 | ||
| Dispersion (robust/model SE) | 1.01 | |||
| Collinearity (max VIF) | 1.03 | |||
| Influence (max dfbetas) | 0.15 | |||
| Interaction (LR): race*party3 | 0.12% | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.035 | |||
| AIC | 28 385 | |||
| BIC | 28 513 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
race*party3 reads “race’s effect, allowed to vary
with party”, and it is a predictor like any other:
one row per cell of the pair, each compared to one common reference
cell, with its own count, the observed rate and the adjusted one beside
it. Nothing else changes — the colours, the stars, the crude companion
and the footer all work as they do on an ordinary variable, because the
pair really is one variable now.
Read one row at a time: “Black Republicans have
a*b is R’s own spelling and means here exactly what it
means in glm(): a + b + a:b. The order picks
the presentation, never the model — a*b and
b*a are the same fit, and the rows are about whichever you
put first. (R’s a:b, the interaction term without
its main effects, is a different model whose fit depends on where each
variable’s zero happens to be, so tab_reg() refuses it by
name rather than treat it as a synonym.)
The parts of a pair are supplied by the interaction,
so do not list them beside it: writing
c(race, party3, race*party3) is an error, not a richer
model. What that buys is the natural way to ask whether the interaction
is worth it at all:
tab_reg(gss_simple, "married",
list(additive = c(race, party3), crossed = c(race*party3)),
stats = "compare_sequential")| levels | n | Obs_OR | additive | crossed | |
|---|---|---|---|---|---|
| <n> | <(obs%) OR> | <OR> | <OR> | ||
| Constant | Reference profile | 6 390 | 1/1.19*** | 1/1.20*** | |
| race | White | 16 301 | (51%) 1 | 1 | |
| Black | 3 093 | (28%) 1/2.67*** | 1/2.28*** | ||
| Other | 1 934 | (48%) 1/1.13*** | 1/1.02 | ||
| party3 | 1-Democrat | 9 679 | (41%) 1 | 1 | |
| 2-Independent, other | 4 512 | (44%) 1.11*** | 1.01 | ||
| 3-Republican | 7 137 | (57%) 1.93*** | 1.65*** | ||
| race*party3 | White · 1-Democrat | 6 390 | (45%) 1 | 1 | |
| Black · 1-Democrat | 2 344 | (29%) 1/2.08*** | 1/2.08*** | ||
| Other · 1-Democrat | 945 | (44%) 1/1.06 | 1/1.06 | ||
| White · 2-Independent, other | 3 365 | (46%) 1.01 | 1.01 | ||
| Black · 2-Independent, other | 513 | (25%) 1/2.47*** | 1/2.47*** | ||
| Other · 2-Independent, other | 634 | (50%) 1.18** | 1.18** | ||
| White · 3-Republican | 6 546 | (59%) 1.71*** | 1.71*** | ||
| Black · 3-Republican | 236 | (27%) 1/2.23*** | 1/2.23*** | ||
| Other · 3-Republican | 355 | (54%) 1.42*** | 1.42*** | ||
| Model fit | N | 21 328 | 21 328 | ||
| Dispersion (robust/model SE) | 1.00 | 1.00 | |||
| Collinearity (max VIF) | 1.04 | ||||
| Influence (max dfbetas) | 0.03 | 0.11 | |||
| Interaction (LR): race*party3 | <0.01% | ||||
| LR vs null | <0.01% | <0.01% | |||
| McFadden R2 | 0.028 | 0.029 | |||
| AIC | 28 675 | 28 659 | |||
| BIC | 28 715 | 28 730 | |||
| LR vs previous | <0.01% | ||||
|
Model: logistic regression of married (‘01-Married’); OR: odds ratio (vs
the reference category).
Obs_OR, additive, crossed — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
|||||
The footer answers it twice, with the same number: the model
comparison, and the Interaction (LR) row,
which is in the default footer of a linear or logistic model. That row
is the one to quote — a cell’s own stars say it differs from the
reference cell, which is mostly the two main effects talking; the footer
row says whether the pattern departs from what those main
effects alone predict.
A continuous predictor: slopes within groups
A continuous variable has no cells to cross, so age*race
gives the other honest reading — age’s slope within
each level of race, straight out of the fit, in
the unit the row names:
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1.20*** (54%) | ||
| race | White | 16 274 | (51%) 1 | 1 (51%) |
| Black | 3 097 | (28%) 1/2.67*** | 1/2.77*** (28%) | |
| Other | 1 936 | (48%) 1/1.14*** | 1.10 (53%) | |
| age*race | age per 34.6 (2SD) — White | 16 274 | 1.25*** | 1.15*** |
| age per 34.6 (2SD) — Black | 3 097 | 1.58*** | 1.48*** | |
| age per 34.6 (2SD) — Other | 1 936 | 2.44*** | 2.32*** | |
| relig | 1-Protestant | 10 817 | (50%) 1 | 1 (51%) |
| 2-Catholic | 5 101 | (50%) 1.00 | 1/1.15*** (47%) | |
| 3-Other christian | 782 | (44%) 1/1.25*** | 1/1.24*** (46%) | |
| 4-Jewish | 387 | (51%) 1.04 | 1/1.16 (47%) | |
| 5-Buddhist/Hinduist | 217 | (51%) 1.06 | 1/1.03 (50%) | |
| 6-Muslim | 103 | (52%) 1.12 | 1.47* (60%) | |
| 7-Other | 386 | (37%) 1/1.66*** | 1/1.74*** (38%) | |
| 8-None | 3 514 | (37%) 1/1.70*** | 1/1.80*** (37%) | |
| Model fit | N | 21 307 | ||
| Dispersion (robust/model SE) | 1.06 | |||
| Collinearity (max VIF) | 1.20 | |||
| Influence (max dfbetas) | 0.16 | |||
| Interaction (LR): age*race | <0.01% | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.032 | |||
| AIC | 28 553 | |||
| BIC | 28 657 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
The moderator keeps its own block (the model contains it), and every
effect works here too: measure = "difference"
gives each group’s average marginal effect, in points. To get the cell
table instead, cut the continuous variable into groups — which is
usually the more readable answer:
The order does not have to be right: if you write
race*age, tabxplor reads it as age*race and
says so in one line — * is symmetric in the fit, and only a
continuous variable has slopes to show within groups, so there is
exactly one table that can exist.
And if both variables are continuous there are no cells to cross at all, so the second one is cut into quartiles, again in one line:
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1/1.42*** (41%) | ||
| tvhours | 0 | 674 | (37%) 1 | 1 (39%) |
| 1 | 2 337 | (53%) 1.91*** | 1.95*** (54%) | |
| 2 or 3 | 4 980 | (49%) 1.64*** | 1.54*** (49%) | |
| 4 to 24 | 3 308 | (36%) 1/1.05 | 1/1.05 (37%) | |
| age*tvhours | age per 34.8 (2SD) — 0 | 674 | 1.86*** | 1.85*** |
| age per 34.8 (2SD) — 1 | 2 337 | 2.25*** | 2.24*** | |
| age per 34.8 (2SD) — 2 or 3 | 4 980 | 1.48*** | 1.40*** | |
| age per 34.8 (2SD) — 4 to 24 | 3 308 | 1.44*** | 1.30*** | |
| race | White | 8 583 | (49%) 1 | 1 (49%) |
| Black | 1 691 | (26%) 1/2.76*** | 1/2.42*** (29%) | |
| Other | 1 025 | (46%) 1/1.14* | 1/1.03 (48%) | |
| Model fit | N | 11 299 | ||
| Dispersion (robust/model SE) | 1.09 | |||
| Collinearity (max VIF) | 1.08 | |||
| Influence (max dfbetas) | 0.14 | |||
| Interaction (LR): age*tvhours | <0.01% | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.039 | |||
| AIC | 14 993 | |||
| BIC | 15 067 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
That cut is a modelling choice, not a presentation one — the footer’s
interaction test moves with the number of bins — which is why it is
stated rather than silent. Choose it yourself with shape
(shape = c(tvhours = "quintiles")), or write
tvhours*age to cut age instead. If you want
the bare product coefficient with no bins at all, write the model as a
formula: outcome = y ~ ... + age * tvhours.
The scale carries the interaction
One caveat worth stating plainly, because it is not a defect of any
method: an interaction depends on the scale it is measured
on. A model with no interaction on the odds-ratio scale
generally has one on the probability scale, and the reverse — that is
what a link function and non-collapsibility do (Ai & Norton 2003).
tabxplor’s answer is to make the scale visible rather than to pick one:
print the same table both ways, measure = "odds_ratio" and
measure = "difference", and say which one you are
reading.
For a specification the argument surface cannot express — custom
contrasts, a hand-written offset, a three-way term — a model
formula in outcome is still the expert exit door
(tab_reg(d, married ~ race * age * relig)). It fits exactly
what you write, but it steps outside the framework: its rows are raw
coefficient names, with no counts, no observed companion and no
unit.
Three ways to get this wrong
A coloured cell is a difference in that measure, not proof that the cause differs. Groups can differ because the outcome is simply rarer in one of them, or more variable — and then their effects differ on every scale, odds ratios, risk ratios and marginal effects alike. There is no scale that escapes this, so the reading has to be careful rather than clever.
So read the pattern, not the single cell. If a whole
column is shifted the same way, the groups probably differ in level or
in variability — that is a property of the group, not of the predictor.
If one row stands out while the others sit still, that
is what genuine effect modification looks like. The colours are useful
precisely because they make that shape visible at a glance. Adding
empirical = TRUE shows each group’s base rate, in the crude
column’s brackets, which usually settles the question.
Each cell is tested on its own, with no correction for the number of tests. In a table with seven comparisons, about one table in five will show a spurious coloured cell. The footer line is free of this: it asks one question per predictor.
For the record: what exactly is tested
The two groups are disjoint samples, so the two estimates are
independent and the standard error of their difference is
sqrt(SE_A² + SE_B²) — the textbook test for a difference
between two independent estimates (Altman & Bland 2003). Both
standard errors are read back from the confidence intervals the table
already prints, so the test and the printed intervals cannot disagree.
The difference is measured on the effect’s own scale — the log-ratio for
an odds ratio, a risk ratio or an incidence-rate ratio, the plain
difference for a beta or a marginal effect — and compared to a normal
(z) threshold, which is mildly conservative in small samples.
The three color_signif policies then read that interval
exactly as they do in part 4 — here “no change” reads “no difference
between the two groups”.
Two details worth knowing. The significance stars in the cells keep reading each estimate’s own p-value — “is this effect different from 1?” — not the gap’s; the gap’s p-value is in the tooltip. And the footer’s aggregated test is a likelihood-ratio test (an F test for linear and quasi models, a design-based Wald test when you supply survey weights — the same rule as the model comparison below) computed on one extra pooled model, on the coefficients: on a marginal column the cells show marginal effects while the footer tests the coefficients, two related but distinct questions, and the line says so.
Comparing several models
Pass a named list of predictor sets instead of a
vector to fit and show several models side by side. A comparison
Likelihood ratio test is one more footer key:
stats = "compare_baseline" tests each model against the
first (name another with
stats = c(compare_baseline = "M2")),
stats = "compare_sequential" tests each against the
previous one:
tab_reg(gss_simple,
"married",
list("Race only" = "race",
"+ age" = c("race", "age"),
"+ party" = c("race", "age", "party3")),
stats = "compare_sequential")| levels | n | Obs_OR | Race only | + age | + party | |
|---|---|---|---|---|---|---|
| <n> | <(obs%) OR> | <OR> | <OR> | <OR> | ||
| Constant | Reference profile | 16 253 | 1.03** | 1.02 | 1/1.21*** | |
| race | White | 16 253 | (51%) 1 | 1 | 1 | 1 |
| Black | 3 081 | (28%) 1/2.67*** | 1/2.67*** | 1/2.58*** | 1/2.19*** | |
| Other | 1 927 | (48%) 1/1.14*** | 1/1.14*** | 1/1.05 | 1.05 | |
| age | per 34.6 (2SD), at 47.2 (mean) | 1.40*** | 1.33*** | 1.33*** | ||
| party3 | 1-Democrat | 9 651 | (41%) 1 | 1 | ||
| 2-Independent, other | 4 491 | (44%) 1.11*** | 1.05 | |||
| 3-Republican | 7 119 | (57%) 1.93*** | 1.65*** | |||
| Model fit | N | 21 261 | 21 261 | 21 261 | ||
| Dispersion (robust/model SE) | 1.00 | 1.00 | 1.00 | |||
| Collinearity (max VIF) | 1.03 | 1.05 | ||||
| Influence (max dfbetas) | 0.03 | 0.03 | 0.03 | |||
| LR vs null | <0.01% | <0.01% | <0.01% | |||
| McFadden R2 | 0.019 | 0.023 | 0.032 | |||
| AIC | 28 847 | 28 744 | 28 489 | |||
| BIC | 28 871 | 28 776 | 28 537 | |||
| LR vs previous | <0.01% | <0.01% | ||||
|
Model: logistic regression of married (‘01-Married’); OR: odds ratio (vs
the reference category).
Obs_OR, Race only, + age, + party — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||||
The same model within sub-populations
tab_vars = is the regression analogue of
tab()’s tab_vars: the same model is fitted
within each level of a grouping variable, and the
per-group tables are stacked into one grouped table. It answers “does
this effect hold in every subgroup?”. With just one dependent variable,
it uses tab_spread() internally to lay the groups out as
side-by-side columns for an easy comparison :
| married: 01-Married | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| levels | 2000 | 2002 | 2004 | 2006 | 2008 | 2010 | 2012 | 2014 | 2000 | 2002 | 2004 | 2006 | 2008 | 2010 | 2012 | 2014 | |
| <OR (adj%)> | <OR (adj%)> | <OR (adj%)> | <OR (adj%)> | <OR (adj%)> | <OR (adj%)> | <OR (adj%)> | <OR (adj%)> | <n> | <n> | <n> | <n> | <n> | <n> | <n> | <n> | ||
| Constant | Reference profile | 1/1.54*** (39%) | 1/1.45*** (41%) | 1/1.23 (45%) | 1/1.22* (45%) | 1/1.60*** (38%) | 1/1.77*** (36%) | 1/2.02*** (33%) | 1/1.92*** (34%) | 241 | 226 | 193 | 271 | 118 | 165 | 147 | 145 |
| race | White | 1 (49%) | 1 (50%) | 1 (56%) | 1 (53%) | 1 (52%) | 1 (49%) | 1 (50%) | 1 (52%) | 1 428 | 1 395 | 1 330 | 1 933 | 901 | 920 | 853 | 1 119 |
| Black | 1/1.89*** (34%) | 1/2.15*** (32%) | 1/2.42*** (35%) | 1/2.44*** (32%) | 1/2.21*** (33%) | 1/2.89*** (26%) | 1/2.16*** (33%) | 1/2.86*** (28%) | 257 | 266 | 236 | 361 | 168 | 173 | 169 | 243 | |
| Other | 1/1.10 (47%) | 1.12 (53%) | 1.04 (57%) | 1.01 (53%) | 1/1.10 (50%) | 1/1.19 (45%) | 1.17 (54%) | 1/1.36* (44%) | 133 | 119 | 122 | 375 | 120 | 109 | 124 | 161 | |
| rincome | 1-Lt $10000 | 1 (37%) | 1 (38%) | 1 (42%) | 1 (42%) | 1 (36%) | 1 (33%) | 1 (31%) | 1 (31%) | 325 | 317 | 256 | 409 | 180 | 235 | 200 | 231 |
| 2-$10000 to 14999 | 1.44** (46%) | 1.48** (48%) | 1.11 (45%) | 1/1.20 (38%) | 1.13 (39%) | 1.44 (41%) | 1/1.01 (31%) | 1.72** (43%) | 197 | 163 | 143 | 231 | 110 | 110 | 103 | 111 | |
| 3-$15000 to 24999 | 1.28 (43%) | 1.22 (43%) | 1.29 (48%) | 1.08 (44%) | 1.43* (44%) | 1.35 (40%) | 1.67** (43%) | 1.76*** (43%) | 381 | 336 | 324 | 469 | 202 | 195 | 194 | 230 | |
| 4-$25000 or more | 1.85*** (52%) | 1.79*** (53%) | 2.04*** (59%) | 1.70*** (55%) | 2.25*** (55%) | 2.30*** (52%) | 2.99*** (57%) | 2.58*** (52%) | 915 | 964 | 965 | 1 560 | 697 | 662 | 649 | 951 | |
| Model fit | N | 1 818 | 1 780 | 1 688 | 2 669 | 1 189 | 1 202 | 1 146 | 1 523 | ||||||||
| Dispersion (robust/model SE) | 1.00 | 1.00 | 1.00 | 1.01 | 1.00 | 1.00 | 1.01 | 1.00 | |||||||||
| Collinearity (max VIF) | 1.01 | 1.00 | 1.01 | 1.01 | 1.01 | 1.01 | 1.00 | 1.00 | |||||||||
| Influence (max dfbetas) | 0.11 | 0.10 | 0.11 | 0.09 | 0.12 | 0.14 | 0.14 | 0.12 | |||||||||
| LR vs null | <0.01% | <0.01% | <0.01% | <0.01% | <0.01% | <0.01% | <0.01% | <0.01% | |||||||||
| McFadden R2 | 0.020 | 0.024 | 0.033 | 0.029 | 0.034 | 0.043 | 0.050 | 0.046 | |||||||||
| AIC | 2 475 | 2 418 | 2 267 | 3 604 | 1 604 | 1 598 | 1 519 | 2 021 | |||||||||
| BIC | 2 508 | 2 451 | 2 299 | 3 640 | 1 635 | 1 628 | 1 549 | 2 053 | |||||||||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
adj%: adjusted/predicted proportion.
OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
|||||||||||||||||
Reading a row across the groups is the same chore as reading a model
against its observed effect, and it has the same answer:
color = "between_groups" colours how far each group’s
effect sits from the first group’s, on the same row.
Where the model-comparison test below says “these models differ” once
for the whole model, this says which effects differ between
groups — a per-predictor reading of what statisticians call effect
modification.
Here the groups are separate people, so the difference between two of
their effects can be tested — and it is, so color_signif
works as everywhere else. Add "grey_non_signif" and a
background colour then means “this really is a different effect, not
just noise”:
tab_reg(gss_simple, "married", c("race", "rincome"), tab_vars = "party3",
color = c(TRUE, "between_groups"), color_signif = "grey_non_signif")| married: 01-Married | |||||||
|---|---|---|---|---|---|---|---|
| levels | 1-Democrat | 2-Independent, other | 3-Republican | 1-Democrat | 2-Independent, other | 3-Republican | |
| <OR (adj%)> | <OR (adj%)> | <OR (adj%)> | <n> | <n> | <n> | ||
| Constant | Reference profile | 1/1.89*** (35%) | 1/1.88*** (35%) | 1/1.04 (49%) | 576 | 383 | 542 |
| race | White | 1 (47%) | 1 (46%) | 1 (60%) | 3 802 | 2 005 | 4 047 |
| Black | 1/1.90*** (32%) | 1/1.91*** (31%) | 1/3.30*** (31%) | 1 435 | 277 | 150 | |
| Other | 1.06 (48%) | 1.18 (49%) | 1/1.11 (57%) | 639 | 370 | 241 | |
| rincome | 1-Lt $10000 | 1 (32%) | 1 (34%) | 1 (48%) | 955 | 548 | 636 |
| 2-$10000 to 14999 | 1.32** (38%) | 1.21 (38%) | 1.18 (52%) | 542 | 304 | 314 | |
| 3-$15000 to 24999 | 1.50*** (41%) | 1.27* (39%) | 1.17 (52%) | 1 060 | 590 | 675 | |
| 4-$25000 or more | 2.04*** (48%) | 2.26*** (53%) | 1.86*** (63%) | 3 319 | 1 210 | 2 813 | |
| Model fit | N | 5 876 | 2 652 | 4 438 | |||
| Dispersion (robust/model SE) | 1.00 | 1.01 | 1.00 | ||||
| Collinearity (max VIF) | 1.00 | 1.00 | 1.00 | ||||
| Influence (max dfbetas) | 0.05 | 0.10 | 0.14 | ||||
| LR vs null | <0.01% | <0.01% | <0.01% | ||||
| McFadden R2 | 0.027 | 0.029 | 0.021 | ||||
| AIC | 7 836 | 3 552 | 5 907 | ||||
| BIC | 7 876 | 3 587 | 5 946 | ||||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
adj%: adjusted/predicted proportion.
Interaction with party3 (likelihood ratio): race p = 3.66%**, rincome p = 30.1%. 1-Democrat married: 01-Married — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Background colour: reference group. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence) or under the first colour threshold (×1.2). 2-Independent, other married: 01-Married, 3-Republican married: 01-Married — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Background colour: OR further from no effect (1) than the reference group’s effect, by ×1.1; ×1.25; ×1.5; ×2; OR closer to no effect (1) than the reference group’s effect (or inversed effect), by ÷1.1; ÷1.25; ÷1.5; ÷2. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence) or under the first colour threshold (×1.2). Some rows carry no test and are left uncoloured. Background: the same rule, applied to the gap with the reference group’s effect (z test on the difference between two independent estimates, 95% confidence). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
|||||||
Text colour = how strong the effect is inside that group. Background
= how far it sits from the first group’s effect on the same row, greyed
out when the difference could be chance. The first group is the baseline
and stays blank (nothing is compared to itself); reorder the levels with
forcats::fct_relevel() to pick another one. Hovering a cell
in the html table gives the exact numbers: the other group’s effect, the
size of the gap, its confidence interval and its p-value.
The footer gained a line at the same time: one test per
predictor, asking “does this predictor act differently between
the groups?” for all its levels at once. That is the aggregated version
of the same question, and it is the one to quote — because it is asked
once, not once per cell. You can ask for it alone, without the colours,
with stats = c("n", "group_interaction").
Weighted and survey data
There are only two ways to hand tabxplor your
weights, and that is deliberate. Either you give
wt = a weight column, or — when your file also carries
strata, clusters, a finite-population correction or calibration — you
build the design once with the survey package and
pass the design itself as data:
tab_reg(data, "outcome", c("pred1", "pred2"), wt = "weight")
library(survey)
d <- svydesign(ids = ~psu, strata = ~stratum, weights = ~w, data = my_survey, nest = TRUE)
tab_reg(d, "outcome", c("pred1", "pred2"), empirical = TRUE)Either way estimation switches to survey::svyglm() and
everything follows that design in one regime: the coefficients and their
intervals, the average marginal effects, the scaling of numeric
predictors, the model-versus-observed gap test — and the
observed Obs_* companion columns, whose intervals
are built on the design variance of each cell. That last point is what
makes this vignette’s central comparison work: the model column and the
observed column beside it are measured the same way, so a difference
between them is about adjustment, not about two notions of
uncertainty.
One consequence is worth stating, because it differs from
tab(): a regression is always
design-based, while a cross-table starts on the plain unweighted sample
size and needs design_effect = TRUE to join it.
vignette("tabxplor-weights") explains that ladder, the two
honest limits of a design-based observed column, and how to tell whether
your own file deserves a full design at all.
6. Checking the model
Reading the footer statistics
The footer summarises model fit; which statistics appear depends on the family:
- N — the number of observations used.
- LR vs null — a likelihood-ratio test of the whole model against the intercept-only model (is the model worth anything?).
- McFadden R² — a pseudo-R² for binomial and poisson models (higher = the predictors explain more; values are much smaller than a linear R²).
- AIC / BIC — information criteria for comparing models on the same data (lower = better; BIC penalises complexity more).
- Overall association — one test per predictor: is this variable associated with the outcome at all? A block of stars against a reference category cannot answer that, which is why a multi-level factor needs more than its cells.
- Linear models (gaussian) add the usual R² / adjusted R² / F / σ; count models add the Pearson dispersion (φ).
For weighted models a reduced set is reported (a Wald test against the null, Nagelkerke / Cox–Snell pseudo-R², a Rao–Scott AIC), because the likelihood-based quantities do not apply to a design-based fit.
The five model checks
The same footer carries five model checks, and there
is nothing to learn beyond the five nouns: each names an assumption, and
the parenthesis names the instrument that measured it. Four are printed
by default. Linearity is the one you ask for by name,
or with stats = "all" — because it refits the model once
per continuous predictor, and because the free half of it is on screen
anyway (see the shape table below).
tab_reg(gss_simple, "married", c("race", "age", "rincome", "relig"), stats = c("n", "linearity", "dispersion", "influence", "collinearity"))| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1/1.19*** (46%) | ||
| race | White | 9 846 | (52%) 1 | 1 (51%) |
| Black | 1 860 | (31%) 1/2.43*** | 1/2.40*** (32%) | |
| Other | 1 254 | (49%) 1/1.12* | 1.11 (54%) | |
| age | per 26.9 (2SD), at 42.4 (mean) | 2.12*** | 1.83*** | |
| rincome | 1-Lt $10000 | 2 142 | (37%) 1 | 1 (39%) |
| 2-$10000 to 14999 | 1 164 | (41%) 1.20** | 1.14* (42%) | |
| 3-$15000 to 24999 | 2 322 | (43%) 1.32*** | 1.27*** (45%) | |
| 4-$25000 or more | 7 332 | (55%) 2.12*** | 1.86*** (54%) | |
| relig | 1-Protestant | 6 269 | (52%) 1 | 1 (53%) |
| 2-Catholic | 3 121 | (52%) 1/1.03 | 1/1.17*** (49%) | |
| 3-Other christian | 507 | (42%) 1/1.53*** | 1/1.43*** (45%) | |
| 4-Jewish | 222 | (55%) 1.10 | 1/1.29* (47%) | |
| 5-Buddhist/Hinduist | 144 | (50%) 1/1.09 | 1/1.36* (46%) | |
| 6-Muslim | 56 | (48%) 1/1.17 | 1.05 (54%) | |
| 7-Other | 267 | (35%) 1/2.04*** | 1/2.04*** (37%) | |
| 8-None | 2 374 | (38%) 1/1.76*** | 1/1.79*** (39%) | |
| Model fit | N | 12 960 | ||
| Linearity (LR): age | <0.01% | |||
| Dispersion (robust/model SE) | 1.03 | |||
| Collinearity (max VIF) | 1.08 | |||
| Influence (max dfbetas) | 0.19 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
Read them in the order they print, which is the order of what each one threatens — first what the number means, then whether its interval can be trusted, then how fragile it is:
-
Proportionality (Brant) — for an ordinal outcome,
is one cumulative odds ratio enough for every cut? Printed by
default there, and a refit anyway, because a cumulative odds ratio that
fails it is not one number but a fiction. When it fails,
measure = "difference"is the robust way to keep reading the same fit (Somers’Dbarely moves when the assumption breaks);family = "multinomial"is the way to stop assuming it at all. - Linearity — is this predictor’s effect really one straight line? One row per continuous predictor, and the row a wrong shape shows up in is rarely the only row it damages. The next two sections are about reading it and curing it.
-
Dispersion (robust/model SE) — are the standard
errors wide enough? About 1 means the family’s variance assumption
holds; 1.4 means the intervals should be about 40 % wider than they are.
For a count outcome,
family = "quasipoisson"fixes exactly that, and the row then returns to about 1 (while the Pearson dispersion φ still reports the over-dispersion itself). Under a survey design it simply says how much the design mattered — the intervals are already the design’s. - Collinearity (max VIF) — can the data tell these predictors apart? From about 5 two predictors are measuring much the same thing and their intervals are inflated; the footer marks the cell from 10, the textbook figure. Collinearity biases nothing, so this is the one check that is a caution rather than a problem.
- Influence (max dfbetas) — does one respondent carry the result? The largest amount, in standard errors, by which dropping a single respondent moves any coefficient. With thousands of respondents this is normally reassuringly small, and that reassurance is the point. Careful: influence is not the same as being an outlier — a surprising answer from an otherwise ordinary respondent moves nothing.
Two things worth knowing. A curvature test on one predictor can pick up another predictor’s wrong shape when the two are strongly correlated — one more reason to read the collinearity row beside it. And with a large survey sample almost any diagnostic p-value ends up significant, which is why three of the five report a magnitude instead.
The shape of a continuous predictor
The linearity row tells you whether one straight line is enough. The small shape table printed under the footer tells you what shape the data has — and it costs nothing, because no model is involved: the outcome is simply cut into ten equal-sized slices of the predictor, and the average is taken in each.
The table below carries one of each, which is why it is worth reading
closely: tvhours has exactly the shape a
coefficient assumes — a straight fall, so its single number
describes it honestly — while age has a shape no
coefficient can express.
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1/1.06*** (48%) | ||
| race | White | 8 583 | (49%) 1 | 1 (49%) |
| Black | 1 691 | (26%) 1/2.76*** | 1/2.35*** (29%) | |
| Other | 1 025 | (46%) 1/1.14* | 1/1.02 (48%) | |
| age | per 34.8 (2SD), at 47.2 (mean) | 1.45*** | 1.50*** | |
| tvhours | per 5.18 (2SD), at 2.98 (mean) | 1/1.69*** | 1/1.64*** | |
| Model fit | N | 11 299 | ||
| Dispersion (robust/model SE) | 1.05 | |||
| Collinearity (max VIF) | 1.07 | |||
| Influence (max dfbetas) | 0.17 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.034 | |||
| AIC | 15 067 | |||
| BIC | 15 103 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
| outcome | numeric predictor | observed range | observed shape (central 95%) |
|---|---|---|---|
| p = %Married ; log(p/(1-p)) | age | 13-57% (OR 8.7) | |
| tvhours | 31-53% (OR 2.6) |
One row per continuous predictor. Read it in three steps.
1. Read the range first, not the picture.
13-57% is the plainest sentence in the whole table: in
the slice of age where marriage is rarest, 13 % are
married; in the slice where it is commonest, 57 % are. Those are
real percentages of real people, counted, with no model in them. The
figure in brackets is the same distance written as the model’s own
comparison — (OR 8.7), an odds ratio of nearly nine — so
you can hold it against the colour ladder you already use for the cells
(1.2, 1.5, 2, 4).
Nine is far past the last rung: age matters a great
deal.
The range always speaks the outcome’s own language — a percentage of
people for a yes/no outcome, an average for a numeric one — and it does
not change when you change measure or link,
because it is what was counted. Only the bracket does.
This is the one number a continuous predictor never has in the table
itself. A cell like 1/2.35*** (28 %) shows an effect
and the percentage it sits on — but age has no
categories, so there is no single percentage to put there. The curve has
one for every slice, and the observed range is simply its two ends.
2. Then read the shape, and only for its shape. The horizontal axis is the predictor as the model sees it, and every point rests on the same number of people, so what you see is where things happen. Three readings matter:
- a straight rise or fall — one number describes it, nothing to do;
- a bend or a plateau — the effect is not the same everywhere, and the cures below fix it;
- a reversal, up then down, like
agehere — the one a single coefficient cannot express at all, because it reports one direction for a variable that has two.ageclimbs steeply, levels off, then falls: the single1.45per two standard deviations printed in its row is the average of a rise and a fall, a number that describes nobody.tvhoursis the contrast, and the reassuring case: it falls throughout, so its1/1.69really is the effect of watching more television, and there is nothing to do about it.
The damage does not stay in the offending row. Letting
age curve instead of run straight moves the top income
category’s odds ratio by about a quarter and flips another income
level’s verdict at the 5 % threshold — in a model where nothing but this
shape table hints at a problem. That is the whole reason to look.
⚠ Do not compare two pictures by eye. Each curve is
drawn at its own scale and fills the height whatever its size — that is
what makes small shapes visible at all. age and
tvhours look about equally dramatic above; the ranges say
one is a nine-fold change and the other closer to two and a half.
The picture answers what shape, the range answers
how big. Only the numbers compare.
3. Check for grey and ns before believing any of
it. A curve smaller than its own sampling noise is greyed and
marked ns: read it as a flat line, however convincing it
looks. The same two predictors, on a 200-row sample of the same
data:
set.seed(20260823)
small <- gss_simple[sample(nrow(gss_simple), 200), ]
tab_reg(small, "married", c("race", "age", "tvhours"), family = "binomial")| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1/1.10 (48%) | ||
| race | White | 84 | (51%) 1 | 1 (49%) |
| Black | 13 | (23%) 1/3.50* | 1/1.85 (35%) | |
| Other | 8 | (75%) 2.86 | 3.51 (74%) | |
| age | per 33.9 (2SD), at 44.3 (mean) | 1.67 | 2.82** | |
| tvhours | per 5.29 (2SD), at 3.07 (mean) | 1/3.57** | 1/5.12** | |
| Model fit | N | 105 | ||
| Dispersion (robust/model SE) | 1.04 | |||
| Collinearity (max VIF) | 1.29 | |||
| Influence (max dfbetas) | 0.52 | |||
| LR vs null | 0.33% | |||
| McFadden R2 | 0.109 | |||
| AIC | 140 | |||
| BIC | 153 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
| outcome | numeric predictor | observed range | observed shape (central 95%) |
|---|---|---|---|
| p = %Married ; log(p/(1-p)) | age | 11-69% (OR 18.3) ns | |
| tvhours | 16-72% (OR 13.7) ns | ||
|
“ns”: the curve is inside its own sampling noise – read it as flat.
|
|||
Still curves, to the eye. They are not: on so few people the slices
wobble by that much on their own — and the ranges, 11-69 %
and 16-72 %, are wider than the ones measured on the whole
sample, which is the giveaway.
⚠ ns is about the shape, not about the
predictor. Ten slices carry ten averages, not two hundred respondents,
so a real effect can be perfectly significant in the table and still
unreadable as a shape. The stars in the predictor’s own row judge the
effect; the shape table judges only whether the picture can be
trusted.
For an ordinal or multinomial
outcome the table draws only the first cut or category — use
reg_check_plots() there, which draws them all, and which is
also where a proportional-odds departure becomes visible.
options(tabxplor.shape_table = "console") keeps the
shape table where you are working and out of your exports;
"no" removes it everywhere.
Curing a shape
shape = is how you fix what the table just showed you,
without leaving the table. The most readable answer is usually to cut
the predictor into groups — it then becomes an ordinary factor, so it
gets one odds ratio per group, its own observed companion, counts and
colours, and the shape becomes visible in the printed numbers
themselves:
tab_reg(gss_simple, "married", c("race", "age"), family = "binomial",
shape = c(age = "quintiles"), empirical = TRUE)| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 2 908 | 1/2.37*** (30%) | |
| race | White | 16 341 | (51%) 1 | 1 (50%) |
| Black | 3 115 | (28%) 1/2.68*** | 1/2.72*** (28%) | |
| Other | 1 951 | (48%) 1/1.14*** | 1/1.06 (49%) | |
| age | 18 to 30 | 4 249 | (27%) 1 | 1 (27%) |
| 31 to 40 | 4 278 | (54%) 3.17*** | 3.24*** (54%) | |
| 41 to 50 | 4 251 | (55%) 3.35*** | 3.38*** (55%) | |
| 51 to 62 | 4 247 | (56%) 3.48*** | 3.44*** (56%) | |
| 63 to 89 | 4 382 | (45%) 2.22*** | 2.10*** (44%) | |
| Model fit | N | 21 407 | ||
| Dispersion (robust/model SE) | 1.00 | |||
| Collinearity (max VIF) | 1.02 | |||
| Influence (max dfbetas) | 0.03 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.055 | |||
| AIC | 27 990 | |||
| BIC | 28 046 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
The parsimonious alternative keeps one variable and adds a curvature
term, so age takes two rows: the slope at the mean, and
age², which says whether the slope flattens (below 1) or
accelerates (above 1) as you move away from it.
| married: 01-Married | ||||
|---|---|---|---|---|
| levels | n | Obs_OR | Model_OR | |
| <n> | <(obs%) OR> | <OR (adj%)> | ||
| Constant | Reference profile | 1.62*** (62%) | ||
| race | White | 16 341 | (51%) 1 | 1 (50%) |
| Black | 3 115 | (28%) 1/2.68*** | 1/2.76*** (28%) | |
| Other | 1 951 | (48%) 1/1.14*** | 1/1.03 (50%) | |
| age | per 34.6 (2SD), at 47.2 (mean) | 2.03*** | 1.96*** | |
| age² | 1/1.59*** | 1/1.61*** | ||
| Model fit | N | 21 407 | ||
| Dispersion (robust/model SE) | 1.01 | |||
| Collinearity (max VIF) | 1.16 | |||
| Influence (max dfbetas) | 0.06 | |||
| LR vs null | <0.01% | |||
| McFadden R2 | 0.065 | |||
| AIC | 27 682 | |||
| BIC | 27 722 | |||
|
Model: logistic regression; OR: odds ratio (vs the reference category);
obs%: observed proportion; adj%: adjusted/predicted
proportion.
Obs_OR, Model_OR — OR ≥ 1.2; 1.5; 2; 4; OR ≤ 1/1.2; 1/1.5; 1/2; 1/4. Uncoloured: not significantly different from the reference category (Wald interval on the log odds-ratio, 95% confidence; matching Woolf interval on the observed column) or under the first colour threshold (×1.2). ***: significantly different from the reference category (in bold) at the 99% confidence level (from 1 for the Constant); **: at the 95% level; *: at the 90% level; no star: not significant. |
||||
| outcome | numeric predictor | observed range | observed shape (central 95%) |
|---|---|---|---|
| p = %Married ; log(p/(1-p)) | age | 13-57% (OR 8.7) |
shape = c(x = "log") and "sqrt" are the
other two — diminishing returns, the shape income data usually has.
Everything else keeps working: the observed Obs_* companion
is fitted with the same shape, so the model-versus-observed comparison
still compares like with like, and the linearity row disappears for a
predictor you have already cured.
(A poly() or spline basis is deliberately never emitted:
the marginal-effects engine silently returns zero for those. If you
reach one through a formula, a warning says so.)
The check plots: reg_check_plots()
reg_check_plots(t) # the default panels, one titled grid per model
reg_check_plots(t, check = "all") # plus dispersion and collinearity
reg_check_plots(t, check = "linearity") # just oneThe same nouns as the footer rows, one panel each, and one
titled grid per model — each drawing the panels its own family
allows, so a table mixing a binomial and an ordinal outcome is diagnosed
correctly. The data is normally found on its own, from the name the
table was built with; pass data = when the table came from
an expression rather than a named object. check = "auto"
leaves out dispersion and
collinearity, whose footer row already says the whole
thing; check = "all" restores them.
These are a teaching companion, not a decision tool:
every verdict they illustrate is already a footer row, in every export,
with no plotting package installed. They exist to show what a violation
looks like. theme = "print_ready" gives a greyscale version
for a thesis appendix; you can also pass a plain fitted model instead of
a table.
Two panels repay a second look. Linearity compares
the observed curve to the shape the model fits — a straight
line normally, a parabola once shape = "quadratic" put a
curvature term in — so it stays honest about a predictor you have
already cured. With an ordinal or a multinomial outcome it draws
one curve per cut of the outcome (per category, against
the reference, for a multinomial): parallel curves are the
proportional-odds assumption holding, which the Brant test in the footer
scores but the Proportionality panel, being about factors,
cannot show for a continuous predictor.
Note the opposite contracts: forest_plot() never re-fits
(the results are in the table), reg_check_plots() always
does (residuals are not).
7. Plots
The results: forest_plot()
forest_plot() draws the finished table — every effect
with its confidence interval and its colour, one panel per model
column:
t <- tab_reg(gss_simple, "married", c("race", "rincome"), family = "binomial")
forest_plot(t)It reads the table and never re-fits anything, so
the figure cannot disagree with the numbers you printed: each whisker
spans the interval in the cell, each gridline is one of your colour
breaks (move them with set_color_breaks() and the axis
moves too), and the whisker takes the cell’s colour whole — which is why
the plot needs no stars. The value is printed just above it, and the
square at its centre is as big as the number of people behind it
(center = "estimate" prints the value alone). A table that
mixes families gets one axis per panel, each in its own unit. It returns
an ordinary ggplot, so + ggplot2::labs(...)
and ggsave() work as usual.
The three ways of reading significance become three things you can see:
color_signif |
in the table | in the plot |
|---|---|---|
"ignore" |
colour by the size of the effect | where the point sits, relative to the null line |
"grey_non_signif" |
grey unless the interval excludes the null | whether the whisker crosses the null line |
"guaranteed_effect" |
colour the interval bound nearest the null | how far the near end of the whisker is from it |
Observed versus modelled, drawn honestly
With empirical = TRUE, each modelled effect carries its
observed (crude) counterpart, and the plot draws it as a hollow
point with a bracket — the margin of error of the
difference between the two:
t <- tab_reg(gss_simple, "married", c("race", "rincome"),
link = "ratio", empirical = TRUE) # risk ratios
forest_plot(t)Read it as one question: is the filled point outside the bracket? If it is, adjustment moved the effect by more than noise — and that is exactly the test the table performs, to the last digit.
It is deliberately not two intervals side by side. Comparing
two intervals by whether they overlap is a known mistake (Schenker &
Gentleman 2001), and it is worse here, because the crude and the
adjusted estimate are computed on the same people and are therefore
correlated: the right interval is the one around the
difference, which is what the bracket is.
observed = "ci" draws the classic two-interval figure if
you want it anyway.
A dotted connector with no bracket means the gap could not be tested — most often a conditional odds ratio, which moves under adjustment even with nothing to adjust for (see Three ways to get this wrong above).
Where to go next
-
vignette("tabxplor-reading-a-regression")— the same material taught as one continuous analysis, with the reading habits that go with it. -
vignette("tabxplor")— cross-tables and the colour helpers. -
vignette("tabxplor-weights")— weighted and survey data, for both producers. -
?tab_regfor every argument (grouped by purpose), and?tab_reg→ Details for the modelling choices. - Out of scope, and best fitted with their own packages: survival / Cox models, mixed (multilevel) models, and pooling over multiply-imputed datasets.
- On comparing a crude and an adjusted coefficient, the canonical
reference is Clogg, Petkova & Haritou (1995, AJS 100(5)),
with Allison’s comment in the same issue — what
color = "adjustment"computes, generalised to GLMs, survey designs and marginal effects. For nested logit models, the KHB decomposition (Karlson, Holm & Breen 2012) separates the part of the change that is confounding from the part that is rescaling.