broom confint_tidy() in R: Tidy Model Confidence Intervals
The broom::confint_tidy() helper calls confint() on a fitted model and returns the result as a two-column tibble with conf.low and conf.high, ready to bind onto a tidy coefficient table. It is the building block that powers the conf.int = TRUE argument of every tidy.lm(), tidy.glm(), and tidy.survreg() method inside broom.
broom:::confint_tidy(fit) # default 95% interval broom:::confint_tidy(fit, conf.level = 0.90) # widen or tighten broom:::confint_tidy(fit, func = MASS::confint.glm) # profile likelihood for glm tidy(fit, conf.int = TRUE) # the public way tidy(fit, conf.int = TRUE, conf.level = 0.99) # public + level as_tibble(confint(fit), rownames = "term") # exported alternative
Need explanation? Read on for examples and pitfalls.
What confint_tidy() does in one sentence
confint_tidy() takes a fitted model, calls confint() on it, and returns a tibble whose two columns are conf.low and conf.high. Base R's confint() method returns a numeric matrix whose column names depend on the conf.level you pass: at 95% you get "2.5 %" and "97.5 %", at 90% you get "5 %" and "95 %", and so on. Those percentage headers are unfriendly for downstream binding because they change with every level and contain whitespace.
The helper fixes that by renaming the matrix columns to the broom canonical names conf.low and conf.high, then wrapping the result in tibble::as_tibble(). Because the function only emits the interval columns, broom's tidy methods bind it next to a coefficient tibble produced by fix_data_frame() rather than treating it as a standalone output. As of broom 1.0+ the function lives in the internal namespace, so direct user code reaches it via broom:::confint_tidy().
Syntax
confint_tidy() has three named arguments and no hidden behavior. The first is the fitted model, the second sets the confidence level, and the third lets you swap in a non-default interval method.
The three arguments are:
x: a fitted model object that has a workingconfint()method (required)conf.level: the confidence level for the interval; defaults to0.95func: the function used to compute the interval; defaults tostats::confint. PassMASS::confint.glmor any function with the same signature to switch from Wald to profile likelihood intervals.
The return value is always a tibble with exactly two columns, conf.low and conf.high, and one row per model coefficient in the same order as the model's coefficient vector.
tidy.* methods, but user code must use the triple-colon operator or call tidy(fit, conf.int = TRUE) and let broom run the helper for you.Common patterns
1. Get conf.low and conf.high for a linear model
The output has three rows because the model has three coefficients: (Intercept), wt, and cyl. Row order matches the order returned by coef(fit), so a positional bind against a tibble of estimates is safe as long as you do not reshuffle either side. The column names are the broom canonical pair, which is the whole reason the helper exists rather than relying on raw confint() output.
This is the entire job of confint_tidy(). It does not include a term column because it is meant to be column-bound onto a fix_data_frame() result that already carries the term names. Using confint_tidy() alone produces a tibble that is hard to interpret in isolation, which is why tidy(fit, conf.int = TRUE) is the user-facing API.
2. Change the confidence level
Lower confidence levels produce narrower intervals, higher levels produce wider ones. The column names stay as conf.low and conf.high regardless, which is the practical benefit over raw confint(): you can vary the level inside a loop or purrr::map() without rewriting downstream binding code that would otherwise break when the percentage headers shift.
3. Use the public wrapper tidy(conf.int = TRUE)
This is the call most user code wants. Internally, tidy.lm() builds the first five columns via fix_data_frame(), then calls confint_tidy() and binds the two interval columns on the right. The end result is one tibble per model that ggplot2, dplyr, and gt all consume without further reshaping. Reach for the internal helper directly only when you are writing a new tidy.* method or debugging an existing one.
4. Swap the interval function for profile likelihood
The func argument is a clean extension point. The default stats::confint does the right thing for lm (Wald intervals from t critical values) and for glm (profile likelihood intervals via MASS). When you need to force profile likelihood explicitly, pass MASS::confint.glm. When you want Wald intervals on a glm because profile fitting is slow, pass stats::confint.default. Any function with the signature function(x, level, ...) works.
confint_tidy() vs the modern alternatives
Three rivals cover the same ground; pick by what you actually want back. The choice is between a public API, a matrix shape, and a different interval method.
| Goal | Use | Returns |
|---|---|---|
Tidy tibble with term + intervals |
tidy(fit, conf.int = TRUE) |
tibble, public, broom canonical |
| Just the two interval columns | broom:::confint_tidy(fit) |
tibble, internal, two columns |
| Base R matrix with level-named cols | stats::confint(fit) |
matrix, public, names vary |
Matrix to tibble with term |
as_tibble(confint(fit), rownames = "term") |
tibble, public, two columns plus term |
| Profile likelihood interval for glm | MASS::confint.glm(fit) |
matrix, public, slower |
For new code, prefer tidy(fit, conf.int = TRUE). It is exported, documented, and produces the term column for free. Use broom:::confint_tidy() only when writing a tidy.* method or maintaining package code that already calls it. For the rare case where you need just the two columns without the rest of the tidy table, as_tibble(confint(fit), rownames = "term") |> dplyr::select(-term) is the public way.
Common pitfalls
broom::confint_tidy() without the triple colon fails on broom 1.0+. The function is no longer exported, so the public form errors with 'confint_tidy' is not an exported object from 'namespace:broom'. Either use broom:::confint_tidy() for direct access, or switch to tidy(fit, conf.int = TRUE) which calls the helper internally.A second trap: confint_tidy() returns no term column. The output is a two-column tibble, so binding next to a coefficient tibble needs positional alignment. Always check coef(fit) and the tibble row order before dplyr::bind_cols(); a silently misaligned bind produces intervals that point at the wrong predictor.
The third trap is models without a confint() method. Calling the helper on a kmeans or prcomp fit aborts with no applicable method for 'confint'. Check methods("confint") for the classes that work: linear models, generalized linear models, and survival models that ship a Wald or profile likelihood method.
Try it yourself
Try it: Fit lm(mpg ~ hp + wt, data = mtcars), call confint_tidy() at the 99% level, and bind the result onto a tibble of coefficients so each row carries term, estimate, conf.low, and conf.high.
Click to reveal solution
Explanation: enframe() lifts the named coefficient vector into a tibble. confint_tidy(conf.level = 0.99) returns matched-order interval rows. bind_cols() joins them positionally because both tibbles share the same row order from coef(fit).
Related broom functions
For day-to-day model output, the higher-level wrappers do more in one call:
- tidy(): one row per term with
estimate,std.error,statistic,p.value, and optionalconf.low,conf.high glance(): one-row model summary with R-squared, AIC, BIC, residual dfaugment(): per-observation fitted values, residuals, influence diagnostics- fix_data_frame(): the sister helper that lifts coefficient rownames into a
termcolumn - tidy_bootstrap(): bootstrap intervals instead of Wald or profile likelihood
FAQ
Why was confint_tidy() unexported in broom 1.0?
Broom 1.0 reorganized the package around three user-facing generics: tidy(), glance(), and augment(). The maintainers moved internal helpers like confint_tidy() and fix_data_frame() into the private namespace to keep the public API small and the documentation focused. The function still ships with broom and is called by every tidy.* method that supports conf.int = TRUE; only direct user calls need the ::: operator.
How is confint_tidy() different from base R confint()?
stats::confint() returns a numeric matrix whose column names encode the confidence level, like "2.5 %" and "97.5 %". Those headers change every time you change conf.level. confint_tidy() calls confint() internally, then renames the columns to the stable pair conf.low and conf.high and wraps the result in a tibble. Downstream binding code stays the same regardless of the level.
Does confint_tidy() work for generalized linear models?
Yes. glm objects ship a confint() method that uses profile likelihood via MASS, so broom:::confint_tidy(glm_fit) returns the profile interval as conf.low and conf.high. Pass func = stats::confint.default to force the faster Wald interval, or func = MASS::confint.glm to be explicit about profile fitting. Either way the column names are stable.
Why doesn't confint_tidy() include the term column?
By design. confint_tidy() is a positional helper meant to be column-bound onto a coefficient tibble built by fix_data_frame(), which already supplies the term column. Returning only the interval columns lets broom's internal methods compose the final tibble with one bind_cols() rather than juggling duplicate term columns. For a complete tibble in one call, use tidy(fit, conf.int = TRUE) from the public API.
Can I use confint_tidy() with a Bayesian model?
Not directly. confint_tidy() requires a frequentist confint() method, which Bayesian fits from rstanarm or brms do not provide. For Bayesian intervals, install broom.mixed and call broom.mixed::tidy(fit, conf.int = TRUE). That implementation reads posterior samples and reports credible intervals instead.