We will be reading Tidy Text Mining this quarter and trying to get a feel for working with text based data.
The book follows the Tidy approach:
- Each variable is a column
- Each observation is a row
- Each type of observational unit is a table
What is a token? What is tokenization? What is a Document Term Matrix? What is a Term Document Matrix? What is Sentiment Analysis? What is Topic Modeling? etc.
Tokens
To start tokens are, at the most basic level, words.
Start with a vector of text.
text <- c("Because I could not stop for Death -",
"He kindly stopped for me -",
"The Carriage held but just Ourselves -",
"and Immortality")
text
[1] "Because I could not stop for Death -" "He kindly stopped for me -"
[3] "The Carriage held but just Ourselves -" "and Immortality"
Put it into a dataframe. Note data_frame() give a tibble from the tiddyverse.
library(dplyr)
text_df <- data_frame(line = 1:4, text = text)
text_df
Tokenize.
library(tidytext)
text_df %>%
unnest_tokens(word, text)
Note capital letters are made lower case and that punctuation is removed.
Books
library(janeaustenr)
library(dplyr)
library(stringr)
original_books <- austen_books() %>%
group_by(book) %>%
mutate(linenumber = row_number(), chapter = cumsum(str_detect(text, regex("^chapter [\\divxlc]",ignore_case = TRUE)))) %>%
ungroup()
original_books
One word per row.
library(tidytext)
tidy_books <- original_books %>%
unnest_tokens(word, text)
tidy_books
Get rid of the stop words.
data(stop_words)
tidy_books <- tidy_books %>%
anti_join(stop_words)
Joining, by = "word"
Produce the word counts.
tidy_books %>%
count(word, sort = TRUE)
What are the most frequent words?
library(ggplot2)
tidy_books %>%
count(word, sort = TRUE) %>%
filter(n > 600) %>%
mutate(word = reorder(word, n)) %>%
ggplot(aes(word, n)) +
geom_col() +
xlab(NULL) +
coord_flip()

library(wordcloud)
tidy_books %>%
anti_join(stop_words) %>%
count(word) %>%
with(wordcloud(word, n, max.words = 100))
Joining, by = "word"

H.G. Wells
library(gutenbergr)
hgwells <- gutenberg_download(c(35, 36, 5230, 159))
hgwells
tidy_hgwells <- hgwells %>%
unnest_tokens(word, text) %>%
anti_join(stop_words)
Joining, by = "word"
tidy_hgwells
tidy_hgwells %>%
count(word, sort = TRUE)
tidy_hgwells %>%
anti_join(stop_words) %>%
count(word) %>%
with(wordcloud(word, n, max.words = 100))
Joining, by = "word"

Emily Bronte
bronte <- gutenberg_download(c(1260, 768, 969, 9182, 767))
bronte
tidy_bronte <- bronte %>%
unnest_tokens(word, text) %>%
anti_join(stop_words)
Joining, by = "word"
tidy_bronte %>%
count(word, sort = TRUE)
library(wordcloud)
tidy_bronte %>%
anti_join(stop_words) %>%
count(word) %>%
with(wordcloud(word, n, max.words = 100))
Joining, by = "word"

Put together
library(tidyr)
frequency <- bind_rows(mutate(tidy_bronte, author = "Brontë Sisters"),
mutate(tidy_hgwells, author = "H.G. Wells"),
mutate(tidy_books, author = "Jane Austen")) %>%
mutate(word = str_extract(word, "[a-z']+")) %>%
count(author, word) %>%
group_by(author) %>%
mutate(proportion = n / sum(n)) %>%
select(-n) %>%
spread(author, proportion) %>%
gather(author, proportion, `Brontë Sisters`:`H.G. Wells`)
library(scales)
# expect a warning about rows with missing values being removed
ggplot(frequency, aes(x = proportion, y = `Jane Austen`, color = abs(`Jane Austen` - proportion))) +
geom_abline(color = "gray40", lty = 2) +
geom_jitter(alpha = 0.1, size = 2.5, width = 0.3, height = 0.3) +
geom_text(aes(label = word), check_overlap = TRUE, vjust = 1.5) +
scale_x_log10(labels = percent_format()) +
scale_y_log10(labels = percent_format()) +
scale_color_gradient(limits = c(0, 0.001), low = "darkslategray4", high = "gray75") +
facet_wrap(~author, ncol = 2) +
theme(legend.position="none") +
labs(y = "Jane Austen", x = NULL)

Austen with Bronte
cor.test(data = frequency[frequency$author == "Brontë Sisters",],
~ proportion + `Jane Austen`)
Pearson's product-moment correlation
data: proportion and Jane Austen
t = 119.65, df = 10404, p-value < 2.2e-16
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
0.7527869 0.7689641
sample estimates:
cor
0.7609938
Austen with Wells
cor.test(data = frequency[frequency$author == "H.G. Wells",],
~ proportion + `Jane Austen`)
Pearson's product-moment correlation
data: proportion and Jane Austen
t = 36.441, df = 6053, p-value < 2.2e-16
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
0.4032800 0.4445987
sample estimates:
cor
0.4241601
Sentiment Analysis
Opinion. Happy or Sad.
From the book.
Sentiment lexicons based on unigrams.
Sentiment and Emotion.
library(tidytext)
sentiments
get_sentiments("afinn")
get_sentiments("bing")
get_sentiments("nrc")
library(janeaustenr)
library(dplyr)
library(stringr)
tidy_books <- austen_books() %>%
group_by(book) %>%
mutate(linenumber = row_number(),
chapter = cumsum(str_detect(text, regex("^chapter [\\divxlc]",
ignore_case = TRUE)))) %>%
ungroup() %>%
unnest_tokens(word, text)
nrcjoy <- get_sentiments("nrc") %>%
filter(sentiment == "joy")
tidy_books %>%
filter(book == "Emma") %>%
inner_join(nrcjoy) %>%
count(word, sort = TRUE)
Joining, by = "word"
library(tidyr)
janeaustensentiment <- tidy_books %>%
inner_join(get_sentiments("bing")) %>%
count(book, index = linenumber %/% 80, sentiment) %>%
spread(sentiment, n, fill = 0) %>%
mutate(sentiment = positive - negative)
Joining, by = "word"
library(ggplot2)
ggplot(janeaustensentiment, aes(index, sentiment, fill = book)) +
geom_col(show.legend = FALSE) +
facet_wrap(~book, ncol = 2, scales = "free_x")

Comparing the three sentiment dictionaries
pride_prejudice <- tidy_books %>%
filter(book == "Pride & Prejudice")
pride_prejudice
afinn <- pride_prejudice %>%
inner_join(get_sentiments("afinn")) %>%
group_by(index = linenumber %/% 80) %>%
summarise(sentiment = sum(score)) %>%
mutate(method = "AFINN")
Joining, by = "word"
bing_and_nrc <- bind_rows(pride_prejudice %>%
inner_join(get_sentiments("bing")) %>%
mutate(method = "Bing et al."),
pride_prejudice %>%
inner_join(get_sentiments("nrc") %>%
filter(sentiment %in% c("positive",
"negative"))) %>%
mutate(method = "NRC")) %>%
count(method, index = linenumber %/% 80, sentiment) %>%
spread(sentiment, n, fill = 0) %>%
mutate(sentiment = positive - negative)
Joining, by = "word"
Joining, by = "word"
bind_rows(afinn,
bing_and_nrc) %>%
ggplot(aes(index, sentiment, fill = method)) +
geom_col(show.legend = FALSE) +
facet_wrap(~method, ncol = 1, scales = "free_y")

get_sentiments("nrc") %>%
filter(sentiment %in% c("positive",
"negative")) %>%
count(sentiment)
get_sentiments("bing") %>%
count(sentiment)
Most common positive and negative words
bing_word_counts <- tidy_books %>%
inner_join(get_sentiments("bing")) %>%
count(word, sentiment, sort = TRUE) %>%
ungroup()
Joining, by = "word"
bing_word_counts
bing_word_counts %>%
group_by(sentiment) %>%
top_n(10) %>%
ungroup() %>%
mutate(word = reorder(word, n)) %>%
ggplot(aes(word, n, fill = sentiment)) +
geom_col(show.legend = FALSE) +
facet_wrap(~sentiment, scales = "free_y") +
labs(y = "Contribution to sentiment",
x = NULL) +
coord_flip()
Selecting by n

custom_stop_words <- bind_rows(data_frame(word = c("miss"),
lexicon = c("custom")),
stop_words)
custom_stop_words
library(wordcloud)
tidy_books %>%
anti_join(stop_words) %>%
count(word) %>%
with(wordcloud(word, n, max.words = 100))
Joining, by = "word"

library(reshape2)
tidy_books %>%
inner_join(get_sentiments("bing")) %>%
count(word, sentiment, sort = TRUE) %>%
acast(word ~ sentiment, value.var = "n", fill = 0) %>%
comparison.cloud(colors = c("gray20", "gray80"),
max.words = 100)
Joining, by = "word"

LS0tCnRpdGxlOiAiSW50cm9kdWN0aW9uIHRvIFRpZHkgVGV4dCBNaW5pbmciCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCldlIHdpbGwgYmUgcmVhZGluZyBbVGlkeSBUZXh0IE1pbmluZ10oaHR0cHM6Ly93d3cudGlkeXRleHRtaW5pbmcuY29tLykgdGhpcyBxdWFydGVyIGFuZCB0cnlpbmcgdG8gZ2V0IGEgZmVlbCBmb3Igd29ya2luZyB3aXRoIHRleHQgYmFzZWQgZGF0YS4KClRoZSBib29rIGZvbGxvd3MgdGhlIFRpZHkgYXBwcm9hY2g6CgotIEVhY2ggdmFyaWFibGUgaXMgYSBjb2x1bW4KLSBFYWNoIG9ic2VydmF0aW9uIGlzIGEgcm93Ci0gRWFjaCB0eXBlIG9mIG9ic2VydmF0aW9uYWwgdW5pdCBpcyBhIHRhYmxlCgpXaGF0IGlzIGEgdG9rZW4/ICBXaGF0IGlzIHRva2VuaXphdGlvbj8gIFdoYXQgaXMgYSBEb2N1bWVudCBUZXJtIE1hdHJpeD8gIFdoYXQgaXMgYSBUZXJtIERvY3VtZW50IE1hdHJpeD8gIFdoYXQgaXMgU2VudGltZW50IEFuYWx5c2lzPyAgV2hhdCBpcyBUb3BpYyBNb2RlbGluZz8gIGV0Yy4KCiMgVG9rZW5zCgpUbyBzdGFydCB0b2tlbnMgYXJlLCBhdCB0aGUgbW9zdCBiYXNpYyBsZXZlbCwgd29yZHMuCgpTdGFydCB3aXRoIGEgdmVjdG9yIG9mIHRleHQuCgpgYGB7cn0KdGV4dCA8LSBjKCJCZWNhdXNlIEkgY291bGQgbm90IHN0b3AgZm9yIERlYXRoIC0iLAogICAgICAgICAgIkhlIGtpbmRseSBzdG9wcGVkIGZvciBtZSAtIiwKICAgICAgICAgICJUaGUgQ2FycmlhZ2UgaGVsZCBidXQganVzdCBPdXJzZWx2ZXMgLSIsCiAgICAgICAgICAiYW5kIEltbW9ydGFsaXR5IikKCnRleHQKYGBgCgpQdXQgaXQgaW50byBhIGRhdGFmcmFtZS4gIE5vdGUgZGF0YV9mcmFtZSgpIGdpdmUgYSB0aWJibGUgZnJvbSB0aGUgdGlkZHl2ZXJzZS4KCmBgYHtyfQpsaWJyYXJ5KGRwbHlyKQp0ZXh0X2RmIDwtIGRhdGFfZnJhbWUobGluZSA9IDE6NCwgdGV4dCA9IHRleHQpCgp0ZXh0X2RmCmBgYAoKVG9rZW5pemUuCgpgYGB7cn0KbGlicmFyeSh0aWR5dGV4dCkKCnRleHRfZGYgJT4lCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0KQoKYGBgCgpOb3RlIGNhcGl0YWwgbGV0dGVycyBhcmUgbWFkZSBsb3dlciBjYXNlIGFuZCB0aGF0IHB1bmN0dWF0aW9uIGlzIHJlbW92ZWQuCgojIEJvb2tzCgoKYGBge3J9CmxpYnJhcnkoamFuZWF1c3RlbnIpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkoc3RyaW5ncikKCm9yaWdpbmFsX2Jvb2tzIDwtIGF1c3Rlbl9ib29rcygpICU+JQogIGdyb3VwX2J5KGJvb2spICU+JQogIG11dGF0ZShsaW5lbnVtYmVyID0gcm93X251bWJlcigpLCBjaGFwdGVyID0gY3Vtc3VtKHN0cl9kZXRlY3QodGV4dCwgcmVnZXgoIl5jaGFwdGVyIFtcXGRpdnhsY10iLGlnbm9yZV9jYXNlID0gVFJVRSkpKSkgJT4lCiAgdW5ncm91cCgpCgpvcmlnaW5hbF9ib29rcwpgYGAKCk9uZSB3b3JkIHBlciByb3cuCgoKYGBge3J9CmxpYnJhcnkodGlkeXRleHQpCnRpZHlfYm9va3MgPC0gb3JpZ2luYWxfYm9va3MgJT4lCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0KQoKdGlkeV9ib29rcwpgYGAKCkdldCByaWQgb2YgdGhlIHN0b3Agd29yZHMuCgpgYGB7cn0KZGF0YShzdG9wX3dvcmRzKQoKdGlkeV9ib29rcyA8LSB0aWR5X2Jvb2tzICU+JQogIGFudGlfam9pbihzdG9wX3dvcmRzKQpgYGAKClByb2R1Y2UgdGhlIHdvcmQgY291bnRzLgoKYGBge3J9CnRpZHlfYm9va3MgJT4lCiAgY291bnQod29yZCwgc29ydCA9IFRSVUUpIApgYGAKCldoYXQgYXJlIHRoZSBtb3N0IGZyZXF1ZW50IHdvcmRzPwoKYGBge3J9CmxpYnJhcnkoZ2dwbG90MikKCnRpZHlfYm9va3MgJT4lCiAgY291bnQod29yZCwgc29ydCA9IFRSVUUpICU+JQogIGZpbHRlcihuID4gNjAwKSAlPiUKICBtdXRhdGUod29yZCA9IHJlb3JkZXIod29yZCwgbikpICU+JQogIGdncGxvdChhZXMod29yZCwgbikpICsKICBnZW9tX2NvbCgpICsKICB4bGFiKE5VTEwpICsKICBjb29yZF9mbGlwKCkKYGBgCgpgYGB7ciB3YXJuaW5nPUZBTFNFfQpsaWJyYXJ5KHdvcmRjbG91ZCkKCnRpZHlfYm9va3MgJT4lCiAgYW50aV9qb2luKHN0b3Bfd29yZHMpICU+JQogIGNvdW50KHdvcmQpICU+JQogIHdpdGgod29yZGNsb3VkKHdvcmQsIG4sIG1heC53b3JkcyA9IDEwMCkpCmBgYAoKIyBCb29rcyBmcm9tIFtQcm9qZWN0IEd1dGVuYmVyZ10oaHR0cHM6Ly93d3cuZ3V0ZW5iZXJnLm9yZy8pLgoKIyMgSC5HLiBXZWxscwoKYGBge3J9CmxpYnJhcnkoZ3V0ZW5iZXJncikKCmhnd2VsbHMgPC0gZ3V0ZW5iZXJnX2Rvd25sb2FkKGMoMzUsIDM2LCA1MjMwLCAxNTkpKQoKaGd3ZWxscwpgYGAKCmBgYHtyfQp0aWR5X2hnd2VsbHMgPC0gaGd3ZWxscyAlPiUKICB1bm5lc3RfdG9rZW5zKHdvcmQsIHRleHQpICU+JQogIGFudGlfam9pbihzdG9wX3dvcmRzKQoKdGlkeV9oZ3dlbGxzCmBgYAoKYGBge3J9CnRpZHlfaGd3ZWxscyAlPiUKICBjb3VudCh3b3JkLCBzb3J0ID0gVFJVRSkKYGBgCgpgYGB7ciB3YXJuaW5nPUZBTFNFfQp0aWR5X2hnd2VsbHMgJT4lCiAgYW50aV9qb2luKHN0b3Bfd29yZHMpICU+JQogIGNvdW50KHdvcmQpICU+JQogIHdpdGgod29yZGNsb3VkKHdvcmQsIG4sIG1heC53b3JkcyA9IDEwMCkpCmBgYAoKCgojIyBFbWlseSBCcm9udGUKCmBgYHtyfQpicm9udGUgPC0gZ3V0ZW5iZXJnX2Rvd25sb2FkKGMoMTI2MCwgNzY4LCA5NjksIDkxODIsIDc2NykpCgpicm9udGUKYGBgCgpgYGB7cn0KdGlkeV9icm9udGUgPC0gYnJvbnRlICU+JQogIHVubmVzdF90b2tlbnMod29yZCwgdGV4dCkgJT4lCiAgYW50aV9qb2luKHN0b3Bfd29yZHMpCnRpZHlfYnJvbnRlICU+JQogIGNvdW50KHdvcmQsIHNvcnQgPSBUUlVFKQpgYGAKCmBgYHtyIHdhcm5pbmc9RkFMU0V9CmxpYnJhcnkod29yZGNsb3VkKQoKdGlkeV9icm9udGUgJT4lCiAgYW50aV9qb2luKHN0b3Bfd29yZHMpICU+JQogIGNvdW50KHdvcmQpICU+JQogIHdpdGgod29yZGNsb3VkKHdvcmQsIG4sIG1heC53b3JkcyA9IDEwMCkpCmBgYAoKIyBQdXQgdG9nZXRoZXIKCmBgYHtyfQpsaWJyYXJ5KHRpZHlyKQoKZnJlcXVlbmN5IDwtIGJpbmRfcm93cyhtdXRhdGUodGlkeV9icm9udGUsIGF1dGhvciA9ICJCcm9udMOrIFNpc3RlcnMiKSwKICAgICAgICAgICAgICAgICAgICAgICBtdXRhdGUodGlkeV9oZ3dlbGxzLCBhdXRob3IgPSAiSC5HLiBXZWxscyIpLCAKICAgICAgICAgICAgICAgICAgICAgICBtdXRhdGUodGlkeV9ib29rcywgYXV0aG9yID0gIkphbmUgQXVzdGVuIikpICU+JSAKICBtdXRhdGUod29yZCA9IHN0cl9leHRyYWN0KHdvcmQsICJbYS16J10rIikpICU+JQogIGNvdW50KGF1dGhvciwgd29yZCkgJT4lCiAgZ3JvdXBfYnkoYXV0aG9yKSAlPiUKICBtdXRhdGUocHJvcG9ydGlvbiA9IG4gLyBzdW0obikpICU+JSAKICBzZWxlY3QoLW4pICU+JSAKICBzcHJlYWQoYXV0aG9yLCBwcm9wb3J0aW9uKSAlPiUgCiAgZ2F0aGVyKGF1dGhvciwgcHJvcG9ydGlvbiwgYEJyb250w6sgU2lzdGVyc2A6YEguRy4gV2VsbHNgKQpgYGAKCmBgYHtyfQpsaWJyYXJ5KHNjYWxlcykKCiMgZXhwZWN0IGEgd2FybmluZyBhYm91dCByb3dzIHdpdGggbWlzc2luZyB2YWx1ZXMgYmVpbmcgcmVtb3ZlZApnZ3Bsb3QoZnJlcXVlbmN5LCBhZXMoeCA9IHByb3BvcnRpb24sIHkgPSBgSmFuZSBBdXN0ZW5gLCBjb2xvciA9IGFicyhgSmFuZSBBdXN0ZW5gIC0gcHJvcG9ydGlvbikpKSArCiAgZ2VvbV9hYmxpbmUoY29sb3IgPSAiZ3JheTQwIiwgbHR5ID0gMikgKwogIGdlb21faml0dGVyKGFscGhhID0gMC4xLCBzaXplID0gMi41LCB3aWR0aCA9IDAuMywgaGVpZ2h0ID0gMC4zKSArCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IHdvcmQpLCBjaGVja19vdmVybGFwID0gVFJVRSwgdmp1c3QgPSAxLjUpICsKICBzY2FsZV94X2xvZzEwKGxhYmVscyA9IHBlcmNlbnRfZm9ybWF0KCkpICsKICBzY2FsZV95X2xvZzEwKGxhYmVscyA9IHBlcmNlbnRfZm9ybWF0KCkpICsKICBzY2FsZV9jb2xvcl9ncmFkaWVudChsaW1pdHMgPSBjKDAsIDAuMDAxKSwgbG93ID0gImRhcmtzbGF0ZWdyYXk0IiwgaGlnaCA9ICJncmF5NzUiKSArCiAgZmFjZXRfd3JhcCh+YXV0aG9yLCBuY29sID0gMikgKwogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbj0ibm9uZSIpICsKICBsYWJzKHkgPSAiSmFuZSBBdXN0ZW4iLCB4ID0gTlVMTCkKYGBgCgojIyBBdXN0ZW4gd2l0aCBCcm9udGUKCmBgYHtyfQpjb3IudGVzdChkYXRhID0gZnJlcXVlbmN5W2ZyZXF1ZW5jeSRhdXRob3IgPT0gIkJyb250w6sgU2lzdGVycyIsXSwKICAgICAgICAgfiBwcm9wb3J0aW9uICsgYEphbmUgQXVzdGVuYCkKYGBgCgojIyBBdXN0ZW4gd2l0aCBXZWxscwoKYGBge3J9CmNvci50ZXN0KGRhdGEgPSBmcmVxdWVuY3lbZnJlcXVlbmN5JGF1dGhvciA9PSAiSC5HLiBXZWxscyIsXSwgCiAgICAgICAgIH4gcHJvcG9ydGlvbiArIGBKYW5lIEF1c3RlbmApCmBgYAoKIyBTZW50aW1lbnQgQW5hbHlzaXMKCk9waW5pb24uICBIYXBweSBvciBTYWQuCgpGcm9tIHRoZSBib29rLgoKIVtdKGh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS9pbWFnZXMvdGlkeWZsb3ctY2gtMi5wbmcpCgpTZW50aW1lbnQgbGV4aWNvbnMgYmFzZWQgb24gdW5pZ3JhbXMuCgpTZW50aW1lbnQgYW5kIEVtb3Rpb24uCgotIEFGSU5OCi0gYmluZwotbnJjCgpgYGB7cn0KbGlicmFyeSh0aWR5dGV4dCkKCnNlbnRpbWVudHMKYGBgCgpgYGB7cn0KZ2V0X3NlbnRpbWVudHMoImFmaW5uIikKYGBgCgoKYGBge3J9CmdldF9zZW50aW1lbnRzKCJiaW5nIikKYGBgCgpgYGB7cn0KZ2V0X3NlbnRpbWVudHMoIm5yYyIpCmBgYAoKYGBge3J9CmxpYnJhcnkoamFuZWF1c3RlbnIpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkoc3RyaW5ncikKCnRpZHlfYm9va3MgPC0gYXVzdGVuX2Jvb2tzKCkgJT4lCiAgZ3JvdXBfYnkoYm9vaykgJT4lCiAgbXV0YXRlKGxpbmVudW1iZXIgPSByb3dfbnVtYmVyKCksCiAgICAgICAgIGNoYXB0ZXIgPSBjdW1zdW0oc3RyX2RldGVjdCh0ZXh0LCByZWdleCgiXmNoYXB0ZXIgW1xcZGl2eGxjXSIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWdub3JlX2Nhc2UgPSBUUlVFKSkpKSAlPiUKICB1bmdyb3VwKCkgJT4lCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0KQpgYGAKCmBgYHtyfQpucmNqb3kgPC0gZ2V0X3NlbnRpbWVudHMoIm5yYyIpICU+JSAKICBmaWx0ZXIoc2VudGltZW50ID09ICJqb3kiKQoKdGlkeV9ib29rcyAlPiUKICBmaWx0ZXIoYm9vayA9PSAiRW1tYSIpICU+JQogIGlubmVyX2pvaW4obnJjam95KSAlPiUKICBjb3VudCh3b3JkLCBzb3J0ID0gVFJVRSkKYGBgCgpgYGB7cn0KbGlicmFyeSh0aWR5cikKCmphbmVhdXN0ZW5zZW50aW1lbnQgPC0gdGlkeV9ib29rcyAlPiUKICBpbm5lcl9qb2luKGdldF9zZW50aW1lbnRzKCJiaW5nIikpICU+JQogIGNvdW50KGJvb2ssIGluZGV4ID0gbGluZW51bWJlciAlLyUgODAsIHNlbnRpbWVudCkgJT4lCiAgc3ByZWFkKHNlbnRpbWVudCwgbiwgZmlsbCA9IDApICU+JQogIG11dGF0ZShzZW50aW1lbnQgPSBwb3NpdGl2ZSAtIG5lZ2F0aXZlKQpgYGAKCmBgYHtyfQpsaWJyYXJ5KGdncGxvdDIpCgpnZ3Bsb3QoamFuZWF1c3RlbnNlbnRpbWVudCwgYWVzKGluZGV4LCBzZW50aW1lbnQsIGZpbGwgPSBib29rKSkgKwogIGdlb21fY29sKHNob3cubGVnZW5kID0gRkFMU0UpICsKICBmYWNldF93cmFwKH5ib29rLCBuY29sID0gMiwgc2NhbGVzID0gImZyZWVfeCIpCmBgYAoKIyBDb21wYXJpbmcgdGhlIHRocmVlIHNlbnRpbWVudCBkaWN0aW9uYXJpZXMKCmBgYHtyfQpwcmlkZV9wcmVqdWRpY2UgPC0gdGlkeV9ib29rcyAlPiUgCiAgZmlsdGVyKGJvb2sgPT0gIlByaWRlICYgUHJlanVkaWNlIikKCnByaWRlX3ByZWp1ZGljZQpgYGAKCgpgYGB7cn0KYWZpbm4gPC0gcHJpZGVfcHJlanVkaWNlICU+JSAKICBpbm5lcl9qb2luKGdldF9zZW50aW1lbnRzKCJhZmlubiIpKSAlPiUgCiAgZ3JvdXBfYnkoaW5kZXggPSBsaW5lbnVtYmVyICUvJSA4MCkgJT4lIAogIHN1bW1hcmlzZShzZW50aW1lbnQgPSBzdW0oc2NvcmUpKSAlPiUgCiAgbXV0YXRlKG1ldGhvZCA9ICJBRklOTiIpCgpiaW5nX2FuZF9ucmMgPC0gYmluZF9yb3dzKHByaWRlX3ByZWp1ZGljZSAlPiUgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBpbm5lcl9qb2luKGdldF9zZW50aW1lbnRzKCJiaW5nIikpICU+JQogICAgICAgICAgICAgICAgICAgICAgICAgICAgbXV0YXRlKG1ldGhvZCA9ICJCaW5nIGV0IGFsLiIpLAogICAgICAgICAgICAgICAgICAgICAgICAgIHByaWRlX3ByZWp1ZGljZSAlPiUgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBpbm5lcl9qb2luKGdldF9zZW50aW1lbnRzKCJucmMiKSAlPiUgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmlsdGVyKHNlbnRpbWVudCAlaW4lIGMoInBvc2l0aXZlIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5lZ2F0aXZlIikpKSAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgICAgIG11dGF0ZShtZXRob2QgPSAiTlJDIikpICU+JQogIGNvdW50KG1ldGhvZCwgaW5kZXggPSBsaW5lbnVtYmVyICUvJSA4MCwgc2VudGltZW50KSAlPiUKICBzcHJlYWQoc2VudGltZW50LCBuLCBmaWxsID0gMCkgJT4lCiAgbXV0YXRlKHNlbnRpbWVudCA9IHBvc2l0aXZlIC0gbmVnYXRpdmUpCmBgYAoKYGBge3J9CmJpbmRfcm93cyhhZmlubiwgCiAgICAgICAgICBiaW5nX2FuZF9ucmMpICU+JQogIGdncGxvdChhZXMoaW5kZXgsIHNlbnRpbWVudCwgZmlsbCA9IG1ldGhvZCkpICsKICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKSArCiAgZmFjZXRfd3JhcCh+bWV0aG9kLCBuY29sID0gMSwgc2NhbGVzID0gImZyZWVfeSIpCmBgYAoKYGBge3J9CmdldF9zZW50aW1lbnRzKCJucmMiKSAlPiUgCiAgICAgZmlsdGVyKHNlbnRpbWVudCAlaW4lIGMoInBvc2l0aXZlIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5lZ2F0aXZlIikpICU+JSAKICBjb3VudChzZW50aW1lbnQpCgpgYGAKCmBgYHtyfQpnZXRfc2VudGltZW50cygiYmluZyIpICU+JSAKICBjb3VudChzZW50aW1lbnQpCmBgYAoKIyBNb3N0IGNvbW1vbiBwb3NpdGl2ZSBhbmQgbmVnYXRpdmUgd29yZHMKCmBgYHtyfQpiaW5nX3dvcmRfY291bnRzIDwtIHRpZHlfYm9va3MgJT4lCiAgaW5uZXJfam9pbihnZXRfc2VudGltZW50cygiYmluZyIpKSAlPiUKICBjb3VudCh3b3JkLCBzZW50aW1lbnQsIHNvcnQgPSBUUlVFKSAlPiUKICB1bmdyb3VwKCkKCmJpbmdfd29yZF9jb3VudHMKYGBgCgoKYGBge3J9CmJpbmdfd29yZF9jb3VudHMgJT4lCiAgZ3JvdXBfYnkoc2VudGltZW50KSAlPiUKICB0b3BfbigxMCkgJT4lCiAgdW5ncm91cCgpICU+JQogIG11dGF0ZSh3b3JkID0gcmVvcmRlcih3b3JkLCBuKSkgJT4lCiAgZ2dwbG90KGFlcyh3b3JkLCBuLCBmaWxsID0gc2VudGltZW50KSkgKwogIGdlb21fY29sKHNob3cubGVnZW5kID0gRkFMU0UpICsKICBmYWNldF93cmFwKH5zZW50aW1lbnQsIHNjYWxlcyA9ICJmcmVlX3kiKSArCiAgbGFicyh5ID0gIkNvbnRyaWJ1dGlvbiB0byBzZW50aW1lbnQiLAogICAgICAgeCA9IE5VTEwpICsKICBjb29yZF9mbGlwKCkKYGBgCgpgYGB7cn0KY3VzdG9tX3N0b3Bfd29yZHMgPC0gYmluZF9yb3dzKGRhdGFfZnJhbWUod29yZCA9IGMoIm1pc3MiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxleGljb24gPSBjKCJjdXN0b20iKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RvcF93b3JkcykKCmN1c3RvbV9zdG9wX3dvcmRzCmBgYAoKYGBge3J9CmxpYnJhcnkod29yZGNsb3VkKQoKdGlkeV9ib29rcyAlPiUKICBhbnRpX2pvaW4oc3RvcF93b3JkcykgJT4lCiAgY291bnQod29yZCkgJT4lCiAgd2l0aCh3b3JkY2xvdWQod29yZCwgbiwgbWF4LndvcmRzID0gMTAwKSkKYGBgCgoKYGBge3J9CmxpYnJhcnkocmVzaGFwZTIpCgp0aWR5X2Jvb2tzICU+JQogIGlubmVyX2pvaW4oZ2V0X3NlbnRpbWVudHMoImJpbmciKSkgJT4lCiAgY291bnQod29yZCwgc2VudGltZW50LCBzb3J0ID0gVFJVRSkgJT4lCiAgYWNhc3Qod29yZCB+IHNlbnRpbWVudCwgdmFsdWUudmFyID0gIm4iLCBmaWxsID0gMCkgJT4lCiAgY29tcGFyaXNvbi5jbG91ZChjb2xvcnMgPSBjKCJncmF5MjAiLCAiZ3JheTgwIiksCiAgICAgICAgICAgICAgICAgICBtYXgud29yZHMgPSAxMDApCmBgYAoK