8  Tabulate data

TipIn this chapter
  • Count categories and numeric values, and distinguish separate distributions from joint and pairwise tables.
  • Combine one-way tables and inspect long results without discarding rows.
  • Distinguish percentages of all cases from percentages of valid answers, and identify the denominator within each comparison group.
  • Describe educational qualifications in the 2024 General Social Survey.

How common is a response, and how does its distribution differ across groups? A frequency table describes the distribution of categories, such as happiness or educational qualifications. It can also count each distinct recorded value of a numeric variable. Every percentage requires us to be clear about who contributes to the calculation.

tab() counts cases and reports percentages and cumulative percentages, tab1() produces a separate distribution for each selected variable, and tab2() cross-tabulates each pair of selected variables. fre() adds total rows to a frequency table.

These tabulation functions come from tidystata and return new tables without changing the data we give them.

The examples build on inspecting a dataset in Chapter 7. We supply the dataset as the first argument, followed by column names without quotation marks: for example, tab(gss24, happy). Commas separate the arguments inside the parentheses. The pipe learned in Chapter 5 gives an equivalent form: gss24 |> tab(happy). We use direct calls when a single operation is enough. A named argument such as .by = sex sets an option; the = here supplies an argument value rather than saving an object. Grouped percentages are explained where they first appear.

After installing the packages (see Section 1.3), load them at the start of your session:

library(tidyverse)
library(tidystata)

We now move from country records to individual survey respondents, using one survey extract throughout this chapter and the following numerical-summary lessons. The 2024 General Social Survey (GSS) records adults’ demographic characteristics and answers about their families, work, education, and attitudes. The GSS has interviewed adults in the United States since 1972.

The one-time recipe in Appendix A explains how to obtain the freely available source data, prepare the variables, and save the gss24 extract as gss24.rds. Complete that recipe before running the examples below. If that file is in your working directory, load it with:

gss24 <- readRDS("gss24.rds")

readRDS() restores the saved R object, and <- stores it under the name gss24. You only need to load it once per session. Each row is one respondent; id is the respondent identifier. A row position or printed line number is not a permanent identity. Unknown answers remain missing rather than becoming response categories. The appendix prepares categorical columns with defined levels so we can tabulate them here without constructing factors ourselves.

Our extract contains 3,986 respondents and 25 variables. The main examples use full gss24. A final application creates a working-age subset and identifies its different percentage denominators explicitly.

8.1 Read frequency tables with tab(), tab1(), and tab2()

8.1.1 Start with numeric frequencies

Frequency tables help us inspect numeric measurements. Are recorded values repeated, and how far apart are the low and high values? In gss24, age records respondents’ ages in years. It measures a quantity rather than a named category, but tab() can still count every distinct recorded value. Store that result so we can inspect it in two ways:

age_freq <- tab(gss24, age)

Assignment with <- saves the full table under age_freq without printing it. Read the values and counts first; the next examples explain the percentage columns. Each row represents an exact recorded age, or a missing value, and n counts respondents with that value. These rows are not age brackets. The counts describe stored values: ages recorded in whole years are shared by many respondents.

There are many distinct values, so use the complementary previews from Section 4.4: head/tail to inspect the boundaries, and interval to inspect positions across the interior. Start with print_headtail():

print_headtail(age_freq)
# A tibble: 73 × 6
     age     n percent     cum   valid valid_cum
   <dbl> <int>   <dbl>   <dbl>   <dbl>     <dbl>
 1    18    33  0.828    0.828  0.855      0.855
 2    19    40  1.00     1.83   1.04       1.89 
 3    20    43  1.08     2.91   1.11       3.01 
 4    21    36  0.903    3.81   0.933      3.94 
 5    22    53  1.33     5.14   1.37       5.31 
 ⋮     ⋮     ⋮   ⋮        ⋮      ⋮          ⋮   
69    86    11  0.276   96.0    0.285     99.1  
70    87     3  0.0753  96.0    0.0777    99.2  
71    88     3  0.0753  96.1    0.0777    99.3  
72    89    28  0.702   96.8    0.726    100    
73    NA   127  3.19   100     NA         NA    
# ... 63 rows omitted in the middle; use print(x, n = Inf) to show all rows.

The header reports 73 rows, although only ten are displayed. Recorded ages run from 18 to 89. Each non-missing row counts respondents sharing a recorded age. The final NA row counts the 127 respondents with unknown age, not the oldest respondents. The omission message marks the undisplayed middle of the table.

For coverage through the middle, use print_interval() with the same default budget of ten data rows. It displays approximately evenly spaced positions through the table, including the first and last:

print_interval(age_freq)
# A tibble: 73 × 6
     age     n percent     cum  valid valid_cum
   <dbl> <int>   <dbl>   <dbl>  <dbl>     <dbl>
 1    18    33   0.828   0.828  0.855     0.855
 9    26    66   1.66   10.8    1.71     11.2  
17    34    82   2.06   24.7    2.12     25.5  
25    42    81   2.03   40.1    2.10     41.4  
33    50    41   1.03   52.4    1.06     54.1  
41    58    55   1.38   63.7    1.43     65.8  
49    66    70   1.76   77.4    1.81     79.9  
57    74    57   1.43   89.2    1.48     92.1  
65    82    18   0.452  94.6    0.466    97.7  
73    NA   127   3.19  100     NA        NA    
# ... 63 rows omitted between displayed positions; use print(x, n = Inf) to
# show all rows.

The row numbers show where those ages occur in the complete table. The selected positions are evenly spaced through distinct recorded values, not through individual respondents. This preview does not produce age brackets, population quantiles, or a random sample. Repeated ages represent multiple respondents, and the missing category still comes last.

Both printing helpers return the entire age_freq table unchanged and invisibly, meaning that R displays the preview without automatically printing the returned object again. They do not remove rows, combine values into bins, or recalculate percentages. In particular, cum and valid_cum still include the categories skipped in the display; adding only the displayed percentages does not reconstruct the distribution. Exact-value frequencies are useful for inspection, while the numerical summaries in Chapter 9 give more compact descriptions of location and spread.

8.1.2 Count response categories

The GSS asks respondents how happy they are, all things considered: very happy, pretty happy, or not too happy. Which answer is most common? Tidystata’s tab() takes a table and the column to describe, returning one row per response category. Start by reading n, the number of respondents in each category:

tab(gss24, happy)
# A tibble: 4 × 6
  happy             n percent   cum valid valid_cum
  <fct>         <int>   <dbl> <dbl> <dbl>     <dbl>
1 very happy      797  20.0    20.0  20.2      20.2
2 pretty happy   2303  57.8    77.8  58.2      78.4
3 not too happy   854  21.4    99.2  21.6     100  
4 NA               32   0.803 100    NA        NA  

Each output row represents a category rather than a respondent. The most common answer is “pretty happy,” with 2,303 respondents. The last row, <NA>, counts the 32 respondents with no recorded answer; they are still part of the dataset. Adding n across all rows gives the original 3,986 respondents.

The result is a new tibble; gss24 is unchanged. Counts tell us how many people gave each answer. The other columns express their relative sizes and make the denominator visible.

8.1.3 Read percentages

Besides the count n, tab() reports:

  • percent: the percentage of all respondents in each category.
  • cum: the running total of percent down the displayed rows.
  • valid: the percentage among respondents with a recorded answer, excluding missing answers from the denominator.
  • valid_cum: the running total of valid across the non-missing categories.

For “pretty happy,” percent divides its count by all 3,986 respondents, while valid divides by the 3,954 who answered. The valid percentage is 58.2%: more than half of those with a recorded answer chose this category. Missing answers have a count and an all-case percentage, but no valid percentage of their own.

Read cumulative percentages in the displayed category order. At “pretty happy,” valid_cum combines “very happy” and “pretty happy”: 78.4% of valid answers fall in those first two categories. Cumulative percentages depend on order and are useful only when combining successive categories answers a meaningful question. They are usually less informative for unordered categories such as marital status.

The appendix prepares happy as a factor, a column with a defined set of category levels. Those levels retain the source-code order: “very happy,” “pretty happy,” then “not too happy.” That is why these categories appear in this order rather than alphabetically. Cumulative totals follow that level order; preparation has not ranked respondents by how happy they are.

Because only 32 happiness answers are missing, percent and valid differ slightly. Section 8.2 returns to the choice of denominator.

8.1.4 Questions not asked of everyone

An unavailable answer need not be a refusal. The GSS uses ballots, different versions of the questionnaire assigned to parts of the sample. The institutional-confidence questions were asked on only two of the 2024 ballots. We therefore expect substantial missingness in conpress, confidence in the press:

tab(gss24, conpress)
# A tibble: 4 × 6
  conpress         n percent    cum valid valid_cum
  <fct>        <int>   <dbl>  <dbl> <dbl>     <dbl>
1 a great deal   225    5.64   5.64  8.68      8.68
2 only some     1045   26.2   31.9  40.3      49.0 
3 hardly any    1321   33.1   65.0  51.0     100   
4 NA            1395   35.0  100    NA        NA   

The missing row counts about a third of the sample. percent includes all respondents, including people never assigned this question, in its denominator. valid describes the distribution among people with recorded confidence answers. That denominator excludes missing answers both by design and for other reasons; it is not the number of everyone who was assigned the question. A frequency table alone cannot distinguish these reasons. Consult the questionnaire and source documentation before describing missingness as refusal or lack of confidence.

8.1.5 Cross-tabulate two variables

Does happiness differ with self-rated health? Start by counting each combination of the two answers:

# Two variables: one row for each combination
tab(gss24, happy, health)
# A tibble: 20 × 7
   happy         health        n percent    cum  valid valid_cum
   <fct>         <fct>     <int>   <dbl>  <dbl>  <dbl>     <dbl>
 1 very happy    excellent   246  6.17     6.17  6.24       6.24
 2 very happy    good        438 11.0     17.2  11.1       17.4 
 3 very happy    fair         96  2.41    19.6   2.44      19.8 
 4 very happy    poor         15  0.376   19.9   0.381     20.2 
 5 pretty happy  excellent   323  8.10    28.0   8.19      28.4 
 6 pretty happy  good       1338 33.6     61.6  33.9       62.3 
 7 pretty happy  fair        565 14.2     75.8  14.3       76.6 
 8 pretty happy  poor         69  1.73    77.5   1.75      78.4 
 9 not too happy excellent    94  2.36    79.9   2.38      80.8 
10 not too happy good        360  9.03    88.9   9.13      89.9 
11 not too happy fair        311  7.80    96.7   7.89      97.8 
12 not too happy poor         87  2.18    98.9   2.21     100   
13 very happy    NA            2  0.0502  98.9  NA         NA   
14 pretty happy  NA            8  0.201   99.1  NA         NA   
15 not too happy NA            2  0.0502  99.2  NA         NA   
16 NA            excellent     2  0.0502  99.2  NA         NA   
17 NA            good         15  0.376   99.6  NA         NA   
18 NA            fair          3  0.0753  99.7  NA         NA   
19 NA            poor          2  0.0502  99.7  NA         NA   
20 NA            NA           10  0.251  100    NA         NA   

Each row represents a pair of categories. The first row describes respondents who are both very happy and in excellent health. Here, percent is a percentage of the entire sample, and valid uses respondents with answers to both questions. Neither column is a percentage within the excellent-health group.

To compare the counts across health groups more easily, arrange the same combinations as a contingency table with tab2():

tab2(gss24, happy, health)
# A tibble: 4 × 6
  `happy \\ health` excellent  good  fair  poor  `NA`
  <fct>                 <int> <int> <int> <int> <int>
1 very happy              246   438    96    15     2
2 pretty happy            323  1338   565    69     8
3 not too happy            94   360   311    87     2
4 NA                        2    15     3     2    10

Happiness categories form the rows and health categories the columns. Each cell is a count, not a percentage. For example, 246 respondents are very happy and report excellent health. The NA row and column retain missing answers; the bottom-right cell counts respondents missing both answers.

For a comparison within excellent health, divide the very-happy count by the sum of the three non-missing happiness counts in that column: 246 / (246 + 323 + 94). This gives about 37%. The corresponding share among respondents in poor health is 9%. These percentages describe those with recorded happiness answers in each health group; they are calculated from the counts, not printed by tab2() above. They show an association in this sample, not that health causes happiness. Section 8.2 shows how to request within-group percentages directly.

8.1.6 Tabulate several variables separately

Sometimes we want a separate distribution for each of several questions, rather than a table of their combinations. tab1(gss24, happy, health, marital) produces three one-way tables:

# Tabulate several variables at once with tab1()
tab1(gss24, happy, health, marital)
$happy
# A tibble: 4 × 6
  value             n percent   cum valid valid_cum
  <fct>         <int>   <dbl> <dbl> <dbl>     <dbl>
1 very happy      797  20.0    20.0  20.2      20.2
2 pretty happy   2303  57.8    77.8  58.2      78.4
3 not too happy   854  21.4    99.2  21.6     100  
4 NA               32   0.803 100    NA        NA  

$health
# A tibble: 5 × 6
  value         n percent   cum valid valid_cum
  <fct>     <int>   <dbl> <dbl> <dbl>     <dbl>
1 excellent   665  16.7    16.7 16.8       16.8
2 good       2151  54.0    70.6 54.3       71.0
3 fair        975  24.5    95.1 24.6       95.6
4 poor        173   4.34   99.4  4.36     100  
5 NA           22   0.552 100   NA         NA  

$marital
# A tibble: 6 × 6
  value             n percent   cum valid valid_cum
  <fct>         <int>   <dbl> <dbl> <dbl>     <dbl>
1 married        1659  41.6    41.6 41.8       41.8
2 widowed         269   6.75   48.4  6.77      48.6
3 divorced        579  14.5    62.9 14.6       63.1
4 separated       130   3.26   66.2  3.27      66.4
5 never married  1334  33.5    99.6 33.6      100  
6 NA               15   0.376 100   NA         NA  

By default, the result is a named list: one object containing three separate tables. Its printed headings, $happy, $health, and $marital, identify those tables. Within each, value holds that variable’s categories; the remaining columns have the same meanings as in tab(). Each table has its own denominator, so do not add counts across the three tables: the same respondents contribute to each distribution.

For a single rectangular result, set .type = "tibble":

tab1(gss24, happy, health, marital, .type = "tibble")
# A tibble: 15 × 7
   variable value             n percent   cum valid valid_cum
   <chr>    <fct>         <int>   <dbl> <dbl> <dbl>     <dbl>
 1 happy    very happy      797  20.0    20.0 20.2       20.2
 2 happy    pretty happy   2303  57.8    77.8 58.2       78.4
 3 happy    not too happy   854  21.4    99.2 21.6      100  
 4 happy    NA               32   0.803 100   NA         NA  
 5 health   excellent       665  16.7    16.7 16.8       16.8
 6 health   good           2151  54.0    70.6 54.3       71.0
 7 health   fair            975  24.5    95.1 24.6       95.6
 8 health   poor            173   4.34   99.4  4.36     100  
 9 health   NA               22   0.552 100   NA         NA  
10 marital  married        1659  41.6    41.6 41.8       41.8
11 marital  widowed         269   6.75   48.4  6.77      48.6
12 marital  divorced        579  14.5    62.9 14.6       63.1
13 marital  separated       130   3.26   66.2  3.27      66.4
14 marital  never married  1334  33.5    99.6 33.6      100  
15 marital  NA               15   0.376 100   NA         NA  

This argument changes the output format, not the calculation. The tables are stacked into one tibble, and the variable column identifies which question each row describes. Read variable and value together: “good” is a health answer, whereas “married” is a marital-status answer. Each row is now a category within a particular variable, rather than a combination of happiness, health, and marital status.

The happiness rows have exactly the same counts and percentages as the $happy table above; the same holds for health and marital status. Within each block, percent adds to 100 across all categories, and valid adds to 100 across non-missing answers. Each block’s counts add to 3,986, so summing n over the whole stacked result would count the same respondents three times. Stacking makes several distributions convenient to inspect together; it does not turn them into one distribution with a shared denominator.

8.1.7 Cross-tabulate several variable pairs

How are happiness, health, and marital status related to one another? We can inspect all three pairs without writing three separate calls. Give tab2() the same variables we just passed to tab1() and save its result. Typing its name displays the list at the normal console width:

pair_tables <- tab2(gss24, happy, health, marital)
pair_tables
$`happy x health`
# A tibble: 4 × 6
  `happy \\ health` excellent  good  fair  poor  `NA`
  <fct>                 <int> <int> <int> <int> <int>
1 very happy              246   438    96    15     2
2 pretty happy            323  1338   565    69     8
3 not too happy            94   360   311    87     2
4 NA                        2    15     3     2    10

$`happy x marital`
# A tibble: 4 × 7
  `happy \\ marital` married widowed divorced separated `never married`  `NA`
  <fct>                <int>   <int>    <int>     <int>           <int> <int>
1 very happy             473      42       94        13             172     3
2 pretty happy           965     160      329        67             774     8
3 not too happy          209      64      152        47             379     3
4 NA                      12       3        4         3               9     1

$`health x marital`
# A tibble: 5 × 7
  `health \\ marital` married widowed divorced separated `never married`  `NA`
  <fct>                 <int>   <int>    <int>     <int>           <int> <int>
1 excellent               295      29       97        13             228     3
2 good                    953     156      285        65             685     7
3 fair                    355      61      161        43             353     2
4 poor                     48      22       34         8              60     1
5 NA                        8       1        2         1               8     2

The result is a list of three cross-tabulations. A narrow display may omit columns and name them in its footer; those columns remain in the returned tables. Inspect the result in your editor’s viewer when necessary. The list names identify the pairs:

  • happy x health: happiness in the rows and health in the columns, repeating our earlier two-variable table.
  • happy x marital: happiness in the rows and marital status in the columns.
  • health x marital: health in the rows and marital status in the columns.

Each cell counts respondents with that pair of answers, and NA rows or columns retain missing answers. Each table uses only its two named variables: someone with missing marital status can still contribute to a recorded happiness-health combination in the first table. Across all cells, including missing categories, each table counts the full sample once.

In the happiness-marital-status table, very-happy answers are more common among married respondents than among those who have never married. Divide the very-happy count in each column by that column’s total across the three recorded happiness categories. The resulting shares are 28.7% for married respondents and 13.0% for never-married respondents. These denominators exclude missing happiness answers within each marital-status group. The comparison describes an association, not an effect of marriage on happiness.

The three functions answer different questions even when given the same variables. tab1(gss24, happy, health, marital) describes each variable separately. tab2(gss24, happy, health, marital) describes each pair separately. By contrast, tab(gss24, happy, health, marital) would count joint combinations of all three, such as very happy, excellent health, and married. Three pairwise tables do not show how the happiness-health relationship varies within marital-status groups; that requires an additional grouping step, planned for a later chapter beyond this chapter.

8.1.8 Include totals with fre()

fre() works like tab() but adds total rows. These make the two denominators visible in the output:

fre(gss24, happy)
# A tibble: 6 × 6
  happy             n percent   cum valid valid_cum
  <chr>         <int>   <dbl> <dbl> <dbl>     <dbl>
1 very happy      797  20.0    20.0  20.2      20.2
2 pretty happy   2303  57.8    77.8  58.2      78.4
3 not too happy   854  21.4    99.2  21.6     100  
4 Valid Total    3954  99.2    NA   100        NA  
5 NA               32   0.803 100    NA        NA  
6 Total          3986 100      NA    NA        NA  

Valid Total counts the 3,954 non-missing answers; Total counts all 3,986 respondents. The missing-answer count bridges the difference. These are summary rows, not additional response categories or additional respondents.

8.1.9 Optional reference: preserve the input table class

You can continue using tibbles throughout this chapter. If you work with other table classes, tidystata normally returns newly calculated tables in the same container as the input: a base data.frame, tibble, or data.table. This preserves the kind of table, not its original rows or columns.

as.data.frame() converts our tibble to a base data frame. Save the converted table separately:

gss_base <- as.data.frame(gss24)

class() takes an object and reports its class names. Compare the same tabulation with the two inputs:

class(tab(gss24, happy))
[1] "tbl_df"     "tbl"        "data.frame"
class(tab(gss_base, happy))
[1] "data.frame"

The tibble result has classes "tbl_df", "tbl", and "data.frame"; the base result has class "data.frame". A tibble is also a kind of data frame, which is why its class listing contains all three names. The category counts and percentages agree; only the container differs.

To try a data.table input, install the optional package once if needed:

install.packages("data.table")

data.table::as.data.table() converts the input without attaching the package. The :: notation names the package that supplies the function:

gss_dt <- data.table::as.data.table(gss24)

class(tab(gss_dt, happy))
[1] "data.table" "data.frame"

The result has classes "data.table" and "data.frame", so a data.table input produces a data.table frequency table. These calls return new results without changing gss24 or its converted copies.

For functions that return several tables, distinguish the outer list from the tables inside it. Store the default tab1() result as tables_dt. Double brackets, [[1]], extract the first table from that list:

tables_dt <- tab1(gss_dt, happy, health, marital)

class(tables_dt)
[1] "list"
class(tables_dt[[1]])
[1] "data.table" "data.frame"

The outer object is a "list"; its first table is a data.table. The other two tables preserve the same class. The three-variable tab2() result follows this pattern too: a list of cross-tabulations, each in the input’s table class.

An explicit output request can override this default. As we saw above, .type = "tibble" in tab1() both stacks the one-way tables and requests a tibble, including when the input is a data.table:

class(tab1(gss_dt, happy, health, marital, .type = "tibble"))
[1] "tbl_df"     "tbl"        "data.frame"

The class names are now "tbl_df", "tbl", and "data.frame". The distributions remain the same. When deciding how to use a result, check both its structure, one table or a list, and the class of the tables it contains.

8.2 Application: educational qualifications in the 2024 GSS

This section puts the tools together to describe education in the 2024 GSS, and shows why every percentage needs a clearly stated denominator.

8.2.1 Who has which degree?

What is the distribution of respondents’ highest educational qualifications? Each category of degree represents a highest degree, so someone with a graduate degree is not counted again under bachelor’s or high school:

tab(gss24, degree)
# A tibble: 6 × 6
  degree                       n percent    cum valid valid_cum
  <fct>                    <int>   <dbl>  <dbl> <dbl>     <dbl>
1 less than high school      362   9.08    9.08  9.10      9.10
2 high school               1817  45.6    54.7  45.7      54.8 
3 associate/junior college   360   9.03   63.7   9.05     63.8 
4 bachelor's                 863  21.7    85.3  21.7      85.5 
5 graduate                   577  14.5    99.8  14.5     100   
6 NA                           7   0.176 100    NA        NA   

The last row shows that 7 respondents did not report a degree. That is why the two kinds of percentages differ:

  • percent divides by all 3,986 respondents.
  • valid divides by the 3,979 respondents who answered.

Here the difference is tiny. Among those who answered, 45.7% have a high school diploma as their highest degree, and 36.2% hold a bachelor’s or graduate degree.

For questions with more missing answers, the choice of denominator matters a great deal (see Exercise 2).

8.2.2 Comparing groups: which denominator?

To compare women and men, we need a distribution of degrees within each category of sex. The argument .by = sex asks tab() to perform its calculation separately for those groups, for this operation only.

tab() retains missing values, including a separate group for the 30 respondents whose sex is missing. This does not change the denominators within the recorded male and female groups. First save and display the returned table:

degrees_by_sex <- tab(gss24, degree, .by = sex)
degrees_by_sex
# A tibble: 17 × 7
   sex    degree                       n percent    cum valid valid_cum
   <fct>  <fct>                    <int>   <dbl>  <dbl> <dbl>     <dbl>
 1 male   less than high school      135  7.55     7.55  7.57      7.57
 2 male   high school                860 48.1     55.7  48.2      55.8 
 3 male   associate/junior college   141  7.89    63.6   7.90     63.7 
 4 male   bachelor's                 395 22.1     85.7  22.1      85.8 
 5 male   graduate                   253 14.2     99.8  14.2     100   
 6 male   NA                           3  0.168  100    NA        NA   
 7 female less than high school      208  9.59     9.59  9.60      9.60
 8 female high school                952 43.9     53.5  43.9      53.5 
 9 female associate/junior college   219 10.1     63.6  10.1      63.6 
10 female bachelor's                 466 21.5     85.1  21.5      85.1 
11 female graduate                   322 14.8     99.9  14.9     100   
12 female NA                           2  0.0922 100    NA        NA   
13 NA     less than high school       19 63.3     63.3  67.9      67.9 
14 NA     high school                  5 16.7     80    17.9      85.7 
15 NA     bachelor's                   2  6.67    86.7   7.14     92.9 
16 NA     graduate                     2  6.67    93.3   7.14    100   
17 NA     NA                           2  6.67   100    NA        NA   

Each output row is now a combination of sex and degree. The normal preview may omit some combinations. For this interpretation, deliberately display every row with print(). The named argument n = Inf removes the finite row limit for this call; it changes the display without selecting or recalculating the data:

print(degrees_by_sex, n = Inf)
# A tibble: 17 × 7
   sex    degree                       n percent    cum valid valid_cum
   <fct>  <fct>                    <int>   <dbl>  <dbl> <dbl>     <dbl>
 1 male   less than high school      135  7.55     7.55  7.57      7.57
 2 male   high school                860 48.1     55.7  48.2      55.8 
 3 male   associate/junior college   141  7.89    63.6   7.90     63.7 
 4 male   bachelor's                 395 22.1     85.7  22.1      85.8 
 5 male   graduate                   253 14.2     99.8  14.2     100   
 6 male   NA                           3  0.168  100    NA        NA   
 7 female less than high school      208  9.59     9.59  9.60      9.60
 8 female high school                952 43.9     53.5  43.9      53.5 
 9 female associate/junior college   219 10.1     63.6  10.1      63.6 
10 female bachelor's                 466 21.5     85.1  21.5      85.1 
11 female graduate                   322 14.8     99.9  14.9     100   
12 female NA                           2  0.0922 100    NA        NA   
13 NA     less than high school       19 63.3     63.3  67.9      67.9 
14 NA     high school                  5 16.7     80    17.9      85.7 
15 NA     bachelor's                   2  6.67    86.7   7.14     92.9 
16 NA     graduate                     2  6.67    93.3   7.14    100   
17 NA     NA                           2  6.67   100    NA        NA   

Read the group and category columns together. Within each sex, percent adds to 100 across all degree rows, including any missing answers. valid adds to 100 across recorded degrees only. To answer “What share of women who reported a degree hold a graduate degree?”, find the female/graduate row and read valid. Its denominator is women with a recorded degree, not all degree holders in the sample.

We can combine categories within the same denominator. Adding the valid percentages for bachelor’s and graduate degrees gives 36.4% of women and 36.3% of men. This comparison describes the degree distribution within each sex among those who reported it.

A different question, “What share of graduate-degree holders are women?”, needs a different denominator. Swap the roles of the two variables and read valid within each recorded degree group:

sex_by_degree <- tab(gss24, sex, .by = degree)
print(sex_by_degree, n = Inf)
# A tibble: 17 × 7
   degree                   sex        n percent   cum valid valid_cum
   <fct>                    <fct>  <int>   <dbl> <dbl> <dbl>     <dbl>
 1 less than high school    male     135  37.3    37.3  39.4      39.4
 2 less than high school    female   208  57.5    94.8  60.6     100  
 3 less than high school    NA        19   5.25  100    NA        NA  
 4 high school              male     860  47.3    47.3  47.5      47.5
 5 high school              female   952  52.4    99.7  52.5     100  
 6 high school              NA         5   0.275 100    NA        NA  
 7 associate/junior college male     141  39.2    39.2  39.2      39.2
 8 associate/junior college female   219  60.8   100    60.8     100  
 9 bachelor's               male     395  45.8    45.8  45.9      45.9
10 bachelor's               female   466  54.0    99.8  54.1     100  
11 bachelor's               NA         2   0.232 100    NA        NA  
12 graduate                 male     253  43.8    43.8  44        44  
13 graduate                 female   322  55.8    99.7  56       100  
14 graduate                 NA         2   0.347 100    NA        NA  
15 NA                       male       3  42.9    42.9  60        60  
16 NA                       female     2  28.6    71.4  40       100  
17 NA                       NA         2  28.6   100    NA        NA  

Now percentages add to 100 within each degree category. The female/graduate row’s valid uses all graduate-degree holders with recorded sex as its denominator. Its percent also includes graduate-degree holders whose sex is missing, so the columns can differ. The missing-degree group is shown separately and is not part of the graduate denominator. Swapping the variables has changed the question, not the respondents’ qualifications.

For the respondents with both answers, the underlying cell counts are the same whichever distribution we calculate. tab2() arranges those counts in rows and columns, retaining missing-answer cells at the edges:

tab2(gss24, degree, sex)
# A tibble: 6 × 4
  `degree \\ sex`           male female  `NA`
  <fct>                    <int>  <int> <int>
1 less than high school      135    208    19
2 high school                860    952     5
3 associate/junior college   141    219     0
4 bachelor's                 395    466     2
5 graduate                   253    322     2
6 NA                           3      2     2

With degree in the rows and sex in the columns, dividing a graduate cell by its column total across recorded degrees asks about degrees within a sex; dividing it by its row total across recorded sexes asks about sex within that degree. Exclude the missing-answer row or column from these valid-answer totals. Before reporting a percentage, state its denominator: all respondents or valid answers, and which group. None of these commands overwrites gss24.

These are unweighted sample descriptions: each respondent contributes equally. They describe associations, not causal effects. Survey weighting is beyond this edition.

8.3 Application: a GSS analysis subset

Suppose our next analysis will concern schooling and working hours among GSS respondents aged 18 through 64. Age alone defines this subset; we are not yet requiring a reported schooling value or working-hours answer.

Use the two inclusive bounds, then the familiar select() to keep the ID and variables needed for this question. Assign the result a new name:

working_age <- gss24 |>
  filter(age >= 18, age <= 64) |>
  select(id, age, educ, hrs1)

head(working_age)
# A tibble: 6 × 4
     id   age  educ  hrs1
  <int> <dbl> <dbl> <dbl>
1     1    33    16    43
2     2    64    16    NA
3     4    19    12    20
4     6    53    14    NA
5     7    48    13    80
6     8    30    14    NA
nrow(gss24)
[1] 3986
nrow(working_age)
[1] 2947

The original has 3986 respondents; working_age contains 2947. Each retained row represents a respondent with a known age in the specified range. educ records years of schooling and hrs1 hours worked last week. Their missing answers remain in the subset, and head() displays only its first six rows. The appendix explains how the survey variables were prepared.

This rule defines who is available for the proposed sample analysis. It does not establish employment, require a valid answer to every question, or produce a population estimate. The original gss24 is unchanged. We can now describe this subset with the same tabulation tools:

tab(working_age, educ)
# A tibble: 20 × 6
    educ     n percent     cum   valid valid_cum
   <dbl> <int>   <dbl>   <dbl>   <dbl>     <dbl>
 1     0    10  0.339    0.339  0.342      0.342
 2     2     1  0.0339   0.373  0.0342     0.376
 3     3     3  0.102    0.475  0.103      0.478
 4     5     6  0.204    0.679  0.205      0.684
 5     6    18  0.611    1.29   0.615      1.30 
 6     7     7  0.238    1.53   0.239      1.54 
 7     8    22  0.747    2.27   0.752      2.29 
 8     9    30  1.02     3.29   1.03       3.32 
 9    10    58  1.97     5.26   1.98       5.30 
10    11    88  2.99     8.25   3.01       8.30 
11    12   706 24.0     32.2   24.1       32.4  
12    13   243  8.25    40.4    8.30      40.7  
13    14   418 14.2     54.6   14.3       55.0  
14    15   172  5.84    60.5    5.88      60.9  
15    16   629 21.3     81.8   21.5       82.4  
16    17   152  5.16    87.0    5.19      87.6  
17    18   193  6.55    93.5    6.60      94.2  
18    19    52  1.76    95.3    1.78      96.0  
19    20   118  4.00    99.3    4.03     100    
20    NA    21  0.713  100     NA         NA    

Each row counts respondents in working_age with that many years of schooling, plus a row for missing schooling answers. Here percent divides by all 2,947 respondents in the age-defined subset. valid divides by the 2,926 subset respondents with recorded schooling. Neither denominator is the full gss24 sample. The earlier degree tables continue to describe full gss24; creating this subset has not changed them. In Chapter 9 we return to full gss24 for numerical summaries.

8.4 Chapter review

  • tab() counts records by category and reports percentages, retaining missing answers.
  • percent uses all observations and valid uses complete answers; fre() adds explicit total rows.
  • tab1() tabulates variables separately; tab2() produces a count matrix for each selected pair.
  • .by requests within-group distributions; check the grouping and missingness rules before interpreting a percentage.
  • print_headtail() and print_interval() preview a long frequency table without changing its contents or recalculating percentages.
  • Optional: class() checks the returned container; supported input classes are preserved by default, while .type can request tibbles explicitly.

Always identify a percentage’s denominator. Next, Chapter 9 describes numerical center and spread.

8.5 Exercises

1. Syntax and meaning. Tabulate marital in gss24 with tab() and then with fre(). Identify the total rows. Which column of tab() gives the share of each marital status among respondents who answered the question?

2. Interpretation. Tabulate conpress (confidence in the press). Use the questionnaire-version explanation to say why percent and valid differ. Which would you report to describe recorded confidence answers, and whose answers does that denominator include?

3. Adapt the comparison. Within each recorded category of race, what percentage of respondents who answered the degree question hold a graduate degree? Keep missing answers visible in the table, but exclude them from the percentage denominator and explain how you did so. State why this differs from asking what share of graduate-degree holders belong to each racial category.

4. Predict and diagnose. Predict how many separate distributions or pairwise tables tab1(gss24, happy, health, marital) and tab2(gss24, happy, health, marital) produce. Compare them with tab(gss24, happy, health, marital). A colleague adds n over the entire stacked tab1() result and reports a sample three times too large. Diagnose that claim, and explain why adding only the percentages shown in an interval preview is also incorrect.

Worked answers

Load the packages and the prepared GSS extract as shown at the start of the chapter before running an answer. Each answer uses the complete extract independently.

tab(gss24, marital)
# A tibble: 6 × 6
  marital           n percent   cum valid valid_cum
  <fct>         <int>   <dbl> <dbl> <dbl>     <dbl>
1 married        1659  41.6    41.6 41.8       41.8
2 widowed         269   6.75   48.4  6.77      48.6
3 divorced        579  14.5    62.9 14.6       63.1
4 separated       130   3.26   66.2  3.27      66.4
5 never married  1334  33.5    99.6 33.6      100  
6 NA               15   0.376 100   NA         NA  
fre(gss24, marital)
# A tibble: 8 × 6
  marital           n percent   cum  valid valid_cum
  <chr>         <int>   <dbl> <dbl>  <dbl>     <dbl>
1 married        1659  41.6    41.6  41.8       41.8
2 widowed         269   6.75   48.4   6.77      48.6
3 divorced        579  14.5    62.9  14.6       63.1
4 separated       130   3.26   66.2   3.27      66.4
5 never married  1334  33.5    99.6  33.6      100  
6 Valid Total    3971  99.6    NA   100         NA  
7 NA               15   0.376 100    NA         NA  
8 Total          3986 100      NA    NA         NA  

Read the valid column. Each category’s count is divided by the number of recorded marital-status answers and multiplied by 100. percent also includes the 15 respondents with a missing answer in its denominator. The category counts agree between the two commands; fre() adds total rows that describe the same respondents. Valid Total counts recorded answers, while Total includes missing answers. Cumulative percentages are not especially useful here because the marital-status categories have no natural low-to-high order.

tab(gss24, conpress)
# A tibble: 4 × 6
  conpress         n percent    cum valid valid_cum
  <fct>        <int>   <dbl>  <dbl> <dbl>     <dbl>
1 a great deal   225    5.64   5.64  8.68      8.68
2 only some     1045   26.2   31.9  40.3      49.0 
3 hardly any    1321   33.1   65.0  51.0     100   
4 NA            1395   35.0  100    NA        NA   

About a third of the values are missing. Most of these are not refusals: the GSS gives different sets of questions to different parts of the sample, and the confidence questions were asked on only two of the 2024 ballots, meaning versions of the questionnaire. percent therefore includes respondents who were never asked in its denominator. To describe the distribution of recorded confidence answers, report valid and state that it excludes missing answers, including those missing by design. It describes those who provided an answer, not everyone assigned the question, and it is still an unweighted sample description.

degrees_by_race <- tab(gss24, degree, .by = race)
print(degrees_by_race, n = Inf)
# A tibble: 23 × 7
   race  degree                       n percent    cum valid valid_cum
   <fct> <fct>                    <int>   <dbl>  <dbl> <dbl>     <dbl>
 1 white less than high school      174  7.13     7.13  7.13      7.13
 2 white high school               1095 44.9     52.0  44.9      52.0 
 3 white associate/junior college   210  8.60    60.6   8.61     60.6 
 4 white bachelor's                 574 23.5     84.1  23.5      84.1 
 5 white graduate                   387 15.9    100.0  15.9     100   
 6 white NA                           1  0.0410 100    NA        NA   
 7 black less than high school       95 11.7     11.7  11.8      11.8 
 8 black high school                443 54.7     66.4  54.8      66.6 
 9 black associate/junior college    81 10       76.4  10.0      76.6 
10 black bachelor's                 120 14.8     91.2  14.9      91.5 
11 black graduate                    69  8.52    99.8   8.54    100   
12 black NA                           2  0.247  100    NA        NA   
13 other less than high school       82 12.5     12.5  12.6      12.6 
14 other high school                245 37.4     49.9  37.6      50.2 
15 other associate/junior college    59  9.01    58.9   9.06     59.3 
16 other bachelor's                 157 24.0     82.9  24.1      83.4 
17 other graduate                   108 16.5     99.4  16.6     100   
18 other NA                           4  0.611  100    NA        NA   
19 NA    less than high school       11 13.8     13.8  13.8      13.8 
20 NA    high school                 34 42.5     56.2  42.5      56.2 
21 NA    associate/junior college    10 12.5     68.8  12.5      68.8 
22 NA    bachelor's                  12 15       83.8  15        83.8 
23 NA    graduate                    13 16.2    100    16.2     100   

Find the graduate-degree row within each recorded racial category and read valid. tab() calculates that column from recorded degrees within the racial category, so missing degrees are excluded from its denominator automatically. The missing-race group remains a separate group; do not include it in comparisons among the recorded racial categories. These denominators are respondents with both answers in each racial category, not all graduate-degree holders. percent retains missing degrees in each group’s denominator and can differ from valid. To ask instead about racial categories within graduate-degree holders, the distribution would be race within groups defined by degree: changing the grouping changes the denominator.

tab1(gss24, happy, health, marital)
$happy
# A tibble: 4 × 6
  value             n percent   cum valid valid_cum
  <fct>         <int>   <dbl> <dbl> <dbl>     <dbl>
1 very happy      797  20.0    20.0  20.2      20.2
2 pretty happy   2303  57.8    77.8  58.2      78.4
3 not too happy   854  21.4    99.2  21.6     100  
4 NA               32   0.803 100    NA        NA  

$health
# A tibble: 5 × 6
  value         n percent   cum valid valid_cum
  <fct>     <int>   <dbl> <dbl> <dbl>     <dbl>
1 excellent   665  16.7    16.7 16.8       16.8
2 good       2151  54.0    70.6 54.3       71.0
3 fair        975  24.5    95.1 24.6       95.6
4 poor        173   4.34   99.4  4.36     100  
5 NA           22   0.552 100   NA         NA  

$marital
# A tibble: 6 × 6
  value             n percent   cum valid valid_cum
  <fct>         <int>   <dbl> <dbl> <dbl>     <dbl>
1 married        1659  41.6    41.6 41.8       41.8
2 widowed         269   6.75   48.4  6.77      48.6
3 divorced        579  14.5    62.9 14.6       63.1
4 separated       130   3.26   66.2  3.27      66.4
5 never married  1334  33.5    99.6 33.6      100  
6 NA               15   0.376 100   NA         NA  
tab2(gss24, happy, health, marital)
$`happy x health`
# A tibble: 4 × 6
  `happy \\ health` excellent  good  fair  poor  `NA`
  <fct>                 <int> <int> <int> <int> <int>
1 very happy              246   438    96    15     2
2 pretty happy            323  1338   565    69     8
3 not too happy            94   360   311    87     2
4 NA                        2    15     3     2    10

$`happy x marital`
# A tibble: 4 × 7
  `happy \\ marital` married widowed divorced separated `never married`  `NA`
  <fct>                <int>   <int>    <int>     <int>           <int> <int>
1 very happy             473      42       94        13             172     3
2 pretty happy           965     160      329        67             774     8
3 not too happy          209      64      152        47             379     3
4 NA                      12       3        4         3               9     1

$`health x marital`
# A tibble: 5 × 7
  `health \\ marital` married widowed divorced separated `never married`  `NA`
  <fct>                 <int>   <int>    <int>     <int>           <int> <int>
1 excellent               295      29       97        13             228     3
2 good                    953     156      285        65             685     7
3 fair                    355      61      161        43             353     2
4 poor                     48      22       34         8              60     1
5 NA                        8       1        2         1               8     2
tab(gss24, happy, health, marital)
# A tibble: 90 × 8
   happy      health    marital           n percent   cum  valid valid_cum
   <fct>      <fct>     <fct>         <int>   <dbl> <dbl>  <dbl>     <dbl>
 1 very happy excellent married         142  3.56    3.56 3.61        3.61
 2 very happy excellent widowed           9  0.226   3.79 0.229       3.84
 3 very happy excellent divorced         30  0.753   4.54 0.764       4.61
 4 very happy excellent separated         3  0.0753  4.62 0.0764      4.68
 5 very happy excellent never married    61  1.53    6.15 1.55        6.24
 6 very happy good      married         272  6.82   13.0  6.92       13.2 
 7 very happy good      widowed          27  0.677  13.6  0.687      13.8 
 8 very happy good      divorced         50  1.25   14.9  1.27       15.1 
 9 very happy good      separated         5  0.125  15.0  0.127      15.2 
10 very happy good      never married    82  2.06   17.1  2.09       17.3 
# ℹ 80 more rows
tab1(gss24, happy, health, marital, .type = "tibble")
# A tibble: 15 × 7
   variable value             n percent   cum valid valid_cum
   <chr>    <fct>         <int>   <dbl> <dbl> <dbl>     <dbl>
 1 happy    very happy      797  20.0    20.0 20.2       20.2
 2 happy    pretty happy   2303  57.8    77.8 58.2       78.4
 3 happy    not too happy   854  21.4    99.2 21.6      100  
 4 happy    NA               32   0.803 100   NA         NA  
 5 health   excellent       665  16.7    16.7 16.8       16.8
 6 health   good           2151  54.0    70.6 54.3       71.0
 7 health   fair            975  24.5    95.1 24.6       95.6
 8 health   poor            173   4.34   99.4  4.36     100  
 9 health   NA               22   0.552 100   NA         NA  
10 marital  married        1659  41.6    41.6 41.8       41.8
11 marital  widowed         269   6.75   48.4  6.77      48.6
12 marital  divorced        579  14.5    62.9 14.6       63.1
13 marital  separated       130   3.26   66.2  3.27      66.4
14 marital  never married  1334  33.5    99.6 33.6      100  
15 marital  NA               15   0.376 100   NA         NA  

tab1() produces three separate one-variable distributions. Each counts the same respondents once, so adding counts across all three blocks counts each respondent three times. Stacking changes the format without changing those denominators. tab2() produces the three variable pairs; each pair counts the full sample once, including missing-answer combinations. tab() produces one distribution of joint three-answer combinations, which also counts the full sample once.

An interval preview displays selected rows from a complete table. Omitted categories still contribute to cumulative percentages and to the denominators. Adding the visible percentages therefore describes only the displayed categories; it does not reconstruct the complete distribution. None of these printing or tabulating calls alters gss24.