Sys.setlocale("LC_ALL","C")
[1] "C"
packages = c(
  "dplyr","ggplot2","googleVis","devtools","magrittr","slam","irlba","plotly",
  "arules","arulesViz","Matrix","recommenderlab")
existing = as.character(installed.packages()[,1])
for(pkg in packages[!(packages %in% existing)]) install.packages(pkg)
rm(list=ls(all=TRUE))
LOAD = FALSE
library(dplyr)
library(ggplot2)
library(googleVis)
library(Matrix)
library(slam)
library(irlba)
library(plotly)
library(arules)
library(arulesViz)
library(recommenderlab)


A. RFM分群

load("data/tf0.rdata")
A = A0; X = X0; Z = Z0; rm(A0,X0,Z0); gc()
           used  (Mb) gc trigger  (Mb)  max used (Mb)
Ncells  3202619 171.1    9601876 512.8  12002346  641
Vcells 12994267  99.2   93476238 713.2 228211284 1741
#Z總交易紀錄
#X單筆交易紀錄彙整
#A會員交易紀錄
k = 8; set.seed(777)
A$group = kmeans(scale(A[,2:6]), k)$cluster; table(A$group)

   1    2    3    4    5    6    7    8 
2371 2825 6789 4252 9758  317 4300 1629 

執行意涵

df = group_by(A, group) %>% summarise( 
  avg_frequency = mean(f),   #平均頻率
  avg_monetary = mean(m),    #平均單筆購買
  total_revenue = sum(rev),  #總收入
  group_size = n(),          #群內個數
  avg_recency = mean(r),     #平均的最近來店天數
  profit = sum(raw)          #總成本
  ) %>% ungroup %>% 
  mutate(  
    pc_revenue = round(100*total_revenue/sum(total_revenue),1), 
    pc_profit = round(100*profit/sum(profit),1),                
    dummy = 2001  
  ) %>% data.frame

執行意涵

head(df)
plot( gvisMotionChart(
  df, "group", "dummy",
  options=list(width=800, height=600) 
  ))

執行意涵

【QUIZ】 如果我們把X,Y軸分別改成log(avg_monetary)和log(avg_recency),根據圖上的顯示 …
圖一、Log後的圖


B. 顧客產品矩陣

n_distinct(Z$cust)  # 32256
[1] 32256
n_distinct(Z$prod)  # 23789
[1] 23789

操作矩陣運算之前,通常我會載入這兩個套件

library(Matrix)
library(slam)

製作顧客產品矩陣其實很快、也很容易

mx = xtabs(~ cust + prod, Z, sparse=T)

顧客產品矩陣通常是一個很稀疏的矩陣

mean(mx > 0)
[1] 0.000968

執行意涵

有一些產品沒什麼人買

table(colSums(mx) < 10)       # 曾經被買過小於10次的商品 

FALSE  TRUE 
11201 12588 

刪去購買次數小於10的產品,然後刪去沒有購買產品的顧客

mx = mx[, colSums(mx) > 10]   

執行意涵

檢查一下矩陣裡面的值分布

max(mx)                       
[1] 49
table(mx@x) %>% prop.table %>% round(4) %>% head(10)

     1      2      3      4      5      6      7      8      9     10 
0.9235 0.0594 0.0112 0.0033 0.0013 0.0006 0.0003 0.0002 0.0001 0.0001 
#取mx裡的x     #佔全部的幾%    #小數點四位  #前十名

執行意涵


【QUIZ】 如果mx[i, j] = 3,這表示 ….



C. 依購買頻率分群

C1. 改變(稀疏)矩陣格式

稀疏矩陣有很多種格式,不同的工具會使用不同的格式

library(slam)
tmx = as(mx,"dgTMatrix")
tmx = simple_triplet_matrix(
  1+tmx@i, 1+tmx@j, tmx@x, dimnames=mx@Dimnames)  
dim(tmx)
[1] 32256 10675

執行意涵

  • simple_triplet_matrix(i,j,v,dimnames):
    1.i:行
    2.j:列
    3.v:向量值
    4.dimnames:矩陣的屬性(此處為cust、prod)
  • 稀疏矩陣多元的格式,因應使用工具而有所不同
C2. 估計各產品的平均重要性 (Average TF-IDF Scores)

我們借用文字分析裡面估計單字在文章之中的重要性的方法(TF-IDF),計算各產品在所有顧客之間的平均重要性

tfidf = tapply(tmx$v/row_sums(tmx)[tmx$i], tmx$j, mean) *
  log2(nrow(tmx)/col_sums(tmx > 0))
summary(tfidf)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  0.137   0.313   0.388   0.444   0.507   4.472 

執行意涵

  • TF:詞頻,文字在文集中出現過的頻率
  • IDF:逆向文件頻率,這個詞在所有文集中出現的頻率,若是太多,代表越不重要
  • TFIDF:兩個參數權重算出的結果
hist(tfidf)

C3. 篩選產品、重新排列

篩去平均重要性比較低的產品、然後將產品依被購買次數降冪排列

tmx = tmx[, tfidf > mean(tfidf)]    # 將小於平均的去除
tmx = tmx[, order(-col_sums(tmx))]  # 降冪
                                    # 10675→3823
dim(tmx)
[1] 32256  3823

執行意涵

  • 將tfidf小於平均的去除並降冪排列
C4. 依(購買頻率最高的)產品對顧客分群
nop= 400  # no. product
k = 200   # no. cluster
set.seed(111); kg = kmeans(tmx[,1:nop], k)$cluster
table(kg) %>% as.vector %>% sort
  [1]     1     1     1     1     1     1     1     1     1     1     1
 [12]     1     1     1     2     2     2     2     3     3     3     4
 [23]     5     5     5     6     6     6     6     6     7     7     7
 [34]     7     7     7     8     8     8     8     9     9     9     9
 [45]     9     9     9    10    10    10    11    12    12    12    13
 [56]    14    14    14    14    15    16    17    19    19    21    22
 [67]    23    24    25    26    27    29    30    32    33    34    34
 [78]    35    35    36    37    38    39    39    40    41    41    42
 [89]    43    43    44    44    45    45    46    46    47    48    48
[100]    48    49    50    51    51    51    52    52    54    55    55
[111]    56    56    56    57    57    57    57    58    60    60    62
[122]    62    62    62    63    64    64    65    65    67    67    68
[133]    69    69    70    72    72    72    72    72    73    73    73
[144]    73    74    74    75    75    76    78    79    79    79    80
[155]    80    81    82    86    88    88    88    89    90    93    93
[166]    93   106   106   107   110   111   112   120   123   126   128
[177]   131   136   137   138   140   141   142   143   147   148   149
[188]   153   165   169   181   182   187   193   212   258   422   494
[199]   554 20061

執行意涵

  • 按照購買頻率由高到低排列,選擇四百個產品
  • 對顧客做200群的分群
C5. 各群組平均屬性

將分群結果併入顧客資料框(A)

df = inner_join(A, data.frame(
  cust = as.integer(tmx$dimnames$cust), kg))
Joining, by = "cust"

執行意涵

  • kg:群編號
  • size:群內個數
  • 將結果用inner_join依照cust併入資料集A

計算各群組的平均屬性

df = data.frame(
  aggregate(. ~ kg, df[,c(2:7,11)], mean),
  size = as.vector(table(kg))
  )
head(df)
C6. 互動式泡泡圖
df$dummy = 2001                        # dummy column for googleViz
plot( gvisMotionChart(
  subset(df[,c(1,4,5,6,8,2,3,7,9)], 
         size >= 20 & size <= 5000     # 將大於20小於5000的列出
         ), 
  "kg", "dummy", options=list(width=800, height=600) ) )
C7. 各群組的代表性產品 (Signature Product)
Sig = function(gx, P=1000, H=10) {
  print(sprintf("Group %d: No. Customers = %d", gx, sum(kg==gx)))
  bx = tmx[,1:P]
  data.frame(n = col_sums(bx[kg==gx,])) %>%      
    mutate(
      share = round(100*n/col_sums(bx),2),       
      conf = round(100*n/sum(kg==gx),2),         
      base = round(100*col_sums(bx)/nrow(bx),2), 
      lift = round(conf/base,1),       
      name = colnames(bx)
    ) %>% arrange(desc(lift)) %>% head(H)
  }

執行意涵

  • p(1000):1~1000的產品
  • n:消費個數總和
  • share:這產品全部的幾%賣給這群人
  • conf:這群人平均購買幾%這類產品
  • base:所有人平均購買幾%這類產品
  • lift:跟所有人比這類人多了幾%可能會購買這類產品
  • 此函數可以找出群內較為顯著性、代表性的產品,最後依照lift由高排至低
Sig(1)
[1] "Group 1: No. Customers = 57"
#第一群
#57個顧客
  • G族群一共買了n個P產品
  • P產品有share%是賣給G族群
  • G族群每人平均購買conf/100個P產品
  • 所有顧客之中,每人平均購買base/100個P產品
  • 跟所有顧客相比,G族群購買P產品的機率上升了lift-1

【QUIZ】 從互動式泡泡圖看來 …

  • 平均客單價、平均購買次數、平均營收貢獻、平均獲利貢獻最大的分別是哪一個族群?它們的特徵產品分別是什麼?
  • 平均客單價最大:第190群
Sig(190)
[1] "Group 190: No. Customers = 32"
  • 平均購買次數最大:第186群
Sig(186)
[1] "Group 186: No. Customers = 51"
  • 平均營收貢獻最大:第19群
Sig(19)
[1] "Group 19: No. Customers = 21"
  • 平均獲利貢獻最大:第19群
  • 獲利=營收-成本 觀察圖上的點後,得出答案為第19群

【QUIZ】 在以上這個段落裡面 …

  • 我們分群的區隔變數是什麼?
  • 各產品對每個顧客產生的距離矩陣

  • 我們在泡泡圖裡面觀察的變數是哪一些?
  • f、m、s、r、rev、raw、size

  • 它們是相同的嗎?
  • 不同

  • 這個分析的程序,跟Airlines CRM那一個例子有甚麼不同?
  • Airlines是分群完觀察每一群的長條圖,做出相應的分析
  • 這個例子是分群完可以再使用泡泡圖,挑選出想觀察的變數,找出目標的群,然後再使用sig()這個函數,觀察到群內的個體

  • 我們從這個例子學到什麼?
  • 分完群後,可以再使用其他方法,找到群中最有影響力的個體,若是企業,則可以依照群內個體的特性,建立相應的行銷手法



D. 使用尺度縮減方法抽取顧客(產品)的特徵向量

D1. 巨大尺度縮減 (SVD, Sigular Value Decomposition)
library(irlba)
if(LOAD) {
  load("data/svd.rdata")
} else {
  smx = mx
  smx@x = pmin(smx@x, 2)            # cap at 2, similar to normalization  
  t0 = Sys.time()
  svd = irlba(smx, 
              nv=400,               # length of feature vector
              maxit=800, work=800)    
  print(Sys.time() - t0)            # 1.8795 mins
  save(svd, file = "data/svd.rdata")
}
Time difference of 1.835 mins

執行意涵

  • pmin(2):超過2的都設為2
  • irlba():
    1.nv:右邊向量要縮減成的數量
    2.nu:左邊向量要縮減成的數量(預設=nv)
    2.maxit:最大的迭代次數
    4.work:工作區的維度
D2. 依顧客向量對顧客分群
set.seed(111); kg = kmeans(svd$u, 200)$cluster
table(kg) %>% as.vector %>% sort
  [1]    1    1    1    1    1    1    1    1    1    1    1    1    1
 [14]    1    1    1    1    1    1    1    1    1    1    1    1    1
 [27]    1    1    1    1    1    1    1    1    1    1    1    1    1
 [40]    1    1    1    1    1    1    1    1    1    1    1    1    1
 [53]    1    1    2    2    2    2    3    3    3    4    4   15   20
 [66]   25   25   26   26   28   29   32   33   33   33   40   40   43
 [79]   44   50   52   52   53   54   55   56   57   59   63   64   70
 [92]   72   80   84   88   89   95  100  102  110  110  111  118  123
[105]  124  129  130  133  134  134  136  145  147  147  155  159  160
[118]  161  162  162  163  163  164  164  164  166  167  170  171  171
[131]  173  175  176  178  178  179  180  180  184  184  185  187  187
[144]  188  190  192  193  193  194  196  198  198  200  202  202  203
[157]  204  205  209  209  210  210  210  211  211  211  213  216  219
[170]  224  227  229  231  232  234  234  237  240  244  245  246  247
[183]  247  250  251  252  256  259  270  270  279  282  289  297  329
[196]  390  426  554  853 9280
D3. 互動式泡泡圖 (Google Motion Chart)
# clustster summary
df = left_join(A, data.frame(         
  cust = as.integer(smx@Dimnames$cust), kg)) %>% 
  group_by(kg) %>% summarise(
    avg_frequency = mean(f),
    avg_monetary = mean(m),
    avg_revenue_contr = mean(rev),
    group_size = n(),
    avg_recency = mean(r),
    avg_gross_profit = mean(raw)) %>% 
  ungroup %>% 
  mutate(dummy = 2001, kg = sprintf("G%03d",kg)) %>% 
  data.frame
Joining, by = "cust"
# Google Motion Chart
plot( gvisMotionChart(
  subset(df, group_size >= 20 & group_size <= 5000),     
  "kg", "dummy", options=list(width=800, height=600) ) )
D4. 互動式泡泡圖 (ggplot + plotly)
filter(df, group_size >= 20 & group_size <= 5000)$group_size %>% 
  sqrt %>% range    # for bubble size adjustment
[1]  4.472 29.206
library(ggplot2)
library(plotly)
p = df %>% filter(group_size >= 20 & group_size <= 5000) %>% 
  ggplot(aes(x=avg_frequency, y=avg_monetary)) +
  geom_point(aes(size=group_size, col=avg_revenue_contr),alpha=0.7) +
  geom_text(aes(label=kg), alpha=0) +
  scale_size(range=c(1.5,12)) +
  #scale_color_gradient(low="green",high="magenta") +
  scale_colour_gradientn(
    colours = rev(c("red","yellow","green","lightblue","darkblue"))) +
  theme_bw() + guides(size=F) + labs(
    title="顧客集群(依購買產品)",
    color="平均營收貢獻", size="集群人數") +
  xlab("平均購買次數") + 
  ylab("平均購買金額")
plotly_build(p)
D5. 群組的代表性產品 (Signature Product)
Sig(138)
[1] "Group 138: No. Customers = 52"


E. 購物籃分析 Baskets Analysis

dim(mx)   # 32066 cust * 10675 prod
[1] 32256 10675
E1. 準備資料 (for Association Rule Analysis)
library(arules)
library(arulesViz)
bx = subset(Z, prod %in% as.numeric(colnames(mx)), 
            select=c("cust","prod"))  # 只選產品及顧客
bx = split(bx$prod, bx$cust)          # 分割
bx = as(bx, "transactions")           # data for arules package
removing duplicated items in transactions

執行意涵

  • 從z選出在mx裡的cust、prod
  • 然後將其分割
E2. Top20 熱賣產品
itemFrequencyPlot(bx, topN=20, type="absolute", cex=0.8)

#top20
E3. 關聯規則和Apriori演算法

關聯規則(A => B)

  • support: A被購買的機率 (A的基礎機率)
  • confidence: A被購買時,B被購買的機率
  • lift: A被購買時,B被購買的機率增加的倍數 (與B的基礎機率相比)
  • 一般來講support、confidence和lift越高的關聯規則越重要
  • support、confidence和lift設的越低(高),找到的關聯規則越多(少)
  • 建議一開始把標準設低,先找到多一點規則,之後再用subset篩選出特定的規則來看
rules = apriori(bx, parameter=list(supp=0.005, conf=0.6))
Apriori

Parameter specification:
 confidence minval smax arem  aval originalSupport maxtime support
        0.6    0.1    1 none FALSE            TRUE       5   0.005
 minlen maxlen target   ext
      1     10  rules FALSE

Algorithmic control:
 filter tree heap memopt load sort verbose
    0.1 TRUE TRUE  FALSE TRUE    2    TRUE

Absolute minimum support count: 160 

set item appearances ...[0 item(s)] done [0.00s].
set transactions ...[10675 item(s), 32066 transaction(s)] done [0.16s].
sorting and recoding items ... [884 item(s)] done [0.01s].
creating transaction tree ... done [0.02s].
checking subsets of size 1 2 3 4 done [0.04s].
writing ... [67 rule(s)] done [0.00s].
creating S4 object  ... done [0.12s].
summary(rules)
set of 67 rules

rule length distribution (lhs + rhs):sizes
 2  3  4 
16 43  8 

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   2.00    3.00    3.00    2.88    3.00    4.00 

summary of quality measures:
    support          confidence         lift          count    
 Min.   :0.00505   Min.   :0.602   Min.   : 3.2   Min.   :162  
 1st Qu.:0.00558   1st Qu.:0.635   1st Qu.:16.8   1st Qu.:179  
 Median :0.00639   Median :0.676   Median :21.1   Median :205  
 Mean   :0.00786   Mean   :0.700   Mean   :22.5   Mean   :252  
 3rd Qu.:0.00957   3rd Qu.:0.751   3rd Qu.:28.0   3rd Qu.:307  
 Max.   :0.01909   Max.   :0.871   Max.   :70.6   Max.   :612  

mining info:
 data ntransactions support confidence
   bx         32066   0.005        0.6
E4. 檢視關聯規則

關聯規則 (A => B):

  • support: A被購買的機率 (A的基礎機率)
  • confidence: A被購買時,B被購買的機率
  • lift: A被購買時,B被購買的機率增加的倍數 (與B的基礎機率相比)
options(digits=4)
inspect(rules)
     lhs                rhs              support confidence   lift count
[1]  {4710030346103} => {4710030346059} 0.005146     0.6762 70.632   165
[2]  {4719090701051} => {4719090790000} 0.005520     0.6367 39.111   177
[3]  {719859796124}  => {719859796117}  0.007297     0.7222 45.859   234
[4]  {4710321861209} => {4710321861186} 0.007017     0.6338 41.392   225
[5]  {4711524000891} => {4711524001041} 0.005832     0.6561 63.564   187
[6]  {4719090790017} => {4719090790000} 0.010510     0.8300 50.989   337
[7]  {4719090790000} => {4719090790017} 0.010510     0.6456 50.989   337
[8]  {4710011401142} => {4710011401128} 0.007765     0.6434 16.679   249
[9]  {4710085120697} => {4710085120680} 0.012537     0.7992 30.691   402
[10] {4710085120710} => {4710085120703} 0.010416     0.6243 29.790   334
[11] {4710011402026} => {4710011402019} 0.010073     0.7210 29.376   323
[12] {4710085172702} => {4710085172696} 0.009543     0.6120 21.354   306
[13] {4710011409056} => {4710011401128} 0.014813     0.7353 19.061   475
[14] {4710011401135} => {4710011401128} 0.019086     0.7897 20.470   612
[15] {4710011405133} => {4710011401128} 0.016840     0.6968 18.062   540
[16] {4710011406123} => {4710011401128} 0.015624     0.6358 16.481   501
[17] {4714981010038,                                                    
      4719090790017} => {4719090790000} 0.005489     0.8263 50.758   176
[18] {4714981010038,                                                    
      4719090790000} => {4719090790017} 0.005489     0.6692 52.854   176
[19] {4710011401135,                                                    
      4710011401142} => {4710011401128} 0.005114     0.7700 19.959   164
[20] {4710011401128,                                                    
      4710011401142} => {4710011401135} 0.005114     0.6586 27.251   164
[21] {4710085120697,                                                    
      4714981010038} => {4710085120680} 0.005395     0.8047 30.901   173
[22] {4710060000099,                                                    
      4711271000014} => {4714981010038} 0.005613     0.6667  3.548   180
[23] {4710085120093,                                                    
      4710085172702} => {4710085172696} 0.005551     0.7206 25.145   178
[24] {4710085120093,                                                    
      4710085172702} => {4710085120628} 0.005052     0.6559 17.778   162
[25] {4710085172696,                                                    
      4710085172702} => {4710085120628} 0.006112     0.6405 17.362   196
[26] {4710085120628,                                                    
      4710085172702} => {4710085172696} 0.006112     0.6712 23.421   196
[27] {4710311703014,                                                    
      4714981010038} => {4711271000014} 0.005863     0.6045  3.702   188
[28] {4710683100015,                                                    
      4714981010038} => {4711271000014} 0.006393     0.6949  4.256   205
[29] {4710088620156,                                                    
      4714981010038} => {4711271000014} 0.005645     0.6177  3.783   181
[30] {4710063031106,                                                    
      4711271000014} => {4714981010038} 0.007984     0.6139  3.267   256
[31] {4710685440362,                                                    
      4714981010038} => {4711271000014} 0.005520     0.6254  3.830   177
[32] {4711022100017,                                                    
      4711271000014} => {4714981010038} 0.005364     0.6277  3.340   172
[33] {4710011401135,                                                    
      4710011409056} => {4710011405133} 0.007266     0.6132 25.370   233
[34] {4710011405133,                                                    
      4710011409056} => {4710011401135} 0.007266     0.6833 28.271   233
[35] {4710011406123,                                                    
      4710011409056} => {4710011401135} 0.006144     0.6611 27.352   197
[36] {4710011401135,                                                    
      4710011409056} => {4710011401128} 0.009917     0.8368 21.693   318
[37] {4710011401128,                                                    
      4710011409056} => {4710011401135} 0.009917     0.6695 27.700   318
[38] {4710011406123,                                                    
      4710011409056} => {4710011405133} 0.005676     0.6107 25.270   182
[39] {4710011405133,                                                    
      4710011409056} => {4710011401128} 0.008389     0.7889 20.449   269
[40] {4710011406123,                                                    
      4710011409056} => {4710011401128} 0.007391     0.7953 20.616   237
[41] {4710011409056,                                                    
      4714981010038} => {4710011401128} 0.005707     0.7409 19.206   183
[42] {4711271000014,                                                    
      4714381003128} => {4714981010038} 0.006362     0.6296  3.350   204
[43] {4710085120093,                                                    
      4710085172696} => {4710085120628} 0.008545     0.6241 16.918   274
[44] {4710011401135,                                                    
      4710011405133} => {4710011401128} 0.010634     0.8158 21.147   341
[45] {4710011401128,                                                    
      4710011405133} => {4710011401135} 0.010634     0.6315 26.128   341
[46] {4710011401135,                                                    
      4710011406123} => {4710011401128} 0.009013     0.8353 21.652   289
[47] {4710011401135,                                                    
      4711271000014} => {4710011401128} 0.005801     0.7782 20.174   186
[48] {4710011401135,                                                    
      4714981010038} => {4710011401128} 0.007048     0.7958 20.628   226
[49] {4710011405133,                                                    
      4710011406123} => {4710011401128} 0.008015     0.7449 19.310   257
[50] {4710011405133,                                                    
      4711271000014} => {4710011401128} 0.005426     0.7468 19.358   174
[51] {4710011405133,                                                    
      4714981010038} => {4710011401128} 0.006674     0.7086 18.369   214
[52] {4710011406123,                                                    
      4714981010038} => {4710011401128} 0.006674     0.6751 17.500   214
[53] {4710421090059,                                                    
      4714981010038} => {4711271000014} 0.009605     0.6299  3.857   308
[54] {4710583996008,                                                    
      4719090900065} => {4714981010038} 0.006019     0.6942  3.694   193
[55] {4710265849066,                                                    
      4719090900065} => {4714981010038} 0.009200     0.6020  3.204   295
[56] {4710265849066,                                                    
      4711271000014} => {4714981010038} 0.011539     0.6390  3.400   370
[57] {4713985863121,                                                    
      4719090900065} => {4714981010038} 0.005894     0.7269  3.868   189
[58] {4711271000014,                                                    
      4713985863121} => {4714981010038} 0.010728     0.6922  3.683   344
[59] {4711271000014,                                                    
      4719090900065} => {4714981010038} 0.014533     0.6099  3.246   466
[60] {4710011401135,                                                    
      4710011405133,                                                    
      4710011409056} => {4710011401128} 0.006331     0.8712 22.585   203
[61] {4710011401128,                                                    
      4710011401135,                                                    
      4710011409056} => {4710011405133} 0.006331     0.6384 26.413   203
[62] {4710011401128,                                                    
      4710011405133,                                                    
      4710011409056} => {4710011401135} 0.006331     0.7546 31.224   203
[63] {4710011401135,                                                    
      4710011406123,                                                    
      4710011409056} => {4710011401128} 0.005333     0.8680 22.501   171
[64] {4710011401128,                                                    
      4710011406123,                                                    
      4710011409056} => {4710011401135} 0.005333     0.7215 29.853   171
[65] {4710011401135,                                                    
      4710011405133,                                                    
      4710011406123} => {4710011401128} 0.005520     0.8634 22.382   177
[66] {4710011401128,                                                    
      4710011401135,                                                    
      4710011406123} => {4710011405133} 0.005520     0.6125 25.341   177
[67] {4710011401128,                                                    
      4710011405133,                                                    
      4710011406123} => {4710011401135} 0.005520     0.6887 28.496   177
# install.packages(
#   "https://cran.r-project.org/bin/windows/contrib/3.5/arulesViz_1.3-1.zip",
#   repos=NULL)
# install.packages("arulesViz_1.3-1.zip", repos=NULL)
# library(plotly)
# plotly_arules(rules,colors=c("red","green"),
#               marker=list(opacity=.6,size=10))
# plotly_arules(rules,method="matrix",
#               shading="lift",
#               colors=c("red", "green"))
# 
E5. 互動圖表顯示
plot(rules,colors=c("red","green"),engine="htmlwidget",
     marker=list(opacity=.6,size=8))
plot(rules,method="matrix",shading="lift",engine="htmlwidget",
     colors=c("red", "green"))

執行意涵

  • LHS:左手邊,可視為關聯規則中A=>的A
  • RHS:右手邊,可視為B
  • 圖中的連續區段顯示,不只一種A對應到B,可能為A1=>B,{A1,A2}=>B
E6. 篩選產品、互動式關聯圖
r1 = subset(rules, subset = rhs %in% c("4719090790000"))
summary(r1)
set of 3 rules

rule length distribution (lhs + rhs):sizes
2 3 
2 1 

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   2.00    2.00    2.00    2.33    2.50    3.00 

summary of quality measures:
    support          confidence         lift          count    
 Min.   :0.00549   Min.   :0.637   Min.   :39.1   Min.   :176  
 1st Qu.:0.00550   1st Qu.:0.732   1st Qu.:44.9   1st Qu.:176  
 Median :0.00552   Median :0.826   Median :50.8   Median :177  
 Mean   :0.00717   Mean   :0.764   Mean   :47.0   Mean   :230  
 3rd Qu.:0.00801   3rd Qu.:0.828   3rd Qu.:50.9   3rd Qu.:257  
 Max.   :0.01051   Max.   :0.830   Max.   :51.0   Max.   :337  

mining info:
 data ntransactions support confidence
   bx         32066   0.005        0.6
plot(r1,method="graph",engine="htmlwidget",itemCol="cyan") 
  • 泡泡大小:support: A被購買的機率 (A的基礎機率)
  • 泡泡顏色:lift: A被購買時,B被購買的機率增加的倍數 (與B的基礎機率相比)
r2 = subset(rules, subset = rhs %in% c("4710011401135"))
summary(r2)
set of 8 rules

rule length distribution (lhs + rhs):sizes
3 4 
5 3 

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   3.00    3.00    3.00    3.38    4.00    4.00 

summary of quality measures:
    support          confidence         lift          count    
 Min.   :0.00511   Min.   :0.631   Min.   :26.1   Min.   :164  
 1st Qu.:0.00547   1st Qu.:0.660   1st Qu.:27.3   1st Qu.:176  
 Median :0.00624   Median :0.676   Median :28.0   Median :200  
 Mean   :0.00703   Mean   :0.684   Mean   :28.3   Mean   :226  
 3rd Qu.:0.00793   3rd Qu.:0.697   3rd Qu.:28.8   3rd Qu.:254  
 Max.   :0.01063   Max.   :0.755   Max.   :31.2   Max.   :341  

mining info:
 data ntransactions support confidence
   bx         32066   0.005        0.6
plot(r2,method="graph",engine="htmlwidget",itemCol="cyan") 


F. 產品推薦 Product Recommendation

F1. 篩選顧客、產品

太少被購買的產品和購買太少產品的顧客都不適合使用Collaborative Filtering這種產品推薦方法,所以我們先對顧客和產品做一次篩選

library(recommenderlab)
rx = mx[, colSums(mx > 0) >= 50]      #篩出大於五十次購買的商品
rx = rx[rowSums(rx > 0) >= 20 & rowSums(rx > 0) <= 300, ] #篩出購買超過二十次小於三百次的顧客
dim(rx)
[1] 8860 3355
F2. 選擇產品評分方式

可以選擇要用

  • 購買次數 (realRatingMatrix)→量化
  • 是否購買 (binaryRatingMatrix)→二分法

做模型。

rx = as(rx, "realRatingMatrix")  # realRatingMatrix
bx = binarize(rx, minRating=1)   # binaryRatingMatrix
dim(bx)
[1] 8860 3355
F3. 設定模型(準確性)驗證方式
set.seed(4321)
scheme = evaluationScheme(     
  bx, method="split", train = .75,  given=5)
F4. 設定推薦方法(參數)
algorithms = list(            
  AR53 = list(name="AR", param=list(support=0.0005, confidence=0.3)),
  AR43 = list(name="AR", param=list(support=0.0004, confidence=0.3)),
  RANDOM = list(name="RANDOM", param=NULL),
  POPULAR = list(name="POPULAR", param=NULL),
  UBCF = list(name="UBCF", param=NULL),
  IBCF = list(name="IBCF", param=NULL) )
F5. 建模、預測、驗證(準確性)
if(LOAD) {
  load("results.rdata")
} else {
  t0 = Sys.time()
  results = evaluate(            
    scheme, algorithms, type="topNList",
    n=c(5, 10, 15, 20))
  print(Sys.time() - t0)
  save(results, file="results.rdata")
}
AR run fold/sample [model time/prediction time]
     1  [3.82sec/163.4sec] 
AR run fold/sample [model time/prediction time]
     1  [7.82sec/399.2sec] 
RANDOM run fold/sample [model time/prediction time]
     1  [0sec/9.89sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [0sec/8.23sec] 
UBCF run fold/sample [model time/prediction time]
     1  [0sec/58.55sec] 
IBCF run fold/sample [model time/prediction time]
     1  [152.2sec/1.46sec] 
Time difference of 13.8 mins
F6. 模型準確性比較
# load("data/results.rdata")
par(mar=c(4,4,3,2),cex=0.8)
cols = c("red", "magenta", "gray", "orange", "blue", "green")
plot(results, annotate=c(1,3), legend="topleft", pch=19, lwd=2, col=cols)
abline(v=seq(0,0.006,0.001), h=seq(0,0.08,0.01), col='lightgray', lty=2)

F7. 儲存產品推薦模型
save(results, file="data/results.rdata")






LS0tDQp0aXRsZTogIlByb2R1Y3TvvJrnlKLlk4HpirfllK7os4foqIoiDQphdXRob3I6ICJHcm91cCAyLCAyMDE4LzA4LzE1Ig0Kb3V0cHV0OiBodG1sX25vdGVib29rDQoNCi0tLQ0KDQo8YnI+DQoNCmBgYHtyfQ0KU3lzLnNldGxvY2FsZSgiTENfQUxMIiwiQyIpDQpwYWNrYWdlcyA9IGMoDQogICJkcGx5ciIsImdncGxvdDIiLCJnb29nbGVWaXMiLCJkZXZ0b29scyIsIm1hZ3JpdHRyIiwic2xhbSIsImlybGJhIiwicGxvdGx5IiwNCiAgImFydWxlcyIsImFydWxlc1ZpeiIsIk1hdHJpeCIsInJlY29tbWVuZGVybGFiIikNCmV4aXN0aW5nID0gYXMuY2hhcmFjdGVyKGluc3RhbGxlZC5wYWNrYWdlcygpWywxXSkNCmZvcihwa2cgaW4gcGFja2FnZXNbIShwYWNrYWdlcyAlaW4lIGV4aXN0aW5nKV0pIGluc3RhbGwucGFja2FnZXMocGtnKQ0KYGBgDQoNCmBgYHtyIHdhcm5pbmc9RiwgbWVzc2FnZT1GLCBjYWNoZT1GLCBlcnJvcj1GfQ0Kcm0obGlzdD1scyhhbGw9VFJVRSkpDQpMT0FEID0gRkFMU0UNCmxpYnJhcnkoZHBseXIpDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KGdvb2dsZVZpcykNCmxpYnJhcnkoTWF0cml4KQ0KbGlicmFyeShzbGFtKQ0KbGlicmFyeShpcmxiYSkNCmxpYnJhcnkocGxvdGx5KQ0KbGlicmFyeShhcnVsZXMpDQpsaWJyYXJ5KGFydWxlc1ZpeikNCmxpYnJhcnkocmVjb21tZW5kZXJsYWIpDQpgYGANCjxicj48aHI+DQoNCiMjIyBBLiBSRk3liIbnvqQNCg0KYGBge3J9DQpsb2FkKCJkYXRhL3RmMC5yZGF0YSIpDQpBID0gQTA7IFggPSBYMDsgWiA9IFowOyBybShBMCxYMCxaMCk7IGdjKCkNCiNa57i95Lqk5piT57SA6YyEDQojWOWWruethuS6pOaYk+e0gOmMhOW9meaVtA0KI0HmnIPlk6HkuqTmmJPntIDpjIQNCmBgYA0KDQpgYGB7cn0NCmsgPSA4OyBzZXQuc2VlZCg3NzcpDQpBJGdyb3VwID0ga21lYW5zKHNjYWxlKEFbLDI6Nl0pLCBrKSRjbHVzdGVyOyB0YWJsZShBJGdyb3VwKQ0KDQpgYGANCioq5Z+36KGM5oSP5ra1KioNCg0KKyDlsI1B55qEMn425qyE55qE6LOH5paZ5YGa5qiZ5rqW5YyW5Lim5YiG576kDQorIEs9OCDlhbHliIblhavnvqQNCg0KDQpgYGB7cn0NCmRmID0gZ3JvdXBfYnkoQSwgZ3JvdXApICU+JSBzdW1tYXJpc2UoIA0KICBhdmdfZnJlcXVlbmN5ID0gbWVhbihmKSwgICAj5bmz5Z2H6aC7546HDQogIGF2Z19tb25ldGFyeSA9IG1lYW4obSksICAgICPlubPlnYfllq7nrYbos7zosrcNCiAgdG90YWxfcmV2ZW51ZSA9IHN1bShyZXYpLCAgI+e4veaUtuWFpQ0KICBncm91cF9zaXplID0gbigpLCAgICAgICAgICAj576k5YWn5YCL5pW4DQogIGF2Z19yZWNlbmN5ID0gbWVhbihyKSwgICAgICPlubPlnYfnmoTmnIDov5HkvoblupflpKnmlbgNCiAgcHJvZml0ID0gc3VtKHJhdykgICAgICAgICAgI+e4veaIkOacrA0KICApICU+JSB1bmdyb3VwICU+JSANCiAgbXV0YXRlKCAgDQogICAgcGNfcmV2ZW51ZSA9IHJvdW5kKDEwMCp0b3RhbF9yZXZlbnVlL3N1bSh0b3RhbF9yZXZlbnVlKSwxKSwgDQogICAgcGNfcHJvZml0ID0gcm91bmQoMTAwKnByb2ZpdC9zdW0ocHJvZml0KSwxKSwgICAgICAgICAgICAgICAgDQogICAgZHVtbXkgPSAyMDAxICANCiAgKSAlPiUgZGF0YS5mcmFtZQ0KYGBgDQoqKuWft+ihjOaEj+a2tSoqDQoNCisg5bCH6LOH5paZ5L6d54WnZ3JvdXDlgZros4fmlpnntbHmlbQNCisg6aGN5aSW5aKe5Yqg5qyE5L2NPGJyPg0KICAxLnBjX3JldmVudWU65q+P5LiA576kcmV2ZW51ZeS9lOe4vemrlOeahCXmlbg8YnI+DQogIDIucGNfcHJvZml0Ouavj+S4gOe+pHByb2ZpdOS9lOe4vemrlOeahCXmlbg8YnI+DQogIDMuZHVtbXk6Z29vZ2xlVml655qE6LOH5paZ5qC85byPPGJyPg0KYGBge3J9DQpoZWFkKGRmKQ0KYGBgDQoNCmBgYHtyfQ0KcGxvdCggZ3Zpc01vdGlvbkNoYXJ0KA0KICBkZiwgImdyb3VwIiwgImR1bW15IiwNCiAgb3B0aW9ucz1saXN0KHdpZHRoPTgwMCwgaGVpZ2h0PTYwMCkgDQogICkpDQpgYGANCioq5Z+36KGM5oSP5ra1KioNCg0KKyDlsIfos4fmlpnnlavngrrlj6/mk43nuLHnmoTli5XlnJYNCjxicj4NCg0KKirjgJBRVUla44CRKiog5aaC5p6c5oiR5YCR5oqKWO+8jFnou7jliIbliKXmlLnmiJBsb2coYXZnX21vbmV0YXJ5KeWSjGxvZyhhdmdfcmVjZW5jeSnvvIzmoLnmk5rlnJbkuIrnmoTpoa/npLogLi4uDQo8Y2VudGVyPg0KIVvlnJbkuIDjgIFMb2flvoznmoTlnJZdKGRhdGEvMS5wbmcpDQo8L2NlbnRlcj4NCg0KKyDkvaDoqo3ngrrpgJnlrrblupfnm67liY3mnIDpnIDopoHlsI3pgqPkuIDlgIvml4/nvqTvvJ/lgZrlk6rkuIDlgIvli5XkvZzvvJ/ngrrku4Dpurw/DQoNCisg5oiR5YCR5omA5pyf5pyb55qE6aGn5a6i77yM5oeJ6Kmy5piv5bi45bi45L6G5LiU5bmz5Z2H5Zau562G5raI6LK76KaB6auY55qE6aGn5a6i77yM5omA5Lul5Zyo6YCZ5by15ZyW5Lit77yM6LaK5b6A5Y+z5LiL5pyD5piv5oiR5YCR55qE6YeR6Zue5q+N5Z6L6aGn5a6i77yM5Y+N5LmL5L2N5pa85bem5LiK55qE6aGn5a6i5piv5bGs5pa85oiR5YCR55qE5rKJ552h6aGn5a6iDQoNCisg5oiR5YCR6KqN54K66Kmy5bCN57SF6Imy55qE5peP576k5YGa5Ye66KGM6Yq344CB5L+D6Yq355qE5YuV5L2c77yM55Sx5pa86YCZ5YCL5peP576k5L2N5pa85YGP5bem5Lit55qE6YOo5YiG77yM5Lul6LO86LK355qE6aC7546H5L6G6Kyb77yM5L2N5pa85Lit5ri477yM5raI6LK76YeR6aGN5YmH5pyJ5b6I5aSn55qE6YCy5q2l56m66ZaT77yM5bGs5pa85oiR5YCR55qE5r2b5Zyo6aGn5a6i77yM6YeN6bue5piv5peP576k55qE5pW46YeP6b6Q5aSn77yM6Iul5piv5Lul6YCZ6aGe5peP576k55qE54m55oCn5YGa5Ye655u45oeJ55qE6KGM6Yq35omL5rOV77yM6IO955u06Z2i5Yiw5pu05aSa55qE6aGn5a6i77yM5Lul5ZWG5a6255qE6KeS5bqm5L6G5oCd6ICD55qE6Kmx77yM6YCZ5qij55qE6YG45pOHQ1DlgLzmnIPmmK/mnIDpq5jnmoQNCiANCg0KPGJyPjxocj4NCg0KIyMjIEIuIOmhp+WuoueUouWTgeefqemZow0KYGBge3J9DQpuX2Rpc3RpbmN0KFokY3VzdCkgICMgMzIyNTYNCm5fZGlzdGluY3QoWiRwcm9kKSAgIyAyMzc4OQ0KYGBgDQoNCuaTjeS9nOefqemZo+mBi+eul+S5i+WJje+8jOmAmuW4uOaIkeacg+i8ieWFpemAmeWFqeWAi+Wll+S7tg0KYGBge3J9DQpsaWJyYXJ5KE1hdHJpeCkNCmxpYnJhcnkoc2xhbSkNCmBgYA0KDQroo73kvZzpoaflrqLnlKLlk4Hnn6npmaPlhbblr6blvojlv6vjgIHkuZ/lvojlrrnmmJMNCmBgYHtyfQ0KbXggPSB4dGFicyh+IGN1c3QgKyBwcm9kLCBaLCBzcGFyc2U9VCkNCmBgYA0KDQrpoaflrqLnlKLlk4Hnn6npmaPpgJrluLjmmK/kuIDlgIvlvojnqIDnlo/nmoTnn6npmaMNCmBgYHtyfQ0KbWVhbihteCA+IDApDQpgYGANCioq5Z+36KGM5oSP5ra1KioNCg0KKyDnn6npmaPlr4bluqbvvIzlj6/ku6Xnn6XpgZPnn6npmaPkuK3mnInlpJrlsJHmrITkvY3mmK/lsazmlrzpm7YgDQoNCuacieS4gOS6m+eUouWTgeaykuS7gOm6vOS6uuiytw0KYGBge3J9DQp0YWJsZShjb2xTdW1zKG14KSA8IDEwKSAgICAgICAjIOabvue2k+iiq+iyt+mBjuWwj+aWvDEw5qyh55qE5ZWG5ZOBIA0KYGBgDQoNCuWIquWOu+izvOiyt+asoeaVuOWwj+aWvDEw55qE55Si5ZOB77yM54S25b6M5Yiq5Y675rKS5pyJ6LO86LK355Si5ZOB55qE6aGn5a6iDQpgYGB7cn0NCm14ID0gbXhbLCBjb2xTdW1zKG14KSA+IDEwXSAgIA0KYGBgDQoqKuWft+ihjOaEj+a2tSoqDQoNCisg5Yiq5Y675bCP5pa8MTDmrKHos7zosrfmrKHmlbjnmoTllYblk4EgDQorIOeVmeS4i+izvOiyt+asoeaVuOWkp+aWvDDnmoTmnIPlk6ENCisg6LOH5paZ5pW477yaMzIyNTY+MzIwNjYgMjM3ODk+MTA2NzUNCisg55+p6Zmj57iu5rib5LqG6Kix5aSaDQoNCuaqouafpeS4gOS4i+efqemZo+ijoemdoueahOWAvOWIhuW4gw0KYGBge3J9DQptYXgobXgpICAgICAgICAgICAgICAgICAgICAgICANCnRhYmxlKG14QHgpICU+JSBwcm9wLnRhYmxlICU+JSByb3VuZCg0KSAlPiUgaGVhZCgxMCkNCiPlj5ZteOijoeeahHggICAgICPkvZTlhajpg6jnmoTlub4lICAgICPlsI/mlbjpu57lm5vkvY0gICPliY3ljYHlkI0NCmBgYA0KKirln7fooYzmhI/mtrUqKg0KDQorIG1heChteCk66KKr6LO86LK36YGO5pyA5aSa5qyh55qE5ZWG5ZOB5pyJ5bm+5qyhDQorIHRhYmxlKG14QHgpOuWPlm145Lit55qEeOWBmuaIkOihqOagvA0KKyBwcm9wLnRhYmxlOuavj+WAi+WVhuWTgeS9lHjlhbbkuK3nmoTlpJrlsJElDQorIOaJvuWHuuevqemBuOW+jOeahOWVhuWTgeijoe+8jOizvOiyt+asoeaVuOacgOWkmueahOWJjeWNgeWQjQ0KDQo8YnI+DQoNCioq44CQUVVJWuOAkSoqIOWmguaenGBteFtpLCBqXSA9IDNg77yM6YCZ6KGo56S6IC4uLi4NCg0KKyAkQ3VzdG9tZXJfaSTnuL3lhbHosrckUHJvZGljdF9qJOiyt+S6huS4ieasoQ0KKyBteOaYr+eUsXrlj5blh7rvvIxp5Luj6KGo55qE5pivY3VzdCxq5Luj6KGo55qE5pivcHJvZO+8jOaVhW14W2ksal09M+ihqOekuu+8jOmhp+Wuomnos7zosrfkuobllYblk4Fq5LiJ5qyhDQoNCjxicj48aHI+DQoNCiMjIyBDLiDkvp3os7zosrfpoLvnjofliIbnvqQgDQoNCiMjIyMjIEMxLiDmlLnorooo56iA55aPKeefqemZo+agvOW8jw0K56iA55aP55+p6Zmj5pyJ5b6I5aSa56iu5qC85byP77yM5LiN5ZCM55qE5bel5YW35pyD5L2/55So5LiN5ZCM55qE5qC85byPDQpgYGB7cn0NCmxpYnJhcnkoc2xhbSkNCnRteCA9IGFzKG14LCJkZ1RNYXRyaXgiKQ0KdG14ID0gc2ltcGxlX3RyaXBsZXRfbWF0cml4KA0KICAxK3RteEBpLCAxK3RteEBqLCB0bXhAeCwgZGltbmFtZXM9bXhARGltbmFtZXMpICANCmRpbSh0bXgpDQpgYGANCioq5Z+36KGM5oSP5ra1KioNCg0KKyBzaW1wbGVfdHJpcGxldF9tYXRyaXgoaSxqLHYsZGltbmFtZXMpOjxicj4NCiAxLmk66KGMPGJyPg0KIDIuajrliJc8YnI+DQogMy52OuWQkemHj+WAvDxicj4NCiA0LmRpbW5hbWVzOuefqemZo+eahOWxrOaApyjmraTomZXngrpjdXN044CBcHJvZCkNCisg56iA55aP55+p6Zmj5aSa5YWD55qE5qC85byP77yM5Zug5oeJ5L2/55So5bel5YW36ICM5pyJ5omA5LiN5ZCMDQoNCiMjIyMjIEMyLiDkvLDoqIjlkITnlKLlk4HnmoTlubPlnYfph43opoHmgKcgKEF2ZXJhZ2UgVEYtSURGIFNjb3JlcykNCuaIkeWAkeWAn+eUqOaWh+Wtl+WIhuaekOijoemdouS8sOioiOWWruWtl+WcqOaWh+eroOS5i+S4reeahOmHjeimgeaAp+eahOaWueazlShURi1JREYp77yM6KiI566X5ZCE55Si5ZOB5Zyo5omA5pyJ6aGn5a6i5LmL6ZaT55qE5bmz5Z2H6YeN6KaB5oCnDQpgYGB7cn0NCnRmaWRmID0gdGFwcGx5KHRteCR2L3Jvd19zdW1zKHRteClbdG14JGldLCB0bXgkaiwgbWVhbikgKg0KICBsb2cyKG5yb3codG14KS9jb2xfc3Vtcyh0bXggPiAwKSkNCnN1bW1hcnkodGZpZGYpDQpgYGANCioq5Z+36KGM5oSP5ra1KioNCg0KKyBURjroqZ7poLvvvIzmloflrZflnKjmlofpm4bkuK3lh7rnj77pgY7nmoTpoLvnjocNCisgSURGOumAhuWQkeaWh+S7tumgu+eOh++8jOmAmeWAi+ipnuWcqOaJgOacieaWh+mbhuS4reWHuuePvueahOmgu+eOh++8jOiLpeaYr+WkquWkmu+8jOS7o+ihqOi2iuS4jemHjeimgQ0KKyBURklERjrlhanlgIvlj4PmlbjmrIrph43nrpflh7rnmoTntZDmnpwNCg0KDQpgYGB7ciBmaWcuaGVpZ2h0PTMsIGZpZy53aWR0aD03LjJ9DQpoaXN0KHRmaWRmKQ0KYGBgDQoNCiMjIyMjIEMzLiDnr6npgbjnlKLlk4HjgIHph43mlrDmjpLliJcNCuevqeWOu+W5s+Wdh+mHjeimgeaAp+avlOi8g+S9jueahOeUouWTgeOAgeeEtuW+jOWwh+eUouWTgeS+neiiq+izvOiyt+asoeaVuOmZjeWGquaOkuWIlw0KYGBge3J9DQp0bXggPSB0bXhbLCB0ZmlkZiA+IG1lYW4odGZpZGYpXSAgICAjIOWwh+Wwj+aWvOW5s+Wdh+eahOWOu+mZpA0KdG14ID0gdG14Wywgb3JkZXIoLWNvbF9zdW1zKHRteCkpXSAgIyDpmY3lhqoNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgMTA2NzXihpIzODIzDQpkaW0odG14KQ0KYGBgDQoqKuWft+ihjOaEj+a2tSoqDQoNCisg5bCHdGZpZGblsI/mlrzlubPlnYfnmoTljrvpmaTkuKbpmY3lhqrmjpLliJcNCg0KDQojIyMjIyBDNC4g5L6dKOizvOiyt+mgu+eOh+acgOmrmOeahCnnlKLlk4HlsI3poaflrqLliIbnvqQNCmBgYHtyfQ0Kbm9wPSA0MDAgICMgbm8uIHByb2R1Y3QNCmsgPSAyMDAgICAjIG5vLiBjbHVzdGVyDQpzZXQuc2VlZCgxMTEpOyBrZyA9IGttZWFucyh0bXhbLDE6bm9wXSwgaykkY2x1c3Rlcg0KdGFibGUoa2cpICU+JSBhcy52ZWN0b3IgJT4lIHNvcnQNCmBgYA0KKirln7fooYzmhI/mtrUqKg0KDQorIOaMieeFp+izvOiyt+mgu+eOh+eUsemrmOWIsOS9juaOkuWIl++8jOmBuOaTh+Wbm+eZvuWAi+eUouWTgQ0KKyDlsI3poaflrqLlgZoyMDDnvqTnmoTliIbnvqQNCg0KIyMjIyMgQzUuIOWQhOe+pOe1hOW5s+Wdh+WxrOaApw0K5bCH5YiG576k57WQ5p6c5L215YWl6aGn5a6i6LOH5paZ5qGGKGBBYCkNCmBgYHtyfQ0KZGYgPSBpbm5lcl9qb2luKEEsIGRhdGEuZnJhbWUoDQogIGN1c3QgPSBhcy5pbnRlZ2VyKHRteCRkaW1uYW1lcyRjdXN0KSwga2cpKQ0KDQpgYGANCioq5Z+36KGM5oSP5ra1KioNCg0KKyBrZzrnvqTnt6jomZ8NCisgc2l6ZTrnvqTlhaflgIvmlbgNCisg5bCH57WQ5p6c55SoaW5uZXJfam9pbuS+neeFp2N1c3TkvbXlhaXos4fmlpnpm4ZBDQoNCuioiOeul+WQhOe+pOe1hOeahOW5s+Wdh+WxrOaApw0KYGBge3J9DQpkZiA9IGRhdGEuZnJhbWUoDQogIGFnZ3JlZ2F0ZSguIH4ga2csIGRmWyxjKDI6NywxMSldLCBtZWFuKSwNCiAgc2l6ZSA9IGFzLnZlY3Rvcih0YWJsZShrZykpDQogICkNCmhlYWQoZGYpDQpgYGANCg0KIyMjIyMgQzYuIOS6kuWLleW8j+azoeazoeWclg0KYGBge3J9DQpkZiRkdW1teSA9IDIwMDEgICAgICAgICAgICAgICAgICAgICAgICAjIGR1bW15IGNvbHVtbiBmb3IgZ29vZ2xlVml6DQpwbG90KCBndmlzTW90aW9uQ2hhcnQoDQogIHN1YnNldChkZlssYygxLDQsNSw2LDgsMiwzLDcsOSldLCANCiAgICAgICAgIHNpemUgPj0gMjAgJiBzaXplIDw9IDUwMDAgICAgICMg5bCH5aSn5pa8MjDlsI/mlrw1MDAw55qE5YiX5Ye6DQogICAgICAgICApLCANCiAgImtnIiwgImR1bW15Iiwgb3B0aW9ucz1saXN0KHdpZHRoPTgwMCwgaGVpZ2h0PTYwMCkgKSApDQpgYGANCg0KIyMjIyMgQzcuIOWQhOe+pOe1hOeahOS7o+ihqOaAp+eUouWTgSAoU2lnbmF0dXJlIFByb2R1Y3QpDQpgYGB7cn0NClNpZyA9IGZ1bmN0aW9uKGd4LCBQPTEwMDAsIEg9MTApIHsNCiAgcHJpbnQoc3ByaW50ZigiR3JvdXAgJWQ6IE5vLiBDdXN0b21lcnMgPSAlZCIsIGd4LCBzdW0oa2c9PWd4KSkpDQogIGJ4ID0gdG14WywxOlBdDQogIGRhdGEuZnJhbWUobiA9IGNvbF9zdW1zKGJ4W2tnPT1neCxdKSkgJT4lICAgICAgDQogICAgbXV0YXRlKA0KICAgICAgc2hhcmUgPSByb3VuZCgxMDAqbi9jb2xfc3VtcyhieCksMiksICAgICAgIA0KICAgICAgY29uZiA9IHJvdW5kKDEwMCpuL3N1bShrZz09Z3gpLDIpLCAgICAgICAgIA0KICAgICAgYmFzZSA9IHJvdW5kKDEwMCpjb2xfc3VtcyhieCkvbnJvdyhieCksMiksIA0KICAgICAgbGlmdCA9IHJvdW5kKGNvbmYvYmFzZSwxKSwgICAgICAgDQogICAgICBuYW1lID0gY29sbmFtZXMoYngpDQogICAgKSAlPiUgYXJyYW5nZShkZXNjKGxpZnQpKSAlPiUgaGVhZChIKQ0KICB9DQpgYGANCioq5Z+36KGM5oSP5ra1KioNCg0KKyBwKDEwMDApOjF+MTAwMOeahOeUouWTgQ0KKyBuOua2iOiyu+WAi+aVuOe4veWSjA0KKyBzaGFyZTrpgJnnlKLlk4Hlhajpg6jnmoTlub4l6LOj57Wm6YCZ576k5Lq6DQorIGNvbmY66YCZ576k5Lq65bmz5Z2H6LO86LK35bm+JemAmemhnueUouWTgQ0KKyBiYXNlOuaJgOacieS6uuW5s+Wdh+izvOiyt+W5viXpgJnpoZ7nlKLlk4ENCisgbGlmdDrot5/miYDmnInkurrmr5TpgJnpoZ7kurrlpJrkuoblub4l5Y+v6IO95pyD6LO86LK36YCZ6aGe55Si5ZOBDQorIOatpOWHveaVuOWPr+S7peaJvuWHuue+pOWFp+i8g+eCuumhr+iRl+aAp+OAgeS7o+ihqOaAp+eahOeUouWTge+8jOacgOW+jOS+neeFp2xpZnTnlLHpq5jmjpLoh7PkvY4NCg0KYGBge3J9DQpTaWcoMSkNCiPnrKzkuIDnvqQNCiM1N+WAi+mhp+Wuog0KYGBgDQoNCisgR+aXj+e+pOS4gOWFseiyt+S6hmBuYOWAi1DnlKLlk4ENCisgUOeUouWTgeaciWBzaGFyZSVg5piv6LOj57WmR+aXj+e+pA0KKyBH5peP576k5q+P5Lq65bmz5Z2H6LO86LK3YGNvbmYvMTAwYOWAi1DnlKLlk4ENCisg5omA5pyJ6aGn5a6i5LmL5Lit77yM5q+P5Lq65bmz5Z2H6LO86LK3YGJhc2UvMTAwYOWAi1DnlKLlk4ENCisg6Lef5omA5pyJ6aGn5a6i55u45q+U77yMR+aXj+e+pOizvOiyt1DnlKLlk4HnmoTmqZ/njofkuIrljYfkuoZgbGlmdC0xYOWAjSANCg0KDQoqKuOAkFFVSVrjgJEqKiDlvp7kupLli5XlvI/ms6Hms6HlnJbnnIvkvoYgLi4uDQoNCisg5bmz5Z2H5a6i5Zau5YO544CB5bmz5Z2H6LO86LK35qyh5pW444CB5bmz5Z2H54ef5pS26LKi542744CB5bmz5Z2H542y5Yip6LKi54275pyA5aSn55qE5YiG5Yil5piv5ZOq5LiA5YCL5peP576k77yf5a6D5YCR55qE54m55b6155Si5ZOB5YiG5Yil5piv5LuA6bq877yfDQorIOW5s+Wdh+WuouWWruWDueacgOWkp++8muesrDE5MOe+pA0KYGBge3J9DQpTaWcoMTkwKQ0KYGBgDQoNCisg5bmz5Z2H6LO86LK35qyh5pW45pyA5aSn77ya56ysMTg2576kDQpgYGB7cn0NClNpZygxODYpDQpgYGANCg0KKyDlubPlnYfnh5/mlLbosqLnjbvmnIDlpKfvvJrnrKwxOee+pA0KYGBge3J9DQpTaWcoMTkpDQpgYGANCg0KKyDlubPlnYfnjbLliKnosqLnjbvmnIDlpKfvvJrnrKwxOee+pA0KKyDnjbLliKk954ef5pS2LeaIkOacrCDop4Dlr5/lnJbkuIrnmoTpu57lvozvvIzlvpflh7rnrZTmoYjngrrnrKwxOee+pA0KDQoqKuOAkFFVSVrjgJEqKiDlnKjku6XkuIrpgJnlgIvmrrXokL3oo6HpnaIgLi4uDQoNCisg5oiR5YCR5YiG576k55qE5Y2A6ZqU6K6K5pW45piv5LuA6bq877yfIA0KKyDlkITnlKLlk4HlsI3mr4/lgIvpoaflrqLnlKLnlJ/nmoTot53pm6Lnn6npmaMNCg0KKyDmiJHlgJHlnKjms6Hms6HlnJboo6HpnaLop4Dlr5/nmoTorormlbjmmK/lk6rkuIDkupvvvJ8gDQorIGbjgIFt44CBc+OAgXLjgIFyZXbjgIFyYXfjgIFzaXplDQoNCisg5a6D5YCR5piv55u45ZCM55qE5ZeO77yfDQorIOS4jeWQjA0KDQorIOmAmeWAi+WIhuaekOeahOeoi+W6j++8jOi3n0FpcmxpbmVzIENSTemCo+S4gOWAi+S+i+WtkOacieeUmum6vOS4jeWQjO+8nw0KKyBBaXJsaW5lc+aYr+WIhue+pOWujOingOWvn+avj+S4gOe+pOeahOmVt+aineWclu+8jOWBmuWHuuebuOaHieeahOWIhuaekA0KKyDpgJnlgIvkvovlrZDmmK/liIbnvqTlrozlj6/ku6Xlho3kvb/nlKjms6Hms6HlnJbvvIzmjJHpgbjlh7rmg7Pop4Dlr5/nmoTorormlbjvvIzmib7lh7rnm67mqJnnmoTnvqTvvIznhLblvozlho3kvb/nlKhzaWcoKemAmeWAi+WHveaVuO+8jOingOWvn+WIsOe+pOWFp+eahOWAi+mrlA0KDQorIOaIkeWAkeW+numAmeWAi+S+i+WtkOWtuOWIsOS7gOm6vO+8nw0KKyDliIblroznvqTlvozvvIzlj6/ku6Xlho3kvb/nlKjlhbbku5bmlrnms5XvvIzmib7liLDnvqTkuK3mnIDmnInlvbHpn7/lipvnmoTlgIvpq5TvvIzoi6XmmK/kvIHmpa3vvIzliYflj6/ku6Xkvp3nhafnvqTlhaflgIvpq5TnmoTnibnmgKfvvIzlu7rnq4vnm7jmh4nnmoTooYzpirfmiYvms5UNCg0KPGJyPjxocj4NCg0KIyMjIEQuIOS9v+eUqOWwuuW6pue4rua4m+aWueazleaKveWPlumhp+WuoijnlKLlk4Ep55qE54m55b615ZCR6YePIA0KDQojIyMjIyBEMS4g5beo5aSn5bC65bqm57iu5ribIChTVkQsIFNpZ3VsYXIgVmFsdWUgRGVjb21wb3NpdGlvbikNCmBgYHtyfQ0KbGlicmFyeShpcmxiYSkNCmlmKExPQUQpIHsNCiAgbG9hZCgiZGF0YS9zdmQucmRhdGEiKQ0KfSBlbHNlIHsNCiAgc214ID0gbXgNCiAgc214QHggPSBwbWluKHNteEB4LCAyKSAgICAgICAgICAgICMgY2FwIGF0IDIsIHNpbWlsYXIgdG8gbm9ybWFsaXphdGlvbiAgDQogIHQwID0gU3lzLnRpbWUoKQ0KICBzdmQgPSBpcmxiYShzbXgsIA0KICAgICAgICAgICAgICBudj00MDAsICAgICAgICAgICAgICAgIyBsZW5ndGggb2YgZmVhdHVyZSB2ZWN0b3INCiAgICAgICAgICAgICAgbWF4aXQ9ODAwLCB3b3JrPTgwMCkgICAgDQogIHByaW50KFN5cy50aW1lKCkgLSB0MCkgICAgICAgICAgICAjIDEuODc5NSBtaW5zDQogIHNhdmUoc3ZkLCBmaWxlID0gImRhdGEvc3ZkLnJkYXRhIikNCn0NCmBgYA0KKirln7fooYzmhI/mtrUqKg0KDQorIHBtaW4oMik66LaF6YGOMueahOmDveioreeCujINCisgaXJsYmEoKTo8YnI+DQogIDEubnY65Y+z6YKK5ZCR6YeP6KaB57iu5rib5oiQ55qE5pW46YePPGJyPg0KICAyLm51OuW3pumCiuWQkemHj+imgee4rua4m+aIkOeahOaVuOmHjyjpoJDoqK09bnYpPGJyPg0KICAyLm1heGl0OuacgOWkp+eahOi/reS7o+asoeaVuDxicj4NCiAgNC53b3JrOuW3peS9nOWNgOeahOe2reW6pg0KDQojIyMjIyBEMi4g5L6d6aGn5a6i5ZCR6YeP5bCN6aGn5a6i5YiG576kDQpgYGB7cn0NCnNldC5zZWVkKDExMSk7IGtnID0ga21lYW5zKHN2ZCR1LCAyMDApJGNsdXN0ZXINCnRhYmxlKGtnKSAlPiUgYXMudmVjdG9yICU+JSBzb3J0DQpgYGANCg0KIyMjIyMgRDMuIOS6kuWLleW8j+azoeazoeWcliAoR29vZ2xlIE1vdGlvbiBDaGFydCkNCmBgYHtSfQ0KIyBjbHVzdHN0ZXIgc3VtbWFyeQ0KZGYgPSBsZWZ0X2pvaW4oQSwgZGF0YS5mcmFtZSggICAgICAgICANCiAgY3VzdCA9IGFzLmludGVnZXIoc214QERpbW5hbWVzJGN1c3QpLCBrZykpICU+JSANCiAgZ3JvdXBfYnkoa2cpICU+JSBzdW1tYXJpc2UoDQogICAgYXZnX2ZyZXF1ZW5jeSA9IG1lYW4oZiksDQogICAgYXZnX21vbmV0YXJ5ID0gbWVhbihtKSwNCiAgICBhdmdfcmV2ZW51ZV9jb250ciA9IG1lYW4ocmV2KSwNCiAgICBncm91cF9zaXplID0gbigpLA0KICAgIGF2Z19yZWNlbmN5ID0gbWVhbihyKSwNCiAgICBhdmdfZ3Jvc3NfcHJvZml0ID0gbWVhbihyYXcpKSAlPiUgDQogIHVuZ3JvdXAgJT4lIA0KICBtdXRhdGUoZHVtbXkgPSAyMDAxLCBrZyA9IHNwcmludGYoIkclMDNkIixrZykpICU+JSANCiAgZGF0YS5mcmFtZQ0KDQojIEdvb2dsZSBNb3Rpb24gQ2hhcnQNCnBsb3QoIGd2aXNNb3Rpb25DaGFydCgNCiAgc3Vic2V0KGRmLCBncm91cF9zaXplID49IDIwICYgZ3JvdXBfc2l6ZSA8PSA1MDAwKSwgICAgIA0KICAia2ciLCAiZHVtbXkiLCBvcHRpb25zPWxpc3Qod2lkdGg9ODAwLCBoZWlnaHQ9NjAwKSApICkNCmBgYA0KDQojIyMjIyBENC4g5LqS5YuV5byP5rOh5rOh5ZyWIChnZ3Bsb3QgKyBwbG90bHkpDQpgYGB7cn0NCmZpbHRlcihkZiwgZ3JvdXBfc2l6ZSA+PSAyMCAmIGdyb3VwX3NpemUgPD0gNTAwMCkkZ3JvdXBfc2l6ZSAlPiUgDQogIHNxcnQgJT4lIHJhbmdlICAgICMgZm9yIGJ1YmJsZSBzaXplIGFkanVzdG1lbnQNCmBgYA0KDQpgYGB7ciBmaWcuaGVpZ2h0PTcsIGZpZy53aWR0aD04fQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShwbG90bHkpDQoNCnAgPSBkZiAlPiUgZmlsdGVyKGdyb3VwX3NpemUgPj0gMjAgJiBncm91cF9zaXplIDw9IDUwMDApICU+JSANCiAgZ2dwbG90KGFlcyh4PWF2Z19mcmVxdWVuY3ksIHk9YXZnX21vbmV0YXJ5KSkgKw0KICBnZW9tX3BvaW50KGFlcyhzaXplPWdyb3VwX3NpemUsIGNvbD1hdmdfcmV2ZW51ZV9jb250ciksYWxwaGE9MC43KSArDQogIGdlb21fdGV4dChhZXMobGFiZWw9a2cpLCBhbHBoYT0wKSArDQogIHNjYWxlX3NpemUocmFuZ2U9YygxLjUsMTIpKSArDQogICNzY2FsZV9jb2xvcl9ncmFkaWVudChsb3c9ImdyZWVuIixoaWdoPSJtYWdlbnRhIikgKw0KICBzY2FsZV9jb2xvdXJfZ3JhZGllbnRuKA0KICAgIGNvbG91cnMgPSByZXYoYygicmVkIiwieWVsbG93IiwiZ3JlZW4iLCJsaWdodGJsdWUiLCJkYXJrYmx1ZSIpKSkgKw0KICB0aGVtZV9idygpICsgZ3VpZGVzKHNpemU9RikgKyBsYWJzKA0KICAgIHRpdGxlPSLpoaflrqLpm4bnvqQo5L6d6LO86LK355Si5ZOBKSIsDQogICAgY29sb3I9IuW5s+Wdh+eHn+aUtuiyoueNuyIsIHNpemU9Iumbhue+pOS6uuaVuCIpICsNCiAgeGxhYigi5bmz5Z2H6LO86LK35qyh5pW4IikgKyANCiAgeWxhYigi5bmz5Z2H6LO86LK36YeR6aGNIikNCnBsb3RseV9idWlsZChwKQ0KYGBgDQoNCiMjIyMjIEQ1LiDnvqTntYTnmoTku6PooajmgKfnlKLlk4EgKFNpZ25hdHVyZSBQcm9kdWN0KQ0KYGBge3J9DQpTaWcoMTM4KQ0KYGBgDQo8YnI+PGhyPg0KDQojIyMgRS4g6LO854mp57GD5YiG5p6QIEJhc2tldHMgQW5hbHlzaXMgDQoNCmBgYHtyfQ0KZGltKG14KSAgICMgMzIwNjYgY3VzdCAqIDEwNjc1IHByb2QNCmBgYA0KDQojIyMjIyBFMS4g5rqW5YKZ6LOH5paZIChmb3IgQXNzb2NpYXRpb24gUnVsZSBBbmFseXNpcykNCmBgYHtyfQ0KbGlicmFyeShhcnVsZXMpDQpsaWJyYXJ5KGFydWxlc1ZpeikNCmJ4ID0gc3Vic2V0KFosIHByb2QgJWluJSBhcy5udW1lcmljKGNvbG5hbWVzKG14KSksIA0KICAgICAgICAgICAgc2VsZWN0PWMoImN1c3QiLCJwcm9kIikpICAjIOWPqumBuOeUouWTgeWPiumhp+Wuog0KYnggPSBzcGxpdChieCRwcm9kLCBieCRjdXN0KSAgICAgICAgICAjIOWIhuWJsg0KYnggPSBhcyhieCwgInRyYW5zYWN0aW9ucyIpICAgICAgICAgICAjIGRhdGEgZm9yIGFydWxlcyBwYWNrYWdlDQpgYGANCioq5Z+36KGM5oSP5ra1KioNCg0KKyDlvp566YG45Ye65ZyobXjoo6HnmoRjdXN044CBcHJvZA0KKyDnhLblvozlsIflhbbliIblibINCg0KIyMjIyMgRTIuIFRvcDIwIOeGseizo+eUouWTgQ0KYGBge3IgZmlnLmhlaWdodD0zLCBmaWcud2lkdGg9Ny4yfQ0KaXRlbUZyZXF1ZW5jeVBsb3QoYngsIHRvcE49MjAsIHR5cGU9ImFic29sdXRlIiwgY2V4PTAuOCkNCiN0b3AyMA0KYGBgDQoNCiMjIyMjIEUzLiDpl5zoga/opo/liYflkoxBcHJpb3Jp5ryU566X5rOVDQoNCumXnOiBr+imj+WJhyhBID0+IEIpDQoNCisgc3VwcG9ydDogQeiiq+izvOiyt+eahOapn+eOhyAoQeeahOWfuuekjuapn+eOhykNCisgY29uZmlkZW5jZTogQeiiq+izvOiyt+aZgu+8jELooqvos7zosrfnmoTmqZ/njocNCisgbGlmdDogQeiiq+izvOiyt+aZgu+8jELooqvos7zosrfnmoTmqZ/njoflop7liqDnmoTlgI3mlbggKOiIh0LnmoTln7rnpI7mqZ/njofnm7jmr5QpDQorIOS4gOiIrOS+huism3N1cHBvcnTjgIFjb25maWRlbmNl5ZKMbGlmdOi2iumrmOeahOmXnOiBr+imj+WJh+i2iumHjeimgQ0KKyBzdXBwb3J044CBY29uZmlkZW5jZeWSjGxpZnToqK3nmoTotorkvY4o6auYKe+8jOaJvuWIsOeahOmXnOiBr+imj+WJh+i2iuWkmijlsJEpDQorIOW7uuitsOS4gOmWi+Wni+aKiuaomea6luioreS9ju+8jOWFiOaJvuWIsOWkmuS4gOm7nuimj+WJh++8jOS5i+W+jOWGjeeUqHN1YnNldOevqemBuOWHuueJueWumueahOimj+WJh+S+hueciw0KKw0KDQpgYGB7cn0NCnJ1bGVzID0gYXByaW9yaShieCwgcGFyYW1ldGVyPWxpc3Qoc3VwcD0wLjAwNSwgY29uZj0wLjYpKQ0Kc3VtbWFyeShydWxlcykNCmBgYA0KDQoNCiMjIyMjIEU0LiDmqqLoppbpl5zoga/opo/liYcNCg0K6Zec6IGv6KaP5YmHIChBID0+IEIp77yaDQoNCisgc3VwcG9ydDogQeiiq+izvOiyt+eahOapn+eOhyAoQeeahOWfuuekjuapn+eOhykNCisgY29uZmlkZW5jZTogQeiiq+izvOiyt+aZgu+8jELooqvos7zosrfnmoTmqZ/njocNCisgbGlmdDogQeiiq+izvOiyt+aZgu+8jELooqvos7zosrfnmoTmqZ/njoflop7liqDnmoTlgI3mlbggKOiIh0LnmoTln7rnpI7mqZ/njofnm7jmr5QpDQoNCmBgYHtyfQ0Kb3B0aW9ucyhkaWdpdHM9NCkNCmluc3BlY3QocnVsZXMpDQpgYGANCg0KYGBge3J9DQojIGluc3RhbGwucGFja2FnZXMoDQojICAgImh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL2Jpbi93aW5kb3dzL2NvbnRyaWIvMy41L2FydWxlc1Zpel8xLjMtMS56aXAiLA0KIyAgIHJlcG9zPU5VTEwpDQoNCiMgaW5zdGFsbC5wYWNrYWdlcygiYXJ1bGVzVml6XzEuMy0xLnppcCIsIHJlcG9zPU5VTEwpDQojIGxpYnJhcnkocGxvdGx5KQ0KIyBwbG90bHlfYXJ1bGVzKHJ1bGVzLGNvbG9ycz1jKCJyZWQiLCJncmVlbiIpLA0KIyAgICAgICAgICAgICAgIG1hcmtlcj1saXN0KG9wYWNpdHk9LjYsc2l6ZT0xMCkpDQojIHBsb3RseV9hcnVsZXMocnVsZXMsbWV0aG9kPSJtYXRyaXgiLA0KIyAgICAgICAgICAgICAgIHNoYWRpbmc9ImxpZnQiLA0KIyAgICAgICAgICAgICAgIGNvbG9ycz1jKCJyZWQiLCAiZ3JlZW4iKSkNCiMgDQpgYGANCg0KIyMjIyMgRTUuIOS6kuWLleWcluihqOmhr+ekug0KYGBge3J9DQpwbG90KHJ1bGVzLGNvbG9ycz1jKCJyZWQiLCJncmVlbiIpLGVuZ2luZT0iaHRtbHdpZGdldCIsDQogICAgIG1hcmtlcj1saXN0KG9wYWNpdHk9LjYsc2l6ZT04KSkNCmBgYA0KDQpgYGB7cn0NCnBsb3QocnVsZXMsbWV0aG9kPSJtYXRyaXgiLHNoYWRpbmc9ImxpZnQiLGVuZ2luZT0iaHRtbHdpZGdldCIsDQogICAgIGNvbG9ycz1jKCJyZWQiLCAiZ3JlZW4iKSkNCg0KYGBgDQoNCioq5Z+36KGM5oSP5ra1KioNCg0KKyBMSFM65bem5omL6YKK77yM5Y+v6KaW54K66Zec6IGv6KaP5YmH5LitQT0+55qEQQ0KKyBSSFM65Y+z5omL6YKK77yM5Y+v6KaW54K6Qg0KKyDlnJbkuK3nmoTpgKPnuozljYDmrrXpoa/npLrvvIzkuI3lj6rkuIDnqK5B5bCN5oeJ5YiwQu+8jOWPr+iDveeCukExPT5CLHtBMSxBMn09PkINCg0KIyMjIyMgRTYuIOevqemBuOeUouWTgeOAgeS6kuWLleW8j+mXnOiBr+Wclg0KYGBge3J9DQpyMSA9IHN1YnNldChydWxlcywgc3Vic2V0ID0gcmhzICVpbiUgYygiNDcxOTA5MDc5MDAwMCIpKQ0Kc3VtbWFyeShyMSkNCnBsb3QocjEsbWV0aG9kPSJncmFwaCIsZW5naW5lPSJodG1sd2lkZ2V0IixpdGVtQ29sPSJjeWFuIikgDQpgYGANCg0KKyDms6Hms6HlpKflsI/vvJpzdXBwb3J0OiBB6KKr6LO86LK355qE5qmf546HIChB55qE5Z+656SO5qmf546HKQ0KKyDms6Hms6HpoY/oibLvvJpsaWZ0OiBB6KKr6LO86LK35pmC77yMQuiiq+izvOiyt+eahOapn+eOh+WinuWKoOeahOWAjeaVuCAo6IiHQueahOWfuuekjuapn+eOh+ebuOavlCkNCg0KDQpgYGB7cn0NCnIyID0gc3Vic2V0KHJ1bGVzLCBzdWJzZXQgPSByaHMgJWluJSBjKCI0NzEwMDExNDAxMTM1IikpDQpzdW1tYXJ5KHIyKQ0KcGxvdChyMixtZXRob2Q9ImdyYXBoIixlbmdpbmU9Imh0bWx3aWRnZXQiLGl0ZW1Db2w9ImN5YW4iKSANCmBgYA0KPGJyPjxocj4NCg0KIyMjIEYuIOeUouWTgeaOqOiWpiBQcm9kdWN0IFJlY29tbWVuZGF0aW9uDQoNCiMjIyMjIEYxLiDnr6npgbjpoaflrqLjgIHnlKLlk4ENCuWkquWwkeiiq+izvOiyt+eahOeUouWTgeWSjOizvOiyt+WkquWwkeeUouWTgeeahOmhp+WuoumDveS4jemBqeWQiOS9v+eUqENvbGxhYm9yYXRpdmUgRmlsdGVyaW5n6YCZ56iu55Si5ZOB5o6o6Jam5pa55rOV77yM5omA5Lul5oiR5YCR5YWI5bCN6aGn5a6i5ZKM55Si5ZOB5YGa5LiA5qyh56+p6YG4DQpgYGB7cn0NCmxpYnJhcnkocmVjb21tZW5kZXJsYWIpDQpyeCA9IG14WywgY29sU3VtcyhteCA+IDApID49IDUwXSAgICAgICPnr6nlh7rlpKfmlrzkupTljYHmrKHos7zosrfnmoTllYblk4ENCnJ4ID0gcnhbcm93U3VtcyhyeCA+IDApID49IDIwICYgcm93U3VtcyhyeCA+IDApIDw9IDMwMCwgXSAj56+p5Ye66LO86LK36LaF6YGO5LqM5Y2B5qyh5bCP5pa85LiJ55m+5qyh55qE6aGn5a6iDQpkaW0ocngpDQpgYGANCg0KIyMjIyMgRjIuIOmBuOaTh+eUouWTgeipleWIhuaWueW8jw0K5Y+v5Lul6YG45pOH6KaB55SoDQoNCisg6LO86LK35qyh5pW4IChyZWFsUmF0aW5nTWF0cml4KeKGkumHj+WMlg0KKyDmmK/lkKbos7zosrcgKGJpbmFyeVJhdGluZ01hdHJpeCnihpLkuozliIbms5UNCg0K5YGa5qih5Z6L44CCDQpgYGB7cn0NCnJ4ID0gYXMocngsICJyZWFsUmF0aW5nTWF0cml4IikgICMgcmVhbFJhdGluZ01hdHJpeA0KYnggPSBiaW5hcml6ZShyeCwgbWluUmF0aW5nPTEpICAgIyBiaW5hcnlSYXRpbmdNYXRyaXgNCmRpbShieCkNCmBgYA0KDQojIyMjIyBGMy4g6Kit5a6a5qih5Z6LKOa6lueiuuaApynpqZforYnmlrnlvI8NCmBgYHtyfQ0Kc2V0LnNlZWQoNDMyMSkNCnNjaGVtZSA9IGV2YWx1YXRpb25TY2hlbWUoICAgICANCiAgYngsIG1ldGhvZD0ic3BsaXQiLCB0cmFpbiA9IC43NSwgIGdpdmVuPTUpDQpgYGANCg0KIyMjIyMgRjQuIOioreWumuaOqOiWpuaWueazlSjlj4PmlbgpDQpgYGB7cn0NCmFsZ29yaXRobXMgPSBsaXN0KCAgICAgICAgICAgIA0KICBBUjUzID0gbGlzdChuYW1lPSJBUiIsIHBhcmFtPWxpc3Qoc3VwcG9ydD0wLjAwMDUsIGNvbmZpZGVuY2U9MC4zKSksDQogIEFSNDMgPSBsaXN0KG5hbWU9IkFSIiwgcGFyYW09bGlzdChzdXBwb3J0PTAuMDAwNCwgY29uZmlkZW5jZT0wLjMpKSwNCiAgUkFORE9NID0gbGlzdChuYW1lPSJSQU5ET00iLCBwYXJhbT1OVUxMKSwNCiAgUE9QVUxBUiA9IGxpc3QobmFtZT0iUE9QVUxBUiIsIHBhcmFtPU5VTEwpLA0KICBVQkNGID0gbGlzdChuYW1lPSJVQkNGIiwgcGFyYW09TlVMTCksICAjDQogIElCQ0YgPSBsaXN0KG5hbWU9IklCQ0YiLCBwYXJhbT1OVUxMKSApDQpgYGANCg0KIyMjIyMgRjUuIOW7uuaooeOAgemgkOa4rOOAgempl+itiSjmupbnorrmgKcpDQpgYGB7cn0NCmlmKExPQUQpIHsNCiAgbG9hZCgicmVzdWx0cy5yZGF0YSIpDQp9IGVsc2Ugew0KICB0MCA9IFN5cy50aW1lKCkNCiAgcmVzdWx0cyA9IGV2YWx1YXRlKCAgICAgICAgICAgIA0KICAgIHNjaGVtZSwgYWxnb3JpdGhtcywgdHlwZT0idG9wTkxpc3QiLA0KICAgIG49Yyg1LCAxMCwgMTUsIDIwKSkNCiAgcHJpbnQoU3lzLnRpbWUoKSAtIHQwKQ0KICBzYXZlKHJlc3VsdHMsIGZpbGU9InJlc3VsdHMucmRhdGEiKQ0KfQ0KYGBgDQoNCiMjIyMjIEY2LiDmqKHlnovmupbnorrmgKfmr5TovIMNCmBgYHtyIGZpZy5oZWlnaHQ9NSwgZmlnLndpZHRoPTV9DQojIGxvYWQoImRhdGEvcmVzdWx0cy5yZGF0YSIpDQpwYXIobWFyPWMoNCw0LDMsMiksY2V4PTAuOCkNCmNvbHMgPSBjKCJyZWQiLCAibWFnZW50YSIsICJncmF5IiwgIm9yYW5nZSIsICJibHVlIiwgImdyZWVuIikNCnBsb3QocmVzdWx0cywgYW5ub3RhdGU9YygxLDMpLCBsZWdlbmQ9InRvcGxlZnQiLCBwY2g9MTksIGx3ZD0yLCBjb2w9Y29scykNCmFibGluZSh2PXNlcSgwLDAuMDA2LDAuMDAxKSwgaD1zZXEoMCwwLjA4LDAuMDEpLCBjb2w9J2xpZ2h0Z3JheScsIGx0eT0yKQ0KYGBgDQoNCiMjIyMjIEY3LiDlhLLlrZjnlKLlk4HmjqjolqbmqKHlnosNCmBgYHtyfQ0Kc2F2ZShyZXN1bHRzLCBmaWxlPSJkYXRhL3Jlc3VsdHMucmRhdGEiKQ0KYGBgDQoNCjxicj48YnI+PGhyPjxicj48YnI+PGJyPg0KDQoNCg0K