這一段程式可以讓同學們在動態網頁上觀察:

對於評論的:

的效果。

透過互動式的圖形, 我們也可以觀察這些效果是否會隨商業類別(508 categories)發生變化。

Sys.setlocale('LC_ALL','C')

[1] “C”

library(magrittr)
library(highcharter)
library(slam)
library(tm)

package ‘tm’ was built under R version 3.4.1Loading required package: NLP

library(SnowballC)
library(RColorBrewer)          
# set color palette
pals = c(brewer.pal(8,"Set2")[c(6)],
         brewer.pal(8,"Dark2"),
         brewer.pal(8,"Set1")[c(1)])
# load data
load('data/yelp1.rdata')
load('data/average.rdata')
load('data/empath.rdata')
load('data/dtm0.rdata')


(1) Preparation

To aviod glitches, we only analyze the business categories with more than 500 reviews.
First we do a quick hi-clustering on these (123) categories by their normolized class scores.

# clusters of categories by classes scores
d = dist(scale(wx[CA$nrev>500,]))
hc = hclust(d,"ward.D2")

Cut them into 10 groups.

kg = cutree(hc,10)
kg = order(order(-table(kg)))[kg]
table(kg)
kg
 1  2  3  4  5  6  7  8  9 10 
44 23 17 10  8  6  5  4  3  3 

Name the groups and combine the group_id with the category profiles.

gnames = c('Restaurant','Bar', 'Shop', 'Venue', 'Service', 
           'Art', 'Beauty', 'Fitness', 'Health', 'Public')
cats = cbind( cat=rownames(CA)[CA$nrev>500], 
              grp=factor(kg,labels=gnames), 
              CA[CA$nrev>500,] )
head(cats)


(2) Helper Function

Make a helper function for interactive charting …

# helper function
make.chart = function(bygroup=F) { # x,X,y,Y,mxBCm=,review
  # overall usage and lift
  N = as.numeric(length(X))
  txt = sprintf("OVERALL: Usage=%.3f, Conf=%.3f, Base=%.3f, Lift=%.3f", 
                sum(X)/N, sum(X&Y)/sum(X), sum(Y)/N,
                N*sum(Y&X)/sum(X)/sum(Y))
  if(bygroup) {
    mx = sapply(levels(cats$grp), function(g) 
      rowSums(mxBC[, as.character(cats$cat[cats$grp == g])]) > 0 )
    bubbles = list(maxSize="20%",minSize=20)
    df2 = aggregate(.~grp,cats[,2:4],sum) }
  else {
    mx = mxBC[,CA$nrev>500]
    bubbles = list(maxSize="10%",minSize=10)
    df2 = cats[,1:4] }
  # cases
  df = mx %>% apply(2,function(v) {
      i = review$bid %in% rownames(mxBC)[v]
      n = as.numeric(sum(i))
      c( usage = sum(X[i])/n,
         base = round(sum(Y[i])/n, 3),
         conf = round(sum(X[i]&Y[i])/sum(X[i]), 3),
         lift = n * sum(Y[i]&X[i]) / sum(X[i]) / sum(Y[i]) )
    }) %>% t %>% data.frame %>% cbind(df2)
  
  # bubble chart
  tips=paste0(ifelse(bygroup, "", "<b>{point.cat}</b><br>"),
              "no.rev/biz: {point.nrev} / {point.nbiz}<br>",
              "conf/base: {point.conf} / {point.base}")
  hchart(df,"scatter",hcaes(x=usage,y=lift,size=nrev,group=grp)) %>%
    hc_title(text=sprintf("The effect of <b>%s</b> on <b>%s</b>",x,y)) %>% 
    hc_subtitle(text = txt) %>% hc_colors(pals) %>% 
    hc_tooltip(hideDelay=100,useHTML=T,pointFormat=tips) %>% 
    hc_plotOptions(bubble = bubbles) %>% hc_size(height=640) %>% 
    hc_chart(zoomType="xy") %>% hc_add_theme(hc_theme_flat())
}


(3) Explore the Effect of Content Classes

3.1 某一種內容

y = "funny";  Y = review[, y] %>% {. > median(.)}
x = "swearing_terms"; X = scores[, x] %>% {. > median(.)}
make.chart()


For the entire corpus and each business categories, we calculate and display:

  • Usage = \(P[X]\) : the base probability of \(X\)
  • Conf = \(P[Y|X]\) : the probability of \(Y\) given \(X\)
  • Base = \(P[Y]\) : the base probability of \(Y\)
  • Lift = \(\frac{P[Y|X]}{P[Y]}\) : the lift of \(X\) on \(P[Y]\)

3.2 某一群內容

可以一次看一群內容,也可以將整群商業類別集合起來一起看 …

y = "funny"; Y = review[, y] %>% {. > median(.)}
x = "sexual+lust"
X = rowSums(sapply(c('sexual','lust'), # <-- 將內容放在括弧裡面
                   function(s) scores[,s] > median(scores[,s]))) > 0 
make.chart(bygroup=TRUE)



3.3 某一群(個)字根

y = "cool"; Y = review[, y] %>% {. > median(.)}
# 將字根放在括弧裡面
terms = grep("^(authen|genuin|pure|innoc|origin|true|truth)",
             dtm$dimnames$Terms[1:10000], value=F)
x = "authenticity"; X = as.integer(row_sums(dtm[,terms]) > 0)
make.chart()



3.4 某一群(個)字

To analyze the effect of words, we need a new document term matrix (dtm2).

corp = Corpus(VectorSource(review$text))
corp = tm_map(corp,  content_transformer(tolower))
corp = tm_map(corp, removePunctuation)
dtm2 = DocumentTermMatrix(corp); dtm2
<<DocumentTermMatrix (documents: 215879, terms: 215402)>>
Non-/sparse entries: 15740971/46485027387
Sparsity           : 100%
Maximal term length: 932
Weighting          : term frequency (tf)
dtm2 = removeSparseTerms(dtm2, .999); dtm2
<<DocumentTermMatrix (documents: 215879, terms: 5230)>>
Non-/sparse entries: 14361152/1114686018
Sparsity           : 99%
Maximal term length: 15
Weighting          : term frequency (tf)
y = "useful"; Y = review[, y] %>% {. > median(.)}
w = which(colnames(dtm2) %in% c('but','however','nonetheless'))
x = "but..."; X = as.integer(row_sums(dtm2[, w]) > 0)
make.chart(bygroup=TRUE)



3.5 標點符號

y = "useful"; Y = review[, y] %>% {. > median(.)}
x = "?"; X = grepl("?",review$text,fixed=T)
make.chart(bygroup=TRUE)



More than one Punctuation …

x = "? ..."; X = grepl("\\?|!",review$text)
make.chart()



3.6 情緒

y = "useful"; Y = review[, y] %>% {. > median(.)}
x = "anger"; X = senti[, x] %>% {. > median(.)}
make.chart(bygroup=TRUE)
x = "positive"; X = senti[, x] %>% {. > median(.)}
make.chart()
x = "negative"; X = senti[, x] %>% {. > median(.)}
make.chart()
LS0tDQp0aXRsZTogIuWFp+WuueWSjOWtl+ipnuWwjeipleirlueahOaViOaenCINCnN1YnRpdGxlOiBZZWxwIEthZ2dsZSwgRWZmZWN0IEV4cGxvcmVyDQphdXRob3I6ICJUb255IENodW8iDQpkYXRlOiAiMjAxNy8wOC8wNCINCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgICBoaWdobGlnaHQ6IHRleHRtYXRlDQogICAgdGhlbWU6IGx1bWVuDQotLS0NCg0KPGJyPg0KDQotIC0gLQ0KDQo8YnI+DQrpgJnkuIDmrrXnqIvlvI/lj6/ku6XorpPlkIzlrbjlgJHlnKjli5XmhYvntrLpoIHkuIrop4Dlr5/vvJoNCg0KKyDmn5DkuIDnqK4o576kKeWFp+WuuSAoMTk0IGNsYXNzZXMp77yMDQorIOafkOS4gOWAiyjnvqQp5a2X6Kme77yM5oiWDQorIOafkOS4gOWAiyjnvqQp5a2X5qC5DQorIOafkOS4gOeoruaDhee3kg0KDQrlsI3mlrzoqZXoq5bnmoTvvJoNCg0KKyB1c2VmdWwNCisgZnVubnkNCisgY29vbA0KDQrnmoTmlYjmnpzjgII8YnI+IDxicj4NCumAj+mBjuS6kuWLleW8j+eahOWcluW9ou+8jA0K5oiR5YCR5Lmf5Y+v5Lul6KeA5a+f6YCZ5Lqb5pWI5p6c5piv5ZCm5pyD6Zqo5ZWG5qWt6aGe5YilKDUwOCBjYXRlZ29yaWVzKeeZvOeUn+iuiuWMluOAgjxicj4NCg0KYGBge3Igc2V0LW9wdGlvbnMsIGVjaG89RkFMU0UsIGNhY2hlPUZBTFNFfQ0KbGlicmFyeShrbml0cikNCm9wdGlvbnMod2lkdGg9OTApDQpvcHRzX2NodW5rJHNldChjb21tZW50ID0gTkEpDQpgYGANCg0KYGBge3IgcmVzdWx0cz0nYXNpcycsIHdhcm5pbmc9RiwgbWVzc2FnZT1GLCBjYWNoZT1GfQ0KU3lzLnNldGxvY2FsZSgnTENfQUxMJywnQycpDQpsaWJyYXJ5KG1hZ3JpdHRyKQ0KbGlicmFyeShoaWdoY2hhcnRlcikNCmxpYnJhcnkoc2xhbSkNCmxpYnJhcnkodG0pDQpsaWJyYXJ5KFNub3diYWxsQykNCmxpYnJhcnkoUkNvbG9yQnJld2VyKSAgICAgICAgICANCiMgc2V0IGNvbG9yIHBhbGV0dGUNCnBhbHMgPSBjKGJyZXdlci5wYWwoOCwiU2V0MiIpW2MoNildLA0KICAgICAgICAgYnJld2VyLnBhbCg4LCJEYXJrMiIpLA0KICAgICAgICAgYnJld2VyLnBhbCg4LCJTZXQxIilbYygxKV0pDQojIGxvYWQgZGF0YQ0KbG9hZCgnZGF0YS95ZWxwMS5yZGF0YScpDQpsb2FkKCdkYXRhL2F2ZXJhZ2UucmRhdGEnKQ0KbG9hZCgnZGF0YS9lbXBhdGgucmRhdGEnKQ0KbG9hZCgnZGF0YS9kdG0wLnJkYXRhJykNCmBgYA0KPGJyPg0KDQojIyAoMSkgUHJlcGFyYXRpb24NClRvIGF2aW9kIGdsaXRjaGVzLCB3ZSBvbmx5IGFuYWx5emUgdGhlIGJ1c2luZXNzIGNhdGVnb3JpZXMgd2l0aCBtb3JlIHRoYW4gNTAwIHJldmlld3MuIDxicj4gRmlyc3Qgd2UgZG8gYSBxdWljayBoaS1jbHVzdGVyaW5nIG9uIHRoZXNlICgxMjMpIGNhdGVnb3JpZXMgYnkgdGhlaXIgbm9ybW9saXplZCBjbGFzcyBzY29yZXMuICANCmBgYHtyfQ0KIyBjbHVzdGVycyBvZiBjYXRlZ29yaWVzIGJ5IGNsYXNzZXMgc2NvcmVzDQpkID0gZGlzdChzY2FsZSh3eFtDQSRucmV2PjUwMCxdKSkNCmhjID0gaGNsdXN0KGQsIndhcmQuRDIiKQ0KYGBgDQoNCkN1dCB0aGVtIGludG8gMTAgZ3JvdXBzLiAgDQpgYGB7cn0NCmtnID0gY3V0cmVlKGhjLDEwKQ0Ka2cgPSBvcmRlcihvcmRlcigtdGFibGUoa2cpKSlba2ddDQp0YWJsZShrZykNCmBgYA0KDQpOYW1lIHRoZSBncm91cHMgYW5kIGNvbWJpbmUgdGhlIGdyb3VwX2lkIHdpdGggdGhlIGNhdGVnb3J5IHByb2ZpbGVzLiANCmBgYHtyfQ0KZ25hbWVzID0gYygnUmVzdGF1cmFudCcsJ0JhcicsICdTaG9wJywgJ1ZlbnVlJywgJ1NlcnZpY2UnLCANCiAgICAgICAgICAgJ0FydCcsICdCZWF1dHknLCAnRml0bmVzcycsICdIZWFsdGgnLCAnUHVibGljJykNCmNhdHMgPSBjYmluZCggY2F0PXJvd25hbWVzKENBKVtDQSRucmV2PjUwMF0sIA0KICAgICAgICAgICAgICBncnA9ZmFjdG9yKGtnLGxhYmVscz1nbmFtZXMpLCANCiAgICAgICAgICAgICAgQ0FbQ0EkbnJldj41MDAsXSApDQpoZWFkKGNhdHMpDQpgYGANCjxicj4NCg0KIyMgKDIpIEhlbHBlciBGdW5jdGlvbg0KTWFrZSBhIGhlbHBlciBmdW5jdGlvbiBmb3IgaW50ZXJhY3RpdmUgY2hhcnRpbmcgLi4uDQpgYGB7cn0NCiMgaGVscGVyIGZ1bmN0aW9uDQptYWtlLmNoYXJ0ID0gZnVuY3Rpb24oYnlncm91cD1GKSB7ICMgeCxYLHksWSxteEJDbT0scmV2aWV3DQogICMgb3ZlcmFsbCB1c2FnZSBhbmQgbGlmdA0KICBOID0gYXMubnVtZXJpYyhsZW5ndGgoWCkpDQogIHR4dCA9IHNwcmludGYoIk9WRVJBTEw6IFVzYWdlPSUuM2YsIENvbmY9JS4zZiwgQmFzZT0lLjNmLCBMaWZ0PSUuM2YiLCANCiAgICAgICAgICAgICAgICBzdW0oWCkvTiwgc3VtKFgmWSkvc3VtKFgpLCBzdW0oWSkvTiwNCiAgICAgICAgICAgICAgICBOKnN1bShZJlgpL3N1bShYKS9zdW0oWSkpDQoNCiAgaWYoYnlncm91cCkgew0KICAgIG14ID0gc2FwcGx5KGxldmVscyhjYXRzJGdycCksIGZ1bmN0aW9uKGcpIA0KICAgICAgcm93U3VtcyhteEJDWywgYXMuY2hhcmFjdGVyKGNhdHMkY2F0W2NhdHMkZ3JwID09IGddKV0pID4gMCApDQogICAgYnViYmxlcyA9IGxpc3QobWF4U2l6ZT0iMjAlIixtaW5TaXplPTIwKQ0KICAgIGRmMiA9IGFnZ3JlZ2F0ZSgufmdycCxjYXRzWywyOjRdLHN1bSkgfQ0KICBlbHNlIHsNCiAgICBteCA9IG14QkNbLENBJG5yZXY+NTAwXQ0KICAgIGJ1YmJsZXMgPSBsaXN0KG1heFNpemU9IjEwJSIsbWluU2l6ZT0xMCkNCiAgICBkZjIgPSBjYXRzWywxOjRdIH0NCg0KICAjIGNhc2VzDQogIGRmID0gbXggJT4lIGFwcGx5KDIsZnVuY3Rpb24odikgew0KICAgICAgaSA9IHJldmlldyRiaWQgJWluJSByb3duYW1lcyhteEJDKVt2XQ0KICAgICAgbiA9IGFzLm51bWVyaWMoc3VtKGkpKQ0KICAgICAgYyggdXNhZ2UgPSBzdW0oWFtpXSkvbiwNCiAgICAgICAgIGJhc2UgPSByb3VuZChzdW0oWVtpXSkvbiwgMyksDQogICAgICAgICBjb25mID0gcm91bmQoc3VtKFhbaV0mWVtpXSkvc3VtKFhbaV0pLCAzKSwNCiAgICAgICAgIGxpZnQgPSBuICogc3VtKFlbaV0mWFtpXSkgLyBzdW0oWFtpXSkgLyBzdW0oWVtpXSkgKQ0KICAgIH0pICU+JSB0ICU+JSBkYXRhLmZyYW1lICU+JSBjYmluZChkZjIpDQogIA0KICAjIGJ1YmJsZSBjaGFydA0KICB0aXBzPXBhc3RlMChpZmVsc2UoYnlncm91cCwgIiIsICI8Yj57cG9pbnQuY2F0fTwvYj48YnI+IiksDQogICAgICAgICAgICAgICJuby5yZXYvYml6OiB7cG9pbnQubnJldn0gLyB7cG9pbnQubmJpen08YnI+IiwNCiAgICAgICAgICAgICAgImNvbmYvYmFzZToge3BvaW50LmNvbmZ9IC8ge3BvaW50LmJhc2V9IikNCiAgaGNoYXJ0KGRmLCJzY2F0dGVyIixoY2Flcyh4PXVzYWdlLHk9bGlmdCxzaXplPW5yZXYsZ3JvdXA9Z3JwKSkgJT4lDQogICAgaGNfdGl0bGUodGV4dD1zcHJpbnRmKCJUaGUgZWZmZWN0IG9mIDxiPiVzPC9iPiBvbiA8Yj4lczwvYj4iLHgseSkpICU+JSANCiAgICBoY19zdWJ0aXRsZSh0ZXh0ID0gdHh0KSAlPiUgaGNfY29sb3JzKHBhbHMpICU+JSANCiAgICBoY190b29sdGlwKGhpZGVEZWxheT0xMDAsdXNlSFRNTD1ULHBvaW50Rm9ybWF0PXRpcHMpICU+JSANCiAgICBoY19wbG90T3B0aW9ucyhidWJibGUgPSBidWJibGVzKSAlPiUgaGNfc2l6ZShoZWlnaHQ9NjQwKSAlPiUgDQogICAgaGNfY2hhcnQoem9vbVR5cGU9Inh5IikgJT4lIGhjX2FkZF90aGVtZShoY190aGVtZV9mbGF0KCkpDQp9DQpgYGANCjxicj4NCg0KIyMgKDMpIEV4cGxvcmUgdGhlIEVmZmVjdCBvZiBDb250ZW50IENsYXNzZXMNCg0KIyMjIDMuMSDmn5DkuIDnqK7lhaflrrkNCmBgYHtyIHJlc3VsdHM9J2FzaXMnLCBmaWcuYWxpZ249J2NlbnRlcid9DQp5ID0gImZ1bm55IjsgIFkgPSByZXZpZXdbLCB5XSAlPiUgey4gPiBtZWRpYW4oLil9DQp4ID0gInN3ZWFyaW5nX3Rlcm1zIjsgWCA9IHNjb3Jlc1ssIHhdICU+JSB7LiA+IG1lZGlhbiguKX0NCm1ha2UuY2hhcnQoKQ0KYGBgDQo8YnI+DQpGb3IgdGhlIGVudGlyZSBjb3JwdXMgYW5kIGVhY2ggYnVzaW5lc3MgY2F0ZWdvcmllcywgd2UgY2FsY3VsYXRlIGFuZCBkaXNwbGF5Og0KDQorIGBVc2FnZWAgPSAkUFtYXSQgOiB0aGUgYmFzZSBwcm9iYWJpbGl0eSBvZiAkWCQNCisgYENvbmZgID0gJFBbWXxYXSQgOiB0aGUgcHJvYmFiaWxpdHkgb2YgJFkkIGdpdmVuICRYJCAgDQorIGBCYXNlYCA9ICRQW1ldJCA6IHRoZSBiYXNlIHByb2JhYmlsaXR5IG9mICRZJCANCisgYExpZnRgID0gJFxmcmFje1BbWXxYXX17UFtZXX0kIDogdGhlIGxpZnQgb2YgJFgkIG9uICRQW1ldJA0KPGJyPg0KPGJyPg0KDQojIyMgMy4yIOafkOS4gOe+pOWFp+WuuQ0K5Y+v5Lul5LiA5qyh55yL5LiA576k5YWn5a6577yM5Lmf5Y+v5Lul5bCH5pW0576k5ZWG5qWt6aGe5Yil6ZuG5ZCI6LW35L6G5LiA6LW355yLIC4uLg0KYGBge3IgcmVzdWx0cz0nYXNpcycsIGZpZy5hbGlnbj0nY2VudGVyJ30NCnkgPSAiZnVubnkiOyBZID0gcmV2aWV3WywgeV0gJT4lIHsuID4gbWVkaWFuKC4pfQ0KeCA9ICJzZXh1YWwrbHVzdCINClggPSByb3dTdW1zKHNhcHBseShjKCdzZXh1YWwnLCdsdXN0JyksICMgPC0tIOWwh+WFp+WuueaUvuWcqOaLrOW8p+ijoemdog0KICAgICAgICAgICAgICAgICAgIGZ1bmN0aW9uKHMpIHNjb3Jlc1ssc10gPiBtZWRpYW4oc2NvcmVzWyxzXSkpKSA+IDAgDQptYWtlLmNoYXJ0KGJ5Z3JvdXA9VFJVRSkNCmBgYA0KPGJyPg0KPGJyPg0KDQojIyMgMy4zIOafkOS4gOe+pCjlgIsp5a2X5qC5DQpgYGB7ciByZXN1bHRzPSdhc2lzJywgZmlnLmFsaWduPSdjZW50ZXInfQ0KeSA9ICJjb29sIjsgWSA9IHJldmlld1ssIHldICU+JSB7LiA+IG1lZGlhbiguKX0NCiMg5bCH5a2X5qC55pS+5Zyo5ous5byn6KOh6Z2iDQp0ZXJtcyA9IGdyZXAoIl4oYXV0aGVufGdlbnVpbnxwdXJlfGlubm9jfG9yaWdpbnx0cnVlfHRydXRoKSIsDQogICAgICAgICAgICAgZHRtJGRpbW5hbWVzJFRlcm1zWzE6MTAwMDBdLCB2YWx1ZT1GKQ0KeCA9ICJhdXRoZW50aWNpdHkiOyBYID0gYXMuaW50ZWdlcihyb3dfc3VtcyhkdG1bLHRlcm1zXSkgPiAwKQ0KbWFrZS5jaGFydCgpDQpgYGANCjxicj4NCjxicj4NCg0KIyMjIDMuNCDmn5DkuIDnvqQo5YCLKeWtlw0KVG8gYW5hbHl6ZSB0aGUgZWZmZWN0IG9mIHdvcmRzLCB3ZSBuZWVkIGEgbmV3IGRvY3VtZW50IHRlcm0gbWF0cml4IChgZHRtMmApLg0KYGBge3J9DQpjb3JwID0gQ29ycHVzKFZlY3RvclNvdXJjZShyZXZpZXckdGV4dCkpDQpjb3JwID0gdG1fbWFwKGNvcnAsICBjb250ZW50X3RyYW5zZm9ybWVyKHRvbG93ZXIpKQ0KY29ycCA9IHRtX21hcChjb3JwLCByZW1vdmVQdW5jdHVhdGlvbikNCmR0bTIgPSBEb2N1bWVudFRlcm1NYXRyaXgoY29ycCk7IGR0bTINCmR0bTIgPSByZW1vdmVTcGFyc2VUZXJtcyhkdG0yLCAuOTk5KTsgZHRtMg0KYGBgDQoNCg0KYGBge3IgcmVzdWx0cz0nYXNpcycsIGZpZy5hbGlnbj0nY2VudGVyJ30NCnkgPSAidXNlZnVsIjsgWSA9IHJldmlld1ssIHldICU+JSB7LiA+IG1lZGlhbiguKX0NCncgPSB3aGljaChjb2xuYW1lcyhkdG0yKSAlaW4lIGMoJ2J1dCcsJ2hvd2V2ZXInLCdub25ldGhlbGVzcycpKQ0KeCA9ICJidXQuLi4iOyBYID0gYXMuaW50ZWdlcihyb3dfc3VtcyhkdG0yWywgd10pID4gMCkNCm1ha2UuY2hhcnQoYnlncm91cD1UUlVFKQ0KYGBgDQo8YnI+DQo8YnI+DQoNCg0KIyMjIDMuNSDmqJnpu57nrKbomZ8NCmBgYHtyIHJlc3VsdHM9J2FzaXMnLCBmaWcuYWxpZ249J2NlbnRlcid9DQp5ID0gInVzZWZ1bCI7IFkgPSByZXZpZXdbLCB5XSAlPiUgey4gPiBtZWRpYW4oLil9DQp4ID0gIj8iOyBYID0gZ3JlcGwoIj8iLHJldmlldyR0ZXh0LGZpeGVkPVQpDQptYWtlLmNoYXJ0KGJ5Z3JvdXA9VFJVRSkNCmBgYA0KPGJyPg0KPGJyPg0KDQpNb3JlIHRoYW4gb25lIFB1bmN0dWF0aW9uIC4uLg0KYGBge3J9DQp4ID0gIj8gLi4uIjsgWCA9IGdyZXBsKCJcXD98ISIscmV2aWV3JHRleHQpDQptYWtlLmNoYXJ0KCkNCmBgYA0KPGJyPg0KPGJyPg0KDQojIyMgMy42IOaDhee3kg0KYGBge3IgcmVzdWx0cz0nYXNpcycsIGZpZy5hbGlnbj0nY2VudGVyJ30NCnkgPSAidXNlZnVsIjsgWSA9IHJldmlld1ssIHldICU+JSB7LiA+IG1lZGlhbiguKX0NCnggPSAiYW5nZXIiOyBYID0gc2VudGlbLCB4XSAlPiUgey4gPiBtZWRpYW4oLil9DQptYWtlLmNoYXJ0KCkNCmBgYA0KDQpgYGB7ciByZXN1bHRzPSdhc2lzJywgZmlnLmFsaWduPSdjZW50ZXInfQ0KeCA9ICJwb3NpdGl2ZSI7IFggPSBzZW50aVssIHhdICU+JSB7LiA+IG1lZGlhbiguKX0NCm1ha2UuY2hhcnQoKQ0KYGBgDQoNCmBgYHtyIHJlc3VsdHM9J2FzaXMnLCBmaWcuYWxpZ249J2NlbnRlcid9DQp4ID0gIm5lZ2F0aXZlIjsgWCA9IHNlbnRpWywgeF0gJT4lIHsuID4gbWVkaWFuKC4pfQ0KbWFrZS5jaGFydCgpDQpgYGANCg0K