Testing Frequency Tables

freq_test() is an S3 generic with methods for one-way and two-way freq_table() results. The method is selected from the result class: no grouping variable gives a goodness-of-fit test against equal category probabilities; one grouping variable gives a test of independence.

Input and One-Way Method

.data must be a complete result with its category/count columns and analysis metadata intact. This applies to the generic and both supported methods. Filter the original observations to define an analysis population, then calculate the table and test; filtering table rows can invalidate a test.

one <- freq_table(mtcars, am)
one |> freq_test()
#> # A tibble: 2 × 10
#>      am     n  prop   lcl   ucl n_expected chi2_contrib chi2_pearson    df
#>   <dbl> <int> <dbl> <dbl> <dbl>      <dbl>        <dbl>        <dbl> <int>
#> 1     0    19 0.594 0.409 0.755         16        0.562         1.12     1
#> 2     1    13 0.406 0.245 0.591         16        0.562         1.12     1
#> # ℹ 1 more variable: p_chi2_pearson <dbl>
one |> freq_test() |> select(am, n, prop, n_expected, p_chi2_pearson)
#> # A tibble: 2 × 5
#>      am     n  prop n_expected p_chi2_pearson
#>   <dbl> <int> <dbl>      <dbl>          <dbl>
#> 1     0    19 0.594         16          0.289
#> 2     1    13 0.406         16          0.289

The two counts are 19 and 13. Under equal proportions, each expected count is 16. The Pearson statistic is 1.125 with 1 degree of freedom, giving p approximately 0.289. The same overall statistic and p-value are repeated on each row to make them available for formatting.

This tests an equal-probability null; it does not accept arbitrary expected probabilities. The generic forwards ... to the selected method, but both methods currently require it to be empty. No method argument or other extra option is supported.

one |> freq_test(method = "fisher")
#> Error:
#> ! freq_test() does not support extra arguments in ....

Two-Way Method and Small Expected Counts

two <- mtcars |> group_by(am) |> freq_table(cyl)
tested <- two |> freq_test()
#> One or more expected cell counts are <= 5. Fisher's exact p-value is also returned.
tested |> select(am, cyl, n, n_expected, p_chi2_pearson, p_fisher)
#> # A tibble: 6 × 6
#>      am   cyl     n n_expected p_chi2_pearson p_fisher
#>   <dbl> <dbl> <int>      <dbl>          <dbl>    <dbl>
#> 1     0     4     3       6.53         0.0126  0.00910
#> 2     0     6     4       4.16         0.0126  0.00910
#> 3     0     8    12       8.31         0.0126  0.00910
#> 4     1     4     8       4.47         0.0126  0.00910
#> 5     1     6     3       2.84         0.0126  0.00910
#> 6     1     8     2       5.69         0.0126  0.00910

This tests independence between am and cyl. Output includes n_col (outcome marginal counts), expected cell counts, chi2_contrib, the summed statistic, numbers of rows/columns (r, c), degrees of freedom, and p_chi2_pearson. The Pearson test is uncorrected, including for a 2 by 2 table.

When any expected cell count is <= 5, the function emits a message and adds p_fisher alongside the Pearson result. Fisher’s calculation uses the full rectangular contingency table. It is not restricted to two group categories. As with R’s fisher.test(), a large exact calculation may fail; that error is propagated.

mtcars |> group_by(cyl) |> freq_table(am) |> freq_test()
#> One or more expected cell counts are <= 5. Fisher's exact p-value is also returned.
#> # A tibble: 6 × 16
#>     cyl    am     n n_group prop_group lcl_group ucl_group n_col n_expected
#>   <dbl> <dbl> <int>   <int>      <dbl>     <dbl>     <dbl> <dbl>      <dbl>
#> 1     4     0     3      11      0.273    0.0716     0.646    19       6.53
#> 2     4     1     8      11      0.727    0.354      0.928    13       4.47
#> 3     6     0     4       7      0.571    0.150      0.909    19       4.16
#> 4     6     1     3       7      0.429    0.0906     0.850    13       2.84
#> 5     8     0    12      14      0.857    0.520      0.971    19       8.31
#> 6     8     1     2      14      0.143    0.0292     0.480    13       5.69
#> # ℹ 7 more variables: chi2_contrib <dbl>, chi2_pearson <dbl>, r <int>, c <int>,
#> #   df <int>, p_chi2_pearson <dbl>, p_fisher <dbl>
# Table-row order does not determine contingency-table structure.
two[c(6, 1, 4, 2, 5, 3), ] |> freq_test()
#> One or more expected cell counts are <= 5. Fisher's exact p-value is also returned.
#> # A tibble: 6 × 16
#>      am   cyl     n n_group prop_group lcl_group ucl_group n_col n_expected
#>   <dbl> <dbl> <int>   <int>      <dbl>     <dbl>     <dbl> <dbl>      <dbl>
#> 1     1     8     2      13      0.154    0.0308     0.510    14       5.69
#> 2     0     4     3      19      0.158    0.0460     0.422    11       6.53
#> 3     1     4     8      13      0.615    0.305      0.854    11       4.47
#> 4     0     6     4      19      0.211    0.0733     0.473     7       4.16
#> 5     1     6     3      13      0.231    0.0632     0.572     7       2.84
#> 6     0     8    12      19      0.632    0.380      0.827    14       8.31
#> # ℹ 7 more variables: chi2_contrib <dbl>, chi2_pearson <dbl>, r <int>, c <int>,
#> #   df <int>, p_chi2_pearson <dbl>, p_fisher <dbl>

reference <- table(mtcars$am, mtcars$cyl)
stopifnot(isTRUE(all.equal(tested$p_fisher[1], fisher.test(reference)$p.value)))

When all expected counts exceed 5, only the Pearson p-value is appended:

larger <- mtcars[rep(seq_len(nrow(mtcars)), 10), ]
larger |> group_by(am) |> freq_table(cyl) |> freq_test()
#> # A tibble: 6 × 15
#>      am   cyl     n n_group prop_group lcl_group ucl_group n_col n_expected
#>   <dbl> <dbl> <int>   <int>      <dbl>     <dbl>     <dbl> <dbl>      <dbl>
#> 1     0     4    30     190      0.158     0.112     0.217   110       65.3
#> 2     0     6    40     190      0.211     0.158     0.275    70       41.6
#> 3     0     8   120     190      0.632     0.560     0.698   140       83.1
#> 4     1     4    80     130      0.615     0.528     0.696   110       44.7
#> 5     1     6    30     130      0.231     0.166     0.312    70       28.4
#> 6     1     8    20     130      0.154     0.101     0.228   140       56.9
#> # ℹ 6 more variables: chi2_contrib <dbl>, chi2_pearson <dbl>, r <int>, c <int>,
#> #   df <int>, p_chi2_pearson <dbl>

This repetition is solely a demonstration of function behavior, not a way to create independent observations for an actual analysis.

Generic Headers and Formatting

Tests depend on counts and category identities, so generic headers and percentage display give the same p-values. Calculate tests before stacking results from different outcomes.

mtcars |> group_by(am) |>
  freq_table(cyl, generic_col_names = TRUE, percent = TRUE) |>
  freq_test() |>
  freq_format("percent_group (lcl_group - ucl_group)", name = "estimate", digits = 2) |>
  select(group_01_cat, cat, estimate, p_chi2_pearson, p_fisher)
#> One or more expected cell counts are <= 5. Fisher's exact p-value is also returned.
#> # A tibble: 6 × 5
#>   group_01_cat cat   estimate              p_chi2_pearson p_fisher
#>   <chr>        <chr> <chr>                          <dbl>    <dbl>
#> 1 0            4     15.79 (4.60 - 42.17)          0.0126  0.00910
#> 2 0            6     21.05 (7.33 - 47.32)          0.0126  0.00910
#> 3 0            8     63.16 (38.05 - 82.72)         0.0126  0.00910
#> 4 1            4     61.54 (30.52 - 85.36)         0.0126  0.00910
#> 5 1            6     23.08 (6.32 - 57.17)          0.0126  0.00910
#> 6 1            8     15.38 (3.08 - 50.96)          0.0126  0.00910

Missing Values, Empty Cells, and Unsupported Inputs

Missing outcomes or group values are categories unless excluded before freq_table(). An unobserved cell is filled with zero for the test even if omitted from the returned table. A completely empty marginal category makes a two-way test undefined: remove unused levels with drop = TRUE. One-way tests can retain a zero-count category as part of their equal-probability null.

d <- data.frame(g = c("a", "a", "b", "b", NA),
                y = c("no", "yes", "yes", NA, "no"))
d |> filter(!is.na(g), !is.na(y)) |>
  group_by(g) |> freq_table(y, drop = TRUE) |> freq_test()
#> One or more expected cell counts are <= 5. Fisher's exact p-value is also returned.
#> # A tibble: 3 × 16
#>   g     y         n n_group prop_group lcl_group ucl_group n_col n_expected
#>   <chr> <chr> <int>   <int>      <dbl>     <dbl>     <dbl> <dbl>      <dbl>
#> 1 a     no        1       2        0.5  9.19e-12     1.000     1      0.667
#> 2 a     yes       1       2        0.5  9.19e-12     1.000     2      1.33 
#> 3 b     yes       1       1        1   NA           NA         2      0.667
#> # ℹ 7 more variables: chi2_contrib <dbl>, chi2_pearson <dbl>, r <int>, c <int>,
#> #   df <int>, p_chi2_pearson <dbl>, p_fisher <dbl>

There is no automatic test for multiple grouping variables, because a joint comparison and a test within each stratum answer different questions. The default method explains this limitation:

mtcars |> group_by(cyl, vs) |> freq_table(am) |> freq_test()
#> Error:
#> ! freq_test() supports freq_table() results with zero or one grouping variable. For multiple groups, select an explicitly defined analysis.
one |> filter(am == 0) |> freq_test()
#> Error:
#> ! freq_test() requires a complete table with intact category/count columns; recreate it from the desired input observations.

For statistical details, see R’s Pearson chi-square and Fisher exact test documentation. Both rely on an appropriate sampling design; counting repeated observations does not make them independent.