9 Работа с текстами: gutenbergr
, tidytext
, stopwords
, udpipe
library(tidyverse)
9.1 Загрузка текста в R
В пакете readr
(входит в tidyverse
) для чтения текста есть функция read_lines()
. В качестве первой переменной может выступать путь к файлу на компьютере или интернет ссылка:
read_lines("https://raw.githubusercontent.com/agricolamz/2020_HSE_DPO/master/data/Chang.txt")
t <-head(t)
[1] "Тед Чан"
[2] "История твоей жизни"
[3] "Твой отец собирается задать мне вопрос. Это самый важный момент в нашей жизни, и я хочу"
[4] "запомнить все до малейшей детали. Уже за полночь, но мы только что вернулись домой после"
[5] "ужина в ресторане и веселого шоу и сразу выходим в патио полюбоваться полной луной. Хочу"
[6] "танцевать! — объявляю я, и твой отец подтрунивает надо мной, но мы начинаем скользить в"
Тексты хранятся в интернете по разному. Часто бывает так, что текст дигитализировали так, как он напечатан, так что в результате каждая строка в печатной книжке соответствует строке в текстовом файле (так, например, в нашем примере). Такой файл следует склеить воедино, используя пробел в качестве разделителя:
str_c(t, collapse = " ")
t2 <-length(t2)
[1] 1
str_length(t2)
[1] 117398
При таком слиянии, стоит проверить, не было ли в анализируемом тексте знаков переноса, иначе они сольются неправильно:
str_c(c("... она запо-", "лучила ..."), collapse = " ")
[1] "... она запо- лучила ..."
9.2 Пакет gutenbergr
Пакет gutenbergr
является API для очень старого проекта Gutenberg.
library(gutenbergr)
Все самое важное в этом пакете хранится в датасете gutenberg_metadata
str(gutenberg_metadata)
tibble [51,997 × 8] (S3: tbl_df/tbl/data.frame)
$ gutenberg_id : int [1:51997] 0 1 2 3 4 5 6 7 8 9 ...
$ title : chr [1:51997] NA "The Declaration of Independence of the United States of America" "The United States Bill of Rights\r\nThe Ten Original Amendments to the Constitution of the United States" "John F. Kennedy's Inaugural Address" ...
$ author : chr [1:51997] NA "Jefferson, Thomas" "United States" "Kennedy, John F. (John Fitzgerald)" ...
$ gutenberg_author_id: int [1:51997] NA 1638 1 1666 3 1 4 NA 3 3 ...
$ language : chr [1:51997] "en" "en" "en" "en" ...
$ gutenberg_bookshelf: chr [1:51997] NA "United States Law/American Revolutionary War/Politics" "American Revolutionary War/Politics/United States Law" NA ...
$ rights : chr [1:51997] "Public domain in the USA." "Public domain in the USA." "Public domain in the USA." "Public domain in the USA." ...
$ has_text : logi [1:51997] TRUE TRUE TRUE TRUE TRUE TRUE ...
- attr(*, "date_updated")= Date[1:1], format: "2016-05-05"
Например, сейчас мы можем понять, сколько книг на разных языках можно скачать из проекта:
%>%
gutenberg_metadata count(language, sort = TRUE)
language <chr> | n <int> | |||
---|---|---|---|---|
en | 42719 | |||
fr | 2658 | |||
de | 1387 | |||
fi | 1341 | |||
nl | 723 | |||
it | 672 | |||
pt | 537 | |||
es | 470 | |||
zh | 408 | |||
el | 216 |
Как видно, в основном это тексты на английском. Сколько авторов в датасете?
%>%
gutenberg_metadata count(author, sort = TRUE)
author <chr> | n <int> | |||
---|---|---|---|---|
NA | 3457 | |||
Various | 3199 | |||
Anonymous | 724 | |||
Shakespeare, William | 317 | |||
Lytton, Edward Bulwer Lytton, Baron | 219 | |||
Twain, Mark | 214 | |||
Ebers, Georg | 171 | |||
Dickens, Charles | 163 | |||
Verne, Jules | 151 | |||
Unknown | 141 |
Сколько произведений Джейн Остин (не перепутайте с другими Остин) есть в датасете?
%>%
gutenberg_metadata filter(author == "Austen, Jane") %>%
distinct(gutenberg_id, title)
gutenberg_id <int> | title <chr> | |
---|---|---|
105 | Persuasion | |
121 | Northanger Abbey | |
141 | Mansfield Park | |
158 | Emma | |
161 | Sense and Sensibility | |
946 | Lady Susan | |
1212 | Love and Freindship [sic] | |
1342 | Pride and Prejudice | |
19839 | Emma | |
20682 | Northanger Abbey |
Давайте скачаем “Эмму”:
gutenberg_download(158) emma <-
Determining mirror for Project Gutenberg from http://www.gutenberg.org/robot/harvest
Using mirror http://aleph.gutenberg.org
emma
gutenberg_id <int> | text <chr> | |||
---|---|---|---|---|
158 | EMMA | |||
158 | ||||
158 | By Jane Austen | |||
158 | ||||
158 | ||||
158 | ||||
158 | ||||
158 | VOLUME I | |||
158 | ||||
158 |
Можно скачивать сразу несколько книг. Давайте добавим еще “Леди Сьюзен”:
gutenberg_download(c(158, 946), meta_fields = "title")
books <- books
gutenberg_id <int> | text <chr> | title <chr> | ||
---|---|---|---|---|
158 | EMMA | Emma | ||
158 | Emma | |||
158 | By Jane Austen | Emma | ||
158 | Emma | |||
158 | Emma | |||
158 | Emma | |||
158 | Emma | |||
158 | VOLUME I | Emma | ||
158 | Emma | |||
158 | Emma |
%>%
books count(title)
title <chr> | n <int> | |||
---|---|---|---|---|
Emma | 16235 | |||
Lady Susan | 2525 |
Сколько уникальных заголовков из базы данных содержит “Sherlock Holmes”?
9.3 Библиотека tidytext
Сейчас скачанные книги записаны в таблицу, где одна строка это один абзац. Хочется мочь посчитать слова. Для этого книги нужно привести в tidy формат и для этого написан пакет tidytext
(онлайн книга доступна здесь). Основное “оружие” пакета tidytext
функция unnest_tokens()
, которая переводит текст в tidy формат. В аргумент output
подается вектор с именем будущей переменной, а аргумент input
принимает переменную с текстом.
library(tidytext)
%>%
books unnest_tokens(output = "word", input = text)
gutenberg_id <int> | title <chr> | word <chr> | ||
---|---|---|---|---|
158 | Emma | emma | ||
158 | Emma | by | ||
158 | Emma | jane | ||
158 | Emma | austen | ||
158 | Emma | volume | ||
158 | Emma | i | ||
158 | Emma | chapter | ||
158 | Emma | i | ||
158 | Emma | emma | ||
158 | Emma | woodhouse |
Теперь можно посчитать самые частотные слова в обоих произведениях:
%>%
books unnest_tokens(output = "word", input = text) %>%
count(title, word, sort = TRUE)
title <chr> | word <chr> | n <int> | ||
---|---|---|---|---|
Emma | to | 5239 | ||
Emma | the | 5201 | ||
Emma | and | 4896 | ||
Emma | of | 4291 | ||
Emma | i | 3177 | ||
Emma | a | 3129 | ||
Emma | it | 2528 | ||
Emma | her | 2462 | ||
Emma | was | 2398 | ||
Emma | she | 2340 |
Ну… Это было ожидаемо. Нужно убрать стопслова. Английские стопслова встроены в пакет (переменная stop_words
):
%>%
books unnest_tokens(word, text) %>%
count(title, word, sort = TRUE) %>%
anti_join(stop_words)
Joining, by = "word"
title <chr> | word <chr> | n <int> | ||
---|---|---|---|---|
Emma | emma | 786 | ||
Emma | miss | 599 | ||
Emma | harriet | 415 | ||
Emma | weston | 389 | ||
Emma | knightley | 356 | ||
Emma | elton | 319 | ||
Emma | jane | 282 | ||
Emma | time | 279 | ||
Emma | woodhouse | 277 | ||
Emma | dear | 241 |
Постройте следующий график, на котором представлены самые частотные 20 слов каждого из произведений.
Как видно, на графике все не упорядочено, давайте начнем с такого примера:
%>%
books unnest_tokens(word, text) %>%
count(word, sort = TRUE) %>%
slice(1:20) %>%
ggplot(aes(n, word))+
geom_col()
Если мы работаем с одним фасетом, то все проблемы может решить функция fct_reorder()
, которая упорядочивает на основании некоторой переменной:
%>%
books unnest_tokens(word, text) %>%
count(word, sort = TRUE) %>%
slice(1:20) %>%
mutate(word = fct_reorder(word, n)) %>%
ggplot(aes(n, word))+
geom_col()
Однако, если мы применим это к нашим данным, то получится неупорядочено:
%>%
books unnest_tokens(word, text) %>%
count(title, word, sort = TRUE) %>%
group_by(title) %>%
slice(1:20) %>%
ungroup() %>%
mutate(word = fct_reorder(word, n)) %>%
ggplot(aes(n, word))+
geom_col()+
facet_wrap(~title, scales = "free")
В пакете tidytext
есть функция reorder_within()
, которая позволяет упорядочить нужным образом:
%>%
books unnest_tokens(word, text) %>%
count(title, word, sort = TRUE) %>%
group_by(title) %>%
slice(1:20) %>%
ungroup() %>%
mutate(word = reorder_within(x = word, by = n, within = title)) %>%
ggplot(aes(n, word))+
geom_col()+
facet_wrap(~title, scales = "free")
Чтобы избавиться от дополнительной подписи нужно использовать scale_y_reordered()
или scale_x_reordered()
:
%>%
books unnest_tokens(word, text) %>%
count(title, word, sort = TRUE) %>%
group_by(title) %>%
slice(1:20) %>%
ungroup() %>%
mutate(word = reorder_within(x = word, by = n, within = title)) %>%
ggplot(aes(n, word))+
geom_col()+
facet_wrap(~title, scales = "free")+
scale_y_reordered()
Функция unnest_tokens()
позволяет работать не только со словами, но и, напрмиер, с биграммами:
%>%
books unnest_tokens(word, text, token = "ngrams", n = 2)
gutenberg_id <int> | title <chr> | word <chr> | ||
---|---|---|---|---|
158 | Emma | emma by | ||
158 | Emma | by jane | ||
158 | Emma | jane austen | ||
158 | Emma | austen volume | ||
158 | Emma | volume i | ||
158 | Emma | i chapter | ||
158 | Emma | chapter i | ||
158 | Emma | i emma | ||
158 | Emma | emma woodhouse | ||
158 | Emma | woodhouse handsome |
Поиск самых частотных слов — не едиснственная задача, которую можно решать при работе с текстом. Иногда имеет смысл узнать распределение слов в произведении. Давайте посмотрим как распределены в романе “Эмма” фамилии главных героев:
%>%
books filter(title == "Emma") %>%
unnest_tokens(word, text) %>%
mutate(narrative_time = 1:n()) %>%
filter(str_detect(word, "knightley$|woodhouse$|churchill$|fairfax$")) %>%
ggplot()+
geom_vline(aes(xintercept = narrative_time))+
facet_wrap(~word, ncol = 1)
9.4 Пакет stopwords
Выше мы упомянули, что в пакет tidytext
встроен список английских стопслов. Стопслова для других язков можно раздобыть списки для других языков, используя пакет stopwords
. Вместо имени языка, функция принимает ISO код языыка:
library(stopwords)
stopwords("ru")
[1] "и" "в" "во" "не" "что" "он" "на"
[8] "я" "с" "со" "как" "а" "то" "все"
[15] "она" "так" "его" "но" "да" "ты" "к"
[22] "у" "же" "вы" "за" "бы" "по" "только"
[29] "ее" "мне" "было" "вот" "от" "меня" "еще"
[36] "нет" "о" "из" "ему" "теперь" "когда" "даже"
[43] "ну" "вдруг" "ли" "если" "уже" "или" "ни"
[50] "быть" "был" "него" "до" "вас" "нибудь" "опять"
[57] "уж" "вам" "сказал" "ведь" "там" "потом" "себя"
[64] "ничего" "ей" "может" "они" "тут" "где" "есть"
[71] "надо" "ней" "для" "мы" "тебя" "их" "чем"
[78] "была" "сам" "чтоб" "без" "будто" "человек" "чего"
[85] "раз" "тоже" "себе" "под" "жизнь" "будет" "ж"
[92] "тогда" "кто" "этот" "говорил" "того" "потому" "этого"
[99] "какой" "совсем" "ним" "здесь" "этом" "один" "почти"
[106] "мой" "тем" "чтобы" "нее" "кажется" "сейчас" "были"
[113] "куда" "зачем" "сказать" "всех" "никогда" "сегодня" "можно"
[120] "при" "наконец" "два" "об" "другой" "хоть" "после"
[127] "над" "больше" "тот" "через" "эти" "нас" "про"
[134] "всего" "них" "какая" "много" "разве" "сказала" "три"
[141] "эту" "моя" "впрочем" "хорошо" "свою" "этой" "перед"
[148] "иногда" "лучше" "чуть" "том" "нельзя" "такой" "им"
[155] "более" "всегда" "конечно" "всю" "между"
Пакет предоставляет несколько источников списков:
stopwords_getsources()
[1] "snowball" "stopwords-iso" "misc" "smart"
[5] "marimo" "ancient" "nltk"
Давайте посмотрем какие языки сейчас доступны:
map(stopwords_getsources(), stopwords_getlanguages)
[[1]]
[1] "da" "de" "en" "es" "fi" "fr" "hu" "ir" "it" "nl" "no" "pt" "ro" "ru" "sv"
[[2]]
[1] "af" "ar" "hy" "eu" "bn" "br" "bg" "ca" "zh" "hr" "cs" "da" "nl" "en" "eo"
[16] "et" "fi" "fr" "gl" "de" "el" "ha" "he" "hi" "hu" "id" "ga" "it" "ja" "ko"
[31] "ku" "la" "lt" "lv" "ms" "mr" "no" "fa" "pl" "pt" "ro" "ru" "sk" "sl" "so"
[46] "st" "es" "sw" "sv" "th" "tl" "tr" "uk" "ur" "vi" "yo" "zu"
[[3]]
[1] "ar" "ca" "el" "gu" "zh"
[[4]]
[1] "en"
[[5]]
[1] "en" "ja" "ar" "he" "zh_tw" "zh_cn"
[[6]]
[1] "grc" "la"
[[7]]
[1] "ar" "az" "da" "nl" "en" "fi" "fr" "de" "el" "hu" "id" "it" "kk" "ne" "no"
[16] "pt" "ro" "ru" "sl" "es" "sv" "tg" "tr"
Мы видим, что есть несколько источников для русского языка:
length(stopwords("ru", source = "snowball"))
[1] 159
length(stopwords("ru", source = "stopwords-iso"))
[1] 559
9.5 Пакет udpipe
Пакет udpipe
представляет лемматизацию, морфологический и синтаксический анализ разных языков. Туториал можно найти здесь, там же есть список доступных языков.
library(udpipe)
Модели качаются очень долго.
udpipe_download_model(language = "english") enmodel <-
Downloading udpipe model from https://raw.githubusercontent.com/jwijffels/udpipe.models.ud.2.4/master/inst/udpipe-ud-2.4-190531/english-ewt-ud-2.4-190531.udpipe to /home/agricolamz/work/materials/2019_2020_ds4dh/class_materials/english-ewt-ud-2.4-190531.udpipe
Visit https://github.com/jwijffels/udpipe.models.ud.2.4 for model license details
Теперь можно распарсить какое-нибудь предложение:
udpipe("The want of Miss Taylor would be felt every hour of every day.", object = enmodel)
doc_id <chr> | paragraph_id <int> | sentence_id <int> | sentence <chr> | start <int> | end <int> | term_id <int> | token_id <chr> | token <chr> | |
---|---|---|---|---|---|---|---|---|---|
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 1 | 3 | 1 | 1 | The | |
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 5 | 8 | 2 | 2 | want | |
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 10 | 11 | 3 | 3 | of | |
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 13 | 16 | 4 | 4 | Miss | |
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 18 | 23 | 5 | 5 | Taylor | |
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 25 | 29 | 6 | 6 | would | |
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 31 | 32 | 7 | 7 | be | |
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 34 | 37 | 8 | 8 | felt | |
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 39 | 43 | 9 | 9 | every | |
doc1 | 1 | 1 | The want of Miss Taylor would be felt every hour of every day. | 45 | 48 | 10 | 10 | hour |
Скачаем русскую модель:
udpipe_download_model(language = "russian-syntagrus") rumodel <-
Downloading udpipe model from https://raw.githubusercontent.com/jwijffels/udpipe.models.ud.2.4/master/inst/udpipe-ud-2.4-190531/russian-syntagrus-ud-2.4-190531.udpipe to /home/agricolamz/work/materials/2019_2020_ds4dh/class_materials/russian-syntagrus-ud-2.4-190531.udpipe
Visit https://github.com/jwijffels/udpipe.models.ud.2.4 for model license details
udpipe("Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина.", object = rumodel)
doc_id <chr> | paragraph_id <int> | sentence_id <int> | sentence <chr> | |
---|---|---|---|---|
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. |
После того, как модель скачана можно уже к ней обращаться просто по имени файла:
udpipe("Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина.", object = "russian-syntagrus-ud-2.4-190531.udpipe")
doc_id <chr> | paragraph_id <int> | sentence_id <int> | sentence <chr> | |
---|---|---|---|---|
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. | |
doc1 | 1 | 1 | Жила-была на свете крыса в морском порту Вальпараисо, на складе мяса и маиса, какао и вина. |