Example 05: More dplyr and tidyr

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(nycdogs)
Loading required package: nyczips
library(socviz)

Using across() to apply functions to multiple columns

gss_sm
# A tibble: 2,867 × 32
    year    id ballot       age childs sibs   degree race  sex   region income16
   <dbl> <dbl> <labelled> <dbl>  <dbl> <labe> <fct>  <fct> <fct> <fct>  <fct>   
 1  2016     1 1             47      3 2      Bache… White Male  New E… $170000…
 2  2016     2 2             61      0 3      High … White Male  New E… $50000 …
 3  2016     3 3             72      2 3      Bache… White Male  New E… $75000 …
 4  2016     4 1             43      4 3      High … White Fema… New E… $170000…
 5  2016     5 3             55      2 2      Gradu… White Fema… New E… $170000…
 6  2016     6 2             53      2 2      Junio… White Fema… New E… $60000 …
 7  2016     7 1             50      2 2      High … White Male  New E… $170000…
 8  2016     8 3             23      3 6      High … Other Fema… Middl… $30000 …
 9  2016     9 1             45      3 5      High … Black Male  Middl… $60000 …
10  2016    10 3             71      4 1      Junio… White Male  Middl… $60000 …
# ℹ 2,857 more rows
# ℹ 21 more variables: relig <fct>, marital <fct>, padeg <fct>, madeg <fct>,
#   partyid <fct>, polviews <fct>, happy <fct>, partners <fct>, grass <fct>,
#   zodiac <fct>, pres12 <labelled>, wtssall <dbl>, income_rc <fct>,
#   agegrp <fct>, ageq <fct>, siblings <fct>, kids <fct>, religion <fct>,
#   bigregion <fct>, partners_rc <fct>, obama <dbl>

Let’s summarize the age, childs, and sibs columns.

gss_sm |>
  summarize(
    age_mean = mean(age, na.rm = TRUE),
    age_sd = sd(age, na.rm = TRUE),
    childs_mean = mean(childs, na.rm = TRUE),
    childs_sd = sd(childs, na.rm = TRUE),
    sibs_mean = mean(sibs, na.rm = TRUE),
    sibs_sd = sd(sibs, na.rm = TRUE)
  )
# A tibble: 1 × 6
  age_mean age_sd childs_mean childs_sd sibs_mean sibs_sd
     <dbl>  <dbl>       <dbl>     <dbl>     <dbl>   <dbl>
1     49.2   17.7        1.85      1.67      3.72    3.21

This is tedious and we have to name all the columns we create, which is error-prone. We can use across() to apply the same functions to multiple columns at once.

gss_sm |>
  summarize(
    across(c(age, childs, sibs),
           list(mean = \(x) mean(x, na.rm = TRUE),
                sd = \(x) sd(x, na.rm = TRUE)))
  )
# A tibble: 1 × 6
  age_mean age_sd childs_mean childs_sd sibs_mean sibs_sd
     <dbl>  <dbl>       <dbl>     <dbl>     <dbl>   <dbl>
1     49.2   17.7        1.85      1.67      3.72    3.21

We can also use where() to select columns based on their type. Here we summarize all numeric columns. First we drop id because it’s just a row-identifier and it doesn’t make sense to summarize it. We drop year, ballot, and the weight variables for similar reasons. We use a tidy selector to find the weight variables, and negate with - before the c() to drop the slected columns. Then in the summarize statement we use where(is.numeric) to select all remaining columns that are numeric.

gss_sm |>
  select(-c(id, ballot, year, starts_with("wts"))) |>
  summarize(
    across(where(is.numeric),
           list(mean = \(x) mean(x, na.rm = TRUE),
                sd = \(x) sd(x, na.rm = TRUE)))
  )
# A tibble: 1 × 10
  age_mean age_sd childs_mean childs_sd sibs_mean sibs_sd pres12_mean pres12_sd
     <dbl>  <dbl>       <dbl>     <dbl>     <dbl>   <dbl>       <dbl>     <dbl>
1     49.2   17.7        1.85      1.67      3.72    3.21        1.42     0.625
# ℹ 2 more variables: obama_mean <dbl>, obama_sd <dbl>

This will of course work with grouping variables as well. Here we also add a count of the number of rows in each group:

gss_sm |>
  select(-c(id, ballot, year, starts_with("wts"))) |>
  group_by(bigregion, degree) |>
  summarize(
    n = n(),
    across(
      where(is.numeric),
      list(mean = \(x) mean(x, na.rm = TRUE), sd = \(x) sd(x, na.rm = TRUE))
    )
  ) |>
  print(n = Inf) # n = Inf means "Print every row to the console"
`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by bigregion and degree.
ℹ Output is grouped by bigregion.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
ℹ Use `summarise(.by = c(bigregion, degree))` for per-operation grouping
  (`?dplyr::dplyr_by`) instead.
# A tibble: 24 × 15
# Groups:   bigregion [4]
   bigregion degree            n age_mean age_sd childs_mean childs_sd sibs_mean
   <fct>     <fct>         <int>    <dbl>  <dbl>       <dbl>     <dbl>     <dbl>
 1 Northeast Lt High Scho…    38     51.3  21.0         2.66      2.26      5.18
 2 Northeast High School     237     50.2  18.6         1.76      1.52      3.44
 3 Northeast Junior Colle…    40     51.5  17.0         1.92      1.87      2.9 
 4 Northeast Bachelor         98     48.8  16.7         1.34      1.29      2.66
 5 Northeast Graduate         74     51.0  15.0         1.23      1.28      2.42
 6 Northeast <NA>              1     63    NA           2        NA         7   
 7 Midwest   Lt High Scho…    67     52.7  18.5         2.99      2.19      5.33
 8 Midwest   High School     378     49.7  18.6         1.99      1.67      3.88
 9 Midwest   Junior Colle…    44     44.8  16.8         1.57      1.47      3.80
10 Midwest   Bachelor        137     46.5  16.4         1.40      1.46      2.91
11 Midwest   Graduate         66     53.2  15.8         1.80      1.53      2.23
12 Midwest   <NA>              3     51.3  33.1         4        NA         3   
13 South     Lt High Scho…   138     53.3  17.9         2.62      1.70      5.71
14 South     High School     547     48.1  17.8         1.93      1.63      4.01
15 South     Junior Colle…    86     46.3  16.5         1.93      1.64      4.49
16 South     Bachelor        178     48.9  16.0         1.57      1.40      3.12
17 South     Graduate        101     54.3  16.3         1.63      1.35      2.47
18 South     <NA>              2     42.5  12.0         4         2.83      5.5 
19 West      Lt High Scho…    85     50.5  19.9         3.02      2.10      6.55
20 West      High School     299     45.1  18.3         1.67      1.73      3.67
21 West      Junior Colle…    46     48.7  16.0         1.51      1.52      3.33
22 West      Bachelor        123     48.2  16.1         1.44      1.56      2.74
23 West      Graduate         77     53.2  15.3         1.40      1.36      2.49
24 West      <NA>              2     59.5   7.78        4        NA        11.5 
# ℹ 7 more variables: sibs_sd <dbl>, pres12_mean <dbl>, pres12_sd <dbl>,
#   obama_mean <dbl>, obama_sd <dbl>, n_mean <dbl>, n_sd <dbl>

See how, when we do this, we don’t put the n() call in the across()? That’s because n() is not a function that we are applying across all the numeric variables. We’re just counting the number of rows within the group, which will not change no matter how many summary statistics we calculate for variables within each group.

Per-Operation Grouping

As an aside, consider the difference between doing it with a separate group_by() line, as above, and doing the same thing with summarize(.by = c(...) ...):

gss_sm |>
  select(-c(id, ballot, year, starts_with("wts"))) |>
  summarize(
    .by = c(bigregion, degree),
    n = n(),
    across(
      where(is.numeric),
      list(mean = \(x) mean(x, na.rm = TRUE), sd = \(x) sd(x, na.rm = TRUE))
    )
  ) |>
  print(n = Inf)
# A tibble: 24 × 15
   bigregion degree            n age_mean age_sd childs_mean childs_sd sibs_mean
   <fct>     <fct>         <int>    <dbl>  <dbl>       <dbl>     <dbl>     <dbl>
 1 Northeast Bachelor         98     48.8  16.7         1.34      1.29      2.66
 2 Northeast High School     237     50.2  18.6         1.76      1.52      3.44
 3 Northeast Graduate         74     51.0  15.0         1.23      1.28      2.42
 4 Northeast Junior Colle…    40     51.5  17.0         1.92      1.87      2.9 
 5 Northeast Lt High Scho…    38     51.3  21.0         2.66      2.26      5.18
 6 West      Bachelor        123     48.2  16.1         1.44      1.56      2.74
 7 West      High School     299     45.1  18.3         1.67      1.73      3.67
 8 West      Graduate         77     53.2  15.3         1.40      1.36      2.49
 9 West      Junior Colle…    46     48.7  16.0         1.51      1.52      3.33
10 West      Lt High Scho…    85     50.5  19.9         3.02      2.10      6.55
11 West      <NA>              2     59.5   7.78        4        NA        11.5 
12 Midwest   High School     378     49.7  18.6         1.99      1.67      3.88
13 Midwest   Junior Colle…    44     44.8  16.8         1.57      1.47      3.80
14 Midwest   Graduate         66     53.2  15.8         1.80      1.53      2.23
15 Midwest   Bachelor        137     46.5  16.4         1.40      1.46      2.91
16 Midwest   Lt High Scho…    67     52.7  18.5         2.99      2.19      5.33
17 South     Graduate        101     54.3  16.3         1.63      1.35      2.47
18 South     Bachelor        178     48.9  16.0         1.57      1.40      3.12
19 South     Junior Colle…    86     46.3  16.5         1.93      1.64      4.49
20 South     High School     547     48.1  17.8         1.93      1.63      4.01
21 South     Lt High Scho…   138     53.3  17.9         2.62      1.70      5.71
22 South     <NA>              2     42.5  12.0         4         2.83      5.5 
23 Midwest   <NA>              3     51.3  33.1         4        NA         3   
24 Northeast <NA>              1     63    NA           2        NA         7   
# ℹ 7 more variables: sibs_sd <dbl>, pres12_mean <dbl>, pres12_sd <dbl>,
#   obama_mean <dbl>, obama_sd <dbl>, n_mean <dbl>, n_sd <dbl>

The values for any particular row are the same, but the row-order may not be.

Summarizing columns while pivoting

# Some made-up data
dfstrat <- read_csv(here::here("data", "dfstrat.csv"))
Rows: 1000 Columns: 5
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): sex, race, educ
dbl (2): stratum, income

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
dfstrat
# A tibble: 1,000 × 5
   stratum sex   race  educ  income
     <dbl> <chr> <chr> <chr>  <dbl>
 1       6 F     W     HS      83.7
 2       5 F     W     BA     128. 
 3       3 F     B     HS      66.3
 4       3 F     W     HS     111. 
 5       6 M     W     BA     116. 
 6       7 M     B     HS     159. 
 7       8 M     W     BA     131. 
 8       3 M     W     BA      94.4
 9       7 F     B     HS     146. 
10       2 F     W     BA      88.8
# ℹ 990 more rows

This is a simulated sample of a 1,000 people in eight survey strata, with measured sex, race, and education characteristics. For simplicity we just have two categories within each measure. We have an income measure for each person. So we have e.g. sex x race x educ combinations of categories and we want to know things about average income within various categories.

Let’s say we want to transform this to a wider format, specifically by widening the educ column, so we end up with columns for both the HS and BA categories. While we do so we also want to calculate both the mean of income and the total n within each group, still within strata, while holding on to the sex and race classifcations. We “over-group” the sex and race categories, and then have education within stratum:

dfstrat |>
  group_by(sex, race, stratum, educ) |>
  summarize(
    mean_inc = mean(income),
    n = n()
  ) |>
  pivot_wider(names_from = (educ), values_from = c(mean_inc, n)) |>
  ungroup()
`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by sex, race, stratum, and educ.
ℹ Output is grouped by sex, race, and stratum.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
ℹ Use `summarise(.by = c(sex, race, stratum, educ))` for per-operation grouping
  (`?dplyr::dplyr_by`) instead.
# A tibble: 32 × 7
   sex   race  stratum mean_inc_BA mean_inc_HS  n_BA  n_HS
   <chr> <chr>   <dbl>       <dbl>       <dbl> <int> <int>
 1 F     B           1        93.8        99.3    19     6
 2 F     B           2        89.7        93.0    11    16
 3 F     B           3       112.         95.0    13    16
 4 F     B           4       108.         96.1    14    15
 5 F     B           5        91.0        92.6    11    15
 6 F     B           6        93.0       116.     15    15
 7 F     B           7       102.        121.     13    13
 8 F     B           8       105.         88.3    14     8
 9 F     W           1        92.6       110.     19    13
10 F     W           2        98.5       101.     15    19
# ℹ 22 more rows

Here we end up with sex-by-race-by-stratum in the rows, and the income-by-education means, and income-by-education Ns, in their own columns.

Again, consider the difference between that and the .by ... way:

dfstrat |>
    summarize(.by = c(sex, race, stratum, educ),
              mean_inc = mean(income),
              n = n()) |>
    pivot_wider(names_from = (educ),
                values_from = c(mean_inc, n))
# A tibble: 32 × 7
   sex   race  stratum mean_inc_HS mean_inc_BA  n_HS  n_BA
   <chr> <chr>   <dbl>       <dbl>       <dbl> <int> <int>
 1 F     W           6        99.1        89.7    20    18
 2 F     W           5        93.1       122.     21    16
 3 F     B           3        95.0       112.     16    13
 4 F     W           3        95.8        73.6    17    18
 5 M     W           6        86.6        89.3    14    20
 6 M     B           7       107.         93.0    17    13
 7 M     W           8       105.        108.     16    21
 8 M     W           3       115.        107.     12    17
 9 F     B           7       121.        102.     13    13
10 F     W           2       101.         98.5    19    15
# ℹ 22 more rows

The values in any particular cells are the same, but the row order may be different, and also the column order (thanks to the way the pivot interacts with the per-operation grouping).