9  Summarize data

TipIn this chapter
  • Describe variables with summ() and compare groups with tabstat().
  • Interpret the mean, median, quartiles, and standard deviation.
  • Identify the observations contributing to each summary and check whether its units and variable type make it meaningful.
  • Compare schooling and family income across educational groups in the 2024 General Social Survey.

What does a typical value look like, and how widely do values vary? Numerical summaries describe quantities such as age, years of schooling, and income through measures of center and spread. As with the percentages in Chapter 8, we must identify which observations contribute to each calculation.

summ() from tidystata produces common statistics for one or more variables. Its companion tabstat() compares selected statistics across groups. We use individual calculations to explain the statistics, then briefly show summarise() for a custom summary.

This chapter builds on inspecting a dataset and reading frequency tables in Chapter 8. We continue with the same 2024 GSS respondents. As in tab(), the dataset is the first argument and column names follow without quotation marks: summ(gss24, age). Commas separate arguments. A named argument, such as .by = degree, sets an option for that call. Each new summary statistic is explained before we interpret it.

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

library(tidyverse)
library(tidystata)

Use Appendix A to create the gss24 extract and save it as gss24.rds. 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.

gss24 comes from the 2024 General Social Survey (GSS), which has interviewed adults in the United States since 1972 about their families, work, education, and attitudes. Our extract has 3,986 respondents and 25 variables. Its degree categories are already prepared; factor construction is not required here. We begin with age, recorded in years, before comparing schooling, working hours, and family income.

9.1 Summarize variables with summ()

In Section 7.3, summ() described complete columns without requiring us to inspect every record. We now use its output to ask what a typical value looks like and how widely values differ. Supply a table and the numeric variable to summarize:

# The type, number of non-missing values, number of distinct values,
# minimum, mean, standard deviation, and maximum of age
summ(gss24, age)
# A tibble: 1 × 7
  type      n unique   min  mean    sd   max
  <chr> <int>  <int> <dbl> <dbl> <dbl> <dbl>
1 dbl    3859     72    18  49.0  17.7    89

Read its columns as follows:

  • type: the type of the variable (or its unit, if the variable has one).
  • n: the number of non-missing values.
  • unique: the number of distinct non-missing values, which cannot exceed n.
  • min: the minimum.
  • mean: the arithmetic mean, the sum of observed values divided by their number.
  • sd: the standard deviation.
  • max: the maximum.

Here, 3859 respondents share 72 distinct recorded ages. Repeated ages are not missing observations. Finer measurement, such as recording age in months rather than whole years, allows more distinct values, but the observed count also depends on the sample. A high unique does not by itself make a variable continuous or substantively informative: even an identifier can be unique for every case.

The statistics describe recorded ages in years, with each respondent contributing equally. These are unweighted sample summaries, rather than estimates adjusted for how the sample represents the population. The n column makes the number of contributing respondents explicit. This one-variable result does not need a name column.

9.1.1 Extract a column

Chapter 7 explains observations, variables, tidy data, and table previews. Recall that a tibble is a form of data frame: it stores columns together, but its class alone does not determine what an observation means. Each row of gss24 represents a respondent.

The $ operator extracts a column by name. Put the table on the left and the column name on the right. length() counts entries, including missing values; var_type() identifies the object’s type:

age_vector <- gss24$age
length(age_vector)
[1] 3986
var_type(age_vector)
[1] "double"

The extracted vector has 3,986 entries, stored as doubles, in the same respondent order as the table. Its length includes missing ages. Extracting a column does not remove it from its source.

9.2 Understand center and spread

9.2.1 Ordered positions

The mean and standard deviation introduced in Section 7.3.1 describe average level and spread. A median or quartile answers a different question: where does a value lie among the ordered observations? We first use abstract numbers so every position is visible. c() combines the values in their written order.

sort() takes a vector and returns its values in increasing order. It does not change the saved input:

x <- c(2, 4, 6, 8, 10)
sort(x)
[1]  2  4  6  8 10

These five values are already ordered. If their written order changed, sort() would still return the same increasing sequence.

median() returns the middle value after ordering the observations:

median(x)
[1] 6

The median is 6: two entries lie below it and two above it. With an even number of values, two occupy the middle. median() averages them:

four_values <- c(2, 4, 6, 8)
median(four_values)
[1] 5

The two middle values are 4 and 6, so the median is (4 + 6) / 2, or 5. Here + adds the middle values, parentheses make that addition happen first, and / 2 divides the sum by two. A median need not be an input value.

quantile() takes a numerical vector and probabilities specifying positions. Probabilities run from zero to one; .25 requests the lower quartile and .75 the upper quartile. Supply both in one c() value:

quantile(x, c(.25, .75))
25% 75% 
  4   8 

The 25% and 75% headings identify the requested positions. Here the quartiles are 4 and 8, the second and fourth values. Quartiles locate one quarter and three quarters of the way through an ordered distribution. They are not 25% and 75% of the largest number. With other inputs, interpolation can place a cutpoint between recorded values.

9.2.2 Summarize a recorded column

The familiar extraction gss24$age supplies all recorded and missing ages. As with mean() and sd() in Section 7.3.1, an unavailable input requires a decision about the calculation. median() supports its own na.rm argument. Its default is FALSE; TRUE excludes missing ages from this calculation:

median(gss24$age)
[1] NA
median(gss24$age, na.rm = TRUE)
[1] 48

The first result is NA; the second locates the middle among the 3,859 recorded ages. Neither command fills missing ages or changes gss24.

quantile() also supports na.rm = TRUE. Without it, missing inputs produce an error rather than defined quartiles. Apply the explicit exclusion when requesting the two age cutpoints:

quantile(gss24$age, c(.25, .75), na.rm = TRUE)
25% 75% 
 34  64 

The 25% and 75% cutpoints are both in years. Recall that sd() uses the same unit as the original measurements:

sd(gss24$age, na.rm = TRUE)
[1] 17.66311

Recorded age ranges from 18 to 89 years. Its median is 48 years, and its standard deviation is about 17.7 years. These describe location and spread among the same respondents with recorded ages. summ() excludes missing measured values automatically and reports the contributing count.

9.3 Summarize several variables

When we summarize several variables, name identifies the variable in each output row:

# Summarize several variables at once
summ(gss24, age, educ, hrs1:realinc)
# A tibble: 4 × 8
  name    type      n unique   min    mean       sd     max
  <chr>   <chr> <int>  <int> <dbl>   <dbl>    <dbl>   <dbl>
1 age     dbl    3859     72   18     49.0    17.7      89 
2 educ    dbl    3952     21    0     14.2     2.92     20 
3 hrs1    dbl    2201     76    0     39.3    13.6      89 
4 realinc dbl    3563     26  182. 34252.  34697.   117565.

Using the tidyselect range introduced in Section 3.3, hrs1:realinc selects the columns from hrs1 through realinc in their dataset order. Here those two columns are adjacent. It is a column range, not a sequence of numeric values. age and educ are selected separately. Each output row describes one variable, not one respondent.

educ records completed years of schooling; hrs1 records hours worked last week and is generally available for respondents who were working. realinc measures family income in constant 1986 dollars, adjusted to the purchasing power of dollars in 1986. It is based on income brackets rather than exact amounts. Compare statistics only with their units in mind: a standard deviation in dollars is not directly comparable with one in years or hours.

summ() calculates these statistics from non-missing values. The n column reveals that working hours are missing for 1,785 respondents and family income for 423. Their means therefore describe different sets of respondents. A missing work-hours value need not mean a refusal: some respondents were not asked because they were not working. The coverage diagnostics in Section 10.2 will examine these different contributing samples more closely.

The income example also shows why we compare a mean with a median. Some families have much higher recorded incomes than others. The .stat argument requests statistics by name. c() combines the quoted names into one argument value; here c("n", "mean", "median") asks for the observed count and two measures of center on the same recorded incomes:

summ(gss24, realinc, .stat = c("n", "mean", "median"))
# A tibble: 1 × 4
  type      n   mean median
  <chr> <int>  <dbl>  <dbl>
1 dbl    3563 34252. 24502.

The mean of $34,252 exceeds the median of $24,502: high family incomes pull the mean upward. The median locates the middle recorded income among respondents who reported it. Both statistics are in constant 1986 dollars and give each respondent equal influence. Neither describes individual earnings or a survey-weighted population estimate.

9.3.1 Optional reference: detailed statistics

You can skip the detailed output on a first reading. The argument .detail = TRUE requests the full set of available statistics. print(width = Inf) allows all columns to be printed, even when they need several blocks of output:

# Show all available statistics
detailed_gss <- summ(gss24, age, educ, hrs1:realinc, .detail = TRUE)
print(detailed_gss, width = Inf)
# A tibble: 4 × 19
  name    type      n unique miss_n valid_pct   min    q1 median    mean
  <chr>   <chr> <int>  <int>  <int>     <dbl> <dbl> <dbl>  <dbl>   <dbl>
1 age     dbl    3859     72    127     0.968   18    34     48     49.0
2 educ    dbl    3952     21     34     0.991    0    12     14     14.2
3 hrs1    dbl    2201     76   1785     0.552    0    35     40     39.3
4 realinc dbl    3563     26    423     0.894  182. 9982. 24502. 34252. 
       mad       sd    q3     max    iqr    skew kurtosis       se        sum
     <dbl>    <dbl> <dbl>   <dbl>  <dbl>   <dbl>    <dbl>    <dbl>      <dbl>
1    22.2     17.7     64     89     30   0.158      2.00   0.284     189223 
2     2.97     2.92    16     20      4  -0.655      5.38   0.0464     56270 
3     7.41    13.6     45     89     10  -0.0694     4.69   0.289      86468 
4 21527.   34697.   43560 117565. 33578.  1.55       4.30 581.     122040909.

Alongside the familiar columns, miss_n counts missing values and valid_pct gives the non-missing proportion. q1, median, and q3 locate the quartiles; iqr is q3 - q1, the width of the middle half of the distribution. mad is a scaled median absolute deviation, another measure of spread. skew describes asymmetry, while kurtosis describes tail weight relative to the distribution’s spread. se is the standard error of the mean, discussed below, and sum adds the observed values. Not every available statistic is useful for every variable: summing respondents’ ages, for example, does not describe a typical respondent.

summ() uses Stata’s percentile rule, so its quartiles can differ from those produced by R’s default quantile(). A small difference in q1 between the two outputs need not be a calculation error. Two patterns in the detailed output help us describe the distributions:

  • For realinc, the mean ($34,252) is above the median ($24,502), and skewness (skew) is positive. High recorded family incomes extend the upper tail and pull the mean upward. The median locates the middle recorded income, following the comparison in the core lesson.
  • For educ, skewness is negative: respondents cluster around high-school and college schooling, with a longer tail toward few completed years. Its observed range is 0 to 20 years. Skewness describes the distribution’s asymmetry; it does not judge the people whose answers produce it.

9.3.2 Choose statistics

Instead of displaying every statistic, .stat requests a few by name. c("n", "valid_pct", "mean") combines three names into a character vector. The name and type columns still identify each variable:

# Choose summaries that show how many respondents contribute
summ(gss24, age, educ, hrs1:realinc, .stat = c("n", "valid_pct", "mean"))
# A tibble: 4 × 5
  name    type      n valid_pct    mean
  <chr>   <chr> <int>     <dbl>   <dbl>
1 age     dbl    3859     0.968    49.0
2 educ    dbl    3952     0.991    14.2
3 hrs1    dbl    2201     0.552    39.3
4 realinc dbl    3563     0.894 34252. 

n counts the non-missing observations. Despite its name, valid_pct is displayed on a 0–1 scale: 1 means complete coverage, and 0.894 means about 89.4% of respondents have a recorded family income. The income mean describes those respondents, not all 3,986 respondents.

Counts alone do not explain why answers are missing. Section 10.2 compares income coverage across degree categories before we use the joint availability of measurements in graphs.

9.3.3 Optional reference: custom calculations

For a custom calculation, first identify the quantities it needs. min() receives a numeric vector and returns its smallest value. Its own na.rm = TRUE option excludes missing inputs from this calculation:

min(gss24$age, na.rm = TRUE)
[1] 18

This is the youngest recorded age, in years. max() instead returns the largest value, with its own missing-input instruction:

max(gss24$age, na.rm = TRUE)
[1] 89

This is the oldest recorded age. The range is the distance between these extremes: subtract the minimum from the maximum. It differs from standard deviation, which uses distances around the mean.

For a calculation outside the standard summary choices, dplyr’s summarise() lets us write each expression ourselves. It takes a table and returns summary columns. Here we request the mean and range. In the range expression, - subtracts the smallest recorded age from the largest:

summarise(
  gss24,
  mean_age = mean(age, na.rm = TRUE),
  range_years = max(age, na.rm = TRUE) - min(age, na.rm = TRUE)
)
# A tibble: 1 × 2
  mean_age range_years
     <dbl>       <dbl>
1     49.0          71

The output has one row and two columns, both in years. A name on the left of = supplies the output heading; the expression on the right supplies its value. The mean agrees with summ(), while the range describes the distance between the extremes rather than the spread around the mean. Neither command changes gss24. All calculations use respondents with recorded ages.

9.3.4 Optional reference: standard error

You can skip this section and continue to the application. Standard deviation describes differences among observations. A standard error concerns how an estimated mean would vary across repeated samples: imagine repeatedly drawing independent random samples of the same size from one population and calculating a mean each time. Those sample means would differ. Their standard deviation is the standard error of the mean.

Under the simple independent-sampling model, we estimate it as sd / sqrt(n), where n counts non-missing observations and sqrt() takes the square root. A larger sample can make a mean more stable without making individual observations less spread out. To inspect that calculation in the teaching data, request n, sd, and se together:

summ(gss24, age, .stat = c("n", "sd", "se"))
# A tibble: 1 × 4
  type      n    sd    se
  <chr> <int> <dbl> <dbl>
1 dbl    3859  17.7 0.284

The two spread measures have the variable’s units, here years, but describe different things. The printed se uses the simple formula above. It does not account for the GSS sampling design and weights or correct systematic missingness, so it should not be treated as a design-adjusted measure of population precision. Inference and survey-design methods require later teaching beyond this chapter.

9.3.5 Check whether a summary is meaningful

With only the dataset argument, summ() examines all columns rather than requiring us to name them individually. This is convenient, but it makes us responsible for deciding which results make sense:

# Select all variables at once
summ(gss24)
sex, race, ... are factors (**).
# A tibble: 25 × 8
   name      type      n unique   min    mean       sd   max
   <chr>     <chr> <int>  <int> <dbl>   <dbl>    <dbl> <dbl>
 1 id        int    3986   3986     1 1994.   1151.     3986
 2 age       dbl    3859     72    18   49.0    17.7      89
 3 **sex     fct    3956      2     1    1.55    0.498     2
 4 **race    fct    3906      3     1    1.54    0.764     3
 5 **region  fct    3986      4     1    2.68    0.987     4
 6 **degree  fct    3979      5     1    2.87    1.27      5
 7 educ      dbl    3952     21     0   14.2     2.92     20
 8 paeduc    dbl    2866     21     0   12.5     3.88     20
 9 maeduc    dbl    3445     21     0   12.5     3.48     20
10 **marital fct    3971      5     1    2.80    1.75      5
# ℹ 15 more rows

summ() marks categorical factors with ** and prints a message. Their numerical summaries describe internal category codes, not meaningful measurements; use tab() to describe the categories. It also summarizes id, whose numbers identify respondents. Its mean of 1993.5 is mathematically defined but has no substantive interpretation: assigning different ID numbers would change it without changing anyone’s age, schooling, or income. Numeric storage alone does not make an average meaningful.

9.4 Application: age, schooling, and income in the 2024 GSS

How old are the respondents, how much schooling have they completed, and how many children have they had? How does family income vary? These quantities use different units, so first identify the variables:

  • age is in years.
  • educ is the number of years of schooling completed.
  • childs is the number of children the respondent has ever had.
  • realinc is family income in constant 1986 dollars: amounts adjusted for price changes to the purchasing power of dollars in 1986. The GSS asks about income in brackets, meaning ranges of amounts rather than exact dollar answers, so realinc has only 26 distinct recorded values.

Use summ() to inspect the distributions together:

summ(gss24, age, educ, childs, realinc)
# A tibble: 4 × 8
  name    type      n unique   min     mean       sd     max
  <chr>   <chr> <int>  <int> <dbl>    <dbl>    <dbl>   <dbl>
1 age     dbl    3859     72   18     49.0     17.7      89 
2 educ    dbl    3952     21    0     14.2      2.92     20 
3 childs  dbl    3944      9    0      1.72     1.65      8 
4 realinc dbl    3563     26  182. 34252.   34697.   117565.

Each output row describes one variable and uses its non-missing observations, so n differs across rows. For example, the mean of childs can be fractional even though each observed number of children is a whole number: an average need not be an observed value. The mean of realinc is in 1986 dollars, not current dollars or individual earnings. A common output layout does not make the variables interchangeable.

tabstat() returns a table of statistics for a numerical variable. Begin with the table and variable, before choosing options:

tabstat(gss24, realinc)
# A tibble: 1 × 2
  type    mean
  <chr>  <dbl>
1 dbl   34252.

This describes the recorded family incomes together. Unlike summ()’s variable-oriented layout, tabstat() arranges selected statistics in columns, making it convenient for the group comparison that follows. The source remains unchanged.

Its .stat option requests statistics by quoted names, as in summ(). First choose the non-missing count, mean, and median without dividing the data into groups:

tabstat(gss24, realinc, .stat = c("n", "mean", "median"))
# A tibble: 1 × 4
  type      n   mean median
  <chr> <int>  <dbl>  <dbl>
1 dbl    3563 34252. 24502.

Those measures use the same recorded incomes. Now add .by = degree to request a separate row for each degree category:

tabstat(gss24, realinc, .stat = c("n", "mean", "median"), .by = degree)
# A tibble: 6 × 5
  degree                   type      n   mean median
  <fct>                    <chr> <int>  <dbl>  <dbl>
1 less than high school    dbl     287 13210.  7714.
2 high school              dbl    1621 24308. 16335 
3 associate/junior college dbl     336 31510. 19965 
4 bachelor's               dbl     783 46575. 36300 
5 graduate                 dbl     530 59746. 43560 
6 NA                       dbl       6 20842. 18513 

A missing degree is included as a separate group by default. That group’s summary describes respondents whose degree is unavailable. To compare only the five recorded categories, add .missing = FALSE. This excludes the unknown-degree group; missing income is still excluded separately within every remaining group:

tabstat(gss24, realinc, .by = degree,
        .stat = c("n", "mean", "median"), .missing = FALSE)
# A tibble: 5 × 5
  degree                   type      n   mean median
  <fct>                    <chr> <int>  <dbl>  <dbl>
1 less than high school    dbl     287 13210.  7714.
2 high school              dbl    1621 24308. 16335 
3 associate/junior college dbl     336 31510. 19965 
4 bachelor's               dbl     783 46575. 36300 
5 graduate                 dbl     530 59746. 43560 

Each row now describes an educational group rather than a variable. Its n counts group members with reported income; a recorded degree does not guarantee a recorded income. Median family income rises at every step of education, from $7,714 among respondents without a high school diploma to $43,560 among those with a graduate degree. In every group, the mean is above the median, consistent with high incomes pulling the mean upward. Comparing the two measures helps show why a mean alone can give an incomplete description.

Two cautions apply to these descriptions:

  • They describe associations in a sample. They do not show that a degree causes higher income.
  • They are unweighted sample statistics: each respondent contributes equally. A survey weight changes a record’s contribution to account for how the sample represents the population. The GSS provides such weights; using them is beyond this edition and planned for later teaching.

9.5 Chapter review

  • summ() describes variables and reports the number of observed values contributing to each calculation.
  • mean() and median() describe center, while sd() describes spread in the variable’s units.
  • min() and max() identify extremes; quantile() locates positions such as the lower and upper quartiles.
  • Check each statistic’s contributing count; excluding missing inputs changes a calculation without replacing missing answers.
  • tabstat() compares selected statistics across groups; .by identifies groups and .stat chooses the reported statistics.
  • Optional: summarise() defines custom calculations; .detail requests additional statistics from summ(), and print() can widen the display.

Check units, column types, and non-missing counts before interpreting these unweighted sample summaries. Next, Chapter 10 examines item and joint coverage. Graphs in Chapter 11 then explore distributions and relationships.

9.6 Exercises

  1. Explain why mean(gss24$age) returns NA, correct the calculation, and compare its result and contributing sample with summ(gss24, age). Then compare mean and median family income. Choose a measure for a description of typical income, and report its count, units, and interpretation.
  2. Use the abstract values c(2, 4, 6, 8) to predict the median before running the code. Find the lower and upper quartiles separately from the median. Why need a cutpoint not equal a recorded value? Then calculate the median and quartiles of recorded GSS schooling in years.
  3. Compare family income across recorded marital-status categories with tabstat(). Report counts, means, and medians. Explain why a row counts income reporters within a category rather than all its members, and why neither the largest mean nor the largest count establishes a causal relationship.
  4. A researcher compares the standard deviation of age with the standard deviation of income and concludes that income is more variable because its number is larger. Another averages id to describe a typical respondent. Diagnose both claims using the meanings of the columns. If you read the optional standard-error section, explain why a smaller standard error does not imply that respondents have more similar ages.

Worked answers

Start a fresh session, load the packages, and restore the prepared GSS extract:

library(tidyverse)
library(tidystata)
answer_gss24 <- readRDS("gss24.rds")
mean(answer_gss24$age)
[1] NA
mean(answer_gss24$age, na.rm = TRUE)
[1] 49.03421
summ(answer_gss24, age)
# A tibble: 1 × 7
  type      n unique   min  mean    sd   max
  <chr> <int>  <int> <dbl> <dbl> <dbl> <dbl>
1 dbl    3859     72    18  49.0  17.7    89
summ(answer_gss24, realinc, .stat = c("n", "mean", "median"))
# A tibble: 1 × 4
  type      n   mean median
  <chr> <int>  <dbl>  <dbl>
1 dbl    3563 34252. 24502.

Missing ages make the first mean NA. The corrected mean agrees with summ() and uses its reported non-missing count; neither replaces missing ages. For income, the median describes the middle recorded family income because high values pull the mean upward. Among 3,563 income reporters, the median is $24,502, compared with the mean of $34,252. Both use constant 1986 dollars and describe unweighted reported family income. A mean remains appropriate when the question explicitly concerns the arithmetic average.

Start a fresh session, load the packages, and restore the prepared GSS extract:

library(tidyverse)
library(tidystata)
answer_gss24 <- readRDS("gss24.rds")
answer_values <- c(2, 4, 6, 8)
sort(answer_values)
[1] 2 4 6 8
median(answer_values)
[1] 5
quantile(answer_values, c(.25, .75))
25% 75% 
3.5 6.5 
median(answer_gss24$educ, na.rm = TRUE)
[1] 14
quantile(answer_gss24$educ, c(.25, .75), na.rm = TRUE)
25% 75% 
 12  16 
summ(answer_gss24, educ)
# A tibble: 1 × 7
  type      n unique   min  mean    sd   max
  <chr> <int>  <int> <dbl> <dbl> <dbl> <dbl>
1 dbl    3952     21     0  14.2  2.92    20

The abstract median is 5, midway between 4 and 6. The quartiles are 3.5 and 6.5 under R’s default interpolation, so neither needs to be an input value. GSS schooling cutpoints describe positions among respondents with recorded completed years; n supplies their contributing count. A lower quartile of schooling is a position in that distribution, not one quarter of its largest recorded years. Default quantile() cutpoints can differ from summ()’s Stata percentile rule.

Start a fresh session, load the packages, and restore the prepared GSS extract:

library(tidyverse)
library(tidystata)
answer_gss24 <- readRDS("gss24.rds")
tabstat(answer_gss24, realinc, .by = marital,
        .stat = c("n", "mean", "median"), .missing = FALSE)
# A tibble: 5 × 5
  marital       type      n   mean median
  <fct>         <chr> <int>  <dbl>  <dbl>
1 married       dbl    1492 49107. 36300 
2 widowed       dbl     227 21084. 13612.
3 divorced      dbl     537 23857. 16335 
4 separated     dbl     118 21644. 14974.
5 never married dbl    1181 23974. 16335 
tab(answer_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  

Each summary row identifies a recorded marital-status category. Its n counts people in that category who also reported income; the frequency table counts everyone in the category. Income means and medians are in constant 1986 dollars. Group size describes how many observations contribute, and the summaries describe the observed distribution. Neither establishes that changing marital status would cause income to change. Differences in ages and other circumstances may also matter.

Start a fresh session, load the packages, and restore the prepared GSS extract:

library(tidyverse)
library(tidystata)
answer_gss24 <- readRDS("gss24.rds")
summ(answer_gss24, age, realinc)
# A tibble: 2 × 8
  name    type      n unique   min    mean      sd     max
  <chr>   <chr> <int>  <int> <dbl>   <dbl>   <dbl>   <dbl>
1 age     dbl    3859     72   18     49.0    17.7     89 
2 realinc dbl    3563     26  182. 34252.  34697.  117565.
summ(answer_gss24, id)
# A tibble: 1 × 7
  type      n unique   min  mean    sd   max
  <chr> <int>  <int> <dbl> <dbl> <dbl> <dbl>
1 int    3986   3986     1 1994. 1151.  3986

Age is measured in years and income in constant 1986 dollars. Their standard deviations retain those units, so comparing the printed magnitudes does not give a unit-free ranking of variation. An ID identifies a record; its arithmetic average changes if we renumber respondents and has no substantive interpretation. In the optional section, standard deviation describes differences among individual ages, while standard error describes the sampling variability of an estimated mean under a stated model. More observations can reduce standard error while leaving individual variation unchanged.