2  Таблицы. Опрятные данные

2.1 Tidyveryse

Существуют два основных “диалекта” 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   3.5.2     ✔ 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.2 Импорт табличных данных

В этом уроке мы будем работать с датасетом из Репозитория открытых данных по русской литературе и фольклору под названием “Программы по литературе для средней школы с 1919 по 1991 гг.” Этот датасет был использован при подготовке интерактивной карты российского школьного литературного канона (1852-2023). Карта была представлена в 2023 г. Лабораторией проектирования содержания образования ВШЭ. Подробнее о проекте можно посмотреть материал “Системного блока”.

Основная функция для скачивания файлов из Сети – download.file(), которой необходимо задать в качестве аргументов url, название сохраняемого файла, иногда также метод.

url <- "https://dataverse.pushdom.ru/api/access/datafile/4229"

# скачивание в папку files в родительской директории
download.file(url, destfile = "../files/curricula.tsv") 

Основные функции для чтения табличных данных в базовом R - это read.table() и read.csv(). Они вернут датафрейм.

Файл, который мы скачали, имеет расширение .tsv (tab separated values). Чтобы его прочитать, используем read.table(), указав тип разделителя:

curricula_df <- read.table("../files/curricula.tsv", sep = "\t", header = TRUE)

curricula_df

Функция read.csv() отличается лишь тем, что автоматически выставляет значения аргументов sep = ",", header = TRUE.

Функция class() позволяет убедиться, что перед нами датафрейм.

class(curricula_df)
[1] "data.frame"

Обратите внимание, как ведет себя датафрейм при индексации.

curricula_df[,1] |> 
  class()
[1] "character"
# вывести сводку
summary(curricula_df)
    author             title             comment           curriculum       
 Length:10306       Length:10306       Length:10306       Length:10306      
 Class :character   Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character  
                                                                            
                                                                            
                                                                            
       id            year               grade          priority        
 Min.   : 1.00   Length:10306       Min.   : 5.000   Length:10306      
 1st Qu.:13.00   Class :character   1st Qu.: 8.000   Class :character  
 Median :31.00   Mode  :character   Median :10.000   Mode  :character  
 Mean   :28.01                      Mean   : 9.195                     
 3rd Qu.:42.00                      3rd Qu.:10.000                     
 Max.   :50.00                      Max.   :11.000                     

2.3 Tibble

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

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

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

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

curricula_tbl <- as_tibble(curricula_df)

Чтобы избежать подобных преобразований, можно воспользоваться пакетом для импорта данных readr:

curricula_tbl <- read_tsv("../files/curricula.tsv")
Rows: 10306 Columns: 8
── Column specification ────────────────────────────────────────────────────────
Delimiter: "\t"
chr (6): author, title, comment, curriculum, year, priority
dbl (2): id, grade

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

Воспользуйтесь любым способом, чтобы получить тиббл.

Задание

Установите курс swirl::install_course("Getting and Cleaning Data"). Загрузите библиотеку library(swirl), запустите swirl(), выберите этот курс и пройдите из него урок 1 Manipulating Data with dplyr. При попытке загрузить урок 1 вы можете получить сообщение об ошибке. В таком случае установите версию курса из github, как указано здесь, или загрузите файл вручную, как указано здесь.

2.4 Dplyr

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

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

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

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

curricula_tbl |> 
  count(curriculum, year) |> 
  print()
# A tibble: 52 × 3
   curriculum    year        n
   <chr>         <chr>   <int>
 1 19 ИРЛ 2 ст   1919      160
 2 22 РЛ 2 ст    1922      191
 3 32 ФЗС 5-7    1932      122
 4 33 ПСШ 8-10   1933      213
 5 33 ПСШ ГС 5-8 1933      129
 6 34 ПСШ 8-10   1934      216
 7 37-38 ЛИТ 7   1937-38    12
 8 38 ЛИТ        1938      107
 9 39 ПСШ 8-10   1933        1
10 39 ПСШ 8-10   1939      375
# ℹ 42 more rows

Отберем две программы для 8 класса и выясним, какие авторы в них представлены лучше всего.

curricula_tbl |> 
  filter(year %in% c(1919, 1922), grade == 8) |> 
  count(author, year) |> 
  arrange(-n)

Теперь упражнения в swirl. Вам придется редактировать код, который предложит программа, так что сгруппируйтесь.

Задание

Запустите swirl(), выберите курс Getting and Cleaning Data и пройдите из него урок 2 Grouping and Chaining with dplyr.

2.5 Опрятные данные

Tidy datasets are all alike, but every messy dataset is messy in its own way.

— Hadley Wickham

Tidyverse – это не только особый синтаксис, но и отдельная идеология “опрятных данных”. “Сырые” данные, с которыми мы работаем, редко бывают опрятны, и перед анализом их следует “почистить” и преобразовать.

Основные принципы опрятных данных:

  • отдельный столбец для каждой переменной;
  • отдельный ряд для каждого наблюдения;
  • у каждого значения отдельная ячейка;
  • один датасет – одна таблица.

Принципы опрятных данных. Источник


Посмотрите на учебные тибблы из пакета tidyr и подумайте, какое из этих правил нарушено в каждом случае.

data("table2")
table2 |> 
  print()
# A tibble: 12 × 4
   country      year type            count
   <chr>       <dbl> <chr>           <dbl>
 1 Afghanistan  1999 cases             745
 2 Afghanistan  1999 population   19987071
 3 Afghanistan  2000 cases            2666
 4 Afghanistan  2000 population   20595360
 5 Brazil       1999 cases           37737
 6 Brazil       1999 population  172006362
 7 Brazil       2000 cases           80488
 8 Brazil       2000 population  174504898
 9 China        1999 cases          212258
10 China        1999 population 1272915272
11 China        2000 cases          213766
12 China        2000 population 1280428583
data("table3")
table3 |> 
  print()
# A tibble: 6 × 3
  country      year rate             
  <chr>       <dbl> <chr>            
1 Afghanistan  1999 745/19987071     
2 Afghanistan  2000 2666/20595360    
3 Brazil       1999 37737/172006362  
4 Brazil       2000 80488/174504898  
5 China        1999 212258/1272915272
6 China        2000 213766/1280428583
data("table4a")
table4a |> 
  print()
# A tibble: 3 × 3
  country     `1999` `2000`
  <chr>        <dbl>  <dbl>
1 Afghanistan    745   2666
2 Brazil       37737  80488
3 China       212258 213766
data("table4b")
table4b |> 
  print()
# A tibble: 3 × 3
  country         `1999`     `2000`
  <chr>            <dbl>      <dbl>
1 Afghanistan   19987071   20595360
2 Brazil       172006362  174504898
3 China       1272915272 1280428583

Важные функции для преобразования данных из пакета tidyr:

  • separate() делит один столбец на новые;
  • unite() объединяет столбцы;
  • pivot_longer() удлиняет таблицу;
  • pivot_wider() расширяет таблицу;
  • drop_na() и replace_na() указывают, что делать с NA и др.

Кроме того, в dplyr есть полезное семейство функций _join, позволяющих объединять данные в различных таблицах.

Дальше мы потренируемся с ними работать, но сначала пройдем урок swirl. Это достаточно сложный урок (снова понадобится редактировать скрипт), но он нам дальше здорово поможет.

Задание

Запустите swirl(), выберите курс Getting and Cleaning Data и пройдите из него урок 3 Tidying Data with tidyr.

Прежде чем двигаться дальше, приведите в порядок table2, 3, 4a-4b, используя dplyr и tidyr.

2.6 Обобщение данных

Теперь вернемся к датасету curricula и попробуем частично воспроизвести результаты, полученные авторами проекта “Список чтения”, упомянутого выше.

У каких авторов больше всего произведений (во всех программах)?

curricula_tbl |> 
  group_by(author, title) |> 
  summarise(n = n()) |> 
  arrange(-n) |> 
  print()
# A tibble: 1,624 × 3
# Groups:   author [461]
   author          title                        n
   <chr>           <chr>                    <int>
 1 Горький М.      Мать                        51
 2 Некрасов Н.А.   Кому на Руси жить хорошо    50
 3 Пушкин А.С.     Евгений Онегин              49
 4 Островский А.Н. Гроза                       48
 5 Тургенев И.С.   Отцы и дети                 48
 6 Гоголь Н.В.     Мертвые души                47
 7 Грибоедов А.С.  Горе от ума                 47
 8 Лермонтов М.Ю.  Герой нашего времени        46
 9 Толстой Л.Н.    Война и мир                 46
10 Толстой Л.Н.    Воскресение                 46
# ℹ 1,614 more rows

Какие произведения упоминаются в программах чаще всего?

curricula_tbl |> 
  count(author, title) |> 
  arrange(-n) |> 
  print()
# A tibble: 1,624 × 3
   author          title                        n
   <chr>           <chr>                    <int>
 1 Горький М.      Мать                        51
 2 Некрасов Н.А.   Кому на Руси жить хорошо    50
 3 Пушкин А.С.     Евгений Онегин              49
 4 Островский А.Н. Гроза                       48
 5 Тургенев И.С.   Отцы и дети                 48
 6 Гоголь Н.В.     Мертвые души                47
 7 Грибоедов А.С.  Горе от ума                 47
 8 Лермонтов М.Ю.  Герой нашего времени        46
 9 Толстой Л.Н.    Война и мир                 46
10 Толстой Л.Н.    Воскресение                 46
# ℹ 1,614 more rows

На принятые в каких годах программы приходится больше всего произведений? (Объяснение здесь.)

curricula_tbl |> 
  group_by(year) |> 
  distinct(author, title) |> 
  summarise(n = n()) |> 
  arrange(-n) |> 
  print()
# A tibble: 49 × 2
   year      n
   <chr> <int>
 1 1991    518
 2 1940    372
 3 1939    371
 4 1933    288
 5 1982    282
 6 1984    282
 7 1983    281
 8 1985    278
 9 1986    278
10 1987    278
# ℹ 39 more rows

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

2.7 Слияние таблиц

Файл с библиографией забираем с сайта, как было показано выше.

bib_tbl <- read_tsv("../files/bibliography.tsv")

Используем уже знакомые функции для объединения данных.

curricula_joined <- curricula_tbl |> 
  select(author, title, year, grade, curriculum) |> 
  left_join(bib_tbl, join_by(curriculum == abbreviation))

curricula_joined |> 
  print()
# A tibble: 10,306 × 7
   author        title                  year  grade curriculum    id description
   <chr>         <chr>                  <chr> <dbl> <chr>      <dbl> <chr>      
 1 Андреев Л.Н.  Жили-были              1919      9 19 ИРЛ 2 …     1 Примерная …
 2 Андреев Л.Н.  Иуда                   1919      9 19 ИРЛ 2 …     1 Примерная …
 3 Андреев Л.Н.  Рассказ о семи повеше… 1919      9 19 ИРЛ 2 …     1 Примерная …
 4 Бальмонт К.Д. <NA>                   1919      9 19 ИРЛ 2 …     1 Примерная …
 5 Брюсов В.Я.   <NA>                   1919      9 19 ИРЛ 2 …     1 Примерная …
 6 Герцен А.И.   Кто виноват?           1919      8 19 ИРЛ 2 …     1 Примерная …
 7 Гоголь Н.В.   Вечера на Хуторе близ… 1919      8 19 ИРЛ 2 …     1 Примерная …
 8 Гоголь Н.В.   Вий                    1919      8 19 ИРЛ 2 …     1 Примерная …
 9 Гоголь Н.В.   Женитьба               1919      8 19 ИРЛ 2 …     1 Примерная …
10 Гоголь Н.В.   Мертвые души           1919      8 19 ИРЛ 2 …     1 Примерная …
# ℹ 10,296 more rows

2.8 Практическое задание

  1. Выполните практическое задание из предыдущего урока, используя функции из библиотеки tidyverse.

  2. Скачайте и исследуйте датасет с корпусом русских элегий 1815-1835 г.

2.9 Видео к уроку

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

  • Создайте учетную запись GitHub, если у вас ее нет: https://github.com/

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

  • После этого GitHub создаст репозиторий для сдачи домашнего задания.

  • Создайте на своем компьютере файл hw1.R, отредактируйте его, используя заготовку ниже.

library(gutenbergr)
library(dplyr)
library(tidyr)

works <- gutenberg_works()

# Отберите ряды, в которых gutenberg_author_id равен 65;
# после этого выберите два столбца: author, title
my_data <- works |> 
  # ваш код здесь
  
# Загрузите данные об авторах и выберите столбцы: author, deathdate
authors <- gutenberg_authors |> 
  # ваш код здесь

# Соедините my_data с данными о смерти автора из authors, 
# так чтобы к my_data добавился новый столбец. 
# После этого используйте функцию separate, 
# чтобы разделить столбец с именем и фамилией на два новых: author, name. 
# Удалите столбец с именем автора, оставив только фамилию.
# Добавьте новый столбец century, 
# используя функцию mutate и данные из столбца deathdate. 
# Используйте оператор pipe, не сохраняйте промежуточные результаты!

my_data |> 
  # ваш код здесь
  • После этого любым способом загрузите свой файл в репозиторий. Если умеете в git, хорошо. Если нет, используйте кнопку Upload files.

  • Не меняйте имена переменных! Проверка будет автоматической.

  • Для пересчета года в век используйте следующую формулу: (deathdate - 1) %/% 100 + 1, где %/% – целочисленное деление.

  • Дедлайн: 25/09/2025 21:00.

  • Оценка: 0/1.