[R] 5. Converting to and from non-tidy formats
반응형
5. Converting to and from non-tidy formats
- 이번 챕터에서는 텍스트 데이터를 tidy text format이 아닌
tm
,quanteda
라이브러리에서 활용될 수 있는 코퍼스(corpus
) 객체로 분석하는 방법에 대해서 설명합니다.
5. 1. Tidying a document-term matrix
- 문서 용어 행렬(DTM, Document-Term Matrix)은 텍스트 분석에서 일반적으로 쓰이는 구조 중 하나 입니다.
- 이는 아래와 같은 형태를 갖습니다.
- 각 행은 하나의 문서(ex. book, article, …)를 나타냅니다.
- 각 열은 하나의 단어를 나타냅니다.
- 일반적으로 각 행렬에 대한 값은 해당 문서에서 해당 단어의 출현 빈도가 됩니다.
- 여러 문서 안에서 문서-단어 쌍이 공통적으로 많이 발생하는 케이스는 드물기에 DTM은 일반적으로 희소 행렬(sparse matrix)로 구현됩니다.
tidytext
라이브러리는 DTM 객체를 직접 사용할 수 없지만 이를 tidy data frame 형태로 변환해주는 함수를 제공합니다.tidy()
: DTM to tidy data (inbroom
library)
5. 1. 1. Tidying DocumentTermMatrix objects
- R에서 가장 널리 이용되는 DTM 구현은
tm
라이브러리 내에DocumentTermMatrix
클래스를 갖는 객체입니다. - 예시를 보이기 위해
topicmodels
라이브러리에 있는Associated Press
뉴스 기사 데이터를 참고합니다.
library(tm)
## 필요한 패키지를 로딩중입니다: NLP
##
## 다음의 패키지를 부착합니다: 'NLP'
## The following object is masked from 'package:ggplot2':
##
## annotate
data("AssociatedPress", package = "topicmodels")
AssociatedPress
## <<DocumentTermMatrix (documents: 2246, terms: 10473)>>
## Non-/sparse entries: 302031/23220327
## Sparsity : 99%
## Maximal term length: 18
## Weighting : term frequency (tf)
- 위에서 보이다시피 이 데이터는 2,246개의 뉴스 기사와 10,473개의 단어로 구성된 DTM 객체이며 99%가 문서-단어 쌍의 값이 0인 희소행렬로 입니다.
Terms()
함수를 사용하여 문서의 단어에 접근할 수 있습니다.
terms <- Terms(AssociatedPress)
terms %>%
head(20)
## [1] "aaron" "abandon" "abandoned" "abandoning" "abbott"
## [6] "abboud" "abc" "abcs" "abctvs" "abdomen"
## [11] "abducted" "abduction" "abductors" "abdul" "abide"
## [16] "abilities" "ability" "ablaze" "able" "abm"
- 이를 tidy data format으로 변환하기 위해서
tidy()
함수를 사용합니다.
ap_tidy <- AssociatedPress %>%
tidy()
ap_tidy
## # A tibble: 302,031 x 3
## document term count
## <int> <chr> <dbl>
## 1 1 adding 1
## 2 1 adult 2
## 3 1 ago 1
## 4 1 alcohol 1
## 5 1 allegedly 1
## 6 1 allen 1
## 7 1 apparently 2
## 8 1 appeared 1
## 9 1 arrested 1
## 10 1 assault 1
## # … with 302,021 more rows
- 이렇게 나온 결과 값은 희소행렬을
reshape2
라이브러리의melt()
함수를 사용한 결과와 유사하다고 보면 됩니다. - 또한
tidy()
함수를 적용함으로써 희소행렬 값이 0으로 취급받는 문서-단어 쌍은 결과에 출력되지 않습니다.
- 이제 이러한 결과를 가지고 감정 분석을 진행해볼 수 있습니다.
ap_sentiments <- ap_tidy %>%
inner_join(get_sentiments("bing"), by = c(term = "word"))
ap_sentiments
## # A tibble: 30,094 x 4
## document term count sentiment
## <int> <chr> <dbl> <chr>
## 1 1 assault 1 negative
## 2 1 complex 1 negative
## 3 1 death 1 negative
## 4 1 died 1 negative
## 5 1 good 2 positive
## 6 1 illness 1 negative
## 7 1 killed 2 negative
## 8 1 like 2 positive
## 9 1 liked 1 positive
## 10 1 miracle 1 positive
## # … with 30,084 more rows
ap_sentiments %>%
count(sentiment, term, wt = count) %>%
ungroup() %>%
filter(n >= 200) %>%
mutate(n = ifelse(sentiment == "negative", -n, n)) %>%
ggplot(aes(x = reorder(term, n), y = n, fill = sentiment)) +
geom_col() +
coord_flip() +
labs(x = "Contribution to sentiment", y = NULL)
- 가장 흔하게 나타나는 긍정적인 단어는 “like”, “work”, “support”, “good”
가장 부정적인 단어는 “killed”, “death” 등이 잇습니다.
5. 1. 2. Tidying dfm
objects
- 다른 라이브러리인
quanteda
에서dfm()
함수도dfm
이라는 클래스로 문서-단어 행렬을 구현을 제공합니다.dtm
,dfm
- 가운데 글자 하나에 차이가 있습니다.
dfm
에서 f는 feature를 의미한다고 합니다. (document-feature matrix)
- 예시를 보기 위해
quanteda
라이브러리의data_corpus_inaugural
데이터를 참고하겠습니다. (취임연설 관련 데이터)
library(quanteda)
## Package version: 3.0.0
## Unicode version: 13.0
## ICU version: 69.1
## Parallel computing: 16 of 16 threads used.
## See https://quanteda.io for tutorials and examples.
##
## 다음의 패키지를 부착합니다: 'quanteda'
## The following object is masked from 'package:tm':
##
## stopwords
## The following objects are masked from 'package:NLP':
##
## meta, meta<-
data("data_corpus_inaugural", pacakge = "quanteda")
## Warning in data("data_corpus_inaugural", pacakge = "quanteda"): data set
## 'quanteda' not found
data_corpus_inaugural
## Corpus consisting of 59 documents and 4 docvars.
## 1789-Washington :
## "Fellow-Citizens of the Senate and of the House of Representa..."
##
## 1793-Washington :
## "Fellow citizens, I am again called upon by the voice of my c..."
##
## 1797-Adams :
## "When it was first perceived, in early times, that no middle ..."
##
## 1801-Jefferson :
## "Friends and Fellow Citizens: Called upon to undertake the du..."
##
## 1805-Jefferson :
## "Proceeding, fellow citizens, to that qualification which the..."
##
## 1809-Madison :
## "Unwilling to depart from examples of the most revered author..."
##
## [ reached max_ndoc ... 53 more documents ]
inaug_dfm <- dfm(data_corpus_inaugural, verbose = FALSE)
## Warning: 'dfm.corpus()' is deprecated. Use 'tokens()' first.
inaug_dfm
## Document-feature matrix of: 59 documents, 9,439 features (91.84% sparse) and 4 docvars.
## features
## docs fellow-citizens of the senate and house representatives :
## 1789-Washington 1 71 116 1 48 2 2 1
## 1793-Washington 0 11 13 0 2 0 0 1
## 1797-Adams 3 140 163 1 130 0 2 0
## 1801-Jefferson 2 104 130 0 81 0 0 1
## 1805-Jefferson 0 101 143 0 93 0 0 0
## 1809-Madison 1 69 104 0 43 0 0 0
## features
## docs among vicissitudes
## 1789-Washington 1 1
## 1793-Washington 0 0
## 1797-Adams 4 0
## 1801-Jefferson 1 0
## 1805-Jefferson 7 0
## 1809-Madison 0 0
## [ reached max_ndoc ... 53 more documents, reached max_nfeat ... 9,429 more features ]
- 이것도 역시
tidy()
함수를 적용할 수 있습니다.
inaug_tidy <- inaug_dfm %>%
tidy()
inaug_tidy
## # A tibble: 45,453 x 3
## document term count
## <chr> <chr> <dbl>
## 1 1789-Washington fellow-citizens 1
## 2 1797-Adams fellow-citizens 3
## 3 1801-Jefferson fellow-citizens 2
## 4 1809-Madison fellow-citizens 1
## 5 1813-Madison fellow-citizens 1
## 6 1817-Monroe fellow-citizens 5
## 7 1821-Monroe fellow-citizens 1
## 8 1841-Harrison fellow-citizens 11
## 9 1845-Polk fellow-citizens 1
## 10 1849-Taylor fellow-citizens 1
## # … with 45,443 more rows
- 이를 가지고
bind_tf_idf()
함수를 사용하여 TF-IDF 값을 구해볼 수 있고 이를 시각화해볼 수 있습니다.
inaug_tf_idf <- inaug_tidy %>%
bind_tf_idf(
term = term,
document = document,
n = count
)
inaug_tf_idf
## # A tibble: 45,453 x 6
## document term count tf idf tf_idf
## <chr> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 1789-Washington fellow-citizens 1 0.000651 1.13 0.000737
## 2 1797-Adams fellow-citizens 3 0.00116 1.13 0.00132
## 3 1801-Jefferson fellow-citizens 2 0.00104 1.13 0.00118
## 4 1809-Madison fellow-citizens 1 0.000793 1.13 0.000899
## 5 1813-Madison fellow-citizens 1 0.000768 1.13 0.000870
## 6 1817-Monroe fellow-citizens 5 0.00136 1.13 0.00154
## 7 1821-Monroe fellow-citizens 1 0.000205 1.13 0.000232
## 8 1841-Harrison fellow-citizens 11 0.00121 1.13 0.00137
## 9 1845-Polk fellow-citizens 1 0.000193 1.13 0.000218
## 10 1849-Taylor fellow-citizens 1 0.000849 1.13 0.000962
## # … with 45,443 more rows
inaug_tf_idf %>%
filter(document %in% c("1861-Lincoln", "1933-Roosevelt", "1961-Kennedy", "2009-Obama")) %>%
group_by(document) %>%
slice_max(tf_idf, n = 10) %>%
ungroup() %>%
ggplot(aes(x = reorder(term, tf_idf), y = tf_idf, fill = document)) +
geom_col(show.legend = FALSE) +
coord_flip() +
facet_wrap(~ document, ncol = 2, scales = "free")
- 또 다른 시각화의 예로, 각 문서의 이름에서 연도를 추출하고 각 연도의 총 단어 수도 게산할 수 있습니다.
year_term_counts <- inaug_tidy %>%
extract(
col = document,
into = "year",
regex = "(\\d+)",
convert = TRUE
) %>%
complete(year, term, fill = list(count = 0)) %>% # 단어가 문서에 나타나지 않는 케이스를 포함하기 위해
group_by(year) %>%
mutate(total_count = sum(count)) %>%
ungroup()
year_term_counts
## # A tibble: 556,901 x 4
## year term count total_count
## <int> <chr> <dbl> <dbl>
## 1 1789 "-" 1 1537
## 2 1789 "," 70 1537
## 3 1789 ";" 8 1537
## 4 1789 ":" 1 1537
## 5 1789 "!" 0 1537
## 6 1789 "?" 0 1537
## 7 1789 "." 23 1537
## 8 1789 "…" 0 1537
## 9 1789 "'" 0 1537
## 10 1789 "\"" 2 1537
## # … with 556,891 more rows
- 주요 특정 단어를 필터링하여 해당 단어들이 시간이 지남에 따라 빈도가 어떻게 변했는지 확인해볼 수 있습니다.
year_term_counts %>%
filter(term %in% c("god", "america", "foreign", "union", "constitution", "freedom")) %>%
mutate(ratio = count/total_count) %>%
ggplot(aes(x = year, y = ratio)) +
geom_point(size = 1.2) +
geom_smooth(formula = y ~ x, method = "loess") +
facet_wrap(~ term, scales = "free_y") +
scale_y_continuous(labels = scales::percent_format()) +
labs(y = "% frequency of word in inaugural address")
5. 2. Casting tidy text data into a matrix
- 어떤 함수들은 tidy format이 아닌 dtm format을 input으로 필요로 하는 케이스가 있을 수도 있습니다.
tidytext
라이브러리에서는 tidy format을 dtm format으로 변환해주는 함수 역시 존재합니다.- 그 함수명은
cast_dtm()
입니다. - 위에서 dtm to tidy로 변환했던
ap_tidy
객체를 활용해보겠습니다.
ap_tidy
## # A tibble: 302,031 x 3
## document term count
## <int> <chr> <dbl>
## 1 1 adding 1
## 2 1 adult 2
## 3 1 ago 1
## 4 1 alcohol 1
## 5 1 allegedly 1
## 6 1 allen 1
## 7 1 apparently 2
## 8 1 appeared 1
## 9 1 arrested 1
## 10 1 assault 1
## # … with 302,021 more rows
ap_dtm <- ap_tidy %>%
cast_dtm(
term = term,
document = document,
value = count
)
ap_dtm
## <<DocumentTermMatrix (documents: 2246, terms: 10473)>>
## Non-/sparse entries: 302031/23220327
## Sparsity : 99%
## Maximal term length: 18
## Weighting : term frequency (tf)
- 마찬가지로
quanteda
라이브러리의dfm
객체 역시cast_dfm()
함수로 변환할 수 있습니다.
ap_dfm <- ap_tidy %>%
cast_dfm(
term = term,
document = document,
value = count
)
ap_dfm
## Document-feature matrix of: 2,246 documents, 10,473 features (98.72% sparse) and 0 docvars.
## features
## docs adding adult ago alcohol allegedly allen apparently appeared arrested
## 1 1 2 1 1 1 1 2 1 1
## 2 0 0 0 0 0 0 0 1 0
## 3 0 0 1 0 0 0 0 1 0
## 4 0 0 3 0 0 0 0 0 0
## 5 0 0 0 0 0 0 0 0 0
## 6 0 0 2 0 0 0 0 0 0
## features
## docs assault
## 1 1
## 2 0
## 3 0
## 4 0
## 5 0
## 6 0
## [ reached max_ndoc ... 2,240 more documents, reached max_nfeat ... 10,463 more features ]
- 이러한 종류의 변환을 통해서 이전 챕터에서 예시로 보았던 Jane Austen 책 역시 dtm 객체로 만들 수 있습니다.
library(janeaustenr)
austen_dtm <- austen_books() %>%
unnest_tokens(input = text, output = "word") %>%
count(book, word) %>%
cast_dtm(
term = word,
document = book,
value = n
)
austen_dtm
## <<DocumentTermMatrix (documents: 6, terms: 14520)>>
## Non-/sparse entries: 40379/46741
## Sparsity : 54%
## Maximal term length: 19
## Weighting : term frequency (tf)
5. 3. Tidying corpus objects with metadata
Corpus
라고 하는 객체는 토큰화 전에 문서 컬렉션들을 저장해놓은 객체입니다.- 여기에는 각 문서의 고유 아이디나 날짜/시간 또는 제목 등 포함할 수 있는 메타데이터와 함께 텍스트를 저장합니다.
- 아래 예시를 들어 살펴보겠습니다 (
tm
라이브러리에서acq
데이터, 뉴스 기사 50개)
data("acq")
acq
## <<VCorpus>>
## Metadata: corpus specific: 0, document level (indexed): 0
## Content: documents: 50
class(acq)
## [1] "VCorpus" "Corpus"
- 아래와 같이
Corpus
객체는 텍스트와 메타데이터가 모두 포함됩니다.
acq[[1]]
## <<PlainTextDocument>>
## Metadata: 15
## Content: chars: 1287
- 이러한 방식은 텍스트 데이터를 저장하는 유연한 방법이지만
tidytext
라이브러리로 처리하기에는 적합하지 않습니다. - 따라서
tidy()
함수를 사용하여 tidy data format으로 변환시켜 분석해야 합니다.
acq_tidy <- acq %>%
tidy()
acq_tidy
## # A tibble: 50 x 16
## author datetimestamp description heading id language origin topics
## <chr> <dttm> <chr> <chr> <chr> <chr> <chr> <chr>
## 1 <NA> 1987-02-26 15:18:06 "" COMPUTE… 10 en Reute… YES
## 2 <NA> 1987-02-26 15:19:15 "" OHIO MA… 12 en Reute… YES
## 3 <NA> 1987-02-26 15:49:56 "" MCLEAN'… 44 en Reute… YES
## 4 By Cal… 1987-02-26 15:51:17 "" CHEMLAW… 45 en Reute… YES
## 5 <NA> 1987-02-26 16:08:33 "" <COFAB … 68 en Reute… YES
## 6 <NA> 1987-02-26 16:32:37 "" INVESTM… 96 en Reute… YES
## 7 By Pat… 1987-02-26 16:43:13 "" AMERICA… 110 en Reute… YES
## 8 <NA> 1987-02-26 16:59:25 "" HONG KO… 125 en Reute… YES
## 9 <NA> 1987-02-26 17:01:28 "" LIEBERT… 128 en Reute… YES
## 10 <NA> 1987-02-26 17:08:27 "" GULF AP… 134 en Reute… YES
## # … with 40 more rows, and 8 more variables: lewissplit <chr>, cgisplit <chr>,
## # oldid <chr>, places <named list>, people <lgl>, orgs <lgl>,
## # exchanges <lgl>, text <chr>
- 이를
unnest_tokens()
함수를 사용하여 단어들을 토큰화시킬 수 있습니다.
acq_tokens <- acq_tidy %>%
select(-places) %>%
unnest_tokens(
input = text,
output = "word"
) %>%
anti_join(stop_words, by = "word")
## Warning: Outer names are only allowed for unnamed scalar atomic inputs
# most common words
acq_tokens %>%
count(word, sort = TRUE)
## # A tibble: 1,566 x 2
## word n
## <chr> <int>
## 1 dlrs 100
## 2 pct 70
## 3 mln 65
## 4 company 63
## 5 shares 52
## 6 reuter 50
## 7 stock 46
## 8 offer 34
## 9 share 34
## 10 american 28
## # … with 1,556 more rows
# tf-idf
acq_tokens %>%
count(id, word) %>%
bind_tf_idf(
term = word,
document = id,
n = n
) %>%
arrange(desc(tf_idf))
## # A tibble: 2,853 x 6
## id word n tf idf tf_idf
## <chr> <chr> <int> <dbl> <dbl> <dbl>
## 1 186 groupe 2 0.133 3.91 0.522
## 2 128 liebert 3 0.130 3.91 0.510
## 3 474 esselte 5 0.109 3.91 0.425
## 4 371 burdett 6 0.103 3.91 0.405
## 5 442 hazleton 4 0.103 3.91 0.401
## 6 199 circuit 5 0.102 3.91 0.399
## 7 162 suffield 2 0.1 3.91 0.391
## 8 498 west 3 0.1 3.91 0.391
## 9 441 rmj 8 0.121 3.22 0.390
## 10 467 nursery 3 0.0968 3.91 0.379
## # … with 2,843 more rows
반응형
'tidytext' 카테고리의 다른 글
[R] 6. Topic modeling (0) | 2021.07.20 |
---|---|
[R] 4. Relationships between words: n-grams and correlations (0) | 2021.07.18 |
[R] 3. Analyzing word and document frequency: TF-IDF (0) | 2021.07.18 |
[R] 한글 형태소 분석 (0) | 2021.07.17 |
[R] unnest_tokens() (0) | 2021.07.17 |
TAGS.