= matrix(c(1, 2, 3, 4), nrow = 2)
M # все ок M
[,1] [,2]
[1,] 1 3
[2,] 2 4
= matrix(c(1, 2, 3, "a"), nrow = 2)
M # все превратилось в строку! M
[,1] [,2]
[1,] "1" "3"
[2,] "2" "a"
В этом уроке речь пойдет о прямоугольных данных, т.е. таких данных, которые имеют форму таблицы. Для начала вспомним, что мы уже знаем о векторах и списках: вектор может хранить данные одного типа (например, числовые или строки), а список – разного типа. Двумерным аналогом вектора в R является матрица, а двумерным аналогом списка – датафрейм.
Матрица – это вектор, который имеет два дополнительных атрибута: количество строк и количество столбцов. Из этого следует, что матрица, как и вектор, может хранить данные одного типа. Проверим.
= matrix(c(1, 2, 3, 4), nrow = 2)
M # все ок M
[,1] [,2]
[1,] 1 3
[2,] 2 4
= matrix(c(1, 2, 3, "a"), nrow = 2)
M # все превратилось в строку! M
[,1] [,2]
[1,] "1" "3"
[2,] "2" "a"
В матрице есть ряды и столбцы. Их количество определяет размер (порядок) матрицы. Выше мы создали матрицу 2 x 2. Элементы матрицы, как и элементы вектора, можно извлекать по индексу. Сначала указывается номер ряда (строки), потом номер столбца.
= matrix(c(1, 2, 3, 4), nrow = 2)
M M
[,1] [,2]
[1,] 1 3
[2,] 2 4
1, ] # первая строка полностью M[
[1] 1 3
2] # второй столбец полностью M[,
[1] 3 4
1,1] # одно значение M[
[1] 1
Обратите внимание, как меняется размерность при индексировании.
= matrix(c(1, 2, 3, 4), nrow = 2)
M dim(M) # функция для извлечения измерений
[1] 2 2
dim(M[1, ])
NULL
Попытка узнать измерения вектора возвращает NULL
, потому что, с точки зрения R, векторы не являются матрицами из одного столбца или одной строки и потому не имеют измерений.
В этом уроке мы не будем много работать с матрицами, но полезно помнить, что они существуют: матрицы и алгебраические операции с ними задействованы при латентно-семантическом анализе и построении эмбеддингов (см. ниже).
Если матрица – это двумерный аналог вектора, то таблица (кадр данных, data frame) – это двумерный аналог списка. Как и список, датафрейм может хранить данные разного типа.
# создание датафрейма
<- data.frame(names = c("John", "Mary"), age = c(18, 25), sport = c("basketball", "tennis"))
df df
Извлечение данных тоже напоминает работу со списком.
$names # забирает весь столбец df
[1] "John" "Mary"
"names"] # то же самое, другой способ df[,
[1] "John" "Mary"
1, ] # забирает ряд df[
В этом уроке мы будем работать с датасетом из Репозитория открытых данных по русской литературе и фольклору под названием “Программы по литературе для средней школы с 1919 по 1991 гг.” Этот датасет был использован при подготовке интерактивной карты российского школьного литературного канона (1852-2023). Карта была представлена в 2023 г. Лабораторией проектирования содержания образования ВШЭ. Подробнее о проекте можно посмотреть материал “Системного блока”.
Основная функция для скачивания файлов из Сети – download.file()
, которой необходимо задать в качестве аргументов url, название сохраняемого файла, иногда также метод.
<- "https://dataverse.pushdom.ru/api/access/datafile/4229"
url
# скачивание в папку files в родительской директории
download.file(url, destfile = "../files/curricula.tsv")
Основные функции для чтения табличных данных в базовом R - это read.table()
и read.csv()
. Файл, который мы скачали, имеет расширение .tsv
(tab separated values). Чтобы его прочитать, используем read.table()
, указав тип разделителя:
<- read.table("../files/curricula.tsv", sep = "\t", header = TRUE)
curricula_df
curricula_df
Функция read.csv()
отличается лишь тем, что автоматически выставляет значения аргументов sep = ","
, header = TRUE.
Функция class()
позволяет убедиться, что перед нами датафрейм.
class(curricula_df)
[1] "data.frame"
# узнать имена столбцов
colnames(curricula_df)
[1] "author" "title" "comment" "curriculum" "id"
[6] "year" "grade" "priority"
# извлечь ряд(ы) по значению
$year == "1919", ] curricula_df[curricula_df
# извлечь столбец
$year |> head() curricula_df
[1] "1919" "1919" "1919" "1919" "1919" "1919"
"year"] |> head() curricula_df[ ,
[1] "1919" "1919" "1919" "1919" "1919" "1919"
6] |> head() curricula_df[ ,
[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 "" "" "*" "*" ...
# преобразовать тип данных в столбцах
$year <- as.numeric(curricula_df$year) curricula_df
# вывести сводку
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
Небольшое упражнение на кодинг позволит закрепить навыки работы с матрицами и датафреймами.
Все ли вы запомнили?
# устанавливаем и загружаем нужный пакет
install.packages("languageR")
library(languageR)
# загружаем датасет
<- spanishMeta
meta
# допишите ваш код ниже
# посчитайте средний год публикации романов Камило Хосе Селы
# вычислите суммарное число слов в романах Эдуардо Мендосы
# извлеките ряды с текстами, опубликованными до 1980 г.
Существуют два основных “диалекта” 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()
;Преобразуем наш датафрейм в тиббл для удобства работы с ним.
<- as_tibble(curricula_df) curricula_tbl
Сравним поведение датафрейма и тиббла.
# индексирование
1] |> class() curricula_df[,
[1] "character"
1] |> class() curricula_tbl[,
[1] "tbl_df" "tbl" "data.frame"
Пора тренироваться.
Время вопросов! Обычный датафрейм или тиббл?
Кстати, обратили внимание, как работает оператор <=
с символьным вектором?
В уроке 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
. Вам придется редактировать код, который предложит программа, так что сгруппируйтесь.
Правда или ложь?
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
. Это достаточно сложный урок (снова понадобится редактировать скрипт), но он нам дальше здорово поможет.
Правда или ложь?
Отличная работа! Прежде чем двигаться дальше, приведите в порядок table2, 3, 4a-4b, используя dplyr
и tidyr
.
::install_github("ropensci/gutenbergr")
devtoolslibrary(gutenbergr)
library(dplyr)
library(tidyr)
<- gutenberg_works()
works
# Отберите ряды, в которых gutenberg_author_id равен 65;
# после этого выберите два столбца: author, title
<- works |>
my_data # ваш код здесь
# Загрузите данные об авторах и выберите столбцы: author, deathdate
<- gutenberg_authors |>
authors # ваш код здесь
# Соедините my_data с данными о смерти автора из authors,
# так чтобы к my_data добавился новый столбец.
# После этого используйте функцию separate,
# чтобы разделить столбец с именем и фамилией на два новых: author, name.
# Удалите столбец с именем автора, оставив только фамилию.
# Добавьте новый столбец century,
# используя функцию mutate и данные из столбца deathdate.
# Используйте оператор pipe, не сохраняйте промежуточные результаты!
|>
my_data # ваш код здесь
Теперь вернемся к датасету 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)
В заключение попробуйте сформулировать новые вопросы и ответить на них при помощи этого датасета.