Counts, Proportions, and Grouped Frequency Tables

Use freq_tbl() for counts and proportions and freq_table() when you also want confidence intervals. Both analyze one outcome; preceding group_by() calls specify any grouping variables. All examples use local data frames.

Inputs and Default Output

Here are 16 observations whose counts can be checked directly: group A has 3 “no” and 7 “yes”; group B has 4 “no” and 2 “yes”. The overall counts are 7 “no” and 9 “yes”. The .data argument is the data frame (supplied by the pipe below), and .freq_var is its outcome column.

study <- data.frame(
  group = rep(c("A", "A", "B", "B"), c(3, 7, 4, 2)),
  outcome = rep(c("no", "yes", "no", "yes"), c(3, 7, 4, 2))
)
freq_tbl(study, outcome)
#> # A tibble: 2 × 3
#>   outcome     n  prop
#>   <chr>   <int> <dbl>
#> 1 no          7 0.438
#> 2 yes         9 0.562
study |> freq_table(outcome)
#> # A tibble: 2 × 5
#>   outcome     n  prop   lcl   ucl
#>   <chr>   <int> <dbl> <dbl> <dbl>
#> 1 no          7 0.438 0.204 0.702
#> 2 yes         9 0.562 0.298 0.796

The first result contains outcome, n, and prop: 7/16 and 9/16. The second adds lcl and ucl. Proportions and interval limits are numbers on the 0–1 scale. Rounding in printed tibbles does not round stored values. Outcomes retain their original type, including factor levels.

The column may also be a literal string. To forward an unquoted column through your own function, embrace it with {{ }}:

freq_tbl(study, "outcome")
#> # A tibble: 2 × 3
#>   outcome     n  prop
#>   <chr>   <int> <dbl>
#> 1 no          7 0.438
#> 2 yes         9 0.562
freq_table(study, "outcome")
#> # A tibble: 2 × 5
#>   outcome     n  prop   lcl   ucl
#>   <chr>   <int> <dbl> <dbl> <dbl>
#> 1 no          7 0.438 0.204 0.702
#> 2 yes         9 0.562 0.298 0.796
count_outcome <- function(data, variable) freq_tbl(data, {{ variable }})
interval_outcome <- function(data, variable) freq_table(data, {{ variable }})
count_outcome(study, outcome)
#> # A tibble: 2 × 3
#>   outcome     n  prop
#>   <chr>   <int> <dbl>
#> 1 no          7 0.438
#> 2 yes         9 0.562
interval_outcome(study, outcome)
#> # A tibble: 2 × 5
#>   outcome     n  prop   lcl   ucl
#>   <chr>   <int> <dbl> <dbl> <dbl>
#> 1 no          7 0.438 0.204 0.702
#> 2 yes         9 0.562 0.298 0.796

One or Multiple Grouping Variables

grouped <- study |> group_by(group)
grouped |> freq_tbl(outcome)
#> # A tibble: 4 × 5
#>   group outcome     n n_group prop_group
#>   <chr> <chr>   <int>   <int>      <dbl>
#> 1 A     no          3      10      0.3  
#> 2 A     yes         7      10      0.7  
#> 3 B     no          4       6      0.667
#> 4 B     yes         2       6      0.333
grouped |> freq_table(outcome)
#> # A tibble: 4 × 7
#>   group outcome     n n_group prop_group lcl_group ucl_group
#>   <chr> <chr>   <int>   <int>      <dbl>     <dbl>     <dbl>
#> 1 A     no          3      10      0.3      0.0764     0.690
#> 2 A     yes         7      10      0.7      0.310      0.924
#> 3 B     no          4       6      0.667    0.149      0.958
#> 4 B     yes         2       6      0.333    0.0418     0.851

n is the cell count; n_group is 10 for A and 6 for B. Thus the within-group proportions are 3/10, 7/10, 4/6, and 2/6. Each group’s proportions sum to 1. freq_table() adds lcl_group and ucl_group.

The same rule applies to each combination of multiple groups:

study$band <- rep(c("young", "old"), 8)
study |> group_by(group, band) |> freq_tbl(outcome)
#> # A tibble: 8 × 6
#>   group band  outcome     n n_group prop_group
#>   <chr> <chr> <chr>   <int>   <int>      <dbl>
#> 1 A     old   no          1       5      0.2  
#> 2 A     old   yes         4       5      0.8  
#> 3 A     young no          2       5      0.4  
#> 4 A     young yes         3       5      0.6  
#> 5 B     old   no          2       3      0.667
#> 6 B     old   yes         1       3      0.333
#> 7 B     young no          2       3      0.667
#> 8 B     young yes         1       3      0.333
study |> group_by(group, band) |> freq_table(outcome)
#> # A tibble: 8 × 8
#>   group band  outcome     n n_group prop_group lcl_group ucl_group
#>   <chr> <chr> <chr>   <int>   <int>      <dbl>     <dbl>     <dbl>
#> 1 A     old   no          1       5      0.2    0.00771      0.889
#> 2 A     old   yes         4       5      0.8    0.111        0.992
#> 3 A     young no          2       5      0.4    0.0377       0.919
#> 4 A     young yes         3       5      0.6    0.0810       0.962
#> 5 B     old   no          2       3      0.667  0.00314      0.999
#> 6 B     old   yes         1       3      0.333  0.000787     0.997
#> 7 B     young no          2       3      0.667  0.00314      0.999
#> 8 B     young yes         1       3      0.333  0.000787     0.997

Results are ungrouped tibbles. This avoids carrying implicit grouping into later calculations. To summarize the results by group, call group_by() again. A one-person group can have a defined proportion but has missing intervals under the package’s t-based convention.

Percentages and Overall Results

percent = FALSE is the default in both functions. Set it to TRUE to multiply proportions, interval bounds, and requested standard errors by 100. Count columns and t critical values do not change. Proportion column names switch from prop* to percent*.

grouped |> freq_tbl(outcome, percent = TRUE)
#> # A tibble: 4 × 5
#>   group outcome     n n_group percent_group
#>   <chr> <chr>   <int>   <int>         <dbl>
#> 1 A     no          3      10          30  
#> 2 A     yes         7      10          70  
#> 3 B     no          4       6          66.7
#> 4 B     yes         2       6          33.3
grouped |> freq_table(outcome, percent = TRUE)
#> # A tibble: 4 × 7
#>   group outcome     n n_group percent_group lcl_group ucl_group
#>   <chr> <chr>   <int>   <int>         <dbl>     <dbl>     <dbl>
#> 1 A     no          3      10          30        7.64      69.0
#> 2 A     yes         7      10          70       31.0       92.4
#> 3 B     no          4       6          66.7     14.9       95.8
#> 4 B     yes         2       6          33.3      4.18      85.1
freq_tbl(study, outcome, percent = TRUE)
#> # A tibble: 2 × 3
#>   outcome     n percent
#>   <chr>   <int>   <dbl>
#> 1 no          7    43.8
#> 2 yes         9    56.2
freq_table(study, outcome, percent = TRUE)
#> # A tibble: 2 × 5
#>   outcome     n percent   lcl   ucl
#>   <chr>   <int>   <dbl> <dbl> <dbl>
#> 1 no          7    43.8  20.4  70.2
#> 2 yes         9    56.2  29.8  79.6

overall = FALSE keeps grouped output focused on within-group results. With TRUE, n_total is the total input rows, and prop_total is the cell count divided by that total. For A/“no”, this is 3/16, not the marginal 7/16 for all “no” outcomes. freq_table() also adds overall interval bounds. For ungrouped data, overall = TRUE adds only n_total, because prop and its intervals are already overall.

grouped |> freq_tbl(outcome, overall = TRUE)
#> # A tibble: 4 × 7
#>   group outcome     n n_group prop_group n_total prop_total
#>   <chr> <chr>   <int>   <int>      <dbl>   <int>      <dbl>
#> 1 A     no          3      10      0.3        16      0.188
#> 2 A     yes         7      10      0.7        16      0.438
#> 3 B     no          4       6      0.667      16      0.25 
#> 4 B     yes         2       6      0.333      16      0.125
grouped |> freq_table(outcome, overall = TRUE)
#> # A tibble: 4 × 11
#>   group outcome     n n_group prop_group n_total prop_total lcl_group ucl_group
#>   <chr> <chr>   <int>   <int>      <dbl>   <int>      <dbl>     <dbl>     <dbl>
#> 1 A     no          3      10      0.3        16      0.188    0.0764     0.690
#> 2 A     yes         7      10      0.7        16      0.438    0.310      0.924
#> 3 B     no          4       6      0.667      16      0.25     0.149      0.958
#> 4 B     yes         2       6      0.333      16      0.125    0.0418     0.851
#> # ℹ 2 more variables: lcl_total <dbl>, ucl_total <dbl>
freq_tbl(study, outcome, overall = TRUE)
#> # A tibble: 2 × 4
#>   outcome     n  prop n_total
#>   <chr>   <int> <dbl>   <int>
#> 1 no          7 0.438      16
#> 2 yes         9 0.562      16
freq_table(study, outcome, overall = TRUE)
#> # A tibble: 2 × 6
#>   outcome     n  prop n_total   lcl   ucl
#>   <chr>   <int> <dbl>   <int> <dbl> <dbl>
#> 1 no          7 0.438      16 0.204 0.702
#> 2 yes         9 0.562      16 0.298 0.796
grouped |> freq_table(outcome, overall = TRUE, percent = TRUE)
#> # A tibble: 4 × 11
#>   group outcome     n n_group percent_group n_total percent_total lcl_group
#>   <chr> <chr>   <int>   <int>         <dbl>   <int>         <dbl>     <dbl>
#> 1 A     no          3      10          30        16          18.8      7.64
#> 2 A     yes         7      10          70        16          43.8     31.0 
#> 3 B     no          4       6          66.7      16          25       14.9 
#> 4 B     yes         2       6          33.3      16          12.5      4.18
#> # ℹ 3 more variables: ucl_group <dbl>, lcl_total <dbl>, ucl_total <dbl>

Missing Values and Unobserved Factor Levels

Missing outcomes and grouping values count as categories by default. To exclude them, filter the original observations. Neither function has an na.rm argument. The denominators always describe the rows after filtering.

with_missing <- data.frame(
  group = factor(c("A", "A", NA), levels = c("A", "B")),
  outcome = factor(c("no", NA, "yes"), levels = c("no", "yes", "unused"))
)
with_missing |> group_by(group) |> freq_tbl(outcome)
#> # A tibble: 10 × 5
#>    group outcome     n n_group prop_group
#>    <fct> <fct>   <int>   <int>      <dbl>
#>  1 A     no          1       2        0.5
#>  2 A     yes         0       2        0  
#>  3 A     unused      0       2        0  
#>  4 A     <NA>        1       2        0.5
#>  5 B     no          0       0       NA  
#>  6 B     yes         0       0       NA  
#>  7 B     unused      0       0       NA  
#>  8 <NA>  no          0       1        0  
#>  9 <NA>  yes         1       1        1  
#> 10 <NA>  unused      0       1        0
with_missing |> group_by(group) |> freq_table(outcome)
#> # A tibble: 10 × 7
#>    group outcome     n n_group prop_group lcl_group ucl_group
#>    <fct> <fct>   <int>   <int>      <dbl>     <dbl>     <dbl>
#>  1 A     no          1       2        0.5  9.19e-12     1.000
#>  2 A     yes         0       2        0   NA           NA    
#>  3 A     unused      0       2        0   NA           NA    
#>  4 A     <NA>        1       2        0.5  9.19e-12     1.000
#>  5 B     no          0       0       NA   NA           NA    
#>  6 B     yes         0       0       NA   NA           NA    
#>  7 B     unused      0       0       NA   NA           NA    
#>  8 <NA>  no          0       1        0   NA           NA    
#>  9 <NA>  yes         1       1        1   NA           NA    
#> 10 <NA>  unused      0       1        0   NA           NA
with_missing |>
  filter(!is.na(group), !is.na(outcome)) |>
  group_by(group) |> freq_tbl(outcome)
#> # A tibble: 6 × 5
#>   group outcome     n n_group prop_group
#>   <fct> <fct>   <int>   <int>      <dbl>
#> 1 A     no          1       1          1
#> 2 A     yes         0       1          0
#> 3 A     unused      0       1          0
#> 4 B     no          0       0         NA
#> 5 B     yes         0       0         NA
#> 6 B     unused      0       0         NA

drop = FALSE retains unobserved factor levels and empty factor-group combinations with n = 0, following dplyr’s count semantics. A completely empty group has n_group = 0 and missing proportions and intervals. This option is independent of missing-value removal.

freq_tbl(with_missing, outcome, drop = FALSE)
#> # A tibble: 4 × 3
#>   outcome     n  prop
#>   <fct>   <int> <dbl>
#> 1 no          1 0.333
#> 2 yes         1 0.333
#> 3 unused      0 0    
#> 4 <NA>        1 0.333
freq_tbl(with_missing, outcome, drop = TRUE)
#> # A tibble: 3 × 3
#>   outcome     n  prop
#>   <fct>   <int> <dbl>
#> 1 no          1 0.333
#> 2 yes         1 0.333
#> 3 <NA>        1 0.333
freq_table(with_missing, outcome, drop = FALSE)
#> # A tibble: 4 × 5
#>   outcome     n  prop       lcl    ucl
#>   <fct>   <int> <dbl>     <dbl>  <dbl>
#> 1 no          1 0.333  0.000787  0.997
#> 2 yes         1 0.333  0.000787  0.997
#> 3 unused      0 0     NA        NA    
#> 4 <NA>        1 0.333  0.000787  0.997
freq_table(with_missing, outcome, drop = TRUE)
#> # A tibble: 3 × 5
#>   outcome     n  prop      lcl   ucl
#>   <fct>   <int> <dbl>    <dbl> <dbl>
#> 1 no          1 0.333 0.000787 0.997
#> 2 yes         1 0.333 0.000787 0.997
#> 3 <NA>        1 0.333 0.000787 0.997
with_missing |> group_by(group, .drop = FALSE) |>
  freq_table(outcome, drop = TRUE)
#> # A tibble: 3 × 7
#>   group outcome     n n_group prop_group lcl_group ucl_group
#>   <fct> <fct>   <int>   <int>      <dbl>     <dbl>     <dbl>
#> 1 A     no          1       2        0.5  9.19e-12     1.000
#> 2 A     <NA>        1       2        0.5  9.19e-12     1.000
#> 3 <NA>  yes         1       1        1   NA           NA

An empty data frame with a factor outcome can therefore produce explicit zero rows; an empty character outcome produces a zero-row result.

Generic Headers for Programming

generic_col_names = FALSE uses variable names as headers and preserves their types. With TRUE, the outcome becomes col/cat, and groups become group_01_col/group_01_cat, group_02_col/ group_02_cat, and so on in group_by() order. All category columns become character, allowing numeric and character outcomes to be stacked.

grouped |> freq_tbl(outcome, generic_col_names = TRUE)
#> # A tibble: 4 × 7
#>   group_01_col group_01_cat col     cat       n n_group prop_group
#>   <chr>        <chr>        <chr>   <chr> <int>   <int>      <dbl>
#> 1 group        A            outcome no        3      10      0.3  
#> 2 group        A            outcome yes       7      10      0.7  
#> 3 group        B            outcome no        4       6      0.667
#> 4 group        B            outcome yes       2       6      0.333
grouped |> freq_table(outcome, generic_col_names = TRUE)
#> # A tibble: 4 × 9
#>   group_01_col group_01_cat col     cat       n n_group prop_group lcl_group
#>   <chr>        <chr>        <chr>   <chr> <int>   <int>      <dbl>     <dbl>
#> 1 group        A            outcome no        3      10      0.3      0.0764
#> 2 group        A            outcome yes       7      10      0.7      0.310 
#> 3 group        B            outcome no        4       6      0.667    0.149 
#> 4 group        B            outcome yes       2       6      0.333    0.0418
#> # ℹ 1 more variable: ucl_group <dbl>
study |> group_by(group, band) |>
  freq_table(outcome, generic_col_names = TRUE)
#> # A tibble: 8 × 11
#>   group_01_col group_01_cat group_02_col group_02_cat col    cat       n n_group
#>   <chr>        <chr>        <chr>        <chr>        <chr>  <chr> <int>   <int>
#> 1 group        A            band         old          outco… no        1       5
#> 2 group        A            band         old          outco… yes       4       5
#> 3 group        A            band         young        outco… no        2       5
#> 4 group        A            band         young        outco… yes       3       5
#> 5 group        B            band         old          outco… no        2       3
#> 6 group        B            band         old          outco… yes       1       3
#> 7 group        B            band         young        outco… no        2       3
#> 8 group        B            band         young        outco… yes       1       3
#> # ℹ 3 more variables: prop_group <dbl>, lcl_group <dbl>, ucl_group <dbl>
bind_rows(
  freq_tbl(study, outcome, generic_col_names = TRUE),
  freq_tbl(mtcars, am, generic_col_names = TRUE)
)
#> # A tibble: 4 × 4
#>   col     cat       n  prop
#>   <chr>   <chr> <int> <dbl>
#> 1 outcome no        7 0.438
#> 2 outcome yes       9 0.562
#> 3 am      0        19 0.594
#> 4 am      1        13 0.406

Calculate intervals and tests before stacking separate analyses. Metadata belongs to one analysis and is not a registry for a stack of tables.

Confidence-Interval Controls

Only freq_table() has interval controls. percent_ci = 95 means 95% confidence, regardless of percent. Any numeric level strictly between 0 and 100 is accepted. ci_type is either "logit" (default) or "wald"; both work with any number of groups.

grouped |> freq_table(outcome, percent_ci = 90, ci_type = "logit")
#> # A tibble: 4 × 7
#>   group outcome     n n_group prop_group lcl_group ucl_group
#>   <chr> <chr>   <int>   <int>      <dbl>     <dbl>     <dbl>
#> 1 A     no          3      10      0.3      0.101      0.619
#> 2 A     yes         7      10      0.7      0.381      0.899
#> 3 B     no          4       6      0.667    0.228      0.931
#> 4 B     yes         2       6      0.333    0.0688     0.772
grouped |> freq_table(outcome, percent_ci = 99, ci_type = "wald")
#> # A tibble: 4 × 7
#>   group outcome     n n_group prop_group lcl_group ucl_group
#>   <chr> <chr>   <int>   <int>      <dbl>     <dbl>     <dbl>
#> 1 A     no          3      10      0.3      -0.196     0.796
#> 2 A     yes         7      10      0.7       0.204     1.20 
#> 3 B     no          4       6      0.667    -0.183     1.52 
#> 4 B     yes         2       6      0.333    -0.517     1.18

se = FALSE and critical_value = FALSE omit intermediate statistics. Enable them independently to include standard errors and Student-t critical values. These are also available for overall results:

grouped |> freq_table(outcome, se = TRUE)
#> # A tibble: 4 × 8
#>   group outcome     n n_group prop_group se_group lcl_group ucl_group
#>   <chr> <chr>   <int>   <int>      <dbl>    <dbl>     <dbl>     <dbl>
#> 1 A     no          3      10      0.3      0.153    0.0764     0.690
#> 2 A     yes         7      10      0.7      0.153    0.310      0.924
#> 3 B     no          4       6      0.667    0.211    0.149      0.958
#> 4 B     yes         2       6      0.333    0.211    0.0418     0.851
grouped |> freq_table(outcome, critical_value = TRUE)
#> # A tibble: 4 × 8
#>   group outcome     n n_group prop_group t_crit_group lcl_group ucl_group
#>   <chr> <chr>   <int>   <int>      <dbl>        <dbl>     <dbl>     <dbl>
#> 1 A     no          3      10      0.3           2.26    0.0764     0.690
#> 2 A     yes         7      10      0.7           2.26    0.310      0.924
#> 3 B     no          4       6      0.667         2.57    0.149      0.958
#> 4 B     yes         2       6      0.333         2.57    0.0418     0.851
grouped |> freq_table(outcome, se = TRUE, critical_value = TRUE,
                       overall = TRUE, percent = TRUE)
#> # A tibble: 4 × 15
#>   group outcome     n n_group percent_group n_total percent_total se_group
#>   <chr> <chr>   <int>   <int>         <dbl>   <int>         <dbl>    <dbl>
#> 1 A     no          3      10          30        16          18.8     15.3
#> 2 A     yes         7      10          70        16          43.8     15.3
#> 3 B     no          4       6          66.7      16          25       21.1
#> 4 B     yes         2       6          33.3      16          12.5     21.1
#> # ℹ 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>

For formulas and boundary behavior, read Confidence Intervals.

Invalid Calls and Reserved Arguments

... must be empty: both functions accept one outcome, with all other options supplied by name. An old two-variable call gives a migration error. Incorrectly named options are errors too.

freq_tbl(study, group, outcome)
#> Error:
#> ! freq_tbl() accepts one outcome and named options only. For grouped analysis use mtcars |> dplyr::group_by(cyl) |> freq_tbl(am), instead of freq_tbl(mtcars, cyl, am). Unused options: outcome.
freq_table(study, group, outcome)
#> Error:
#> ! freq_table() accepts one outcome and named options only. For grouped analysis use mtcars |> dplyr::group_by(cyl) |> freq_table(am), instead of freq_table(mtcars, cyl, am). Unused options: outcome.
freq_table(study, outcome, na.rm = TRUE)
#> Error:
#> ! freq_table() accepts one outcome and named options only. For grouped analysis use mtcars |> dplyr::group_by(cyl) |> freq_table(am), instead of freq_table(mtcars, cyl, am). Unused options: TRUE.

Create derived variables with mutate() first. Do not use the outcome as its own grouping variable. Rowwise input requires ungroup(). Analysis names that conflict with generated columns (for example n or prop) must be renamed first, so result columns never silently overwrite categories.