Composable Confidence Intervals

The interval helpers allow a pipeline to stop after counts, or add intervals later. Both accept the same arguments: .data, percent_ci, se, and critical_value. They share their calculations with freq_table().

Inputs and Defaults

.data is normally a complete or filtered result from freq_tbl(). Its metadata retains the original sample size, so filtering a result does not change a denominator. Grouped tables also carry explicit n_group.

counts <- freq_tbl(mtcars, am)
counts |> wald_ci()
#> # A tibble: 2 × 5
#>      am     n  prop   lcl   ucl
#>   <dbl> <int> <dbl> <dbl> <dbl>
#> 1     0    19 0.594 0.414 0.774
#> 2     1    13 0.406 0.226 0.586
counts |> logit_ci()
#> # A tibble: 2 × 5
#>      am     n  prop   lcl   ucl
#>   <dbl> <int> <dbl> <dbl> <dbl>
#> 1     0    19 0.594 0.409 0.755
#> 2     1    13 0.406 0.245 0.591
counts |> filter(am == 0) |> logit_ci()
#> # A tibble: 1 × 5
#>      am     n  prop   lcl   ucl
#>   <dbl> <int> <dbl> <dbl> <dbl>
#> 1     0    19 0.594 0.409 0.755

Default output appends lcl/ucl for ungrouped results and lcl_group/ucl_group for grouped results. If overall proportions are present, the helpers also compute lcl_total/ucl_total.

A plain data frame is supported with explicit denominators. The required columns are n, n_total, and prop (or percent), or n, n_group, and prop_group (or percent_group). Counts and denominators must be finite nonnegative integers, with n <= denominator. Displayed proportions must agree with them.

plain <- data.frame(n = c(19L, 13L), n_total = 32L, prop = c(19, 13) / 32)
wald_ci(plain)
#>    n n_total    prop       lcl       ucl
#> 1 19      32 0.59375 0.4138446 0.7736554
#> 2 13      32 0.40625 0.2263446 0.5861554
logit_ci(plain)
#>    n n_total    prop       lcl       ucl
#> 1 19      32 0.59375 0.4094225 0.7549765
#> 2 13      32 0.40625 0.2450235 0.5905775
plain_group <- data.frame(n = c(3L, 7L), n_group = 10L, prop_group = c(.3, .7))
wald_ci(plain_group)
#>   n n_group prop_group   lcl_group ucl_group
#> 1 3      10        0.3 -0.04555021 0.6455502
#> 2 7      10        0.7  0.35444979 1.0455502
logit_ci(plain_group)
#>   n n_group prop_group  lcl_group ucl_group
#> 1 3      10        0.3 0.07636623 0.6895849
#> 2 7      10        0.7 0.31041511 0.9236338

The helpers never infer an absent original denominator by summing a potentially filtered count table. If metadata has been removed, provide the original n_total explicitly.

Formula and Confidence Level

For continuity, both methods use the existing package convention:

p = n / N
SE = sqrt(p * (1 - p) / (N - 1))
t = qt((1 + percent_ci / 100) / 2, df = N - 1)

For a grouped interval, N is the group size in both formulas. For an overall interval, it is the total number of input observations. The former implementation used total-sample degrees of freedom for within-group limits; those limits change with this correction.

wald_ci() returns p - t * SE and p + t * SE. logit_ci() applies t * SE / (p * (1 - p)) on the log-odds scale, then transforms back with plogis(). This is a t-based compatibility convention; it is not the usual normal-z binomial Wald formula.

percent_ci = 95 is the default; use a number strictly between 0 and 100 to change it. Larger levels produce wider intervals for interior proportions.

counts |> wald_ci(percent_ci = 90)
#> # A tibble: 2 × 5
#>      am     n  prop   lcl   ucl
#>   <dbl> <int> <dbl> <dbl> <dbl>
#> 1     0    19 0.594 0.444 0.743
#> 2     1    13 0.406 0.257 0.556
counts |> wald_ci(percent_ci = 99)
#> # A tibble: 2 × 5
#>      am     n  prop   lcl   ucl
#>   <dbl> <int> <dbl> <dbl> <dbl>
#> 1     0    19 0.594 0.352 0.836
#> 2     1    13 0.406 0.164 0.648
counts |> logit_ci(percent_ci = 90)
#> # A tibble: 2 × 5
#>      am     n  prop   lcl   ucl
#>   <dbl> <int> <dbl> <dbl> <dbl>
#> 1     0    19 0.594 0.440 0.731
#> 2     1    13 0.406 0.269 0.560
counts |> logit_ci(percent_ci = 99)
#> # A tibble: 2 × 5
#>      am     n  prop   lcl   ucl
#>   <dbl> <int> <dbl> <dbl> <dbl>
#> 1     0    19 0.594 0.349 0.799
#> 2     1    13 0.406 0.201 0.651

# Independently verify the first logit lower bound.
p <- 19 / 32
expected <- plogis(qlogis(p) - qt(.975, 31) *
                    sqrt(p * (1 - p) / 31) / (p * (1 - p)))
stopifnot(isTRUE(all.equal(logit_ci(counts)$lcl[1], expected)))

Standard Errors and Critical Values

se = TRUE appends se, se_group, and/or se_total. critical_value = TRUE appends t_crit, t_crit_group, and/or t_crit_total. Each option defaults to FALSE; either can be enabled without the other.

counts |> wald_ci(se = TRUE)
#> # A tibble: 2 × 6
#>      am     n  prop     se   lcl   ucl
#>   <dbl> <int> <dbl>  <dbl> <dbl> <dbl>
#> 1     0    19 0.594 0.0882 0.414 0.774
#> 2     1    13 0.406 0.0882 0.226 0.586
counts |> wald_ci(critical_value = TRUE)
#> # A tibble: 2 × 6
#>      am     n  prop t_crit   lcl   ucl
#>   <dbl> <int> <dbl>  <dbl> <dbl> <dbl>
#> 1     0    19 0.594   2.04 0.414 0.774
#> 2     1    13 0.406   2.04 0.226 0.586
counts |> logit_ci(se = TRUE)
#> # A tibble: 2 × 6
#>      am     n  prop     se   lcl   ucl
#>   <dbl> <int> <dbl>  <dbl> <dbl> <dbl>
#> 1     0    19 0.594 0.0882 0.409 0.755
#> 2     1    13 0.406 0.0882 0.245 0.591
counts |> logit_ci(critical_value = TRUE)
#> # A tibble: 2 × 6
#>      am     n  prop t_crit   lcl   ucl
#>   <dbl> <int> <dbl>  <dbl> <dbl> <dbl>
#> 1     0    19 0.594   2.04 0.409 0.755
#> 2     1    13 0.406   2.04 0.245 0.591
grouped <- mtcars |> group_by(cyl) |> freq_tbl(am, overall = TRUE)
grouped |> wald_ci(se = TRUE, critical_value = TRUE)
#> # A tibble: 6 × 15
#>     cyl    am     n n_group prop_group n_total prop_total se_group t_crit_group
#>   <dbl> <dbl> <int>   <int>      <dbl>   <int>      <dbl>    <dbl>        <dbl>
#> 1     4     0     3      11      0.273      32     0.0938   0.141          2.23
#> 2     4     1     8      11      0.727      32     0.25     0.141          2.23
#> 3     6     0     4       7      0.571      32     0.125    0.202          2.45
#> 4     6     1     3       7      0.429      32     0.0938   0.202          2.45
#> 5     8     0    12      14      0.857      32     0.375    0.0971         2.16
#> 6     8     1     2      14      0.143      32     0.0625   0.0971         2.16
#> # ℹ 6 more variables: lcl_group <dbl>, ucl_group <dbl>, se_total <dbl>,
#> #   t_crit_total <dbl>, lcl_total <dbl>, ucl_total <dbl>
grouped |> logit_ci(se = TRUE, critical_value = TRUE)
#> # A tibble: 6 × 15
#>     cyl    am     n n_group prop_group n_total prop_total se_group t_crit_group
#>   <dbl> <dbl> <int>   <int>      <dbl>   <int>      <dbl>    <dbl>        <dbl>
#> 1     4     0     3      11      0.273      32     0.0938   0.141          2.23
#> 2     4     1     8      11      0.727      32     0.25     0.141          2.23
#> 3     6     0     4       7      0.571      32     0.125    0.202          2.45
#> 4     6     1     3       7      0.429      32     0.0938   0.202          2.45
#> 5     8     0    12      14      0.857      32     0.375    0.0971         2.16
#> 6     8     1     2      14      0.143      32     0.0625   0.0971         2.16
#> # ℹ 6 more variables: lcl_group <dbl>, ucl_group <dbl>, se_total <dbl>,
#> #   t_crit_total <dbl>, lcl_total <dbl>, ucl_total <dbl>

Percentage Scale and Composition

When counts use percent = TRUE, bounds and standard errors also use the 0–100 scale. Critical values are unchanged. The helpers obtain the scale from the prop* or percent* columns; percent_ci always specifies the confidence level, not the display scale.

pct <- mtcars |> group_by(cyl) |> freq_tbl(am, percent = TRUE, overall = TRUE)
pct |> wald_ci(se = TRUE, critical_value = TRUE)
#> # A tibble: 6 × 15
#>     cyl    am     n n_group percent_group n_total percent_total se_group
#>   <dbl> <dbl> <int>   <int>         <dbl>   <int>         <dbl>    <dbl>
#> 1     4     0     3      11          27.3      32          9.38    14.1 
#> 2     4     1     8      11          72.7      32         25       14.1 
#> 3     6     0     4       7          57.1      32         12.5     20.2 
#> 4     6     1     3       7          42.9      32          9.38    20.2 
#> 5     8     0    12      14          85.7      32         37.5      9.71
#> 6     8     1     2      14          14.3      32          6.25     9.71
#> # ℹ 7 more variables: t_crit_group <dbl>, lcl_group <dbl>, ucl_group <dbl>,
#> #   se_total <dbl>, t_crit_total <dbl>, lcl_total <dbl>, ucl_total <dbl>
pct |> logit_ci(se = TRUE, critical_value = TRUE)
#> # A tibble: 6 × 15
#>     cyl    am     n n_group percent_group n_total percent_total se_group
#>   <dbl> <dbl> <int>   <int>         <dbl>   <int>         <dbl>    <dbl>
#> 1     4     0     3      11          27.3      32          9.38    14.1 
#> 2     4     1     8      11          72.7      32         25       14.1 
#> 3     6     0     4       7          57.1      32         12.5     20.2 
#> 4     6     1     3       7          42.9      32          9.38    20.2 
#> 5     8     0    12      14          85.7      32         37.5      9.71
#> 6     8     1     2      14          14.3      32          6.25     9.71
#> # ℹ 7 more variables: t_crit_group <dbl>, lcl_group <dbl>, ucl_group <dbl>,
#> #   se_total <dbl>, t_crit_total <dbl>, lcl_total <dbl>, ucl_total <dbl>

# Replacing Wald intervals with logit intervals does not multiply by 100 again.
pct |> wald_ci(se = TRUE) |> logit_ci()
#> # A tibble: 6 × 11
#>     cyl    am     n n_group percent_group n_total percent_total lcl_group
#>   <dbl> <dbl> <int>   <int>         <dbl>   <int>         <dbl>     <dbl>
#> 1     4     0     3      11          27.3      32          9.38      7.16
#> 2     4     1     8      11          72.7      32         25        35.4 
#> 3     6     0     4       7          57.1      32         12.5      15.0 
#> 4     6     1     3       7          42.9      32          9.38      9.06
#> 5     8     0    12      14          85.7      32         37.5      52.0 
#> 6     8     1     2      14          14.3      32          6.25      2.92
#> # ℹ 3 more variables: ucl_group <dbl>, lcl_total <dbl>, ucl_total <dbl>

Repeated calls replace interval columns, and unrequested se* and t_crit* columns are removed. Renaming proportions or removing denominator columns/metadata can make the table unsuitable for subsequent calculations. Use formatting only after all computations.

Boundaries and Errors

At p = 0 or 1, logit limits are missing because the logit is infinite; no continuity correction is applied. Wald limits equal p when N > 1. For interior values, Wald intervals can extend beyond [0, 1] and are not clipped. N = 0 gives an undefined proportion. N <= 1 gives missing standard errors, critical values, and limits in either method.

boundary <- data.frame(y = factor(rep("yes", 4), levels = c("no", "yes")))
freq_tbl(boundary, y) |> logit_ci()
#> # A tibble: 2 × 5
#>   y         n  prop   lcl   ucl
#>   <fct> <int> <dbl> <dbl> <dbl>
#> 1 no        0     0    NA    NA
#> 2 yes       4     1    NA    NA
freq_tbl(boundary, y) |> wald_ci()
#> # A tibble: 2 × 5
#>   y         n  prop   lcl   ucl
#>   <fct> <int> <dbl> <dbl> <dbl>
#> 1 no        0     0     0     0
#> 2 yes       4     1     1     1
freq_tbl(boundary[1, , drop = FALSE], y) |> wald_ci(se = TRUE)
#> # A tibble: 2 × 6
#>   y         n  prop    se   lcl   ucl
#>   <fct> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 no        0     0    NA    NA    NA
#> 2 yes       1     1    NA    NA    NA
freq_tbl(boundary[FALSE, , drop = FALSE], y) |> logit_ci()
#> # A tibble: 2 × 5
#>   y         n  prop   lcl   ucl
#>   <fct> <int> <dbl> <dbl> <dbl>
#> 1 no        0    NA    NA    NA
#> 2 yes       0    NA    NA    NA
wald_ci(data.frame(n = 1, n_total = 3, prop = 1/3))
#>   n n_total      prop       lcl      ucl
#> 1 1       3 0.3333333 -1.100884 1.767551

Missing logit bounds do not mean the population proportion is known to be exactly zero or one. These approximations need care with small or extreme counts; the package does not automatically select another interval method.

counts |> logit_ci(percent_ci = 100)
#> Error:
#> ! percent_ci must be a number strictly between 0 and 100.
wald_ci(data.frame(n = 6, n_total = 5, prop = 1.2))
#> Error:
#> ! n_total must contain finite integer denominators at least as large as n.
logit_ci(data.frame(n = 19, prop = 19/32))
#> Error:
#> ! Missing n_total; retain freqtables metadata or supply the original denominator.

See R’s Student t distribution documentation for the critical-value function used here.