library(tidyverse)

Что такое списки?

Допустим, вы забыли, что такое список:

x <- list(m = "a", n = c(2, 4), o = c("blue", "green", "red"))
x
## $m
## [1] "a"
## 
## $n
## [1] 2 4
## 
## $o
## [1] "blue"  "green" "red"
x$m
## [1] "a"
x[1]
## $m
## [1] "a"
x[[2]]
## [1] 2 4
x[[2]][2]
## [1] 4

Списки вне R обычно хранят в файлах типа .json (по-английски говорят с ударением на первый слог, а вот по-русски прижился вариант с ударением на второй слог: джейсо́н) или .xml (мы обсудим только .json).

Превращение JSON в список и обратно

Самое важное, что нужно знать, про .json, это то, что там могут хранится:

  • пары ключ и значение, заключенные в фигурные скобки {"ключ":"значение"}
  • упорядочные множества значений, заключенные в квадратные скобки ["значение_1","значение_2",...]
  • некоторые другие типы данных

Чтение и запись .json файлов осуществляется при помощи пакета jsonlite (не входит в tidyverse). Для примера скачаем датасет 30 героев Игры престолов, который Дж. Р. Р. Мартин достал с ресурса An API of Ice And Fire.

got_chars <- jsonlite::read_json("https://raw.githubusercontent.com/agricolamz/2019.12.21_EU_purrr/master/data/got_chars.json")
got_chars[1]
## [[1]]
## [[1]]$url
## [1] "https://www.anapioficeandfire.com/api/characters/1022"
## 
## [[1]]$id
## [1] 1022
## 
## [[1]]$name
## [1] "Theon Greyjoy"
## 
## [[1]]$gender
## [1] "Male"
## 
## [[1]]$culture
## [1] "Ironborn"
## 
## [[1]]$born
## [1] "In 278 AC or 279 AC, at Pyke"
## 
## [[1]]$died
## [1] ""
## 
## [[1]]$alive
## [1] TRUE
## 
## [[1]]$titles
## [[1]]$titles[[1]]
## [1] "Prince of Winterfell"
## 
## [[1]]$titles[[2]]
## [1] "Captain of Sea Bitch"
## 
## [[1]]$titles[[3]]
## [1] "Lord of the Iron Islands (by law of the green lands)"
## 
## 
## [[1]]$aliases
## [[1]]$aliases[[1]]
## [1] "Prince of Fools"
## 
## [[1]]$aliases[[2]]
## [1] "Theon Turncloak"
## 
## [[1]]$aliases[[3]]
## [1] "Reek"
## 
## [[1]]$aliases[[4]]
## [1] "Theon Kinslayer"
## 
## 
## [[1]]$father
## [1] ""
## 
## [[1]]$mother
## [1] ""
## 
## [[1]]$spouse
## [1] ""
## 
## [[1]]$allegiances
## [1] "House Greyjoy of Pyke"
## 
## [[1]]$books
## [[1]]$books[[1]]
## [1] "A Game of Thrones"
## 
## [[1]]$books[[2]]
## [1] "A Storm of Swords"
## 
## [[1]]$books[[3]]
## [1] "A Feast for Crows"
## 
## 
## [[1]]$povBooks
## [[1]]$povBooks[[1]]
## [1] "A Clash of Kings"
## 
## [[1]]$povBooks[[2]]
## [1] "A Dance with Dragons"
## 
## 
## [[1]]$tvSeries
## [[1]]$tvSeries[[1]]
## [1] "Season 1"
## 
## [[1]]$tvSeries[[2]]
## [1] "Season 2"
## 
## [[1]]$tvSeries[[3]]
## [1] "Season 3"
## 
## [[1]]$tvSeries[[4]]
## [1] "Season 4"
## 
## [[1]]$tvSeries[[5]]
## [1] "Season 5"
## 
## [[1]]$tvSeries[[6]]
## [1] "Season 6"
## 
## 
## [[1]]$playedBy
## [1] "Alfie Allen"

Так как jsonlite конфликтует в одной функции с tidyverse, я не загружаю библиотеку полностью при помощи команды library(jsonlite), а обращаюсь к функциям пакета при помощи выражения jsonlite::...(). Пакет jsonlite позволяет записывать .json файлы при помощи функции write_json():

jsonlite::write_json(got_chars, "got_chars.json", auto_unbox = TRUE)
# аргумент auto_unbox позволяет сохранять структуру, которую мы видим в R

Просмотр списков в R

Просмоторщик списков встроен в RStudio. Его можно увидеть, если ткнуть в объект в R или написать команду View(got_chars). Альтернативой может стать функция jsonedit() из пакета listviewer. Кроме того, что-то можно попробовать выяснить, используя функцию str().

str(got_chars[1])
## List of 1
##  $ :List of 18
##   ..$ url        : chr "https://www.anapioficeandfire.com/api/characters/1022"
##   ..$ id         : int 1022
##   ..$ name       : chr "Theon Greyjoy"
##   ..$ gender     : chr "Male"
##   ..$ culture    : chr "Ironborn"
##   ..$ born       : chr "In 278 AC or 279 AC, at Pyke"
##   ..$ died       : chr ""
##   ..$ alive      : logi TRUE
##   ..$ titles     :List of 3
##   .. ..$ : chr "Prince of Winterfell"
##   .. ..$ : chr "Captain of Sea Bitch"
##   .. ..$ : chr "Lord of the Iron Islands (by law of the green lands)"
##   ..$ aliases    :List of 4
##   .. ..$ : chr "Prince of Fools"
##   .. ..$ : chr "Theon Turncloak"
##   .. ..$ : chr "Reek"
##   .. ..$ : chr "Theon Kinslayer"
##   ..$ father     : chr ""
##   ..$ mother     : chr ""
##   ..$ spouse     : chr ""
##   ..$ allegiances: chr "House Greyjoy of Pyke"
##   ..$ books      :List of 3
##   .. ..$ : chr "A Game of Thrones"
##   .. ..$ : chr "A Storm of Swords"
##   .. ..$ : chr "A Feast for Crows"
##   ..$ povBooks   :List of 2
##   .. ..$ : chr "A Clash of Kings"
##   .. ..$ : chr "A Dance with Dragons"
##   ..$ tvSeries   :List of 6
##   .. ..$ : chr "Season 1"
##   .. ..$ : chr "Season 2"
##   .. ..$ : chr "Season 3"
##   .. ..$ : chr "Season 4"
##   .. ..$ : chr "Season 5"
##   .. ..$ : chr "Season 6"
##   ..$ playedBy   : chr "Alfie Allen"
str(got_chars[1], max.level = 2)
## List of 1
##  $ :List of 18
##   ..$ url        : chr "https://www.anapioficeandfire.com/api/characters/1022"
##   ..$ id         : int 1022
##   ..$ name       : chr "Theon Greyjoy"
##   ..$ gender     : chr "Male"
##   ..$ culture    : chr "Ironborn"
##   ..$ born       : chr "In 278 AC or 279 AC, at Pyke"
##   ..$ died       : chr ""
##   ..$ alive      : logi TRUE
##   ..$ titles     :List of 3
##   ..$ aliases    :List of 4
##   ..$ father     : chr ""
##   ..$ mother     : chr ""
##   ..$ spouse     : chr ""
##   ..$ allegiances: chr "House Greyjoy of Pyke"
##   ..$ books      :List of 3
##   ..$ povBooks   :List of 2
##   ..$ tvSeries   :List of 6
##   ..$ playedBy   : chr "Alfie Allen"

Пакет purrr

В tidyverse встроен пакет purrr, который среди прочего позволяет работать со списками. Существует cheat sheet по purrr. Ну и конечно полный туториал.

Функция map() и map...()

Можно выводить значения в списке на основе имени:

got_chars %>% 
  map("name")
## [[1]]
## [1] "Theon Greyjoy"
## 
## [[2]]
## [1] "Tyrion Lannister"
## 
## [[3]]
## [1] "Victarion Greyjoy"
## 
## [[4]]
## [1] "Will"
## 
## [[5]]
## [1] "Areo Hotah"
## 
## [[6]]
## [1] "Chett"
## 
## [[7]]
## [1] "Cressen"
## 
## [[8]]
## [1] "Arianne Martell"
## 
## [[9]]
## [1] "Daenerys Targaryen"
## 
## [[10]]
## [1] "Davos Seaworth"
## 
## [[11]]
## [1] "Arya Stark"
## 
## [[12]]
## [1] "Arys Oakheart"
## 
## [[13]]
## [1] "Asha Greyjoy"
## 
## [[14]]
## [1] "Barristan Selmy"
## 
## [[15]]
## [1] "Varamyr"
## 
## [[16]]
## [1] "Brandon Stark"
## 
## [[17]]
## [1] "Brienne of Tarth"
## 
## [[18]]
## [1] "Catelyn Stark"
## 
## [[19]]
## [1] "Cersei Lannister"
## 
## [[20]]
## [1] "Eddard Stark"
## 
## [[21]]
## [1] "Jaime Lannister"
## 
## [[22]]
## [1] "Jon Connington"
## 
## [[23]]
## [1] "Jon Snow"
## 
## [[24]]
## [1] "Aeron Greyjoy"
## 
## [[25]]
## [1] "Kevan Lannister"
## 
## [[26]]
## [1] "Melisandre"
## 
## [[27]]
## [1] "Merrett Frey"
## 
## [[28]]
## [1] "Quentyn Martell"
## 
## [[29]]
## [1] "Samwell Tarly"
## 
## [[30]]
## [1] "Sansa Stark"

Можно выводить значения в списке на основе индекса:

got_chars %>% 
  map(3)
## [[1]]
## [1] "Theon Greyjoy"
## 
## [[2]]
## [1] "Tyrion Lannister"
## 
## [[3]]
## [1] "Victarion Greyjoy"
## 
## [[4]]
## [1] "Will"
## 
## [[5]]
## [1] "Areo Hotah"
## 
## [[6]]
## [1] "Chett"
## 
## [[7]]
## [1] "Cressen"
## 
## [[8]]
## [1] "Arianne Martell"
## 
## [[9]]
## [1] "Daenerys Targaryen"
## 
## [[10]]
## [1] "Davos Seaworth"
## 
## [[11]]
## [1] "Arya Stark"
## 
## [[12]]
## [1] "Arys Oakheart"
## 
## [[13]]
## [1] "Asha Greyjoy"
## 
## [[14]]
## [1] "Barristan Selmy"
## 
## [[15]]
## [1] "Varamyr"
## 
## [[16]]
## [1] "Brandon Stark"
## 
## [[17]]
## [1] "Brienne of Tarth"
## 
## [[18]]
## [1] "Catelyn Stark"
## 
## [[19]]
## [1] "Cersei Lannister"
## 
## [[20]]
## [1] "Eddard Stark"
## 
## [[21]]
## [1] "Jaime Lannister"
## 
## [[22]]
## [1] "Jon Connington"
## 
## [[23]]
## [1] "Jon Snow"
## 
## [[24]]
## [1] "Aeron Greyjoy"
## 
## [[25]]
## [1] "Kevan Lannister"
## 
## [[26]]
## [1] "Melisandre"
## 
## [[27]]
## [1] "Merrett Frey"
## 
## [[28]]
## [1] "Quentyn Martell"
## 
## [[29]]
## [1] "Samwell Tarly"
## 
## [[30]]
## [1] "Sansa Stark"

Достаточно полезно знать о функции unlist(), которая “убивает” все сложную структуру:

got_chars %>% 
  map("name") %>% 
  unlist()
##  [1] "Theon Greyjoy"      "Tyrion Lannister"   "Victarion Greyjoy" 
##  [4] "Will"               "Areo Hotah"         "Chett"             
##  [7] "Cressen"            "Arianne Martell"    "Daenerys Targaryen"
## [10] "Davos Seaworth"     "Arya Stark"         "Arys Oakheart"     
## [13] "Asha Greyjoy"       "Barristan Selmy"    "Varamyr"           
## [16] "Brandon Stark"      "Brienne of Tarth"   "Catelyn Stark"     
## [19] "Cersei Lannister"   "Eddard Stark"       "Jaime Lannister"   
## [22] "Jon Connington"     "Jon Snow"           "Aeron Greyjoy"     
## [25] "Kevan Lannister"    "Melisandre"         "Merrett Frey"      
## [28] "Quentyn Martell"    "Samwell Tarly"      "Sansa Stark"

Для этого есть и отдельные функции, которые позволяют превратить все в вектор заданного типа:

got_chars %>% 
  map_chr("name")
##  [1] "Theon Greyjoy"      "Tyrion Lannister"   "Victarion Greyjoy" 
##  [4] "Will"               "Areo Hotah"         "Chett"             
##  [7] "Cressen"            "Arianne Martell"    "Daenerys Targaryen"
## [10] "Davos Seaworth"     "Arya Stark"         "Arys Oakheart"     
## [13] "Asha Greyjoy"       "Barristan Selmy"    "Varamyr"           
## [16] "Brandon Stark"      "Brienne of Tarth"   "Catelyn Stark"     
## [19] "Cersei Lannister"   "Eddard Stark"       "Jaime Lannister"   
## [22] "Jon Connington"     "Jon Snow"           "Aeron Greyjoy"     
## [25] "Kevan Lannister"    "Melisandre"         "Merrett Frey"      
## [28] "Quentyn Martell"    "Samwell Tarly"      "Sansa Stark"
got_chars %>% 
  map_int("id")
##  [1] 1022 1052 1074 1109 1166 1267 1295  130 1303 1319  148  149  150  168 2066
## [16]  208  216  232  238  339  529  576  583   60  605  743  751  844  954  957
got_chars %>% 
  map_lgl("alive")
##  [1]  TRUE  TRUE  TRUE FALSE  TRUE FALSE FALSE  TRUE  TRUE  TRUE  TRUE FALSE
## [13]  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE FALSE  TRUE  TRUE  TRUE  TRUE
## [25] FALSE  TRUE FALSE FALSE  TRUE  TRUE

Можно даже создать новый датафрейм:

got_chars %>% 
  tibble(name = map_chr(., "name"), # точка обозначает, в какой аргумент должно все пайпиться
         id = map_dbl(., "id"),
         alive = map_lgl(., "alive"))

Также существует способ создание датафреймов на основе вектора значений при помощи функции enframe():

got_chars %>% 
  map_chr("name") %>% 
  enframe()

Задание 1

Скачайте частотный словарь русского языка [Шаров, Ляшевская 2011] (он храниться в .tsv), разбейте столбец lemma на буквы при помощи функции str_split(dict$lemma, ""), а на основе полученного списка постройте график, на котором изображено, сколько раз встретилась какая буква:

📋 список подсказок ➡
👁 tidyverse включили? ➡ library(tidyverse).
👁 .tsv? Почему словарь скачивается как один столбец? ➡ .tsv — это как .csv, только в качестве разделителя не запятая, а табуляция (\t). Попробуйте использовать другой разделитель или функцию read_tsv() из пакета readr (входит в tidyverse).
👁 Почему словарь как-то криво скачивается? ➡ Cкорее всего полетела кодировка. Я все делаю в UTF-8, так что либо поменяйте кодировку в функции, которую Вы используете, либо исполбзуйте функцию read_tsv() из пакета readr (входит в tidyverse) — она сама борется с кодировками.
👁 Получился список. А-а-а-а! Как его убрать? ➡ Это простой случай, так что в каком-то смысле списочная структура не очень нужна. Так что можно просто unlist().
👁 Я получил вектор. А что дальше? ➡ В принципе, график ggplot2 требует на вход датафрейм, так что его можно сделать при помощи чего угодно. Я использовал enframe().
👁 Какие-то буквы большие, какие-то маленькие… Что делать? ➡ Существует много способов привести все к строчным буквам: tolower("ВАША СТРОКА") из базого R, или str_to_lower() из пакета stringr (входит в tidyverse).
👁 А как убрать дефис и апостроф? ➡ Ну во-первых, что-то нужно убрать, а что-то оставить — значит нужна функция filter() из пакета dplyr. А дальше уже вопрос техники: можно использовать констуркцию value != "-". А можно убить двух зайцев одним выстрелом и использовать регулярку: filter(str_detect(value, "\\w")).
👁 А как посчитать кого сколько? ➡ Для этого можно использовать функцию count(). Можно было вообще не считать и использовать geom_bar(), но тогда не получиться отсортировать по возрастанию.
👁 А какой geom_ использовать? ➡ Я использовал geom_col(). Можно еще использовать geom_bar() на неагрегированных данных, но тогда не удастся отсортировать.
👁 А как перевернуть оси? ➡ Добавьте coord_flip() к объекту ggplot.
👁 А как отсортировать по возрастанию? ➡ Для этого нужно превратить переменную в фактор с правильным порядком. Я это обычно делаю при помощи функции fct_reorder() из пакета forcats (входит в tidyverse) уже в ggplot(): data %>% ggplot(aes(x = fct_reorder(value, n), y = n)) + ....

Задание 2 (домой)

В датасете, собранный Adithya Ganesh, хранится информация об играках Английской Премьер-лиги. На основании этих данных (лежат в папке data) постройте график 30 игроков, которые забили больше всего голов и раскрасьте на основании их футбольного клуба.

Фильтрация списков

Если в списке есть логические выражения, их можно использовать как фильтры. Например, вот так мы оставим только живых героев:

got_chars %>% 
  keep("alive") %>% 
  map_chr("name")
##  [1] "Theon Greyjoy"      "Tyrion Lannister"   "Victarion Greyjoy" 
##  [4] "Areo Hotah"         "Arianne Martell"    "Daenerys Targaryen"
##  [7] "Davos Seaworth"     "Arya Stark"         "Asha Greyjoy"      
## [10] "Barristan Selmy"    "Brandon Stark"      "Brienne of Tarth"  
## [13] "Cersei Lannister"   "Jaime Lannister"    "Jon Connington"    
## [16] "Jon Snow"           "Aeron Greyjoy"      "Melisandre"        
## [19] "Samwell Tarly"      "Sansa Stark"

А так – только мертвых:

got_chars %>% 
  discard("alive") %>% 
  map_chr("name")
##  [1] "Will"            "Chett"           "Cressen"         "Arys Oakheart"  
##  [5] "Varamyr"         "Catelyn Stark"   "Eddard Stark"    "Kevan Lannister"
##  [9] "Merrett Frey"    "Quentyn Martell"

Также есть особый фильтр head_while() и tail_while(), который выделяет единицы (с начала и конца) до первого FALSE.

got_chars %>% 
  head_while("alive") %>% 
  map_chr("name")
## [1] "Theon Greyjoy"     "Tyrion Lannister"  "Victarion Greyjoy"
got_chars %>% 
  tail_while("alive") %>% 
  map_chr("name")
## [1] "Samwell Tarly" "Sansa Stark"

Если все еще не понятно, взгляните на нашу таблицу:

got_chars %>% 
  tibble(name = map_chr(., "name"), # точка обозначает, в какой аргумент должно все пайпиться
         alive = map_lgl(., "alive"))

Операции со списками

Рассмотрим простой пример:

my_l <- list(a = 1:3, b = c("a", "b"), c = list("z", c(8, 9)))
my_l
## $a
## [1] 1 2 3
## 
## $b
## [1] "a" "b"
## 
## $c
## $c[[1]]
## [1] "z"
## 
## $c[[2]]
## [1] 8 9

Как уже говорилось выше, функция unlist() линеаризует списки, превращая их в векторы:

my_l %>% 
  unlist()
##  a1  a2  a3  b1  b2  c1  c2  c3 
## "1" "2" "3" "a" "b" "z" "8" "9"

Получился поименнованный вектор, если этого недостаточно, можно уничтожить и имена при помощи функции unname():

my_l %>% 
  unlist() %>% 
  unname()
## [1] "1" "2" "3" "a" "b" "z" "8" "9"

Функция flatten позволяет уничтожить лишь один, верхний,, уровень иерархии:

my_l %>% 
  flatten()
## [[1]]
## [1] 1
## 
## [[2]]
## [1] 2
## 
## [[3]]
## [1] 3
## 
## [[4]]
## [1] "a"
## 
## [[5]]
## [1] "b"
## 
## [[6]]
## [1] "z"
## 
## [[7]]
## [1] 8 9

Как видно из этого примера, первый элемент списка my_l превратился в три первых элемента списка, а вот подсписок c = list("z", c(8, 9)) превратился в элемент z и вектор c(8, 9).

Существуют также функции append() и prepend(), которые позволяют добавлять новый посписок после (или до) старого:

list(c(1, 2), c("a", "b", "c")) %>% 
  append(list(new = c(TRUE, FALSE)))
## [[1]]
## [1] 1 2
## 
## [[2]]
## [1] "a" "b" "c"
## 
## $new
## [1]  TRUE FALSE
list(c(1, 2), c("a", "b", "c")) %>% 
  prepend(list(new = c(TRUE, FALSE)))
## $new
## [1]  TRUE FALSE
## 
## [[2]]
## [1] 1 2
## 
## [[3]]
## [1] "a" "b" "c"

Существует также функции cross() и cross2(), которые позволяют получить уникальные комбинации объектов из двух списков:

list(c("a", "b"), 1:3) %>% 
  cross() %>% 
  str()
## List of 6
##  $ :List of 2
##   ..$ : chr "a"
##   ..$ : int 1
##  $ :List of 2
##   ..$ : chr "b"
##   ..$ : int 1
##  $ :List of 2
##   ..$ : chr "a"
##   ..$ : int 2
##  $ :List of 2
##   ..$ : chr "b"
##   ..$ : int 2
##  $ :List of 2
##   ..$ : chr "a"
##   ..$ : int 3
##  $ :List of 2
##   ..$ : chr "b"
##   ..$ : int 3
list("a", "b") %>% 
  cross2(1:3) %>% 
  str()
## List of 6
##  $ :List of 2
##   ..$ : chr "a"
##   ..$ : int 1
##  $ :List of 2
##   ..$ : chr "b"
##   ..$ : int 1
##  $ :List of 2
##   ..$ : chr "a"
##   ..$ : int 2
##  $ :List of 2
##   ..$ : chr "b"
##   ..$ : int 2
##  $ :List of 2
##   ..$ : chr "a"
##   ..$ : int 3
##  $ :List of 2
##   ..$ : chr "b"
##   ..$ : int 3

Ребрендинг функции map()

На самом деле все функции map() — являются циклами со следующей структурой:

map(you_object, your_function)

Это значит, что мы можем использовать map()

  • для изменения значений в одном из векторов датафрейма
diamonds %>% 
  mutate(price_rubl = map_dbl(price, function(x){x*62.24}))
# сравните!
# diamonds %>% 
#   mutate(price_rubl = price*62.24)
  • для работы с кучей файлов

В датасетах содержаться информация об объектах, выданных библиотекой Сиэтла 100 и более раз (исходные данные доступны здесь). Датасет состоит из кучи .csv файлов с 5 переменными:

  • id – идентификационный номер объекта
  • type – тип объекта (bk – книга, bknh – другая категория с книгами, cas – кассеты, cd – CD, dvd – DVD, kit – комплект (я сам пока не разобрался что там…), vhs – видеокассеты VHS)
  • name – название
  • n – сколько раз взяли в том или иному году
  • year – год

Данные лежат вот здесь:

Давайте считаем их все:

links <- str_c("https://raw.githubusercontent.com/agricolamz/2019.12.21_EU_purrr/master/data/seattle_public_library_", 2005:2019, ".csv")

df <- map(links, read_csv)
str(df, max.level = 1)
## List of 15
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 3864 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 11197 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 12141 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 15526 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 16821 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 15046 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 13793 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 13091 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 15092 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 14197 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 12313 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 10705 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 10485 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 9593 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )
##  $ :Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 6631 obs. of  5 variables:
##   ..- attr(*, "spec")=
##   .. .. cols(
##   .. ..   id = col_double(),
##   .. ..   type = col_character(),
##   .. ..   name = col_character(),
##   .. ..   n = col_double(),
##   .. ..   year = col_double()
##   .. .. )

Можно даже соединить их в один датафрейм:

df_2 <- map_dfr(links, read_csv)
df_2
df_2 %>%
  group_by(type, name) %>%
  summarise(sum_n = sum(n))  %>%
  ungroup() %>%
  arrange(-sum_n) %>%
  slice(1:100) %>%
  ggplot(aes(fct_reorder(str_trunc(name, 40), sum_n), sum_n, fill = type))+
  geom_col()+
  coord_flip()+
  labs(x = "", y = "",
       title = "Most popular physical item checkouts from Seattle Public Library (2005-2019)",
       caption = "data from https://data.seattle.gov/dataset/Checkouts-by-Title-Physical-Items-/3h5r-qv5w")





© G. Moroz code on GitHub