list(1, "a")[[1]]
[1] 1
[[2]]
[1] "a"
An ordinary atomic vector holds values of one basic type. A list can keep objects of different types or lengths together without converting them to a common type. A list column stores one such component for each row of a table. We will split existing entity names into words, retain the words together, then expand them while preserving their connection to the original names.
By the end of this chapter, you will be able to:
The first example needs only base R. The application uses the public country data already used in Section 15.7, not participant information. Selection and column creation are prerequisites; the later reshaping chapter is not.
A list can hold different types together, such as a number and some text:
list(1, "a")[[1]]
[1] 1
[[2]]
[1] "a"
The printed result has two components: the number 1 and the character value "a". Their types remain distinct. A list is useful when the pieces we want to keep together are not all the same kind of object.
A list is not necessarily a dataset with observations in rows. Tables and model results can themselves be stored as list components. The fuller comparison in Chapter 28 explains how lists relate to the other structures used in R.
Our World in Data, abbreviated OWID, supplies the captured life-expectancy CSV used earlier. It contains aggregate entity-year statistics. It has no workshop participants. We reuse its entity names because names can contain different numbers of words. Copy the book’s captured CSV into your project’s examples folder:
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
owid <- read_csv("examples/life-expectancy.csv")Rows: 21565 Columns: 4
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): Entity, Code
dbl (2): Year, Life expectancy
ℹ 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.
entity_names <- owid |>
filter(Year == 2023) |>
select(Entity, Code, Year)
head(entity_names)# A tibble: 6 × 3
Entity Code Year
<chr> <chr> <dbl>
1 Afghanistan AFG 2023
2 Africa OWID_AFR 2023
3 Albania ALB 2023
4 Algeria DZA 2023
5 American Samoa ASM 2023
6 Americas NA 2023
The selected year has 261 entities. Some are territories or aggregates, so not every name denotes a sovereign country. Retaining one year avoids repeating each name across years. Code is missing for some aggregates; retain Entity and Year as well rather than assuming codes identify every row.
str_split() takes character values and a separator pattern, returning a list with one character vector for each input value. Here the familiar fixed(" ") specifies a literal space, as in Section 15.7. The captured names use single spaces between words; we are splitting their recorded form, not repairing source errors:
name_lists <- entity_names |>
mutate(words = str_split(Entity, fixed(" ")))
head(name_lists)# A tibble: 6 × 4
Entity Code Year words
<chr> <chr> <dbl> <list>
1 Afghanistan AFG 2023 <chr [1]>
2 Africa OWID_AFR 2023 <chr [1]>
3 Albania ALB 2023 <chr [1]>
4 Algeria DZA 2023 <chr [1]>
5 American Samoa ASM 2023 <chr [2]>
6 Americas NA 2023 <chr [1]>
name_lists |>
filter(Entity == "United States")# A tibble: 1 × 4
Entity Code Year words
<chr> <chr> <dbl> <list>
1 United States USA 2023 <chr [2]>
The new words column is a list column. Its component for “United States” contains two strings, “United” and “States”. The printed <chr [2]> abbreviation describes a two-element character vector stored in that cell; it is not the literal cell text. Names with one word have one-element components. The table still has one row per entity in 2023, and all original name and code values remain.
When we need to inspect words separately, unnest_longer() from tidyr expands a list column. First supply the table through the pipe and select the list column words. Each component’s values become separate rows, with the other columns repeated alongside them. Assignment stores a new table; name_lists retains the complete components:
name_words_default <- name_lists |>
unnest_longer(words)
head(name_words_default)# A tibble: 6 × 4
Entity Code Year words
<chr> <chr> <dbl> <chr>
1 Afghanistan AFG 2023 Afghanistan
2 Africa OWID_AFR 2023 Africa
3 Albania ALB 2023 Albania
4 Algeria DZA 2023 Algeria
5 American Samoa ASM 2023 American
6 American Samoa ASM 2023 Samoa
The column is still called words, but it now contains one character value per row. The original entity, code, and year stay attached to each word. Give the expanded value column a singular name with values_to:
name_words_named <- name_lists |>
unnest_longer(words, values_to = "word")
head(name_words_named)# A tibble: 6 × 4
Entity Code Year word
<chr> <chr> <dbl> <chr>
1 Afghanistan AFG 2023 Afghanistan
2 Africa OWID_AFR 2023 Africa
3 Albania ALB 2023 Albania
4 Algeria DZA 2023 Algeria
5 American Samoa ASM 2023 American
6 American Samoa ASM 2023 Samoa
The option changes the output name, not the number of rows or the words. The components also have an order within each name. Add indices_to to name the column that records those positions:
name_words <- name_lists |>
unnest_longer(
words,
values_to = "word",
indices_to = "word_position"
)
head(name_words)# A tibble: 6 × 5
Entity Code Year word_position word
<chr> <chr> <dbl> <int> <chr>
1 Afghanistan AFG 2023 1 Afghanistan
2 Africa OWID_AFR 2023 1 Africa
3 Albania ALB 2023 1 Albania
4 Algeria DZA 2023 1 Algeria
5 American Samoa ASM 2023 1 American
6 American Samoa ASM 2023 2 Samoa
name_words |>
filter(Entity == "United States")# A tibble: 2 × 5
Entity Code Year word_position word
<chr> <chr> <dbl> <int> <chr>
1 United States USA 2023 1 United
2 United States USA 2023 2 States
The United States now contributes two rows. Each retains its original entity, code, and year, while positions 1 and 2 identify “United” and “States”. A row now represents an entity-word position. It is not an additional entity or an independent observation of life expectancy.
count() counts rows within combinations of columns, as in the age-group checks in Chapter 14. Its default result column is n. Start with the number of word positions for each entity-year:
word_counts_default <- name_words |>
count(Entity, Year)
head(word_counts_default)# A tibble: 6 × 3
Entity Year n
<chr> <dbl> <int>
1 Afghanistan 2023 1
2 Africa 2023 1
3 Albania 2023 1
4 Algeria 2023 1
5 American Samoa 2023 2
6 Americas 2023 1
Each output row describes one entity in 2023; n counts that name’s words, not additional entities. The name option gives this count a more informative heading. It names the returned count column, rather than selecting a source variable:
word_counts <- name_words |>
count(Entity, Year, name = "words")
head(word_counts)# A tibble: 6 × 3
Entity Year words
<chr> <dbl> <int>
1 Afghanistan 2023 1
2 Africa 2023 1
3 Albania 2023 1
4 Algeria 2023 1
5 American Samoa 2023 2
6 Americas 2023 1
The values match the previous n column, but the heading now states what was counted. We can reconcile the expanded table’s row total with these component counts. sum() adds numeric input values and returns one total without changing them. Here $ extracts the numeric count column:
word_total <- sum(word_counts$words)
word_total[1] 389
nrow(name_words)[1] 389
word_total == nrow(name_words)[1] TRUE
The equality returns TRUE: adding the words across names gives exactly the number of expanded rows. These counts contain no missing entries, so there is no missing-value option to set in this calculation. The total counts word positions, with repeated words counted each time they occur.
A separate check asks whether any entity-year-position combination occurs more than once. An identifying combination should occur exactly once in this example:
name_words |>
count(Entity, Year, word_position) |>
filter(n != 1)# A tibble: 0 × 4
# ℹ 4 variables: Entity <chr>, Year <dbl>, word_position <int>, n <int>
nrow(name_lists)[1] 261
nrow(word_counts)[1] 261
nrow(name_words)[1] 389
The duplicate-position check returns zero rows. There are still 261 entities, matching the 261 source rows, but 389 word positions. Counting rows of the expanded table would give longer names more weight. A mean of a measurement repeated across these rows would therefore answer a different question from a mean across entities.
Words are parts of names. Splitting “Trinidad and Tobago” does not create separate records for three places, and “and” is not an entity. This example teaches storage and expansion, not geographical classification. Similarly, if another source stores paired aliases and descriptions, splitting both columns requires checking that each alias still matches its own description. Expanding them independently can create incorrect combinations; this file supplies no such paired lists.
list() keeps components together while allowing different types.str_split() returns one list component per input string; a list column retains one component per table row.unnest_longer() expands components into rows; retain identifiers and within-component positions.count() names and checks component counts; sum() reconciles their total with the expanded row count.The later Chapter 19 changes the layout of questionnaire and monthly data. It also checks which columns identify each resulting row.
list(1, "a") preserves two types. What does <chr [2]> mean for a list cell containing the words of "United States"? How does it differ from literal character text?words, add those counts, and reconcile the result with the expanded row count. Check that every entity-year-position combination is unique. A colleague keeps only word after expansion and calculates a mean of a measurement repeated beside those words. Diagnose the loss of identifiers and the changed weighting.library(tidyverse)
list(1, "a")[[1]]
[1] 1
[[2]]
[1] "a"
str_split("United States", fixed(" "))[[1]]
[1] "United" "States"
The first list retains a number and a character value as separate components. The second contains one component holding two character values. <chr [2]> describes a character vector of length two inside one list cell. It is a compact description of the cell, not literal text stored there. A literal character cell holds the complete name as one string; a list cell can hold the vector of its words without adding table rows.
library(tidyverse)
answer_owid <- read_csv("examples/life-expectancy.csv")Rows: 21565 Columns: 4
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): Entity, Code
dbl (2): Year, Life expectancy
ℹ 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.
answer_names <- answer_owid |>
filter(Year == 2023) |>
select(Entity, Code, Year) |>
mutate(words = str_split(Entity, fixed(" ")))
answer_words <- answer_names |>
unnest_longer(words, values_to = "word", indices_to = "word_position")
answer_names |>
filter(Entity == "Trinidad and Tobago")# A tibble: 1 × 4
Entity Code Year words
<chr> <chr> <dbl> <list>
1 Trinidad and Tobago TTO 2023 <chr [3]>
answer_words |>
filter(Entity == "Trinidad and Tobago")# A tibble: 3 × 5
Entity Code Year word_position word
<chr> <chr> <dbl> <int> <chr>
1 Trinidad and Tobago TTO 2023 1 Trinidad
2 Trinidad and Tobago TTO 2023 2 and
3 Trinidad and Tobago TTO 2023 3 Tobago
One entity row becomes three word-position rows. Each retains the same complete name, code, and year. The three words are not three countries.
library(tidyverse)
answer_owid <- read_csv("examples/life-expectancy.csv")Rows: 21565 Columns: 4
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): Entity, Code
dbl (2): Year, Life expectancy
ℹ 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.
answer_names <- answer_owid |>
filter(Year == 2023) |>
select(Entity, Code, Year)
answer_words <- answer_names |>
mutate(words = str_split(Entity, fixed(" "))) |>
unnest_longer(words, values_to = "word", indices_to = "word_position")
answer_counts <- answer_words |>
count(Entity, Year, name = "words")
answer_total <- sum(answer_counts$words)
answer_total[1] 389
nrow(answer_words)[1] 389
answer_total == nrow(answer_words)[1] TRUE
nrow(answer_counts) == nrow(answer_names)[1] TRUE
answer_words |>
count(Entity, Year, word_position) |>
filter(n != 1)# A tibble: 0 × 4
# ℹ 4 variables: Entity <chr>, Year <dbl>, word_position <int>, n <int>
Both equalities return TRUE, and the duplicate-position check returns zero rows. The same entities remain, but those with longer names contribute more rows. A row count describes word positions; a mean of repeated entity measurements would weight entities by their number of words unless we first return to one record per entity.
Keeping only word would remove the complete name, code, year, and within-name position. A repeated word could then no longer be traced to its source name. Retain these columns in the working table; select a short display separately if needed. Likewise, repeating a measurement once per word gives longer names more weight. Count or summarize at the entity level when the question concerns entities.