Tidy text oraz tidy data, czyli w wolnym tłumaczeniu schludny lub czysty tekst i czyste dane to pewnego rodzaju paradygmat programistyczny związany z pakietem R, wprowadzany i propagowany przez Hadleya Wickhama (Chief Scientist w RStudio). Założenia struktur, na których pracuje się w tidy text sprowadzają się do następujących punktów:
W efekcie schludny tekst jest zdefiniowany jako tabela z pojedynczym tokenem na rząd, przy czym słowo token będziemy najczęsciej rozumieć jako wyraz lub słowo.
Warto też wiedzieć, że podstawowa forma danych związana z tidy text to tzw. tibble. Jest to pewna wariacja na temat ramki danych, posiadająca wygodne listowanie, pomijająca nazwy rzędów. Dodatkowo w tibble nie następuje konwersja z typu char do typu factor (co jest prawdziwą zmorą dla początkujących, jeśli chodzi o ramki danych).
Aby prześledzić jak wygląda przekształcanie zwykłego tekstu na tidy text posłużymy się pierwszymi czterema wersami Pana Tadeusza:
# PRZYKŁAD 2.1
mr.ted <- c(" Litwo! Ojczyzno moja! ty jesteś jak zdrowie;",
"Ile cię trzeba cenić, ten tylko się dowie",
"Kto cię stracił. Dziś piękność twą w całéj ozdobie",
"Widzę i opisuję, bo tęsknię po tobie.")
mr.ted
## [1] " Litwo! Ojczyzno moja! ty jesteś jak zdrowie;"
## [2] "Ile cię trzeba cenić, ten tylko się dowie"
## [3] "Kto cię stracił. Dziś piękność twą w całéj ozdobie"
## [4] "Widzę i opisuję, bo tęsknię po tobie."
Aby otrzymać tibble wykorzystujemy eponimiczną funkcję z biblioteki dplyr (w zasadzie z biblioteki tibble), dodatkowo podając numery linii:
# PRZYKŁAD 2.2
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
df <- tibble(lines = 1:length(mr.ted), text = mr.ted)
df
## # A tibble: 4 x 2
## lines text
## <int> <chr>
## 1 1 " Litwo! Ojczyzno moja! ty jesteś jak zdrowie;"
## 2 2 Ile cię trzeba cenić, ten tylko się dowie
## 3 3 Kto cię stracił. Dziś piękność twą w całéj ozdobie
## 4 4 Widzę i opisuję, bo tęsknię po tobie.
Sęk w tym, taka postać do końca spełnia "wymogi" czystego tekstu - powinniśmy przekształcić tę strukturę w taką tabelę, w której w każdym rzędzie znajdzie się słowo. Przydatna do tego będzie funkcja unnest_tokens() z pakietu tidytext. Funkcja przyjmuje jako argumenty strukture z danymi, nazwę wynikowej kolumny, w której mają być przetworzone dane oraz nazwę kolumny, z której ma korzystać przy przekształcaniu danych. Domyślnie ustawione są opcje to_lower=TRUE oraz drop=TRUE, czyli w wyrazach zmieniane są wielkie litey na małe oraz usuwa się z wejściowe dane.
# PRZYKŁAD 2.3
library(tidytext)
df.words <- df %>%
unnest_tokens(word, text)
df.words
## # A tibble: 31 x 2
## lines word
## <int> <chr>
## 1 1 litwo
## 2 1 ojczyzno
## 3 1 moja
## 4 1 ty
## 5 1 jesteś
## 6 1 jak
## 7 1 zdrowie
## 8 2 ile
## 9 2 cię
## 10 2 trzeba
## # ... with 21 more rows
Do dalszej obróbki danych będziemy potrzebować jeszcze kilku istotnych funkcji. Pierwszą z nich jest count(), która zlicza wystąpienia poszczególnych słów - jeśli wyposażymy ją w opcję sort=TRUE otrzymamy tibble zawierająca słowa od najczęstszego do najrzadszego:
# PRZYKŁAD 2.3
df.words %>%
count(word, sort = TRUE)
## # A tibble: 30 x 2
## word n
## <chr> <int>
## 1 cię 2
## 2 bo 1
## 3 całéj 1
## 4 cenić 1
## 5 dowie 1
## 6 dziś 1
## 7 i 1
## 8 ile 1
## 9 jak 1
## 10 jesteś 1
## # ... with 20 more rows
Bardzo często w naszej strukturze musimy dodać jakieś zmienne (kolumny) lub po prostu je zmienić. Używamy wtedy funkcji mutate() z dplyr (ta sama klasa funkcji, co arrange, filter etc) np
# PRZYKŁAD 2.4
df <- tibble(x = 1:5, y = letters[1:5])
df
## # A tibble: 5 x 2
## x y
## <int> <chr>
## 1 1 a
## 2 2 b
## 3 3 c
## 4 4 d
## 5 5 e
df %>%
mutate(y = letters[6:10], z = runif(5))
## # A tibble: 5 x 3
## x y z
## <int> <chr> <dbl>
## 1 1 f 0.253
## 2 2 g 0.494
## 3 3 h 0.487
## 4 4 i 0.406
## 5 5 j 0.0608
Stworzymy teraz wykres kolumnowy (a w zasadzie wierszowy) częstości słów z rozpatrywanego kawałka tekstu. Posłużymy się pakietem ggplot, żeby jednak można było wykonać wykres musimy przekształcić zmienne char na typ wyliczeniowy (factor) z określoną kolejnością za pomocą funkcji reorder().
# PRZYKŁAD 2.3
library(ggplot2)
df.words %>%
count(word, sort = TRUE) %>%
mutate(word1 = reorder(word, n)) %>%
ggplot() +
geom_col(aes(word1, n)) +
coord_flip()
Większość słów na otrzymanym wykresie mało wnosi do jakiejkolwiek analizy tekstu - są to tak zwane "stopwords" (słowa przestankowe, funkcyjne). Warto się ich pozbyć z tekstu. Aby jednak to zrobić, wprowadzimy jeszcze jedną istotną funkcję anti_join(). Należy ona do zestawu funkcji działających podobnie do zapytań SQL a zawierających między innymi inner_join(), left_join(), right_join() oraz full_join(). Skorzystamy z dwóch wbudowanych zbiorów danych
band_members
## # A tibble: 3 x 2
## name band
## <chr> <chr>
## 1 Mick Stones
## 2 John Beatles
## 3 Paul Beatles
band_instruments
## # A tibble: 3 x 2
## name plays
## <chr> <chr>
## 1 John guitar
## 2 Paul bass
## 3 Keith guitar
i sprawdzimy jakie są efekty
band_members %>% inner_join(band_instruments)
## Joining, by = "name"
## # A tibble: 2 x 3
## name band plays
## <chr> <chr> <chr>
## 1 John Beatles guitar
## 2 Paul Beatles bass
band_members %>% left_join(band_instruments)
## Joining, by = "name"
## # A tibble: 3 x 3
## name band plays
## <chr> <chr> <chr>
## 1 Mick Stones <NA>
## 2 John Beatles guitar
## 3 Paul Beatles bass
band_members %>% right_join(band_instruments)
## Joining, by = "name"
## # A tibble: 3 x 3
## name band plays
## <chr> <chr> <chr>
## 1 John Beatles guitar
## 2 Paul Beatles bass
## 3 Keith <NA> guitar
band_members %>% full_join(band_instruments)
## Joining, by = "name"
## # A tibble: 4 x 3
## name band plays
## <chr> <chr> <chr>
## 1 Mick Stones <NA>
## 2 John Beatles guitar
## 3 Paul Beatles bass
## 4 Keith <NA> guitar
band_members %>% anti_join(band_instruments)
## Joining, by = "name"
## # A tibble: 1 x 2
## name band
## <chr> <chr>
## 1 Mick Stones
Teraz skorzystamy już z pliku stopw.dat (pobrany z stąd)
stops <- read.table("http://www.if.pw.edu.pl/~julas/TEXT/lab/stopw.dat", stringsAsFactors = F)
stops <- tibble(word = stops$V1)
stops
## # A tibble: 350 x 1
## word
## <chr>
## 1 a
## 2 aby
## 3 ach
## 4 acz
## 5 aczkolwiek
## 6 aj
## 7 albo
## 8 ale
## 9 alez
## 10 ależ
## # ... with 340 more rows
i za jego pomocą pozbędziemy się słów funkcyjnych
df.words %>%
count(word, sort = TRUE) %>%
anti_join(stops)
## Joining, by = "word"
## # A tibble: 14 x 2
## word n
## <chr> <int>
## 1 całéj 1
## 2 cenić 1
## 3 dowie 1
## 4 jesteś 1
## 5 litwo 1
## 6 ojczyzno 1
## 7 opisuję 1
## 8 ozdobie 1
## 9 piękność 1
## 10 stracił 1
## 11 tęsknię 1
## 12 twą 1
## 13 widzę 1
## 14 zdrowie 1
Przytoczona anliza jest oczywiście tylko przykładem wykonanym na bardzo krótkim tekście. Aby dostać się do większych zbiorów, można skorzystać z biblioteki gutenbergr i zawartych w niej funkcji gutenberg_works() oraz gutenberg_download()
library(gutenbergr)
gutenberg_works()
## # A tibble: 40,737 x 8
## gutenberg_id title author gutenberg_autho.. language gutenberg_books..
## <int> <chr> <chr> <int> <chr> <chr>
## 1 0 <NA> <NA> NA en <NA>
## 2 1 The .. Jeffe.. 1638 en United States L..
## 3 2 "The.. Unite.. 1 en American Revolu..
## 4 3 John.. Kenne.. 1666 en <NA>
## 5 4 "Lin.. Linco.. 3 en US Civil War
## 6 5 The .. Unite.. 1 en American Revolu..
## 7 6 Give.. Henry.. 4 en American Revolu..
## 8 7 The .. <NA> NA en <NA>
## 9 8 Abra.. Linco.. 3 en US Civil War
## 10 9 Abra.. Linco.. 3 en US Civil War
## # ... with 40,727 more rows, and 2 more variables: rights <chr>,
## # has_text <lgl>
gutenberg_works(languages = "pl") %>%
filter(author == "Mickiewicz, Adam")
## # A tibble: 6 x 8
## gutenberg_id title author gutenberg_autho.. language gutenberg_books..
## <int> <chr> <chr> <int> <chr> <chr>
## 1 27081 Sone.. Micki.. 32429 pl <NA>
## 2 27723 Moja.. Micki.. 32429 pl <NA>
## 3 27729 Bajki Micki.. 32429 pl <NA>
## 4 28049 Bala.. Micki.. 32429 pl <NA>
## 5 28153 Graż.. Micki.. 32429 pl <NA>
## 6 31536 "Pan.. Micki.. 32429 pl <NA>
## # ... with 2 more variables: rights <chr>, has_text <lgl>
gutenberg_download(31536)
## Determining mirror for Project Gutenberg from http://www.gutenberg.org/robot/harvest
## Using mirror http://aleph.gutenberg.org
## # A tibble: 5,370 x 2
## gutenberg_id text
## <int> <chr>
## 1 31536 PAN TADEUSZ.
## 2 31536 ""
## 3 31536 ""
## 4 31536 TOM PIERWSZY.
## 5 31536 ""
## 6 31536 ""
## 7 31536 ""
## 8 31536 ""
## 9 31536 PAN TADEUSZ.
## 10 31536 ""
## # ... with 5,360 more rows