2  Таблицы

В этом уроке речь пойдет о прямоугольных данных, т.е. таких данных, которые имеют форму таблицы. Для начала вспомним, что мы уже знаем о векторах и списках: вектор может хранить данные одного типа (например, числовые или строки), а список – разного типа. Двумерным аналогом вектора в R является матрица, а двумерным аналогом списка – датафрейм.

2.1 Матрицы

Матрица – это вектор, который имеет два дополнительных атрибута: количество строк и количество столбцов. Из этого следует, что матрица, как и вектор, может хранить данные одного типа. Проверим.

M = matrix(c(1, 2, 3, 4), nrow = 2)
M # все ок
     [,1] [,2]
[1,]    1    3
[2,]    2    4
M = matrix(c(1, 2, 3, "a"), nrow = 2)
M # все превратилось в строку! 
     [,1] [,2]
[1,] "1"  "3" 
[2,] "2"  "a" 

В матрице есть ряды и столбцы. Их количество определяет размер (порядок) матрицы. Выше мы создали матрицу 2 x 2. Элементы матрицы, как и элементы вектора, можно извлекать по индексу. Сначала указывается номер ряда (строки), потом номер столбца.

M = matrix(c(1, 2, 3, 4), nrow = 2)
M
     [,1] [,2]
[1,]    1    3
[2,]    2    4
M[1, ] # первая строка полностью
[1] 1 3
M[,2] # второй столбец полностью
[1] 3 4
M[1,1] # одно значение
[1] 1

Обратите внимание, как меняется размерность при индексировании.

M = matrix(c(1, 2, 3, 4), nrow = 2)
dim(M) # функция для извлечения измерений
[1] 2 2
dim(M[1, ]) 
NULL

Попытка узнать измерения вектора возвращает NULL, потому что, с точки зрения R, векторы не являются матрицами из одного столбца или одной строки и потому не имеют измерений.

В этом уроке мы не будем много работать с матрицами, но полезно помнить, что они существуют: матрицы и алгебраические операции с ними задействованы при латентно-семантическом анализе и построении эмбеддингов (см. ниже).

2.2 Таблицы (датафреймы)

Если матрица – это двумерный аналог вектора, то таблица (кадр данных, data frame) – это двумерный аналог списка. Как и список, датафрейм может хранить данные разного типа.

# создание датафрейма
df <- data.frame(names = c("John", "Mary"), age = c(18, 25), sport = c("basketball", "tennis"))
df

Извлечение данных тоже напоминает работу со списком.

df$names # забирает весь столбец
[1] "John" "Mary"
df[,"names"] # то же самое, другой способ
[1] "John" "Mary"
df[1, ] # забирает ряд

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

В этом уроке мы будем работать с датасетом из Репозитория открытых данных по русской литературе и фольклору под названием “Программы по литературе для средней школы с 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"

2.4 Работа с датафреймами

# узнать имена столбцов
colnames(curricula_df) 
[1] "author"     "title"      "comment"    "curriculum" "id"        
[6] "year"       "grade"      "priority"  
# извлечь ряд(ы) по значению
curricula_df[curricula_df$year == "1919", ]
# извлечь столбец 
curricula_df$year |> head()
[1] "1919" "1919" "1919" "1919" "1919" "1919"
curricula_df[ , "year"] |> head()
[1] "1919" "1919" "1919" "1919" "1919" "1919"
curricula_df[ , 6] |>  head()
[1] "1919" "1919" "1919" "1919" "1919" "1919"
# узнать тип данных в столбцах
str(curricula_df) 
'data.frame':   10306 obs. of  8 variables:
 $ author    : chr  "Андреев Л.Н." "Андреев Л.Н." "Андреев Л.Н." "Бальмонт К.Д." ...
 $ title     : chr  "Жили-были" "Иуда" "Рассказ о семи повешенных" "" ...
 $ comment   : chr  "" "" "" "" ...
 $ curriculum: chr  "19 ИРЛ 2 ст" "19 ИРЛ 2 ст" "19 ИРЛ 2 ст" "19 ИРЛ 2 ст" ...
 $ id        : int  1 1 1 1 1 1 1 1 1 1 ...
 $ year      : chr  "1919" "1919" "1919" "1919" ...
 $ grade     : int  9 9 9 9 9 8 8 8 8 8 ...
 $ priority  : chr  "" "" "*" "*" ...
# преобразовать тип данных в столбцах
curricula_df$year <- as.numeric(curricula_df$year)
# вывести сводку
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   Min.   :1919   Min.   : 5.000   Length:10306      
 1st Qu.:13.00   1st Qu.:1946   1st Qu.: 8.000   Class :character  
 Median :31.00   Median :1966   Median :10.000   Mode  :character  
 Mean   :28.01   Mean   :1963   Mean   : 9.195                     
 3rd Qu.:42.00   3rd Qu.:1981   3rd Qu.:10.000                     
 Max.   :50.00   Max.   :1991   Max.   :11.000                     
                 NA's   :12                                        

Небольшое упражнение на кодинг позволит закрепить навыки работы с матрицами и датафреймами.

Задание

Запустите swirl() и пройдите урок 7 Matrices and Data Frames.

Все ли вы запомнили?

Вопрос

Для чего нужна функция cbind()?





Вопрос

Функция colnames() позволяет как назначать новые имена таблице, так и извлекать существующие.

Задание

Практическое задание “Испанские писатели”.

# устанавливаем и загружаем нужный пакет
install.packages("languageR")
library(languageR)

# загружаем датасет
meta <- spanishMeta

# допишите ваш код ниже
# посчитайте средний год публикации романов Камило Хосе Селы


# вычислите суммарное число слов в романах Эдуардо Мендосы


# извлеките ряды с текстами, опубликованными до 1980 г.

2.5 Tibble

Существуют два основных “диалекта” 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.1     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.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

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

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

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

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

curricula_tbl <- as_tibble(curricula_df)

Сравним поведение датафрейма и тиббла.

# индексирование 
curricula_df[, 1] |> class()
[1] "character"
curricula_tbl[,1]  |> class()
[1] "tbl_df"     "tbl"        "data.frame"

Пора тренироваться.

Задание

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

Время вопросов! Обычный датафрейм или тиббл?

Вопрос

По умолчанию распечатывает только первые 10 рядов в консоль.



Вопрос

Молчаливо исправляет некорректные названия столбцов.



Вопрос

Не имеет названий рядов.



Кстати, обратили внимание, как работает оператор <= с символьным вектором?

2.6 Dplyr

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

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

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

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

curricula_tbl |> 
  count(curriculum, year) 

Отберем две программы для 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.

Правда или ложь?

Вопрос

Функция n_distinct() возвращает все уникальные значения.



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

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

— Hadley Wickham

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

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

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

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


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

data("table2")
table2
data("table3")
table3
data("table4a")
table4a
data("table4b")
table4b

Важные функции для преобразования данных из пакета 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.

Правда или ложь?

Вопрос

Функция separate() обязательно требует указать разделитель.

Вопрос

Принципы опрятных данных требуют, чтобы одному наблюдению соответствовал один столбец.

Вопрос

Функция contains() используется вместе с filter() для выбора рядов.


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

Задание

Практическое задание “Библиотека Gutenberg”

devtools::install_github("ropensci/gutenbergr")
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 |> 
  # ваш код здесь

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

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

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

curricula_tbl |> 
  group_by(author, title) |> 
  summarise(n = n()) |> 
  arrange(-n)

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

curricula_tbl |> 
  count(author, title) |> 
  arrange(-n)

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

curricula_tbl |> 
  group_by(year) |> 
  distinct(author, title) |> 
  summarise(n = n()) |> 
  arrange(-n)

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