This notebook provides an example of a sentiment analysis, a dictionary based method that is easy computed from the document term matrix.

Setup R

The methods in this notebook add another package to the standard list.

require(tm)
Loading required package: tm
Loading required package: NLP
require(tidytext)   # has dictionaries for sentiment analysis
Loading required package: tidytext
require(stringr)    # not in the tidyverse
Loading required package: stringr
require(tidyverse)
Loading required package: tidyverse
Loading tidyverse: ggplot2
Loading tidyverse: tibble
Loading tidyverse: tidyr
Loading tidyverse: readr
Loading tidyverse: purrr
Loading tidyverse: dplyr
Conflicts with tidy packages ----------------------------------------------------------------------
annotate(): ggplot2, NLP
filter():   dplyr, stats
lag():      dplyr, stats

Sentiment analysis

The tidytext package has 4 sentiment dictionaries. You can get a list of these from get_sentiments.

# get_sentiments("")

View is a very nice function to use in R-Studio. You cannot edit the data, just view it; it does, however, have some very nice sorting features. (If the option eval=FALSE is set in the header to an “R chunk”, R-Studio won’t evaluate that chunk unless you do it manually.)

View(get_sentiments('bing'))

The sorting is particularly handy in dictionaries with a variety of codings (Loughran and NRC).

View(get_sentiments('loughran'))

Negative words tend to dominate dictionaries. For example the Bing dictionary is dominated by negative words.

table(get_sentiments('bing')$sentiment)

negative positive 
    4782     2006 

Read the wine data from its CSV file. If this corpus were larger and the calculations took longer, it would be a good idea to save the “processed” data in a “.sav” file rather than process it again here.

Wine <- read_csv("../data/Wine.csv", col_types = cols(alcohol = col_double()))
dim(Wine)
[1] 20508    14
WineCorpus <- Corpus(VectorSource(Wine$description))
replace <- content_transformer(function(text, from, to) str_replace_all(text, from, to))
toSpace <- content_transformer(function(text, pattern) str_replace_all(text, pattern, " "))
toLower <- content_transformer(function(text) tolower(text))
WineCorpus <- tm_map(WineCorpus, toLower)
WineCorpus <- tm_map(WineCorpus, replace, "wieght", "weight")
WineCorpus <- tm_map(WineCorpus, toSpace, '-|/|,|\\.')     # otherwise runs together; dot is special regex
WineCorpus <- tm_map(WineCorpus, removePunctuation)
WineCorpus <- tm_map(WineCorpus, stripWhitespace)
WineCorpus <- tm_map(WineCorpus, removeWords, stopwords("english")) 

Sentiment analysis can be done efficiently from the document-term matrix because the DTM has the counts of all the words in the documents. (See the tidytext book for a different approach.)

Now compute the document term matrix, along with the row and column marginal counts. (The DTM is a little smaller, with fewer types – 5,412 – here than in the first slides because of handling the comma differently and removing the stop words.)

dtm <- DocumentTermMatrix(WineCorpus)
dtm
<<DocumentTermMatrix (documents: 20508, terms: 5412)>>
Non-/sparse entries: 475966/110513330
Sparsity           : 100%
Maximal term length: 15
Weighting          : term frequency (tf)
ni <- rowSums(as.matrix(dtm))
mj <- colSums(as.matrix(dtm))
word.types <- names(mj)   # for convenience and clarity

Looks like I have finally gotten rid of the run-together words (at least those that lead to long word types).

word.types[j <- which.max(str_length(word.types))]
[1] "extraordinarily"

I’ll do the sentiment analysis of the wines using the Bing lexicon of positive and negative words. (As some practice you should try do do this with the AFINN lexicon, the lexicon that assigns weights rather than just positive or negative.)

Bing <- get_sentiments('bing')
dim(Bing)
[1] 6788    2

Which of these words appear in the wine corpus? Pick out the word types that both appear in the wine corpus and in the Bing lexicon. The other are not relevant for the sentiment calculations.

keep.types <- intersect(Bing$word, word.types)
length(keep.types)
[1] 722

dplyr is handy for filtering the Bing data frame, picking the word types that meet this condition. Also add a numeric score to simplify a later calculation and keep the positive and negative terms separated.

Bing <- Bing %>%
    filter(word %in% keep.types) %>%
    mutate(pos = ifelse(sentiment=='positive',+1,0),
           neg = ifelse(sentiment=='negative',+1,0),
           score = pos - neg)
dim(Bing)
[1] 722   5

Have a peek, noticing that the words are sorted alphabetically.

View(Bing)

Now filter the DTM. the dtm object is a matrix, so use indices.

bing.dtm <- dtm[,word.types %in% keep.types]
bing.dtm
<<DocumentTermMatrix (documents: 20508, terms: 722)>>
Non-/sparse entries: 74785/14731991
Sparsity           : 99%
Maximal term length: 15
Weighting          : term frequency (tf)

Get the columns lined up. We want to make sure that the columns of the DTM are align with the elements of the sentiment dictionary. (Yes, its a little tricky to manage matrices mixed with data frames, but not that hard since we have one of each.)

counts <- bing.dtm[,Bing$word]
any(colnames(counts) != Bing$word)
[1] FALSE

Now counting the number of positive and negative words is easy (easy if you recognize the matrix connection). It’s a matrix multiplication; the call to as.vector converts the 1-column matrix into a vector.

rating.sentiment <- as.vector(as.matrix(bing.dtm) %*% Bing$score)

Since longer reviews accompany better wines, it is not too surprising that the typical net sentiment is also positive.

summary(rating.sentiment)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 -7.000   0.000   2.000   1.796   3.000  12.000 
hist(rating.sentiment)

Are these sentiments related to the number of points assigned to the wine?

plot(rating.sentiment, Wine$points, xlab="Sentiment", ylab="Points")

Some “dithering” (adding random variation – a bit of fuzz – to avoid over-printing) improves the plot, but it’s still overwhelmed by the volume of points. So I just drew a subset of the points.

dither <- function(x) return (x + rnorm(length(x),sd=0.05*sd(x, na.rm=TRUE)))
dither(1:10)
 [1]  1.418513  2.111122  2.927421  3.993517  5.222360  5.870933  6.940083  8.066629  9.233551
[10] 10.081846
i <- sample(1:length(rating.sentiment), 5000)
plot(dither(rating.sentiment[i]), dither(Wine$points[i]))

And if you make a data frame, ggplot is prettier still. It’s also then easy to add the regression line. There’s association, but it is not very strong.

data_frame(points = Wine$points, sentiment=rating.sentiment) %>%
    ggplot(aes(sentiment,points)) +
    geom_jitter(alpha=0.1) +        # alpha determines the opacity
    geom_smooth(method='lm')

The fit is significant, but explains very little variation.

summary(lm(Wine$points ~ rating.sentiment))

Call:
lm(formula = Wine$points ~ rating.sentiment)

Residuals:
    Min      1Q  Median      3Q     Max 
-7.7359 -2.1830 -0.0652  1.9348 12.2762 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)      86.39450    0.02878 3001.73   <2e-16 ***
rating.sentiment  0.22356    0.01064   21.01   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 3.07 on 20327 degrees of freedom
  (179 observations deleted due to missingness)
Multiple R-squared:  0.02126,   Adjusted R-squared:  0.02121 
F-statistic: 441.6 on 1 and 20327 DF,  p-value: < 2.2e-16

Who says negative words and positive words should have the same weight?

rating.pos <- as.vector(as.matrix(bing.dtm) %*% Bing$pos)
rating.neg <- as.vector(as.matrix(bing.dtm) %*% Bing$neg)
any(rating.pos-rating.neg != rating.sentiment)   # all should match!
[1] FALSE

It seems like negative words have a much stronger connection to the points than those effusive positive words, with some noticeable nonlinearity too. The interaction is not significant, but the nonlinear terms (squares) are, particularly the for the count of negative words.

summary(lm(Wine$points ~ rating.pos * rating.neg + I(rating.pos^2) + I(rating.neg^2)))

Call:
lm(formula = Wine$points ~ rating.pos * rating.neg + I(rating.pos^2) + 
    I(rating.neg^2))

Residuals:
     Min       1Q   Median       3Q      Max 
-11.1680  -1.9064   0.0936   1.8084  12.1369 

Coefficients:
                       Estimate Std. Error  t value Pr(>|t|)    
(Intercept)           84.800892   0.063968 1325.670  < 2e-16 ***
rating.pos             0.582570   0.032998   17.655  < 2e-16 ***
rating.neg             0.293899   0.054162    5.426 5.82e-08 ***
I(rating.pos^2)       -0.014903   0.004193   -3.554  0.00038 ***
I(rating.neg^2)        0.096777   0.011990    8.072 7.31e-16 ***
rating.pos:rating.neg  0.019317   0.011319    1.707  0.08791 .  
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 2.883 on 20323 degrees of freedom
  (179 observations deleted due to missingness)
Multiple R-squared:  0.137, Adjusted R-squared:  0.1368 
F-statistic: 645.4 on 5 and 20323 DF,  p-value: < 2.2e-16

Make your own dictionary

Because we have a response (namely, the points awarded to a wine), we can get a hint of how to make your own task-specific dictionary. Let’s focus for this example on the words that show up frequently, those that show up in at least 500 times in the tasting notes. (Note: If this were “for real”, I would need to reserve a substantial portion of the tasting notes for a test data set.)

sum(500 <= mj)
[1] 175

Now regress the points on the number of times these words appear.

X <- as.matrix(dtm[,500 <= mj])

Most only occur once in a document.

sum(X)
[1] 360936
sum(1<X)
[1] 12645

Now do the big regression. Most of the wines have the points variable.

i <- which(!is.na(Wine$points))
length(i)
[1] 20329
y <- Wine$points[i]
X <- X[i,]

Run the regression of the points on these counts of words.

regr <- lm(y ~ X)
summary(regr)$r.squared
[1] 0.5755442

When looking at the estimates, it seems “lemon” is indeed a negative word when it comes to wine tasting points, but the effect is very small.

b <- coef(regr)[-1]  # drop the intercept
b <- b[order(abs(b), decreasing=TRUE)]
b[1:20]  #  big ones
     Xgreat       Xlong      Xquick    Xlengthy  Xdelicious  Xexcellent       Xlush    Xelegant 
  1.7001641   1.6775833  -1.6437744   1.6311694   1.5958857   1.3278610   1.0956059   1.0868234 
     Xdrink      Xshort Xstructured       Xfull    Xvibrant       Xrich       Xwill     Xmodest 
 -1.0501087  -0.9903800   0.9339117   0.9140514   0.8840144   0.8488617   0.8426404  -0.8184256 
 Xflavorful      Xzesty    Xhoneyed      Xsilky 
  0.8154570   0.8080806   0.7904664   0.7619647 
tail(b, n=20)  # small ones
      Xlemon       Xthyme       Xcedar       Xround      Xfollow      Xcherry     Xlightly 
-0.035476250 -0.035210841 -0.033114671 -0.031758445 -0.028855524 -0.026968057 -0.025587830 
    Xberries       Xwhite        Xplum     Xgrilled         Xdry         Xoak      Xmedium 
-0.025236926 -0.021022113  0.020827858 -0.016786299  0.015965913 -0.014241626 -0.012870548 
   Xpleasant     Xacidity      Xbodied       Xdried  Xblackberry     Xpeppery 
 0.011626942  0.009080301  0.007850889 -0.006091515  0.002402311  0.001943533 

Just because you have a large estimated coefficient doesn’t mean that the word should be scored with that weight – you should also check for statistical precision. Some rounding and perhaps shrinkage toward zero would also make sense, perhaps by ridge or lasso methods. You don’t see weights in lexicons with several digits of precision.

Finally, R has “messed up” the names by prefixing the name of the matrix “X”. We can fix that easily with the string tools: use a regular expression to replace the first character by nothing.

str_replace(names(b),".","")[1:10]
 [1] "great"     "long"      "quick"     "lengthy"   "delicious" "excellent" "lush"     
 [8] "elegant"   "drink"     "short"    

names

LS0tCnRpdGxlOiAiVGV4dCBhcyBEYXRhOiBWZWN0b3IgU3BhY2UgTW9kZWxzIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKYXV0aG9yOiBSb2JlcnQgU3RpbmUKZGF0ZTogSnVseSAyMDE3Ci0tLQoKVGhpcyBub3RlYm9vayBwcm92aWRlcyBhbiBleGFtcGxlIG9mIGEgICpzZW50aW1lbnQgYW5hbHlzaXMqLCBhIGRpY3Rpb25hcnkgYmFzZWQgbWV0aG9kIHRoYXQgaXMgZWFzeSBjb21wdXRlZCBmcm9tIHRoZSBkb2N1bWVudCB0ZXJtIG1hdHJpeC4KCgojIFNldHVwIFIKClRoZSBtZXRob2RzIGluIHRoaXMgbm90ZWJvb2sgYWRkIGFub3RoZXIgcGFja2FnZSB0byB0aGUgc3RhbmRhcmQgbGlzdC4KCmBgYHtyfQpyZXF1aXJlKHRtKQoKcmVxdWlyZSh0aWR5dGV4dCkgICAjIGhhcyBkaWN0aW9uYXJpZXMgZm9yIHNlbnRpbWVudCBhbmFseXNpcwpyZXF1aXJlKHN0cmluZ3IpICAgICMgbm90IGluIHRoZSB0aWR5dmVyc2UKcmVxdWlyZSh0aWR5dmVyc2UpCmBgYAoKIyBTZW50aW1lbnQgYW5hbHlzaXMKClRoZSBgdGlkeXRleHRgIHBhY2thZ2UgaGFzIDQgc2VudGltZW50IGRpY3Rpb25hcmllcy4gIFlvdSBjYW4gZ2V0IGEgbGlzdCBvZiB0aGVzZSBmcm9tIGBnZXRfc2VudGltZW50c2AuCgpgYGB7cn0KIyBnZXRfc2VudGltZW50cygiIikKYGBgCgpgVmlld2AgaXMgYSB2ZXJ5IG5pY2UgZnVuY3Rpb24gdG8gdXNlIGluIFItU3R1ZGlvLiAgWW91IGNhbm5vdCBlZGl0IHRoZSBkYXRhLCBqdXN0IHZpZXcgaXQ7IGl0IGRvZXMsIGhvd2V2ZXIsIGhhdmUgc29tZSB2ZXJ5IG5pY2Ugc29ydGluZyBmZWF0dXJlcy4gKElmIHRoZSBvcHRpb24gZXZhbD1GQUxTRSBpcyBzZXQgaW4gdGhlIGhlYWRlciB0byBhbiAiUiBjaHVuayIsIFItU3R1ZGlvIHdvbid0IGV2YWx1YXRlIHRoYXQgY2h1bmsgdW5sZXNzIHlvdSBkbyBpdCBtYW51YWxseS4pCgpgYGB7ciBldmFsPUZBTFNFfQpWaWV3KGdldF9zZW50aW1lbnRzKCdiaW5nJykpCmBgYAoKVGhlIHNvcnRpbmcgaXMgcGFydGljdWxhcmx5IGhhbmR5IGluIGRpY3Rpb25hcmllcyB3aXRoIGEgdmFyaWV0eSBvZiBjb2RpbmdzIChMb3VnaHJhbiBhbmQgTlJDKS4KCmBgYHtyIGV2YWw9RkFMU0V9ClZpZXcoZ2V0X3NlbnRpbWVudHMoJ2xvdWdocmFuJykpCmBgYAoKKk5lZ2F0aXZlKiB3b3JkcyB0ZW5kIHRvIGRvbWluYXRlIGRpY3Rpb25hcmllcy4gIEZvciBleGFtcGxlIHRoZSBCaW5nIGRpY3Rpb25hcnkgaXMgZG9taW5hdGVkIGJ5IG5lZ2F0aXZlIHdvcmRzLgoKYGBge3J9CnRhYmxlKGdldF9zZW50aW1lbnRzKCdiaW5nJykkc2VudGltZW50KQpgYGAKClJlYWQgdGhlIHdpbmUgZGF0YSBmcm9tIGl0cyBDU1YgZmlsZS4gIElmIHRoaXMgY29ycHVzIHdlcmUgbGFyZ2VyIGFuZCB0aGUgY2FsY3VsYXRpb25zIHRvb2sgbG9uZ2VyLCBpdCB3b3VsZCBiZSBhIGdvb2QgaWRlYSB0byBzYXZlIHRoZSAicHJvY2Vzc2VkIiBkYXRhIGluIGEgIi5zYXYiIGZpbGUgcmF0aGVyIHRoYW4gcHJvY2VzcyBpdCBhZ2FpbiBoZXJlLgoKYGBge3J9CldpbmUgPC0gcmVhZF9jc3YoIi4uL2RhdGEvV2luZS5jc3YiLCBjb2xfdHlwZXMgPSBjb2xzKGFsY29ob2wgPSBjb2xfZG91YmxlKCkpKQpkaW0oV2luZSkKYGBgCgpgYGB7cn0KV2luZUNvcnB1cyA8LSBDb3JwdXMoVmVjdG9yU291cmNlKFdpbmUkZGVzY3JpcHRpb24pKQoKcmVwbGFjZSA8LSBjb250ZW50X3RyYW5zZm9ybWVyKGZ1bmN0aW9uKHRleHQsIGZyb20sIHRvKSBzdHJfcmVwbGFjZV9hbGwodGV4dCwgZnJvbSwgdG8pKQp0b1NwYWNlIDwtIGNvbnRlbnRfdHJhbnNmb3JtZXIoZnVuY3Rpb24odGV4dCwgcGF0dGVybikgc3RyX3JlcGxhY2VfYWxsKHRleHQsIHBhdHRlcm4sICIgIikpCnRvTG93ZXIgPC0gY29udGVudF90cmFuc2Zvcm1lcihmdW5jdGlvbih0ZXh0KSB0b2xvd2VyKHRleHQpKQoKV2luZUNvcnB1cyA8LSB0bV9tYXAoV2luZUNvcnB1cywgdG9Mb3dlcikKV2luZUNvcnB1cyA8LSB0bV9tYXAoV2luZUNvcnB1cywgcmVwbGFjZSwgIndpZWdodCIsICJ3ZWlnaHQiKQpXaW5lQ29ycHVzIDwtIHRtX21hcChXaW5lQ29ycHVzLCB0b1NwYWNlLCAnLXwvfCx8XFwuJykgICAgICMgb3RoZXJ3aXNlIHJ1bnMgdG9nZXRoZXI7IGRvdCBpcyBzcGVjaWFsIHJlZ2V4CldpbmVDb3JwdXMgPC0gdG1fbWFwKFdpbmVDb3JwdXMsIHJlbW92ZVB1bmN0dWF0aW9uKQpXaW5lQ29ycHVzIDwtIHRtX21hcChXaW5lQ29ycHVzLCBzdHJpcFdoaXRlc3BhY2UpCldpbmVDb3JwdXMgPC0gdG1fbWFwKFdpbmVDb3JwdXMsIHJlbW92ZVdvcmRzLCBzdG9wd29yZHMoImVuZ2xpc2giKSkgCmBgYAoKU2VudGltZW50IGFuYWx5c2lzIGNhbiBiZSBkb25lIGVmZmljaWVudGx5IGZyb20gdGhlIGRvY3VtZW50LXRlcm0gbWF0cml4IGJlY2F1c2UgdGhlIERUTSBoYXMgdGhlIGNvdW50cyBvZiBhbGwgdGhlIHdvcmRzIGluIHRoZSBkb2N1bWVudHMuICAoU2VlIHRoZSB0aWR5dGV4dCBib29rIGZvciBhIGRpZmZlcmVudCBhcHByb2FjaC4pIAoKTm93IGNvbXB1dGUgdGhlIGRvY3VtZW50IHRlcm0gbWF0cml4LCBhbG9uZyB3aXRoIHRoZSByb3cgYW5kIGNvbHVtbiBtYXJnaW5hbCBjb3VudHMuIChUaGUgRFRNIGlzIGEgbGl0dGxlIHNtYWxsZXIsIHdpdGggZmV3ZXIgdHlwZXMgLS0gNSw0MTIgLS0gaGVyZSB0aGFuIGluIHRoZSBmaXJzdCBzbGlkZXMgYmVjYXVzZSBvZiBoYW5kbGluZyB0aGUgY29tbWEgZGlmZmVyZW50bHkgYW5kIHJlbW92aW5nIHRoZSBzdG9wIHdvcmRzLikKCmBgYHtyfQpkdG0gPC0gRG9jdW1lbnRUZXJtTWF0cml4KFdpbmVDb3JwdXMpCmR0bQoKbmkgPC0gcm93U3Vtcyhhcy5tYXRyaXgoZHRtKSkKbWogPC0gY29sU3Vtcyhhcy5tYXRyaXgoZHRtKSkKCndvcmQudHlwZXMgPC0gbmFtZXMobWopICAgIyBmb3IgY29udmVuaWVuY2UgYW5kIGNsYXJpdHkKYGBgCgpMb29rcyBsaWtlIEkgaGF2ZSBmaW5hbGx5IGdvdHRlbiByaWQgb2YgdGhlIHJ1bi10b2dldGhlciB3b3JkcyAoYXQgbGVhc3QgdGhvc2UgdGhhdCBsZWFkIHRvIGxvbmcgd29yZCB0eXBlcykuCgpgYGB7cn0Kd29yZC50eXBlc1tqIDwtIHdoaWNoLm1heChzdHJfbGVuZ3RoKHdvcmQudHlwZXMpKV0KYGBgCgpJJ2xsIGRvIHRoZSBzZW50aW1lbnQgYW5hbHlzaXMgb2YgdGhlIHdpbmVzIHVzaW5nIHRoZSBCaW5nIGxleGljb24gb2YgcG9zaXRpdmUgYW5kIG5lZ2F0aXZlIHdvcmRzLiAoQXMgc29tZSBwcmFjdGljZSB5b3Ugc2hvdWxkIHRyeSBkbyBkbyB0aGlzIHdpdGggdGhlIEFGSU5OIGxleGljb24sIHRoZSBsZXhpY29uIHRoYXQgYXNzaWducyB3ZWlnaHRzIHJhdGhlciB0aGFuIGp1c3QgcG9zaXRpdmUgb3IgbmVnYXRpdmUuKQoKYGBge3J9CkJpbmcgPC0gZ2V0X3NlbnRpbWVudHMoJ2JpbmcnKQpkaW0oQmluZykKYGBgCgpXaGljaCBvZiB0aGVzZSB3b3JkcyBhcHBlYXIgaW4gdGhlIHdpbmUgY29ycHVzPyAgUGljayBvdXQgdGhlIHdvcmQgdHlwZXMgdGhhdCBib3RoIGFwcGVhciBpbiB0aGUgd2luZSBjb3JwdXMgKmFuZCogaW4gdGhlIEJpbmcgbGV4aWNvbi4gVGhlIG90aGVyIGFyZSBub3QgcmVsZXZhbnQgZm9yIHRoZSBzZW50aW1lbnQgY2FsY3VsYXRpb25zLgoKYGBge3J9CmtlZXAudHlwZXMgPC0gaW50ZXJzZWN0KEJpbmckd29yZCwgd29yZC50eXBlcykKbGVuZ3RoKGtlZXAudHlwZXMpCmBgYAoKYGRwbHlyYCBpcyBoYW5keSBmb3IgZmlsdGVyaW5nIHRoZSBCaW5nIGRhdGEgZnJhbWUsIHBpY2tpbmcgdGhlIHdvcmQgdHlwZXMgdGhhdCBtZWV0IHRoaXMgY29uZGl0aW9uLiAgQWxzbyBhZGQgYSBudW1lcmljIHNjb3JlIHRvIHNpbXBsaWZ5IGEgbGF0ZXIgY2FsY3VsYXRpb24gYW5kIGtlZXAgdGhlIHBvc2l0aXZlIGFuZCBuZWdhdGl2ZSB0ZXJtcyBzZXBhcmF0ZWQuCiAKYGBge3J9CkJpbmcgPC0gQmluZyAlPiUKICAgIGZpbHRlcih3b3JkICVpbiUga2VlcC50eXBlcykgJT4lCiAgICBtdXRhdGUocG9zID0gaWZlbHNlKHNlbnRpbWVudD09J3Bvc2l0aXZlJywrMSwwKSwKICAgICAgICAgICBuZWcgPSBpZmVsc2Uoc2VudGltZW50PT0nbmVnYXRpdmUnLCsxLDApLAogICAgICAgICAgIHNjb3JlID0gcG9zIC0gbmVnKQpkaW0oQmluZykKYGBgCgpIYXZlIGEgcGVlaywgbm90aWNpbmcgdGhhdCB0aGUgd29yZHMgYXJlIHNvcnRlZCBhbHBoYWJldGljYWxseS4KCmBgYHtyIGV2YWw9RkFMU0V9ClZpZXcoQmluZykKYGBgCgpOb3cgZmlsdGVyIHRoZSBEVE0uICB0aGUgYGR0bWAgb2JqZWN0IGlzIGEgbWF0cml4LCBzbyB1c2UgaW5kaWNlcy4gCgpgYGB7cn0KYmluZy5kdG0gPC0gZHRtWyx3b3JkLnR5cGVzICVpbiUga2VlcC50eXBlc10KYmluZy5kdG0KYGBgCgpHZXQgdGhlIGNvbHVtbnMgbGluZWQgdXAuIFdlIHdhbnQgdG8gbWFrZSBzdXJlIHRoYXQgdGhlIGNvbHVtbnMgb2YgdGhlIERUTSBhcmUgYWxpZ24gd2l0aCB0aGUgZWxlbWVudHMgb2YgdGhlIHNlbnRpbWVudCBkaWN0aW9uYXJ5LiAoWWVzLCBpdHMgYSBsaXR0bGUgdHJpY2t5IHRvIG1hbmFnZSBtYXRyaWNlcyBtaXhlZCB3aXRoIGRhdGEgZnJhbWVzLCBidXQgbm90IHRoYXQgaGFyZCBzaW5jZSB3ZSBoYXZlIG9uZSBvZiBlYWNoLikKCmBgYHtyfQpjb3VudHMgPC0gYmluZy5kdG1bLEJpbmckd29yZF0KYGBgCgpgYGB7cn0KYW55KGNvbG5hbWVzKGNvdW50cykgIT0gQmluZyR3b3JkKQpgYGAKCk5vdyBjb3VudGluZyB0aGUgbnVtYmVyIG9mIHBvc2l0aXZlIGFuZCBuZWdhdGl2ZSB3b3JkcyBpcyBlYXN5IChlYXN5IGlmIHlvdSByZWNvZ25pemUgdGhlIG1hdHJpeCBjb25uZWN0aW9uKS4gIEl0J3MgYSBtYXRyaXggbXVsdGlwbGljYXRpb247IHRoZSBjYWxsIHRvIGBhcy52ZWN0b3JgIGNvbnZlcnRzIHRoZSAxLWNvbHVtbiBtYXRyaXggaW50byBhIHZlY3Rvci4KCmBgYHtyfQpyYXRpbmcuc2VudGltZW50IDwtIGFzLnZlY3Rvcihhcy5tYXRyaXgoYmluZy5kdG0pICUqJSBCaW5nJHNjb3JlKQpgYGAKClNpbmNlIGxvbmdlciByZXZpZXdzIGFjY29tcGFueSBiZXR0ZXIgd2luZXMsIGl0IGlzIG5vdCB0b28gc3VycHJpc2luZyB0aGF0IHRoZSB0eXBpY2FsIG5ldCBzZW50aW1lbnQgaXMgYWxzbyBwb3NpdGl2ZS4gCgpgYGB7cn0Kc3VtbWFyeShyYXRpbmcuc2VudGltZW50KQpgYGAKCmBgYHtyfQpoaXN0KHJhdGluZy5zZW50aW1lbnQpCmBgYAoKQXJlIHRoZXNlIHNlbnRpbWVudHMgcmVsYXRlZCB0byB0aGUgbnVtYmVyIG9mIHBvaW50cyBhc3NpZ25lZCB0byB0aGUgd2luZT8KCmBgYHtyfQpwbG90KHJhdGluZy5zZW50aW1lbnQsIFdpbmUkcG9pbnRzLCB4bGFiPSJTZW50aW1lbnQiLCB5bGFiPSJQb2ludHMiKQpgYGAKClNvbWUgImRpdGhlcmluZyIgKGFkZGluZyByYW5kb20gdmFyaWF0aW9uIC0tIGEgYml0IG9mIGZ1enogLS0gdG8gYXZvaWQgb3Zlci1wcmludGluZykgaW1wcm92ZXMgdGhlIHBsb3QsIGJ1dCBpdCdzIHN0aWxsIG92ZXJ3aGVsbWVkIGJ5IHRoZSB2b2x1bWUgb2YgcG9pbnRzLiAgU28gSSBqdXN0IGRyZXcgYSBzdWJzZXQgb2YgdGhlIHBvaW50cy4KCmBgYHtyfQpkaXRoZXIgPC0gZnVuY3Rpb24oeCkgcmV0dXJuICh4ICsgcm5vcm0obGVuZ3RoKHgpLHNkPTAuMDUqc2QoeCwgbmEucm09VFJVRSkpKQpkaXRoZXIoMToxMCkKYGBgCgpgYGB7cn0KaSA8LSBzYW1wbGUoMTpsZW5ndGgocmF0aW5nLnNlbnRpbWVudCksIDUwMDApCnBsb3QoZGl0aGVyKHJhdGluZy5zZW50aW1lbnRbaV0pLCBkaXRoZXIoV2luZSRwb2ludHNbaV0pKQpgYGAKCkFuZCBpZiB5b3UgbWFrZSBhIGRhdGEgZnJhbWUsIGBnZ3Bsb3RgIGlzIHByZXR0aWVyIHN0aWxsLiAgSXQncyBhbHNvIHRoZW4gZWFzeSB0byBhZGQgdGhlIHJlZ3Jlc3Npb24gbGluZS4gIFRoZXJlJ3MgYXNzb2NpYXRpb24sIGJ1dCBpdCBpcyBub3QgdmVyeSBzdHJvbmcuCgpgYGB7cn0KZGF0YV9mcmFtZShwb2ludHMgPSBXaW5lJHBvaW50cywgc2VudGltZW50PXJhdGluZy5zZW50aW1lbnQpICU+JQogICAgZ2dwbG90KGFlcyhzZW50aW1lbnQscG9pbnRzKSkgKwogICAgZ2VvbV9qaXR0ZXIoYWxwaGE9MC4xKSArICAgICAgICAjIGFscGhhIGRldGVybWluZXMgdGhlIG9wYWNpdHkKICAgIGdlb21fc21vb3RoKG1ldGhvZD0nbG0nKQpgYGAKClRoZSBmaXQgaXMgc2lnbmlmaWNhbnQsIGJ1dCBleHBsYWlucyB2ZXJ5IGxpdHRsZSB2YXJpYXRpb24uCgpgYGB7cn0Kc3VtbWFyeShsbShXaW5lJHBvaW50cyB+IHJhdGluZy5zZW50aW1lbnQpKQpgYGAKCldobyBzYXlzIG5lZ2F0aXZlIHdvcmRzIGFuZCBwb3NpdGl2ZSB3b3JkcyBzaG91bGQgaGF2ZSB0aGUgc2FtZSB3ZWlnaHQ/CgpgYGB7cn0KcmF0aW5nLnBvcyA8LSBhcy52ZWN0b3IoYXMubWF0cml4KGJpbmcuZHRtKSAlKiUgQmluZyRwb3MpCnJhdGluZy5uZWcgPC0gYXMudmVjdG9yKGFzLm1hdHJpeChiaW5nLmR0bSkgJSolIEJpbmckbmVnKQoKYW55KHJhdGluZy5wb3MtcmF0aW5nLm5lZyAhPSByYXRpbmcuc2VudGltZW50KSAgICMgYWxsIHNob3VsZCBtYXRjaCEKYGBgCgpJdCBzZWVtcyBsaWtlIG5lZ2F0aXZlIHdvcmRzIGhhdmUgYSBtdWNoIHN0cm9uZ2VyIGNvbm5lY3Rpb24gdG8gdGhlIHBvaW50cyB0aGFuIHRob3NlIGVmZnVzaXZlIHBvc2l0aXZlIHdvcmRzLCB3aXRoIHNvbWUgbm90aWNlYWJsZSBub25saW5lYXJpdHkgdG9vLiAgVGhlIGludGVyYWN0aW9uIGlzIG5vdCBzaWduaWZpY2FudCwgYnV0IHRoZSBub25saW5lYXIgdGVybXMgKHNxdWFyZXMpIGFyZSwgcGFydGljdWxhcmx5IHRoZSBmb3IgdGhlIGNvdW50IG9mIG5lZ2F0aXZlIHdvcmRzLgoKYGBge3J9CnN1bW1hcnkobG0oV2luZSRwb2ludHMgfiByYXRpbmcucG9zICogcmF0aW5nLm5lZyArIEkocmF0aW5nLnBvc14yKSArIEkocmF0aW5nLm5lZ14yKSkpCmBgYAoKCiMgTWFrZSB5b3VyIG93biBkaWN0aW9uYXJ5CgpCZWNhdXNlIHdlIGhhdmUgYSByZXNwb25zZSAobmFtZWx5LCB0aGUgcG9pbnRzIGF3YXJkZWQgdG8gYSB3aW5lKSwgd2UgY2FuIGdldCBhIGhpbnQgb2YgaG93IHRvIG1ha2UgeW91ciBvd24gdGFzay1zcGVjaWZpYyBkaWN0aW9uYXJ5LiAgTGV0J3MgZm9jdXMgZm9yIHRoaXMgZXhhbXBsZSBvbiB0aGUgd29yZHMgdGhhdCBzaG93IHVwIGZyZXF1ZW50bHksIHRob3NlIHRoYXQgc2hvdyB1cCBpbiBhdCBsZWFzdCA1MDAgdGltZXMgaW4gdGhlIHRhc3Rpbmcgbm90ZXMuICAoTm90ZTogSWYgdGhpcyB3ZXJlICJmb3IgcmVhbCIsIEkgd291bGQgbmVlZCB0byByZXNlcnZlIGEgc3Vic3RhbnRpYWwgcG9ydGlvbiBvZiB0aGUgdGFzdGluZyBub3RlcyBmb3IgYSB0ZXN0IGRhdGEgc2V0LikKCmBgYHtyfQpzdW0oNTAwIDw9IG1qKQpgYGAKCk5vdyByZWdyZXNzIHRoZSBwb2ludHMgb24gdGhlIG51bWJlciBvZiB0aW1lcyB0aGVzZSB3b3JkcyBhcHBlYXIuCgpgYGB7cn0KWCA8LSBhcy5tYXRyaXgoZHRtWyw1MDAgPD0gbWpdKQpgYGAKCk1vc3Qgb25seSBvY2N1ciBvbmNlIGluIGEgZG9jdW1lbnQuCgpgYGB7cn0Kc3VtKFgpCnN1bSgxPFgpCmBgYAoKTm93IGRvIHRoZSBiaWcgcmVncmVzc2lvbi4gIE1vc3Qgb2YgdGhlIHdpbmVzIGhhdmUgdGhlIHBvaW50cyB2YXJpYWJsZS4KCmBgYHtyfQppIDwtIHdoaWNoKCFpcy5uYShXaW5lJHBvaW50cykpCmxlbmd0aChpKQpgYGAKCmBgYHtyfQp5IDwtIFdpbmUkcG9pbnRzW2ldClggPC0gWFtpLF0KYGBgCgpSdW4gdGhlIHJlZ3Jlc3Npb24gb2YgdGhlIHBvaW50cyBvbiB0aGVzZSBjb3VudHMgb2Ygd29yZHMuCgpgYGB7cn0KcmVnciA8LSBsbSh5IH4gWCkKc3VtbWFyeShyZWdyKSRyLnNxdWFyZWQKYGBgCgpXaGVuIGxvb2tpbmcgYXQgdGhlIGVzdGltYXRlcywgaXQgc2VlbXMgImxlbW9uIiBpcyBpbmRlZWQgYSBuZWdhdGl2ZSB3b3JkIHdoZW4gaXQgY29tZXMgdG8gd2luZSB0YXN0aW5nIHBvaW50cywgYnV0IHRoZSBlZmZlY3QgaXMgdmVyeSBzbWFsbC4KCmBgYHtyfQpiIDwtIGNvZWYocmVncilbLTFdICAjIGRyb3AgdGhlIGludGVyY2VwdApiIDwtIGJbb3JkZXIoYWJzKGIpLCBkZWNyZWFzaW5nPVRSVUUpXQpiWzE6MjBdICAjICBiaWcgb25lcwp0YWlsKGIsIG49MjApICAjIHNtYWxsIG9uZXMKYGBgCgpKdXN0IGJlY2F1c2UgeW91IGhhdmUgYSBsYXJnZSBlc3RpbWF0ZWQgY29lZmZpY2llbnQgZG9lc24ndCBtZWFuIHRoYXQgdGhlIHdvcmQgc2hvdWxkIGJlIHNjb3JlZCB3aXRoIHRoYXQgd2VpZ2h0IC0tIHlvdSBzaG91bGQgYWxzbyBjaGVjayBmb3Igc3RhdGlzdGljYWwgcHJlY2lzaW9uLiAgU29tZSByb3VuZGluZyBhbmQgcGVyaGFwcyBzaHJpbmthZ2UgdG93YXJkIHplcm8gd291bGQgYWxzbyBtYWtlIHNlbnNlLCBwZXJoYXBzIGJ5IHJpZGdlIG9yIGxhc3NvIG1ldGhvZHMuICBZb3UgZG9uJ3Qgc2VlIHdlaWdodHMgaW4gbGV4aWNvbnMgd2l0aCBzZXZlcmFsIGRpZ2l0cyBvZiBwcmVjaXNpb24uCgpGaW5hbGx5LCBSIGhhcyAibWVzc2VkIHVwIiB0aGUgbmFtZXMgYnkgcHJlZml4aW5nIHRoZSBuYW1lIG9mIHRoZSBtYXRyaXggIlgiLiAgV2UgY2FuIGZpeCB0aGF0IGVhc2lseSB3aXRoIHRoZSBzdHJpbmcgdG9vbHM6IHVzZSBhIHJlZ3VsYXIgZXhwcmVzc2lvbiB0byByZXBsYWNlIHRoZSBmaXJzdCBjaGFyYWN0ZXIgYnkgbm90aGluZy4KCmBgYHtyfQpzdHJfcmVwbGFjZShuYW1lcyhiKSwiLiIsIiIpWzE6MTBdCmBgYAoKbmFtZXM=