2  Табличные данные. Анализ датасета

В этом уроке мы научимся работать с “прямоугольными”, т.е. табличными, данными на примере корпуса русской элегии 1815—1835 гг., собранного и опубликованного Антониной Мартыненко в 2020 г.

Существуют два основных “диалекта” R, один из которых опирается главным образом на функции и структуры данных базового R, а другой пользуется синтаксисом tidyverse. Tidyverse – это семейство пакетов (метапакет), разработанных Хадли Уикхемом и др., которое включает в себя в том числе пакеты dplyr, ggplot2 и многие другие.

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   4.0.0     ✔ tibble    3.2.1
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.0.4     
── 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

2.1 Импорт табличных данных

Файл можно скачать вручную по ссылке выше или воспользоваться специальной функцией.

url <- "https://dataverse.pushdom.ru/api/access/datafile/:persistentId?persistentId=doi:10.31860/openlit-2019.11-C001/6EPZFO"

В окружении появится объект url. Это строка, т.е. последовательность символов. Передаем ее в качестве аргумента функции download.file(); вторым аргументом указываем название файла-назначения:

download.file(url, destfile = "elegies.tab")

После этого можно прочитать файл в окружение:

elegies_tbl <- read_tsv("elegies.tab")
Rows: 509 Columns: 20
── Column specification ────────────────────────────────────────────────────────
Delimiter: "\t"
chr (18): Signature, Author, Title, First line, Meter, Razmer, Razmer_wclaus...
dbl  (2): id, Year1

ℹ 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.

2.2 Анализ и обобщение данных

2.2.1 Tibble

Основная структура данных в tidyverse – это tibble, современный вариант датафрейма. Тиббл, как говорят его разработчики, это ленивые и недовольные датафреймы: они делают меньше и жалуются больше. Это позволяет решать проблемы на более ранних этапах, что, как правило, приводит к созданию более чистого и выразительного кода.

Основные отличия от обычного датафрейма:

  • усовершенствованный метод print(), не нужно постоянно вызывать head();
  • нет имен рядов;
  • допускает синтаксически “неправильные” имена столбцов;
  • при индексировании не превращается в вектор.

Преобразуем наш тиббл в датафрейм для сравнения.

elegies_df <- as.data.frame(elegies_tbl)

2.2.2 Dplyr

“Грамматика манипуляции данных”, лежащая в основе dplyr, предоставляет последовательный набор глаголов, которые помогают решать наиболее распространенные задачи манипулирования данными:

  • mutate() добавляет новые переменные, которые являются функциями существующих переменных;
  • select() выбирает переменные (столбцы) на основе их имен;
  • filter() выбирает наблюдения (ряды) на основе их значений;
  • summarise() обобщает значения;
  • arrange() изменяет порядок следования строк.

Все эти глаголы естественным образом сочетаются с функцией group_by(), которая позволяет выполнять любые операции “по группам”, и с оператором pipe |> из пакета magrittr.

В итоге получается лаконичный и читаемый код. Узнаем, за какие года у нас есть элегии.

elegies_tbl |> 
  count(Year) |> 
  print()
# A tibble: 23 × 2
   Year      n
   <chr> <int>
 1 1815      5
 2 1816      8
 3 1817     33
 4 1818     13
 5 1819     15
 6 1820     33
 7 1821     37
 8 1822     29
 9 1823     25
10 1824     31
# ℹ 13 more rows

Отберем элегии 1824 г. и выясним, какие авторы их писали.

elegies_tbl |> 
  filter(Year == 1824) |> # используем логический оператор для выбора
  count(Author) |>  #  можно задать  аргумент sort = TRUE
  arrange(-n)  |>   # не нужно, если sort = TRUE
  print()
# A tibble: 24 × 2
   Author                  n
   <chr>               <int>
 1 Жуковский В.А.          3
 2 Бестужев-Рюмин М.А.     2
 3 Дмитриев М.А.           2
 4 Павлов Н.Ф.             2
 5 Туманский В.И.          2
 6 Языков Н.М.             2
 7 [**]                    1
 8 [-й-]                   1
 9 [2.17]                  1
10 [Без подписи]           1
# ℹ 14 more rows
Задание

Теперь попробуйте сформулировать новые вопросы и ответить на них при помощи этого датасета.

2.3 Импорт текстовых данных

Скачаем архив элегий и распакуем его.

url = "https://dataverse.pushdom.ru/api/access/datafile/:persistentId?persistentId=doi:10.31860/openlit-2019.11-C001/SKGO9Q"

download.file(url, "corpus.zip")
# trying URL 'https://dataverse.pushdom.ru/api/access/datafile/:persistentId?persistentId=doi:10.31860/openlit-2019.11-C001/SKGO9Q'
# Content type 'application/zip; name="elegies_corpus.zip";charset=UTF-8' length 806772 bytes (787 KB)
# ==================================================
# downloaded 787 KB

После выполнения команды ниже в рабочей директории должна появиться папка corpus.

unzip("corpus.zip")

Заглянем в папку и сохраним список файлов.

elegies_files <- list.files("corpus", full.names = TRUE)

Чтобы распечатать пути к первым шести файлам, используйте команду head().

head(elegies_files)
[1] "corpus/1_DGlebov_1818.txt"     "corpus/10_Baratynsky_1820.txt"
[3] "corpus/100_Pushkin_1825.txt"   "corpus/101_Jazykov_1825.txt"  
[5] "corpus/102_Jazykov_1825.txt"   "corpus/103_Jazykov_1825.txt"  

Создадим таблицу со всеми текстами и их id.

Сначала напишем небольшую вспомогательную функцию read_text. Тело функции всегда заключается в фигурные скобки. Эта функция читает построчно каждый файл, а затем “схлопывает” строки в единый вектор через пробел (это делает функция str_c() из пакета stringr). Функция map_chr() из пакета purrr позволяет запустить нашу “доморощенную” функцию 509 раз, по числу файлов (их список отдаем ей первым аргументом). Результат возвращается в виде длинного вектора, который мы превращаем в столбец нового тиббла.

read_text <- function(file){
  read_lines(file) |> 
    str_c(collapse = " ")
  }

elegies_texts <- tibble(
  title = elegies_files, # в столбец title кладем имена файлов
  text = map_chr(elegies_files, read_text) # в столбец text кладем тексты элегий
  )

elegies_texts |> 
  print()
# A tibble: 509 × 2
   title                         text                                           
   <chr>                         <chr>                                          
 1 corpus/1_DGlebov_1818.txt     "Надеясь сердца грусть разлукой облегчить, Беж…
 2 corpus/10_Baratynsky_1820.txt "Мечты волшебные, вы скрылись от очей! Сбылися…
 3 corpus/100_Pushkin_1825.txt   "Когда, любовию и негой упоенный, Безмолвно пр…
 4 corpus/101_Jazykov_1825.txt   "Счастлив, кто с юношеских дней, Живыми чувств…
 5 corpus/102_Jazykov_1825.txt   "Свободен я: уже не трачу Ни дня, ни ночи, ни …
 6 corpus/103_Jazykov_1825.txt   "Я знал живое заблужденье; Любовь певал я - бы…
 7 corpus/104_Jazykov_1825.txt   "Моя Камена ей певала; Но сила взора красоты Н…
 8 corpus/105_Unknown_1826.txt   "Время быстро Пролетает; Пламя жизни Угасает. …
 9 corpus/11_Pushkin_1820.txt    "Погасло дневное светило;  На море синее вечер…
10 corpus/112_Jazykov_1824.txt   "Скажи: воротишься ли ты,  Моя пленительная ра…
# ℹ 499 more rows

Теперь преобразуем столбец title: оставим только id.

elegies_sep <- elegies_texts |> 
  mutate(title = str_remove(title, "corpus/")) |> 
  separate(title, into = c("id", NA)) |>  # отбрасываем все после id 
  mutate(id = as.numeric(id))
Warning: Expected 2 pieces. Additional pieces discarded in 509 rows [1, 2, 3, 4, 5, 6,
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...].
elegies_sep |> 
  print()
# A tibble: 509 × 2
      id text                                                                   
   <dbl> <chr>                                                                  
 1     1 "Надеясь сердца грусть разлукой облегчить, Бежал я милых мест, неверно…
 2    10 "Мечты волшебные, вы скрылись от очей! Сбылися времени угрозы! Хладеет…
 3   100 "Когда, любовию и негой упоенный, Безмолвно пред тобой коленопреклонен…
 4   101 "Счастлив, кто с юношеских дней, Живыми чувствами убогой, Идет просело…
 5   102 "Свободен я: уже не трачу Ни дня, ни ночи, ни стихов За милый взгляд, …
 6   103 "Я знал живое заблужденье; Любовь певал я - были дни: Теперь умчалися …
 7   104 "Моя Камена ей певала; Но сила взора красоты Не мучала, не услаждала М…
 8   105 "Время быстро Пролетает; Пламя жизни Угасает. Нет замены Наслажденьям;…
 9    11 "Погасло дневное светило;  На море синее вечерний пал туман.  Шуми, шу…
10   112 "Скажи: воротишься ли ты,  Моя пленительная радость?  Ужель моя погасн…
# ℹ 499 more rows

2.4 Объединение данных

Теперь мы можем объединить метаданные с конкретными текстами.

Отберем из датасета только Пушкина и Баратынского (Пушкиных там двое, так что указываем инициалы. Вертикальная черта - это логичеческий оператор “ИЛИ”. Функция str_detect() возвращает логический вектор, который используется для фильтрации.

elegies_selection <- elegies_tbl |> 
  filter(str_detect(Author, "Баратынский |Пушкин А.С.")) |> 
  rename(First_line = `First line`) |>  # убираем пробел из названия столбца
  select(id, Author, Year, Source_name, Title, First_line)

После этого объединяем два тиббла:

elegies_joined <- elegies_selection |> 
  left_join(elegies_sep)
Joining with `by = join_by(id)`

2.5 Токенизация

Разделим тексты на токены. Для этого надо установить библиотеку tidytext.

library(tidytext)
elegies_tokens <- elegies_joined |> 
  unnest_tokens(output = "word", input = "text")

Слова можно лемматизировать, подробнее об этом см. здесь.

2.6 Удаление стоп-слов

Удалим самые частотные слова. Для этого сначала сохраним их список. Подробнее см. здесь.

library(stopwords)
sw <- stopwords("ru")
sw
  [1] "и"       "в"       "во"      "не"      "что"     "он"      "на"     
  [8] "я"       "с"       "со"      "как"     "а"       "то"      "все"    
 [15] "она"     "так"     "его"     "но"      "да"      "ты"      "к"      
 [22] "у"       "же"      "вы"      "за"      "бы"      "по"      "только" 
 [29] "ее"      "мне"     "было"    "вот"     "от"      "меня"    "еще"    
 [36] "нет"     "о"       "из"      "ему"     "теперь"  "когда"   "даже"   
 [43] "ну"      "вдруг"   "ли"      "если"    "уже"     "или"     "ни"     
 [50] "быть"    "был"     "него"    "до"      "вас"     "нибудь"  "опять"  
 [57] "уж"      "вам"     "сказал"  "ведь"    "там"     "потом"   "себя"   
 [64] "ничего"  "ей"      "может"   "они"     "тут"     "где"     "есть"   
 [71] "надо"    "ней"     "для"     "мы"      "тебя"    "их"      "чем"    
 [78] "была"    "сам"     "чтоб"    "без"     "будто"   "человек" "чего"   
 [85] "раз"     "тоже"    "себе"    "под"     "жизнь"   "будет"   "ж"      
 [92] "тогда"   "кто"     "этот"    "говорил" "того"    "потому"  "этого"  
 [99] "какой"   "совсем"  "ним"     "здесь"   "этом"    "один"    "почти"  
[106] "мой"     "тем"     "чтобы"   "нее"     "кажется" "сейчас"  "были"   
[113] "куда"    "зачем"   "сказать" "всех"    "никогда" "сегодня" "можно"  
[120] "при"     "наконец" "два"     "об"      "другой"  "хоть"    "после"  
[127] "над"     "больше"  "тот"     "через"   "эти"     "нас"     "про"    
[134] "всего"   "них"     "какая"   "много"   "разве"   "сказала" "три"    
[141] "эту"     "моя"     "впрочем" "хорошо"  "свою"    "этой"    "перед"  
[148] "иногда"  "лучше"   "чуть"    "том"     "нельзя"  "такой"   "им"     
[155] "более"   "всегда"  "конечно" "всю"     "между"  
elegies_clean <- elegies_tokens |> 
  filter(!word %in% sw)

elegies_clean |> 
  print()
# A tibble: 5,993 × 7
      id Author           Year  Source_name   Title First_line             word 
   <dbl> <chr>            <chr> <chr>         <chr> <chr>                  <chr>
 1    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… мечты
 2    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… волш…
 3    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… скры…
 4    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… очей 
 5    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… сбыл…
 6    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… врем…
 7    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… угро…
 8    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… хлад…
 9    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… серд…
10    10 Баратынский Е.А. 1820  Соревнователь Весна Мечты волшебные, вы с… юнос…
# ℹ 5,983 more rows

2.7 Подсчет частотностей

Узнаем, сколько всего слов приходится на каждого автора в корпусе.

elegies_clean |> 
  group_by(Author) |> 
  summarise(n = n()) |> 
  print()
# A tibble: 3 × 2
  Author               n
  <chr>            <int>
1 Баратынский Е.А   2066
2 Баратынский Е.А.  1538
3 Пушкин А.С.       2389

Упс! У нас два Баратынских. Исправим:

elegies_clean <- elegies_clean |> 
  mutate(Author = case_when(str_detect(Author, "Баратынский") ~ "Баратынский Е.А.",
                   .default = Author))

Снова проверим.

elegies_clean |> 
  group_by(Author) |> 
  summarise(n = n()) |> 
  print()
# A tibble: 2 × 2
  Author               n
  <chr>            <int>
1 Баратынский Е.А.  3604
2 Пушкин А.С.       2389

Найдем самые частотные слова у Пушкина и Баратынского. Обратите внимание, что результат вычислений не сохраняется.

elegies_clean |> 
  count(word, sort = TRUE) |> 
  print()
# A tibble: 3,620 × 2
   word       n
   <chr>  <int>
 1 моей      46
 2 любви     41
 3 твой      32
 4 ль        22
 5 любовь    22
 6 друг      21
 7 мечты     21
 8 мной      20
 9 вновь     18
10 душе      17
# ℹ 3,610 more rows

Так мы получили абсолютные значения. Чтобы посчитать долю, немного изменим код:

top_words <- elegies_clean |> 
  group_by(Author) |> 
  count(word, sort = TRUE) |> 
  mutate(perc = (n / sum(n)) * 100) |> 
  arrange(-perc) |> 
  slice_max(n = 10, order_by = perc)

top_words |> 
  print()
# A tibble: 21 × 4
# Groups:   Author [2]
   Author           word          n  perc
   <chr>            <chr>     <int> <dbl>
 1 Баратынский Е.А. моей         31 0.860
 2 Баратынский Е.А. любви        17 0.472
 3 Баратынский Е.А. твой         17 0.472
 4 Баратынский Е.А. душе         14 0.388
 5 Баратынский Е.А. друг         13 0.361
 6 Баратынский Е.А. дней         12 0.333
 7 Баратынский Е.А. ль           12 0.333
 8 Баратынский Е.А. душой        11 0.305
 9 Баратынский Е.А. мечты        11 0.305
10 Баратынский Е.А. последний    11 0.305
# ℹ 11 more rows

2.8 Визуализации

В tidyverse входит пакет ggplot2 для визуализации данных. В его основе лежит идея “грамматики графических элементов” Лиланда Уилкинсона (Мастицкий 2017) (отсюда “gg” в названии).

Функция ggplot() имеет два основных аргумента: data и mapping. Аргумент mapping задает эстетические атрибуты геометрических объектов. Обычно используется в виде mapping = aes(x, y), где aes() означает aesthetics.

Под “эстетикой” подразумеваются графические атрибуты, такие как размер, форма или цвет. Вы не увидите их на графике, пока не добавите какие-нибудь “геомы” – геометрические объекты (точки, линии, столбики и т.п.). Эти объекты могут слоями накладываться друг на друга (Wickham и Grolemund 2016).

Мы построим столбиковую диаграмму.

top_words |> 
  ggplot(aes(word, perc, fill = Author)) +
  geom_col() +
  facet_wrap(~Author, scales="free") +
  coord_flip() +
  theme_bw()

Каждый геометрический объект может иметь свои специфические параметры. Например, geom_point() может варьировать размер, цвет, форму и прозрачность точек, а geom_line() — тип, толщину и цвет линии. Эти параметры можно задавать как внутри aes() (когда они зависят от данных), так и вне её (когда задаются константы).

2.9 Видео к этому уроку

  • Видео 2025 г.

2.10 Домашнее задание

  • В этом задании вы исследуете корпус русской песни. Источник.

  • Оценка 0-10. Выполнять все пункты задания не обязательно: на оценку 9-10 требования очень высокие (так и задумано).

  • Сам корпус и заготовку для кода вы найдете, приняв задание по ссылке: https://classroom.github.com/a/zlrC_zDL. Если вы не видите себя в списке студентов, напишите преподавателю.

  • После этого GitHub создаст репозиторий для сдачи домашнего задания. Клонируйте его (т.е. создайте локальный проект под контролем версий, как мы делали в первом уроке).

  • Внесите необходимые изменения в файл hw2.R, после чего закоммитьте изменения и сделайте push. Если не получится, используйте кнопку Upload files.

Критерии оценивания:


  • Весь код должен запускаться без ошибок (в противном случае – минус 4 балла).

  • В случае выявления плагиата всем соучастникам ставится оценка 0 без возможности пересдачи.

  • После дедлайна (3 октября 21-00 мск) задание не принимается.

Wickham, Hadley, и Garrett Grolemund. 2016. R for Data Science. O’Reilly. https://r4ds.had.co.nz/index.html.
Мастицкий, Сергей. 2017. Визуализация данных с помощью ggplot2. ДМК.