One question I’ve heard repeatedly since beginning to work with text data and sentiment analysis is how much text we really need to get a solid idea of aggregate sentiments. The law of large numbers suggests that more text is better, but if we only have a little bit of text, can sentiment analysis still work?

In a previous post I examined the accuracy of the sentiment lexicons in the tidytext package. Here I attempt to find the point at which we have enough text to measure sentiments precisely.

Getting Started

The first step is to read a sample of the Yelp Academic Dataset. In this case, I’m starting with a subset of 200,000 reviews out of the 4.7 million provided.

Now, to produce sentiment scores, we split out the text field to create a dataframe with one row per word. Then we join that dataframe with two of the sentiment lexicons available from the tidytext R package. The existing positive or negative values are converted to 1 and -1 respectively. For the nrc lexicon where there are several additional sentiment categories are available, only positive and negative tags are retained.

library(tidytext)
review_words <- reviews %>%
  select(review_id, business_id, stars, text) %>%
  unnest_tokens(word, text)%>%
  filter(!word %in% stop_words$word,
         str_detect(word, "^[a-z']+$"))
# set up each lexicon as it's own df
nrc <- sentiments%>%
  filter(sentiment %in% c('positive','negative')
         & lexicon == 'nrc')%>%
  mutate(nrc = ifelse(sentiment == 'positive',1,-1))%>%
  select(word, nrc)
bing <- sentiments%>%
  filter(lexicon == 'bing')%>%
  mutate(bing = ifelse(sentiment == 'positive',1,-1))%>%
  select(word, bing)
# Join each lexicon to the review_words dataframe
reviews_scored <- review_words%>%
  left_join(nrc, by = 'word')%>%
  left_join(bing, by = 'word')

Evaluating the precision of sentiment lexicons requires two steps. For a range of sample sizes, we’ll repeatedly sample our dataset (with replacement) and calculate the average sentiment of the selected words. Then we’ll examine the distribution of the average scores to determine what sample size is needed to produce consistent average sentiment measurements.

dfs <- list()
for(i in seq(25,5000,25)){
  mean_nrc <- c()
  sd_nrc <- c()
  mean_bing <- c()
  sd_bing <- c()
  
  for(j in seq(1,250)){
    words <- sample_n(tbl = reviews_scored, size = i, replace = TRUE)
    mean_nrc[j] <- mean(words$nrc, na.rm = TRUE)
    sd_nrc[j] <- sd(words$nrc, na.rm = T)
    mean_bing[j] <- mean(words$bing, na.rm = TRUE)
    sd_bing[j] <- sd(words$bing, na.rm = T)
  }
  dfs[[i]] <- data.frame(n_size = rep(i,1000), mean_nrc, sd_nrc, mean_bing, sd_bing)
}
scores <- bind_rows(dfs)

Analyzing the Distributions

First, sample mean values are plotted against sample word counts. As expected, the points form a funnel shape with wider distributions for lower sample sizes that converge towards the overall mean as the sample size increases. The NRC lexicon tends to be generally more positive than the Bing lexicon, but also appears to converge towards the global mean more

library(ggplot2)
library(yaztheme)
library(reshape2)
ggplot(scores%>%
         select(n_size, mean_nrc, mean_bing)%>%
         melt(id.vars = 'n_size'), aes(x = n_size, y = value))+
  geom_point(aes(color = variable), alpha = .025)+
  theme_yaz()+
  labs(title = 'Distribution of Sample Mean Sentiment Scores by Sample Size',
       y = 'Average Score', x = 'Sample Size',
       subtitle = 'Based on samples of words from Yelp Reviews and the NRC and Bing sentiment lexicons')+
  annotate('text', x = 4900, y = .7, label = 'bold("Bing")', color = yaz_cols[3], parse = TRUE)+
  annotate('text', x = 4900, y = .9, label = 'bold("NRC")', color = yaz_cols[1], parse = TRUE)+
  scale_color_manual(values = c(yaz_cols[1], yaz_cols[3]))+
  theme(legend.position = 'none')

This proves out when examining the standard deviations of scores by sample size. Both lexicons show significant marginal improvements from increasing sample size up to about 200-300 words but NRC improves slightly more rapidly than Bing.

ggplot(scores%>%
         group_by(n_size)%>%
         summarise(sd_bing = sd(mean_bing, na.rm = T),
                   sd_nrc = sd(mean_nrc, na.rm = T))%>%
         melt(id.vars = 'n_size'),
       aes(x = n_size, y = value, color = variable))+
  geom_line(size = 1.5)+
  labs(title = 'Standard Deviation of Mean Sentiment Scores by Sample Size',
       x = 'Sample Size', y = 'Standard Deviation')+
  scale_color_manual(name = 'Lexicon', values = c(yaz_cols[c(3,1)]), labels = c('Bing','NRC'))+
  theme_yaz()

Conclusion

There are several potential explanations for the superior precision of NRC over Bing. NRC could just be more precise for reviews data. The Bing lexicon could have scored less relevant words and therefore have lower coverage in the reviews data. Coupled with previous analysis that demonstrates Bing’s superior accuracy, the differences in precision are not enough to warrant discarding the lexicon - especially if you have more than a few hundred words to score.

What do you do if you don’t have enough data? Analyzing tweet text or headlines (as I often do) means you typically don’t have enough words from single utterances to produce stable sentiment estimates. What we can do is aggregate text data by some additional factor before calculating summary scores. For example, in this analysis of tweets during a VA Governor’s debate, sentiments are calculated based on the text from all tweets mentioning one candidate or another rather than on an individual basis.

LS0tCnRpdGxlOiAiSG93IE11Y2ggVGV4dCBEbyBXZSBSZWFsbHkgTmVlZCBmb3IgU2VudGltZW50IEFuYWx5c2lzPyIKYXV0aG9yOiAiSm9zaCBZYXptYW4iCmRhdGU6ICIxMC8wNy8yMDE3IgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgpPbmUgcXVlc3Rpb24gSSd2ZSBoZWFyZCByZXBlYXRlZGx5IHNpbmNlIGJlZ2lubmluZyB0byB3b3JrIHdpdGggdGV4dCBkYXRhIGFuZCBzZW50aW1lbnQgYW5hbHlzaXMgaXMgaG93IG11Y2ggdGV4dCB3ZSByZWFsbHkgbmVlZCB0byBnZXQgYSBzb2xpZCBpZGVhIG9mIGFnZ3JlZ2F0ZSBzZW50aW1lbnRzLiBUaGUgbGF3IG9mIGxhcmdlIG51bWJlcnMgc3VnZ2VzdHMgdGhhdCBtb3JlIHRleHQgaXMgYmV0dGVyLCBidXQgaWYgd2Ugb25seSBoYXZlIGEgbGl0dGxlIGJpdCBvZiB0ZXh0LCBjYW4gc2VudGltZW50IGFuYWx5c2lzIHN0aWxsIHdvcms/IAoKSW4gYSBwcmV2aW91cyBbcG9zdF0oaHR0cDovL3JwdWJzLmNvbS9qb3NoeWF6bWFuL3NlbnRpbWVudC1hbmFseXNpcy1sZXhpY29uLWNvbXBhcmlzb24pIEkgZXhhbWluZWQgdGhlIGFjY3VyYWN5IG9mIHRoZSBzZW50aW1lbnQgbGV4aWNvbnMgaW4gdGhlIGB0aWR5dGV4dGAgcGFja2FnZS4gSGVyZSBJIGF0dGVtcHQgdG8gZmluZCB0aGUgcG9pbnQgYXQgd2hpY2ggd2UgaGF2ZSBlbm91Z2ggdGV4dCB0byBtZWFzdXJlIHNlbnRpbWVudHMgcHJlY2lzZWx5LiAKCiMjIEdldHRpbmcgU3RhcnRlZApUaGUgZmlyc3Qgc3RlcCBpcyB0byByZWFkIGEgc2FtcGxlIG9mIHRoZSBbWWVscCBBY2FkZW1pYyBEYXRhc2V0XShodHRwczovL3d3dy55ZWxwLmNvbS9kYXRhc2V0L2NoYWxsZW5nZSkuIEluIHRoaXMgY2FzZSwgSSdtIHN0YXJ0aW5nIHdpdGggYSBzdWJzZXQgb2YgMjAwLDAwMCByZXZpZXdzIG91dCBvZiB0aGUgNC43IG1pbGxpb24gcHJvdmlkZWQuICAgCgpgYGB7cn0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQpsaWJyYXJ5KHJlYWRyKQpsaWJyYXJ5KGRwbHlyKQoKcmV2aWV3X2xpbmVzIDwtIHJlYWRfbGluZXMoJ3Jldmlldy5qc29uJywgbl9tYXggPSAyMDAwMDAsIHByb2dyZXNzID0gRkFMU0UpCgpsaWJyYXJ5KHN0cmluZ3IpCmxpYnJhcnkoanNvbmxpdGUpCgojIEVhY2ggbGluZSBpcyBhIEpTT04gb2JqZWN0LSB0aGUgZmFzdGVzdCB3YXkgdG8gcHJvY2VzcyBpcyB0byBjb21iaW5lIGludG8gYSBzaW5nbGUgSlNPTiBzdHJpbmcgYW5kIHVzZSBmcm9tSlNPTiBhbmQgZmxhdHRlbgptYWtlX2RmcyA8LSBmdW5jdGlvbihsaW5lcyl7CiAgbGluZXNfY29tYmluZWQgPC0gc3RyX2MoIlsiLCBzdHJfYyhsaW5lcywgY29sbGFwc2UgPSAiLCAiKSwgIl0iKQogIAogIGRmIDwtIGZyb21KU09OKGxpbmVzX2NvbWJpbmVkKSAlPiUKICAgIGZsYXR0ZW4oKSAlPiUKICAgIHRibF9kZigpCiAgcmV0dXJuKGRmKQp9CgpyZXZpZXdzIDwtIG1ha2VfZGZzKHJldmlld19saW5lcykKcmV2aWV3cwpgYGAKCk5vdywgdG8gcHJvZHVjZSBzZW50aW1lbnQgc2NvcmVzLCB3ZSBzcGxpdCBvdXQgdGhlIHRleHQgZmllbGQgdG8gY3JlYXRlIGEgZGF0YWZyYW1lIHdpdGggb25lIHJvdyBwZXIgd29yZC4gVGhlbiB3ZSBqb2luIHRoYXQgZGF0YWZyYW1lIHdpdGggdHdvIG9mIHRoZSBzZW50aW1lbnQgbGV4aWNvbnMgYXZhaWxhYmxlIGZyb20gdGhlIGB0aWR5dGV4dGAgUiBwYWNrYWdlLiBUaGUgZXhpc3RpbmcgYHBvc2l0aXZlYCBvciBgbmVnYXRpdmVgIHZhbHVlcyBhcmUgY29udmVydGVkIHRvIGAxYCBhbmQgYC0xYCByZXNwZWN0aXZlbHkuIEZvciB0aGUgYG5yY2AgbGV4aWNvbiB3aGVyZSB0aGVyZSBhcmUgc2V2ZXJhbCBhZGRpdGlvbmFsIHNlbnRpbWVudCBjYXRlZ29yaWVzIGFyZSBhdmFpbGFibGUsIG9ubHkgYHBvc2l0aXZlYCBhbmQgYG5lZ2F0aXZlYCB0YWdzIGFyZSByZXRhaW5lZC4gCgpgYGB7cn0KbGlicmFyeSh0aWR5dGV4dCkKCnJldmlld193b3JkcyA8LSByZXZpZXdzICU+JQogIHNlbGVjdChyZXZpZXdfaWQsIGJ1c2luZXNzX2lkLCBzdGFycywgdGV4dCkgJT4lCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0KSU+JQogIGZpbHRlcighd29yZCAlaW4lIHN0b3Bfd29yZHMkd29yZCwKICAgICAgICAgc3RyX2RldGVjdCh3b3JkLCAiXlthLXonXSskIikpCgojIHNldCB1cCBlYWNoIGxleGljb24gYXMgaXQncyBvd24gZGYKbnJjIDwtIHNlbnRpbWVudHMlPiUKICBmaWx0ZXIoc2VudGltZW50ICVpbiUgYygncG9zaXRpdmUnLCduZWdhdGl2ZScpCiAgICAgICAgICYgbGV4aWNvbiA9PSAnbnJjJyklPiUKICBtdXRhdGUobnJjID0gaWZlbHNlKHNlbnRpbWVudCA9PSAncG9zaXRpdmUnLDEsLTEpKSU+JQogIHNlbGVjdCh3b3JkLCBucmMpCgpiaW5nIDwtIHNlbnRpbWVudHMlPiUKICBmaWx0ZXIobGV4aWNvbiA9PSAnYmluZycpJT4lCiAgbXV0YXRlKGJpbmcgPSBpZmVsc2Uoc2VudGltZW50ID09ICdwb3NpdGl2ZScsMSwtMSkpJT4lCiAgc2VsZWN0KHdvcmQsIGJpbmcpCgojIEpvaW4gZWFjaCBsZXhpY29uIHRvIHRoZSByZXZpZXdfd29yZHMgZGF0YWZyYW1lCnJldmlld3Nfc2NvcmVkIDwtIHJldmlld193b3JkcyU+JQogIGxlZnRfam9pbihucmMsIGJ5ID0gJ3dvcmQnKSU+JQogIGxlZnRfam9pbihiaW5nLCBieSA9ICd3b3JkJykKYGBgCgpFdmFsdWF0aW5nIHRoZSBwcmVjaXNpb24gb2Ygc2VudGltZW50IGxleGljb25zIHJlcXVpcmVzIHR3byBzdGVwcy4gRm9yIGEgcmFuZ2Ugb2Ygc2FtcGxlIHNpemVzLCB3ZSdsbCByZXBlYXRlZGx5IHNhbXBsZSBvdXIgZGF0YXNldCAod2l0aCByZXBsYWNlbWVudCkgYW5kIGNhbGN1bGF0ZSB0aGUgYXZlcmFnZSBzZW50aW1lbnQgb2YgdGhlIHNlbGVjdGVkIHdvcmRzLiBUaGVuIHdlJ2xsIGV4YW1pbmUgdGhlIGRpc3RyaWJ1dGlvbiBvZiB0aGUgYXZlcmFnZSBzY29yZXMgdG8gZGV0ZXJtaW5lIHdoYXQgc2FtcGxlIHNpemUgaXMgbmVlZGVkIHRvIHByb2R1Y2UgY29uc2lzdGVudCBhdmVyYWdlIHNlbnRpbWVudCBtZWFzdXJlbWVudHMuIAoKYGBge3IsIGZpZy5hbGlnbj0nY2VudGVyJ30KZGZzIDwtIGxpc3QoKQoKZm9yKGkgaW4gc2VxKDI1LDUwMDAsMjUpKXsKICBtZWFuX25yYyA8LSBjKCkKICBzZF9ucmMgPC0gYygpCiAgbWVhbl9iaW5nIDwtIGMoKQogIHNkX2JpbmcgPC0gYygpCiAgCiAgZm9yKGogaW4gc2VxKDEsMjUwKSl7CiAgICB3b3JkcyA8LSBzYW1wbGVfbih0YmwgPSByZXZpZXdzX3Njb3JlZCwgc2l6ZSA9IGksIHJlcGxhY2UgPSBUUlVFKQogICAgbWVhbl9ucmNbal0gPC0gbWVhbih3b3JkcyRucmMsIG5hLnJtID0gVFJVRSkKICAgIHNkX25yY1tqXSA8LSBzZCh3b3JkcyRucmMsIG5hLnJtID0gVCkKICAgIG1lYW5fYmluZ1tqXSA8LSBtZWFuKHdvcmRzJGJpbmcsIG5hLnJtID0gVFJVRSkKICAgIHNkX2Jpbmdbal0gPC0gc2Qod29yZHMkYmluZywgbmEucm0gPSBUKQogIH0KICBkZnNbW2ldXSA8LSBkYXRhLmZyYW1lKG5fc2l6ZSA9IHJlcChpLDEwMDApLCBtZWFuX25yYywgc2RfbnJjLCBtZWFuX2JpbmcsIHNkX2JpbmcpCn0KCnNjb3JlcyA8LSBiaW5kX3Jvd3MoZGZzKQpgYGAKCiMjIEFuYWx5emluZyB0aGUgRGlzdHJpYnV0aW9ucwpGaXJzdCwgc2FtcGxlIG1lYW4gdmFsdWVzIGFyZSBwbG90dGVkIGFnYWluc3Qgc2FtcGxlIHdvcmQgY291bnRzLiBBcyBleHBlY3RlZCwgdGhlIHBvaW50cyBmb3JtIGEgZnVubmVsIHNoYXBlIHdpdGggd2lkZXIgZGlzdHJpYnV0aW9ucyBmb3IgbG93ZXIgc2FtcGxlIHNpemVzIHRoYXQgY29udmVyZ2UgdG93YXJkcyB0aGUgb3ZlcmFsbCBtZWFuIGFzIHRoZSBzYW1wbGUgc2l6ZSBpbmNyZWFzZXMuIFRoZSBOUkMgbGV4aWNvbiB0ZW5kcyB0byBiZSBnZW5lcmFsbHkgbW9yZSBwb3NpdGl2ZSB0aGFuIHRoZSBCaW5nIGxleGljb24sIGJ1dCBhbHNvIGFwcGVhcnMgdG8gY29udmVyZ2UgdG93YXJkcyB0aGUgZ2xvYmFsIG1lYW4gbW9yZSAKCmBgYHtyLCBmaWcud2lkdGg9NiwgZmlnLmhlaWdodD0zfQpsaWJyYXJ5KGdncGxvdDIpCmxpYnJhcnkoeWF6dGhlbWUpCmxpYnJhcnkocmVzaGFwZTIpCgpnZ3Bsb3Qoc2NvcmVzJT4lCiAgICAgICAgIHNlbGVjdChuX3NpemUsIG1lYW5fbnJjLCBtZWFuX2JpbmcpJT4lCiAgICAgICAgIG1lbHQoaWQudmFycyA9ICduX3NpemUnKSwgYWVzKHggPSBuX3NpemUsIHkgPSB2YWx1ZSkpKwogIGdlb21fcG9pbnQoYWVzKGNvbG9yID0gdmFyaWFibGUpLCBhbHBoYSA9IC4wMjUpKwogIHRoZW1lX3lheigpKwogIGxhYnModGl0bGUgPSAnRGlzdHJpYnV0aW9uIG9mIFNhbXBsZSBNZWFuIFNlbnRpbWVudCBTY29yZXMgYnkgU2FtcGxlIFNpemUnLAogICAgICAgeSA9ICdBdmVyYWdlIFNjb3JlJywgeCA9ICdTYW1wbGUgU2l6ZScsCiAgICAgICBzdWJ0aXRsZSA9ICdCYXNlZCBvbiBzYW1wbGVzIG9mIHdvcmRzIGZyb20gWWVscCBSZXZpZXdzIGFuZCB0aGUgTlJDIGFuZCBCaW5nIHNlbnRpbWVudCBsZXhpY29ucycpKwogIGFubm90YXRlKCd0ZXh0JywgeCA9IDQ5MDAsIHkgPSAuNywgbGFiZWwgPSAnYm9sZCgiQmluZyIpJywgY29sb3IgPSB5YXpfY29sc1szXSwgcGFyc2UgPSBUUlVFKSsKICBhbm5vdGF0ZSgndGV4dCcsIHggPSA0OTAwLCB5ID0gLjksIGxhYmVsID0gJ2JvbGQoIk5SQyIpJywgY29sb3IgPSB5YXpfY29sc1sxXSwgcGFyc2UgPSBUUlVFKSsKICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzID0gYyh5YXpfY29sc1sxXSwgeWF6X2NvbHNbM10pKSsKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAnbm9uZScpCmBgYAoKVGhpcyBwcm92ZXMgb3V0IHdoZW4gZXhhbWluaW5nIHRoZSBzdGFuZGFyZCBkZXZpYXRpb25zIG9mIHNjb3JlcyBieSBzYW1wbGUgc2l6ZS4gQm90aCBsZXhpY29ucyBzaG93IHNpZ25pZmljYW50IG1hcmdpbmFsIGltcHJvdmVtZW50cyBmcm9tIGluY3JlYXNpbmcgc2FtcGxlIHNpemUgdXAgdG8gYWJvdXQgMjAwLTMwMCB3b3JkcyBidXQgTlJDIGltcHJvdmVzIHNsaWdodGx5IG1vcmUgcmFwaWRseSB0aGFuIEJpbmcuCgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRSwgZmlnLndpZHRoPTYsIGZpZy5oZWlnaHQ9M30KZ2dwbG90KHNjb3JlcyU+JQogICAgICAgICBncm91cF9ieShuX3NpemUpJT4lCiAgICAgICAgIHN1bW1hcmlzZShzZF9iaW5nID0gc2QobWVhbl9iaW5nLCBuYS5ybSA9IFQpLAogICAgICAgICAgICAgICAgICAgc2RfbnJjID0gc2QobWVhbl9ucmMsIG5hLnJtID0gVCkpJT4lCiAgICAgICAgIG1lbHQoaWQudmFycyA9ICduX3NpemUnKSwKICAgICAgIGFlcyh4ID0gbl9zaXplLCB5ID0gdmFsdWUsIGNvbG9yID0gdmFyaWFibGUpKSsKICBnZW9tX2xpbmUoc2l6ZSA9IDEuNSkrCiAgbGFicyh0aXRsZSA9ICdTdGFuZGFyZCBEZXZpYXRpb24gb2YgTWVhbiBTZW50aW1lbnQgU2NvcmVzIGJ5IFNhbXBsZSBTaXplJywKICAgICAgIHggPSAnU2FtcGxlIFNpemUnLCB5ID0gJ1N0YW5kYXJkIERldmlhdGlvbicpKwogIHNjYWxlX2NvbG9yX21hbnVhbChuYW1lID0gJ0xleGljb24nLCB2YWx1ZXMgPSBjKHlhel9jb2xzW2MoMywxKV0pLCBsYWJlbHMgPSBjKCdCaW5nJywnTlJDJykpKwogIHRoZW1lX3lheigpCmBgYAoKIyMgQ29uY2x1c2lvbgpUaGVyZSBhcmUgc2V2ZXJhbCBwb3RlbnRpYWwgZXhwbGFuYXRpb25zIGZvciB0aGUgc3VwZXJpb3IgcHJlY2lzaW9uIG9mIE5SQyBvdmVyIEJpbmcuIE5SQyBjb3VsZCBqdXN0IGJlIG1vcmUgcHJlY2lzZSBmb3IgcmV2aWV3cyBkYXRhLiBUaGUgQmluZyBsZXhpY29uIGNvdWxkIGhhdmUgc2NvcmVkIGxlc3MgcmVsZXZhbnQgd29yZHMgYW5kIHRoZXJlZm9yZSBoYXZlIGxvd2VyIGNvdmVyYWdlIGluIHRoZSByZXZpZXdzIGRhdGEuIENvdXBsZWQgd2l0aCBwcmV2aW91cyBhbmFseXNpcyB0aGF0IGRlbW9uc3RyYXRlcyBCaW5nJ3Mgc3VwZXJpb3IgYWNjdXJhY3ksIHRoZSBkaWZmZXJlbmNlcyBpbiBwcmVjaXNpb24gYXJlIG5vdCBlbm91Z2ggdG8gd2FycmFudCBkaXNjYXJkaW5nIHRoZSBsZXhpY29uIC0gZXNwZWNpYWxseSBpZiB5b3UgaGF2ZSBtb3JlIHRoYW4gYSBmZXcgaHVuZHJlZCB3b3JkcyB0byBzY29yZS4gCgpXaGF0IGRvIHlvdSBkbyBpZiB5b3UgZG9uJ3QgaGF2ZSBlbm91Z2ggZGF0YT8gQW5hbHl6aW5nIHR3ZWV0IHRleHQgb3IgaGVhZGxpbmVzIChhcyBJIG9mdGVuIGRvKSBtZWFucyB5b3UgdHlwaWNhbGx5IGRvbid0IGhhdmUgZW5vdWdoIHdvcmRzIGZyb20gc2luZ2xlIHV0dGVyYW5jZXMgdG8gcHJvZHVjZSBzdGFibGUgc2VudGltZW50IGVzdGltYXRlcy4gV2hhdCB3ZSBjYW4gZG8gaXMgYWdncmVnYXRlIHRleHQgZGF0YSBieSBzb21lIGFkZGl0aW9uYWwgZmFjdG9yIGJlZm9yZSBjYWxjdWxhdGluZyBzdW1tYXJ5IHNjb3Jlcy4gRm9yIGV4YW1wbGUsIGluIHRoaXMgW2FuYWx5c2lzIG9mIHR3ZWV0cyBkdXJpbmcgYSBWQSBHb3Zlcm5vcidzIGRlYmF0ZV0oaHR0cDovL3JwdWJzLmNvbS9qb3NoeWF6bWFuL3ZhZ292LWRlYmF0ZS10d2l0dGVyLWFuYWx5c2lzKSwgc2VudGltZW50cyBhcmUgY2FsY3VsYXRlZCBiYXNlZCBvbiB0aGUgdGV4dCBmcm9tIGFsbCB0d2VldHMgbWVudGlvbmluZyBvbmUgY2FuZGlkYXRlIG9yIGFub3RoZXIgcmF0aGVyIHRoYW4gb24gYW4gaW5kaXZpZHVhbCBiYXNpcy4g