這一段程式可以讓同學們在動態網頁上觀察:
- 某一種(群)內容 (194 classes),
- 某一個(群)字詞,或
- 某一個(群)字根
- 某一種情緒
對於評論的:
的效果。
透過互動式的圖形, 我們也可以觀察這些效果是否會隨商業類別(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