1 Giới thiệu

Thân chào các bạn, hôm nay Nhi muốn giới thiệu với các bạn một R package rất tiện lợi, đó là broom. Công dụng của broom là trích xuất nội dung của một output object - kết quả của một mô hình thống kê và tóm tắt thông tin này vào một dataframe. Tên gọi broom chính là để diễn tả việc dọn dẹp từ một mô hình rối rắm thành một dataframe gọn gàng, sạch đẹp, sẵn sàng để đưa vào báo cáo, hoặc làm nguyên liệu để vẽ những biểu đồ.

Package broom do tác giả David Robinson tạo ra từ năm 2014, và trong 3 năm nay nhóm tác giả của broom đã âm thầm mở rộng khả năng của package này. Ở thời điểm hiện nay, broom rất mạnh, nó tương thích với hầu hết những package Hồi quy trong R, kể cả những mô hình phức tạp như gamlss, nlmer, lme4, mgcv, brms…

broom có 4 hàm quan trọng:

  1. glance: hàm này trích xuất thông tin về phẩm chất mô hình, thí dụ goodness of fit, các trị số AIC, BIC, Rsq, F test…

  2. tidy: hàm này trích xuất nội dung mô hình, thí dụ các tham số hồi quy, S.E, khoảng tin cậy, kiểm định t và p_value

  3. augment: hàm này cho phép kiểm định mô hình trên dataset hiện thời hoặc mới, và cung cấp thông tin về độ chính xác của mô hình, thí dụ Residual error, …

  4. bootstrap: broom có một hàm bootstrap của riêng nó, khi kết hợp với hàm tidy, sẽ cho phép bạn áp dụng bootstrap cho bất kì mô hình nào. Quy trình này cần sự hỗ trợ của dplyr

Tuy đã hiện diện từ rất lâu, nhưng broom chỉ thực sự gây chú ý từ năm 2016, khi Hadley Wickham hợp nhất broom với những bạn bè của nó là các package như dplyr, purr, tidyr, và ggplot2 để tạo thành một quy trình phân tích dữ liệu khép kín, trong đó broom vẫn giữ nhiệm vụ dọn dẹp và đóng gói dataframe nội dung mô hình, nhưng nó không làm việc một mình mà nhận được sự trợ thủ của dplyr cũng như kết quả mà broom xuất ra được chuyền qua cho ggplot một cách nhanh chóng nhờ các nhân tử “pipe”.

broom rất tiện lợi, nhưng có rất ít tutorial về nó ngoại trừ 3 thí dụ minh họa của chính tác giả và một vài đoạn code trong 2nd Ed. của quyển ggplot2 của Wickham. Do đó, Nhi viết bài hướng dẫn sau với thông điệp khuyến khích các bạn sử dụng broom mỗi khi bạn làm việc với mô hình hồi quy.

Trong bài, Nhi sẽ minh họa khả năng của broom với 4 thí dụ

  1. Trích xuất thông tin từ một mô hình Logistic dựng bằng Gamlss, và bootstrap mô hình logistic này

  2. Trích xuất thông tin từ một mô hình hồi quy Polynomial dựng bằng glm()

  3. Bootstraping một mô hình GLM với family Student t chứa 3 tham số, dựng bằng Gamlss

  4. Bootstraping một mô hình Cox-PH

2 Mô hình Logistic với gamlss

Ta dựng một mô hình logistic cho dataset Biopsy, nhưng sử dụng gamlss chứ không dùng hàm glm().

library(tidyverse)

library(gamlss)
nC<-detectCores()

library(broom)

library(ggridges)

df=read.csv("http://vincentarelbundock.github.io/Rdatasets/csv/MASS/biopsy.csv")%>%as_tibble()%>%.[,c(3:12)]%>%na.omit()

names(df)=c("clumpthickness",
            "SizeUniformity",
            "ShapeUniformity",
            "Margin_adhesion",
            "EpiCellSize",
            "Barenuclei",
            "BlandChromatin",
            "NormalNucleoli",
            "Mitoses",
            "Class"
)

df2=df%>%mutate(.,Class=as.integer(.$Class)-1L)

glslogit=gamlss(Class~clumpthickness+
                     ShapeUniformity+
                     Margin_adhesion+
                     Barenuclei+
                     BlandChromatin,
                   data=df2,
                   family=BI(),
                   trace=F,
                parallel="multicore",
                ncpus = nC)


summary(glslogit)
## ******************************************************************
## Family:  c("BI", "Binomial") 
## 
## Call:  gamlss(formula = Class ~ clumpthickness + ShapeUniformity +  
##     Margin_adhesion + Barenuclei + BlandChromatin,  
##     family = BI(), data = df2, trace = F, parallel = "multicore",  
##     ncpus = nC) 
## 
## Fitting method: RS() 
## 
## ------------------------------------------------------------------
## Mu link function:  logit
## Mu Coefficients:
##                 Estimate Std. Error t value Pr(>|t|)    
## (Intercept)     -9.74114    1.04989  -9.278  < 2e-16 ***
## clumpthickness   0.62576    0.13373   4.679 3.48e-06 ***
## ShapeUniformity  0.48994    0.15379   3.186 0.001510 ** 
## Margin_adhesion  0.33918    0.11221   3.023 0.002599 ** 
## Barenuclei       0.37330    0.09381   3.979 7.65e-05 ***
## BlandChromatin   0.55731    0.16341   3.411 0.000687 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## ------------------------------------------------------------------
## No. of observations in the fit:  683 
## Degrees of Freedom for the fit:  6
##       Residual Deg. of Freedom:  677 
##                       at cycle:  2 
##  
## Global Deviance:     112.566 
##             AIC:     124.566 
##             SBC:     151.725 
## ******************************************************************

Ta lần lượt thử hàm tidy trên mô hình này: Lưu ý: kết quả của hàm tidy luôn là 1 dataframe, do đây là mô hình logistic nên hàm augment không cho kết quả chính xác, bạn phải dùng mutate để tính probability từ predicted bằng hàm plogis.

tidy(glslogit)%>%knitr::kable()
parameter term estimate std.error statistic p.value
mu (Intercept) -9.7411399 1.0498889 -9.278258 0.0000000
mu clumpthickness 0.6257619 0.1337317 4.679233 0.0000035
mu ShapeUniformity 0.4899369 0.1537879 3.185797 0.0015099
mu Margin_adhesion 0.3391770 0.1122074 3.022767 0.0025993
mu Barenuclei 0.3732985 0.0938082 3.979382 0.0000766
mu BlandChromatin 0.5573061 0.1634060 3.410562 0.0006868

Với kết quả hàm tidy mà broom xuất ra, ta có thể tính vẽ biểu đồ Odds-ratio dễ dàng:

logitsum=glslogit%>%tidy()%>%mutate(OR=exp(estimate),
                                    UL=exp(estimate+1.96*std.error),
                                    LL=exp(estimate-1.96*std.error))

logitsum%>%dplyr::select(term,OR,UL,LL)%>%ggplot(aes(x=term,
                                                     y=OR,
                                                     ymin=LL,
                                                     ymax=UL,
                                                     col=term))+
  geom_hline(yintercept = c(1,2),linetype=2,col=c("blue","red"))+
  geom_pointrange(show.legend = F,size=1)+
  coord_flip()+
  theme_bw()

Trong R có một package khác là sjPlot cho phép làm những việc như thế này chỉ với 1 hàm plot_model, tuy nhiên sjPlot hoàn toàn bất lực với mô hình gamlss, và khi làm thủ công mọi chuyện, bạn sẽ kiểm soát tốt hơn tất cả mọi thứ, từ việc tính OR, 95%CI cho đến tùy chỉnh màu sắc cho ggplot; và làm thủ công không khó lắm khi broom đã giúp bạn làm phần khó nhất là chuyển model thành dataframe. Có dataframe là mọi việc đều khả thi.

Tiếp theo, ta sẽ thử bootstrap mô hình logistic trên đây và một lần nữa, xác định lại khoảng tin cậy cho OR dựa vào 100 mô hình bootstrap:

Để nối hàm bootstrap của broom vào hàm tidy, có 2 cách, Nhi giới thiệu cách dễ trước, đó là dùng hàm do của dplyr. Lợi thế của hàm bootstrap này đó là:

  1. Bạn không cần phải dùng package boot, không cần viết hàm gì cả
  2. Bạn có thể bootstrap mọi mô hình bạn muốn, từ lme4 cho đến survival…
  3. Kết quả bootstrap được hàm tidy đóng gói luôn, rất gọn gàng và sẵn sàng để vẽ biểu đồ

Kết quả bootstrap như sau:

df2$Class=as.numeric(df2$Class)

bootlogit<- df2 %>%
  bootstrap(100) %>%
  do(tidy(gamlss(.$Class~.$clumpthickness+
                   .$ShapeUniformity+
                   .$Margin_adhesion+
                   .$Barenuclei+
                   .$BlandChromatin,
                 family=BI(),
                 trace=F,
                 parallel="multicore",
                 ncpus = nC)))

bootlogit<-mutate(bootlogit,OR=exp(estimate))

bootlogit%>%dplyr::select(term,OR)%>%
  ggplot(aes(y=term,
             x=OR))+
  geom_density_ridges(aes(fill=term),alpha=0.5,scale=1.5)+
  geom_vline(xintercept = c(1,2),linetype=2,col=c("blue","red"))+
  theme_bw()

muDF<-bootlogit%>%
  dplyr::select(replicate,term,estimate)%>%
  spread(key=term,value=estimate)

names(muDF)=c("replicate",
              "(Intercept)",
              "Barenuclei",
              "BlandChromatin",
              "clumpthickness",
              "Margin_adhesion",
              "ShapeUniformity")
predboot<-df%>%mutate(pred=NA,Iter=NA)

for(i in 1:nrow(muDF)){
  beta0=muDF$`(Intercept)`[i]
  beta1=muDF$Barenuclei[i]
  beta2=muDF$BlandChromatin[i]
  beta3=muDF$clumpthickness[i]
  beta4=muDF$Margin_adhesion[i]
  beta5=muDF$ShapeUniformity[i]
  tempdf<-df%>%mutate(pred=plogis(beta0+
                           ShapeUniformity*beta5+
                           Margin_adhesion*beta4+
                           Barenuclei*beta1+
                           BlandChromatin*beta2+
                           clumpthickness*beta3),
                         Iter=i)
  predboot=rbind(predboot,tempdf)%>%na.omit()
}


binomial_smooth <- function(...) {
  geom_smooth(method = "glm", method.args = list(family = "binomial"), se=T,show.legend = F,...)
}

longdf=predboot%>%dplyr::select(Iter,
                         ShapeUniformity,
                         Margin_adhesion,
                         Barenuclei,
                         BlandChromatin,
                         clumpthickness,
                         pred,
                         Class)%>%
  gather(ShapeUniformity:clumpthickness,key="Feature",value="Score")

  longdf%>%ggplot(aes(x=Score,
                      y=pred,
                      group=as.factor(Iter),
                      fill=as.factor(Iter),
                      col=as.factor(Iter)))+
  binomial_smooth(alpha=0.1)+
  theme_bw()+
  facet_wrap(~Feature,scales="free",ncol=3)+
    scale_fill_brewer("PuRd")+
    scale_color_brewer("PuRd")

3 Mô hình Polynomial bậc 5 dùng GLM

Thí dụ tiếp theo, Nhi dựng một mô hình polynomial bậc 5 khảo sát tương quan giữa log(chiều cao) và tuổi của các bệnh nhi trong dataset Leukemia.

Leukemia%>%ggplot(aes(x=age,y=log(height)))+
  geom_point(col="blue",alpha=0.3)+
  geom_smooth(method="glm",formula = y ~ poly(x,5),col="red",fill="red",alpha=0.5)+
  theme_bw()

Mô hình đươc dựng đơn giản bằng hàm glm, với family = gaussian và link function = log:

lmod=glm(Leukemia,formula=height~poly(age,5),family=gaussian(link="log"))

summary(lmod)
## 
## Call:
## glm(formula = height ~ poly(age, 5), family = gaussian(link = "log"), 
##     data = Leukemia)
## 
## Deviance Residuals: 
##      Min        1Q    Median        3Q       Max  
## -21.9027   -3.4454   -0.0024    3.5268   24.6226  
## 
## Coefficients:
##                Estimate Std. Error  t value Pr(>|t|)    
## (Intercept)    4.815358   0.001015 4743.948  < 2e-16 ***
## poly(age, 5)1  6.326672   0.044497  142.181  < 2e-16 ***
## poly(age, 5)2 -1.250836   0.044197  -28.301  < 2e-16 ***
## poly(age, 5)3 -0.134181   0.044419   -3.021  0.00255 ** 
## poly(age, 5)4 -0.124521   0.044173   -2.819  0.00487 ** 
## poly(age, 5)5  0.095655   0.042151    2.269  0.02335 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for gaussian family taken to be 29.90352)
## 
##     Null deviance: 699301  on 1987  degrees of freedom
## Residual deviance:  59269  on 1982  degrees of freedom
## AIC: 12405
## 
## Number of Fisher Scoring iterations: 4

Ta dùng 2 hàm glance và tidy của broom cho mô hình trên, như bạn thấy, ta có 2 dataframe rất gọn đẹp:

glance(lmod)%>%knitr::kable()
null.deviance df.null logLik AIC BIC deviance df.residual
699301.1 1987 -6195.434 12404.87 12444.03 59268.78 1982
tidy(lmod)%>%knitr::kable()
term estimate std.error statistic p.value
(Intercept) 4.8153583 0.0010151 4743.948410 0.0000000
poly(age, 5)1 6.3266725 0.0444972 142.181380 0.0000000
poly(age, 5)2 -1.2508362 0.0441974 -28.301153 0.0000000
poly(age, 5)3 -0.1341815 0.0444194 -3.020788 0.0025534
poly(age, 5)4 -0.1245206 0.0441734 -2.818906 0.0048666
poly(age, 5)5 0.0956553 0.0421512 2.269337 0.0233549

Khi có dataframe, bạn sẽ nghĩ ngay đến ggplot2, đúng như vậy, ta có thể vẽ rất nhiều biểu đồ từ dataframe mà broom cung cấp:

lmod%>%tidy()%>%
  ggplot(aes(x=term,
             y=estimate,
             ymin=estimate-1.96*std.error,
             ymax=estimate+1.96*std.error,
             col=term))+
  geom_hline(yintercept = 0,linetype=2,col="blue")+
  geom_pointrange(show.legend = F,size=1)+
  coord_flip()+
  theme_bw()

lmod%>%tidy()%>%
  ggplot(aes(x=term,
             y=p.value,
             col=term))+
  geom_hline(yintercept = 0.005,linetype=2,col="blue")+
  geom_point(show.legend = F,size=5)+
  coord_flip()+
  theme_bw()

Hàm augment cho phép tạo ra 1 dataframe chồng lên dataframe gốc, chứa các trị số như fitted, se.fit, residual error, cooksd…

diagDF<-augment(lmod)

diagDF%>%head()%>%knitr::kable()
## Warning in `[<-.data.frame`(`*tmp*`, , isn, value = structure(list(height =
## structure(c("105.5", : provided 13 variables to replace 9 variables
height poly.age..5. .fitted .se.fit .resid .hat .sigma .cooksd .std.resid
105.5 -0.0230743292 0.0110197426 0.0063424413 -0.0196888610 0.0209255870 4.659193 0.0022789 -0.0508689
106.5 -0.0200972669 0.0053346290 0.0119236175 -0.0207242879 0.0146627775 4.683920 0.0021049 -1.6933626
108.0 -0.0159293797 -0.0016066597 0.0167457510 -0.0179317966 0.0036937488 4.716927 0.0018470 -3.8241205
111.5 -0.0129523174 -0.0058376726 0.0182884993 -0.0138407745 -0.0039692970 4.739605 0.0016984 -2.8890149
113.5 -0.0087844302 -0.0107432200 0.0181540203 -0.0064736150 -0.0125556528 4.770389 0.0015880 -4.4651563
115.0 -0.0052119555 -0.0140028095 0.0162320005 0.0003761595 -0.0169252462 4.796055 0.0015701 -6.0320482
diagDF%>%ggplot(aes(x=.fitted,y=.resid,col=.resid))+
  geom_point(alpha=0.2,show.legend = F)+
  geom_smooth(alpha=0.2,show.legend = F,fill="blue",col="blue4")+
  theme_bw()+
  scale_color_gradient2(low="red",high="red",mid="blue",midpoint = 0)
## `geom_smooth()` using method = 'gam'

Trong một số trường hợp, bạn muốn dựng mô hình riêng cho một số phân nhóm trong dữ liệu, ta có thể làm điều này dễ dàng khi kết hợp dplyr và broom: Thí dụ ta dựng cùng mô hình polynomial nói trên riêng cho 3 phân nhóm điều trị 1,2,3:

Kết quả vẫn là 1 dataframe:

models <- Leukemia %>%
  group_by(treatment) %>%
  do(mod = glm(data=.,
               formula=height~poly(age,5),
               family=gaussian(link="log")))

model_sum <- models %>% tidy(mod)

model_sum%>%knitr::kable()
treatment term estimate std.error statistic p.value
1 (Intercept) 4.8229908 0.0011808 4084.6389927 0.0000000
1 poly(age, 5)1 5.1997563 0.0428299 121.4046879 0.0000000
1 poly(age, 5)2 -1.0255819 0.0424972 -24.1329397 0.0000000
1 poly(age, 5)3 -0.0275165 0.0427571 -0.6435539 0.5199745
1 poly(age, 5)4 -0.1476890 0.0424684 -3.4776209 0.0005220
1 poly(age, 5)5 0.0447927 0.0405215 1.1054077 0.2691808
2 (Intercept) 4.8055117 0.0025728 1867.8239016 0.0000000
2 poly(age, 5)1 2.6602309 0.0454282 58.5590702 0.0000000
2 poly(age, 5)2 -0.5174254 0.0451399 -11.4627170 0.0000000
2 poly(age, 5)3 -0.1365923 0.0457617 -2.9848593 0.0030564
2 poly(age, 5)4 0.0289002 0.0457650 0.6314922 0.5281710
2 poly(age, 5)5 0.1094441 0.0431043 2.5390551 0.0115899
3 (Intercept) 4.7922652 0.0024658 1943.4838759 0.0000000
3 poly(age, 5)1 2.4228498 0.0424623 57.0588894 0.0000000
3 poly(age, 5)2 -0.4721817 0.0420656 -11.2248949 0.0000000
3 poly(age, 5)3 -0.1269940 0.0419478 -3.0274272 0.0026755
3 poly(age, 5)4 -0.0435665 0.0419031 -1.0396963 0.2992991
3 poly(age, 5)5 0.0731085 0.0406323 1.7992699 0.0729579

Ta vẽ được 3 biểu đồ riêng biệt từ dataframe này:

model_sum%>%
  ggplot(aes(x=term,
             y=estimate,
             ymin=estimate-1.96*std.error,
             ymax=estimate+1.96*std.error,
             col=term))+
  geom_hline(yintercept = 0,linetype=2,col="blue")+
  geom_pointrange(show.legend = F)+
  coord_flip()+
  theme_bw()+facet_wrap(~treatment,ncol=1,scales="free")

4 Mô hình gamlss nhiều tham số

Ta trở lại package gamlss, lần này Nhi dựng một mô hình phức tạp hơn cho một dataset đơn giản. Bài toán ở đây là ước tính chiều dài catherter thông tim dựa vào chiều cao của bệnh nhân.

Mục tiêu của minh họa này là để cho thấy broom làm việc hiệu quả ra sao cho một mô hình gamlss với phân phối student t, với 3 mô hình con cho 3 tham số Mu(link=identity), sigma (link=log) và Nu (link=log).

data(heart, package="robustbase")

heart%>%ggplot(aes(x=height,y=clength))+
  geom_point(size=5,col="blue",alpha=0.3)+
  geom_smooth(method="lm",fill="blue",alpha=0.2)+
  theme_bw()

Trước tiên, ta dựng mô hình và dùng broom để trích xuất kết quả:

gmod=gamlss(data=heart,family=TF(),
            formula=clength ~ height,
            trace=FALSE,
            parallel="multicore",
            ncpus = nC            
            )


summary(gmod)
## ******************************************************************
## Family:  c("TF", "t Family") 
## 
## Call:  gamlss(formula = clength ~ height, family = TF(), data = heart,  
##     trace = FALSE, parallel = "multicore", ncpus = nC) 
## 
## Fitting method: RS() 
## 
## ------------------------------------------------------------------
## Mu link function:  identity
## Mu Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept) 11.47898    3.73706   3.072   0.0118 *  
## height       0.61171    0.08909   6.866 4.37e-05 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## ------------------------------------------------------------------
## Sigma link function:  log
## Sigma Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   1.2605     0.2041   6.175 6.96e-05 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## ------------------------------------------------------------------
## Nu link function:  log 
## Nu Coefficients:
##              Estimate Std. Error  t value Pr(>|t|)    
## (Intercept) 2.895e+01  2.887e-06 10028569   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## ------------------------------------------------------------------
## No. of observations in the fit:  12 
## Degrees of Freedom for the fit:  4
##       Residual Deg. of Freedom:  8 
##                       at cycle:  8 
##  
## Global Deviance:     64.30629 
##             AIC:     72.30629 
##             SBC:     74.24591 
## ******************************************************************
tidy(gmod)%>%knitr::kable()
parameter term estimate std.error statistic p.value
mu (Intercept) 11.4789756 3.7370645 3.071656e+00 0.0118074
mu height 0.6117124 0.0890944 6.865889e+00 0.0000437
sigma (Intercept) 1.2604900 0.2041241 6.175115e+00 0.0000696
nu (Intercept) 28.9499863 0.0000029 1.002857e+07 0.0000000

Tiếp theo, Nhi sẽ dùng broom để bootstrap 100 phiên bản mô hình trên đây:

bootgmod <- heart %>%
  bootstrap(100) %>%
  do(tidy(gamlss(family=TF(),
                 formula=.$clength~.$height,
                 trace=FALSE,
                 parallel="multicore",
                 ncpus = nC)
  ))
bootgmod%>%head()%>%knitr::kable()
replicate parameter term estimate std.error statistic p.value
1 mu (Intercept) 18.2500034 0.0000108 1.693156e+06 0.0000000
1 mu .$height 0.4999926 0.0000002 2.270082e+06 0.0000000
1 sigma (Intercept) -7.3949481 1.2906166 -5.729779e+00 0.0001322
1 nu (Intercept) -1.9103709 0.3075115 -6.212356e+00 0.0000660
2 mu (Intercept) 8.0263124 4.1159903 1.950032e+00 0.0797441
2 mu .$height 0.7048566 0.1012699 6.960180e+00 0.0000390

Có một cách khác để dùng hàm bootstrap của broom, cách này phức tạp hơn 1 chút vì phải thông qua 2 hàm nest và unnest của package tidy,

bootgmod2 <- heart %>%
  modelr::bootstrap(100) %>%
  mutate(mod = map(strap, ~ gamlss(data=.,family=TF(),
                                   formula=clength ~ height,
                                   trace=FALSE,
                                   parallel="multicore",
                                   ncpus = nC)))%>%
  unnest(map(mod, tidy))

bootgmod2%>%head()%>%knitr::kable()
.id parameter term estimate std.error statistic p.value
001 mu (Intercept) 9.4097442 4.8973504 1.9213949 0.0836159
001 mu height 0.6398555 0.1075773 5.9478701 0.0001416
001 sigma (Intercept) 1.3662393 0.2041264 6.6931055 0.0000341
001 nu (Intercept) 11.9264567 66.1976703 0.1801643 0.8602998
002 mu (Intercept) 19.9369256 2.2153694 8.9993684 0.0000041
002 mu height 0.4697114 0.0533093 8.8110663 0.0000050

Cấu trúc 2 dataframe là tương đương, chỉ khác ở biến .id cho cách làm thứ 2 thay vì biến replicate với hàm do, nhưng đổi lại tên của các predictor của cách 2 rõ ràng hơn.

Tiếp theo, giả sử Nhi đang dùng cách thứ 2, và muốn vẽ biểu đồ 100 mô hình tiên lượng cho Mu. Để làm việc này, trước hết ta đưa dataframe bootgmod2 vào hàm filter để lọc riêng giá trị parameter=mu, sau đó dùng hàm spread để tách Intercept và Height ra thành 2 biến (cột). Sau đó ta dùng 1 vòng lặp để tính Mu từ mô hình tuyến tính với Intercept và Height. Kết quả vòng lặp là 1 dataframe predboot.

Cuối cùng ta vẽ được biểu đồ như sau:

muDF<-bootgmod2%>%filter(parameter=="mu")%>%
  dplyr::select(.id:estimate)%>%
  spread(key=term,value=estimate)

predboot<-heart%>%mutate(pred=NA,Iter=NA)

for(i in 1:nrow(muDF)){
  intercept=muDF$`(Intercept)`[i]
  beta=muDF$height[i]
  tempdf<-heart%>%mutate(pred=heart$height*beta+intercept,
                         Iter=i)
  predboot=rbind(predboot,tempdf)%>%na.omit()
}

predboot%>%
  ggplot()+
  geom_point(aes(x=height,y=clength),alpha=0.1,size=4,col="blue4")+
  geom_point(aes(x=height,y=pred,group=Iter,col=Iter),alpha=0.02,size=3,show.legend = F)+
  geom_line(aes(x=height,y=pred,group=Iter,col=Iter),alpha=0.1,show.legend = F)+
  geom_smooth(method="lm",se=F,aes(x=height,y=pred),col="blue4")+
  scale_color_distiller(palette="PuRd")+
  theme_bw()

5 Mô hình Cox-PH

Trong thí dụ cuối cùng này, Nhi sẽ dùng broom cho một mô hình Cox-Ph trên dataset lung.

Hàm tidy cho ra một dataframe coxdf với cấu trúc hoàn toàn khác với mô hình glm. Từ dataframe này ta có thể vẽ được biểu đồ cho mô hình Cox:

library(survival)

data(lung)

coxmod <- survfit(coxph(Surv(time, status) ~ age + sex, data=lung))

glance(coxmod)%>%knitr::kable()
records n.max n.start events rmean rmean.std.error median conf.low conf.high
228 228 228 165 380.9381 20.27475 320 285 363
coxdf=tidy(coxmod)

coxdf%>%head()%>%knitr::kable()
time n.risk n.event n.censor estimate std.error conf.high conf.low
5 228 1 0 0.9958207 0.0041893 1.0000000 0.9876776
11 227 3 0 0.9832688 0.0084466 0.9996823 0.9671247
12 224 1 0 0.9790670 0.0094750 0.9974188 0.9610529
13 223 2 0 0.9706383 0.0112867 0.9923495 0.9494021
15 221 1 0 0.9664127 0.0121065 0.9896182 0.9437513
26 220 1 0 0.9621802 0.0128837 0.9867862 0.9381877
coxmod%>%tidy()%>%
  ggplot(aes(time, estimate)) +
  geom_line(col="red4") +
  geom_ribbon(aes(ymin = conf.low, 
                  ymax = conf.high),
              fill="red",
              alpha = 0.4)+
  theme_bw()

Ta có thể bootstrap mô hình Cox này dễ dàng bằng broom:

bootcox<- lung %>%
  bootstrap(200) %>%
  do(tidy(survfit(coxph(Surv(time, status) ~ age + sex, .))))
bootcox%>%head%>%knitr::kable()
replicate time n.risk n.event n.censor estimate std.error conf.high conf.low
1 5 228 1 0 0.9957728 0.0042371 1.0000000 0.9875375
1 11 227 4 0 0.9788329 0.0095779 0.9973815 0.9606293
1 12 223 1 0 0.9745817 0.0105243 0.9948934 0.9546847
1 13 222 1 0 0.9703219 0.0114035 0.9922532 0.9488754
1 30 221 1 0 0.9660584 0.0122284 0.9894918 0.9431799
1 31 220 1 0 0.9617874 0.0130107 0.9866289 0.9375713

Đây là biểu đồ của 200 mô hình Cox từ bootstrap:

bootcox%>%
  ggplot(aes(time, estimate,group=replicate,col=replicate)) +
  geom_line(alpha=0.1,show.legend = F) +
  scale_color_distiller(palette="RdBu")+
  theme_bw()

Hoặc giản dị hơn: Chỉ trình bày trung vị và 97.5% CI của giá trị dự báo từ 200 mô hình bootstrap;

bootcoxsum <- bootcox %>%
  group_by(time) %>%
  summarize(median = median(estimate),
            LL= quantile(estimate, .025),
            UL = quantile(estimate, .975))

bootcoxsum%>%ggplot()+
  geom_ribbon(aes(x=time,
                  ymin = LL, 
                  ymax = UL),
              fill="red",
              alpha =0.1)+
  geom_line(aes(time, median),col="red4",size=1)+
  theme_bw()

6 Tổng kết

Bài thực hành đến đây là chấm dứt. Tuy Nhi chỉ mới khảo sát package broom với 3 loại mô hình: GLM, Gamlss và Survival; tuy nhiên như đã giới thiệu, broom còn tương thích với rất nhiều package hồi quy khác, bao gồm nlme, lme4, gam, mgcv, rstanarm và brms… và với mỗi package nó đều làm được chức năng đóng gói mô hình bằng hàm tidy và tái chọn mẫu bằng hàm bootstrap.

Như ta thấy, khi tải broom đồng thời với tidyverse chúng ta đã kết nối những công dụng tiện lợi của dplyr, broom và ggplot2 trong một quy trình khép kín và rất tiện lợi.Nhi khuyến khích các bạn sử dụng quy trình này khi phân tích dữ liệu bằng mô hình hồi quy, bất kể bạn đang dùng algorithm nào.

Tạm biệt các bạn và hẹn gặp lại.

LS0tDQp0aXRsZTogIkdp4bubaSB0aGnhu4d1IHBhY2thZ2UgYnJvb20iIA0KYXV0aG9yOiAiTMOqIE5n4buNYyBLaOG6oyBOaGkiDQpkYXRlOiAiMjQgVGjDoW5nIDEyIDIwMTciDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZGVmYXVsdCINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpgYGANCg0KIVtdKGJyb29tMS5wbmcpDQoNCiMgR2nhu5tpIHRoaeG7h3UNCg0KVGjDom4gY2jDoG8gY8OhYyBi4bqhbiwgaMO0bSBuYXkgTmhpIG114buRbiBnaeG7m2kgdGhp4buHdSB24bubaSBjw6FjIGLhuqFuIG3hu5l0IFIgcGFja2FnZSBy4bqldCB0aeG7h24gbOG7o2ksIMSRw7MgbMOgIGJyb29tLiBDw7RuZyBk4bulbmcgY+G7p2EgYnJvb20gbMOgIHRyw61jaCB4deG6pXQgbuG7mWkgZHVuZyBj4bunYSBt4buZdCBvdXRwdXQgb2JqZWN0IC0ga+G6v3QgcXXhuqMgY+G7p2EgbeG7mXQgbcO0IGjDrG5oIHRo4buRbmcga8OqIHbDoCB0w7NtIHThuq90IHRow7RuZyB0aW4gbsOgeSB2w6BvIG3hu5l0IGRhdGFmcmFtZS4gVMOqbiBn4buNaSBicm9vbSBjaMOtbmggbMOgIMSR4buDIGRp4buFbiB04bqjIHZp4buHYyBk4buNbiBk4bq5cCB04burIG3hu5l0IG3DtCBow6xuaCBy4buRaSBy4bqvbSB0aMOgbmggbeG7mXQgZGF0YWZyYW1lIGfhu41uIGfDoG5nLCBz4bqhY2ggxJHhurlwLCBz4bq1biBzw6BuZyDEkeG7gyDEkcawYSB2w6BvIGLDoW8gY8OhbywgaG/hurdjIGzDoG0gbmd1ecOqbiBsaeG7h3UgxJHhu4MgduG6vSBuaOG7r25nIGJp4buDdSDEkeG7ky4gDQoNClBhY2thZ2UgYnJvb20gZG8gdMOhYyBnaeG6oyBEYXZpZCBSb2JpbnNvbiB04bqhbyByYSB04burIG7Eg20gMjAxNCwgdsOgIHRyb25nIDMgbsSDbSBuYXkgbmjDs20gdMOhYyBnaeG6oyBj4bunYSBicm9vbSDEkcOjIMOibSB0aOG6p20gbeG7nyBy4buZbmcga2jhuqMgbsSDbmcgY+G7p2EgcGFja2FnZSBuw6B5LiDhu54gdGjhu51pIMSRaeG7g20gaGnhu4duIG5heSwgYnJvb20gcuG6pXQgbeG6oW5oLCBuw7MgdMawxqFuZyB0aMOtY2ggduG7m2kgaOG6p3UgaOG6v3Qgbmjhu69uZyBwYWNrYWdlIEjhu5NpIHF1eSB0cm9uZyBSLCBr4buDIGPhuqMgbmjhu69uZyBtw7QgaMOsbmggcGjhu6ljIHThuqFwIG5oxrAgZ2FtbHNzLCBubG1lciwgbG1lNCwgbWdjdiwgYnJtcy4uLiANCg0KYnJvb20gY8OzIDQgaMOgbSBxdWFuIHRy4buNbmc6DQoNCjEpIGdsYW5jZTogaMOgbSBuw6B5IHRyw61jaCB4deG6pXQgdGjDtG5nIHRpbiB24buBIHBo4bqpbSBjaOG6pXQgbcO0IGjDrG5oLCB0aMOtIGThu6UgZ29vZG5lc3Mgb2YgZml0LCBjw6FjIHRy4buLIHPhu5EgQUlDLCBCSUMsIFJzcSwgRiB0ZXN0Li4uDQoNCjIpIHRpZHk6IGjDoG0gbsOgeSB0csOtY2ggeHXhuqV0IG7hu5lpIGR1bmcgbcO0IGjDrG5oLCB0aMOtIGThu6UgY8OhYyB0aGFtIHPhu5EgaOG7k2kgcXV5LCBTLkUsIGtob+G6o25nIHRpbiBj4bqteSwga2nhu4NtIMSR4buLbmggdCB2w6AgcF92YWx1ZQ0KDQozKSBhdWdtZW50OiBow6BtIG7DoHkgY2hvIHBow6lwIGtp4buDbSDEkeG7i25oIG3DtCBow6xuaCB0csOqbiBkYXRhc2V0IGhp4buHbiB0aOG7nWkgaG/hurdjIG3hu5tpLCB2w6AgY3VuZyBj4bqlcCB0aMO0bmcgdGluIHbhu4EgxJHhu5kgY2jDrW5oIHjDoWMgY+G7p2EgbcO0IGjDrG5oLCB0aMOtIGThu6UgUmVzaWR1YWwgZXJyb3IsIC4uLiANCg0KNCkgYm9vdHN0cmFwOiBicm9vbSBjw7MgbeG7mXQgaMOgbSBib290c3RyYXAgY+G7p2EgcmnDqm5nIG7Dsywga2hpIGvhur90IGjhu6NwIHbhu5tpIGjDoG0gdGlkeSwgc+G6vSBjaG8gcGjDqXAgYuG6oW4gw6FwIGThu6VuZyBib290c3RyYXAgY2hvIGLhuqV0IGvDrCBtw7QgaMOsbmggbsOgby4gUXV5IHRyw6xuaCBuw6B5IGPhuqduIHPhu7EgaOG7lyB0cuG7oyBj4bunYSBkcGx5cg0KDQpUdXkgxJHDoyBoaeG7h24gZGnhu4duIHThu6sgcuG6pXQgbMOidSwgbmjGsG5nIGJyb29tIGNo4buJIHRo4buxYyBz4buxIGfDonkgY2jDuiDDvSB04burIG7Eg20gMjAxNiwga2hpIEhhZGxleSBXaWNraGFtIGjhu6NwIG5o4bqldCBicm9vbSB24bubaSBuaOG7r25nIGLhuqFuIGLDqCBj4bunYSBuw7MgbMOgIGPDoWMgcGFja2FnZSBuaMawIGRwbHlyLCBwdXJyLCB0aWR5ciwgdsOgIGdncGxvdDIgxJHhu4MgdOG6oW8gdGjDoG5oIG3hu5l0IHF1eSB0csOsbmggcGjDom4gdMOtY2ggZOG7ryBsaeG7h3Uga2jDqXAga8OtbiwgdHJvbmcgxJHDsyBicm9vbSB24bqrbiBnaeG7ryBuaGnhu4dtIHbhu6UgZOG7jW4gZOG6uXAgdsOgIMSRw7NuZyBnw7NpIGRhdGFmcmFtZSBu4buZaSBkdW5nIG3DtCBow6xuaCwgbmjGsG5nIG7DsyBraMO0bmcgbMOgbSB2aeG7h2MgbeG7mXQgbcOsbmggbcOgIG5o4bqtbiDEkcaw4bujYyBz4buxIHRy4bujIHRo4bunIGPhu6dhIGRwbHlyIGPFqW5nIG5oxrAga+G6v3QgcXXhuqMgbcOgIGJyb29tIHh14bqldCByYSDEkcaw4bujYyBjaHV54buBbiBxdWEgY2hvIGdncGxvdCBt4buZdCBjw6FjaCBuaGFuaCBjaMOzbmcgbmjhu50gY8OhYyBuaMOibiB04butICJwaXBlIi4gDQoNCmJyb29tIHLhuqV0IHRp4buHbiBs4bujaSwgbmjGsG5nIGPDsyBy4bqldCDDrXQgdHV0b3JpYWwgduG7gSBuw7Mgbmdv4bqhaSB0cuG7qyAzIHRow60gZOG7pSBtaW5oIGjhu41hIGPhu6dhIGNow61uaCB0w6FjIGdp4bqjIHbDoCBt4buZdCB2w6BpIMSRb+G6oW4gY29kZSB0cm9uZyAybmQgRWQuIGPhu6dhIHF1eeG7g24gZ2dwbG90MiBj4bunYSBXaWNraGFtLiBEbyDEkcOzLCBOaGkgdmnhur90IGLDoGkgaMaw4bubbmcgZOG6q24gc2F1IHbhu5tpIHRow7RuZyDEkWnhu4dwIGtodXnhur9uIGtow61jaCBjw6FjIGLhuqFuIHPhu60gZOG7pW5nIGJyb29tIG3hu5dpIGtoaSBi4bqhbiBsw6BtIHZp4buHYyB24bubaSBtw7QgaMOsbmggaOG7k2kgcXV5Lg0KDQpUcm9uZyBiw6BpLCBOaGkgc+G6vSBtaW5oIGjhu41hIGto4bqjIG7Eg25nIGPhu6dhIGJyb29tIHbhu5tpIDQgdGjDrSBk4bulDQoNCjEpIFRyw61jaCB4deG6pXQgdGjDtG5nIHRpbiB04burIG3hu5l0IG3DtCBow6xuaCBMb2dpc3RpYyBk4buxbmcgYuG6sW5nIEdhbWxzcywgdsOgIGJvb3RzdHJhcCBtw7QgaMOsbmggbG9naXN0aWMgbsOgeQ0KDQoyKSBUcsOtY2ggeHXhuqV0IHRow7RuZyB0aW4gdOG7qyBt4buZdCBtw7QgaMOsbmggaOG7k2kgcXV5IFBvbHlub21pYWwgZOG7sW5nIGLhurFuZyBnbG0oKQ0KDQozKSBCb290c3RyYXBpbmcgbeG7mXQgbcO0IGjDrG5oIEdMTSB24bubaSBmYW1pbHkgU3R1ZGVudCB0IGNo4bupYSAzIHRoYW0gc+G7kSwgZOG7sW5nIGLhurFuZyBHYW1sc3MNCg0KNCkgQm9vdHN0cmFwaW5nIG3hu5l0IG3DtCBow6xuaCBDb3gtUEggDQoNCg0KIyBNw7QgaMOsbmggTG9naXN0aWMgduG7m2kgZ2FtbHNzDQoNClRhIGThu7FuZyBt4buZdCBtw7QgaMOsbmggbG9naXN0aWMgY2hvIGRhdGFzZXQgQmlvcHN5LCBuaMawbmcgc+G7rSBk4bulbmcgZ2FtbHNzIGNo4bupIGtow7RuZyBkw7luZyBow6BtIGdsbSgpLg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KDQpsaWJyYXJ5KGdhbWxzcykNCm5DPC1kZXRlY3RDb3JlcygpDQoNCmxpYnJhcnkoYnJvb20pDQoNCmxpYnJhcnkoZ2dyaWRnZXMpDQoNCmRmPXJlYWQuY3N2KCJodHRwOi8vdmluY2VudGFyZWxidW5kb2NrLmdpdGh1Yi5pby9SZGF0YXNldHMvY3N2L01BU1MvYmlvcHN5LmNzdiIpJT4lYXNfdGliYmxlKCklPiUuWyxjKDM6MTIpXSU+JW5hLm9taXQoKQ0KDQpuYW1lcyhkZik9YygiY2x1bXB0aGlja25lc3MiLA0KICAgICAgICAgICAgIlNpemVVbmlmb3JtaXR5IiwNCiAgICAgICAgICAgICJTaGFwZVVuaWZvcm1pdHkiLA0KICAgICAgICAgICAgIk1hcmdpbl9hZGhlc2lvbiIsDQogICAgICAgICAgICAiRXBpQ2VsbFNpemUiLA0KICAgICAgICAgICAgIkJhcmVudWNsZWkiLA0KICAgICAgICAgICAgIkJsYW5kQ2hyb21hdGluIiwNCiAgICAgICAgICAgICJOb3JtYWxOdWNsZW9saSIsDQogICAgICAgICAgICAiTWl0b3NlcyIsDQogICAgICAgICAgICAiQ2xhc3MiDQopDQoNCmRmMj1kZiU+JW11dGF0ZSguLENsYXNzPWFzLmludGVnZXIoLiRDbGFzcyktMUwpDQoNCmdsc2xvZ2l0PWdhbWxzcyhDbGFzc35jbHVtcHRoaWNrbmVzcysNCiAgICAgICAgICAgICAgICAgICAgIFNoYXBlVW5pZm9ybWl0eSsNCiAgICAgICAgICAgICAgICAgICAgIE1hcmdpbl9hZGhlc2lvbisNCiAgICAgICAgICAgICAgICAgICAgIEJhcmVudWNsZWkrDQogICAgICAgICAgICAgICAgICAgICBCbGFuZENocm9tYXRpbiwNCiAgICAgICAgICAgICAgICAgICBkYXRhPWRmMiwNCiAgICAgICAgICAgICAgICAgICBmYW1pbHk9QkkoKSwNCiAgICAgICAgICAgICAgICAgICB0cmFjZT1GLA0KICAgICAgICAgICAgICAgIHBhcmFsbGVsPSJtdWx0aWNvcmUiLA0KICAgICAgICAgICAgICAgIG5jcHVzID0gbkMpDQoNCg0Kc3VtbWFyeShnbHNsb2dpdCkNCg0KYGBgDQoNClRhIGzhuqduIGzGsOG7o3QgdGjhu60gaMOgbSB0aWR5IHRyw6puIG3DtCBow6xuaCBuw6B5Og0KTMawdSDDvToga+G6v3QgcXXhuqMgY+G7p2EgaMOgbSB0aWR5IGx1w7RuIGzDoCAxIGRhdGFmcmFtZSwgZG8gxJHDonkgbMOgIG3DtCBow6xuaCBsb2dpc3RpYyBuw6puIGjDoG0gYXVnbWVudCBraMO0bmcgY2hvIGvhur90IHF14bqjIGNow61uaCB4w6FjLCBi4bqhbiBwaOG6o2kgZMO5bmcgbXV0YXRlIMSR4buDIHTDrW5oIHByb2JhYmlsaXR5IHThu6sgcHJlZGljdGVkIGLhurFuZyBow6BtIHBsb2dpcy4gDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KdGlkeShnbHNsb2dpdCklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNClbhu5tpIGvhur90IHF14bqjIGjDoG0gdGlkeSBtw6AgYnJvb20geHXhuqV0IHJhLCB0YSBjw7MgdGjhu4MgdMOtbmggduG6vSBiaeG7g3UgxJHhu5MgT2Rkcy1yYXRpbyBk4buFIGTDoG5nOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxvZ2l0c3VtPWdsc2xvZ2l0JT4ldGlkeSgpJT4lbXV0YXRlKE9SPWV4cChlc3RpbWF0ZSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBVTD1leHAoZXN0aW1hdGUrMS45NipzdGQuZXJyb3IpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgTEw9ZXhwKGVzdGltYXRlLTEuOTYqc3RkLmVycm9yKSkNCg0KbG9naXRzdW0lPiVkcGx5cjo6c2VsZWN0KHRlcm0sT1IsVUwsTEwpJT4lZ2dwbG90KGFlcyh4PXRlcm0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHk9T1IsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHltaW49TEwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHltYXg9VUwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbD10ZXJtKSkrDQogIGdlb21faGxpbmUoeWludGVyY2VwdCA9IGMoMSwyKSxsaW5ldHlwZT0yLGNvbD1jKCJibHVlIiwicmVkIikpKw0KICBnZW9tX3BvaW50cmFuZ2Uoc2hvdy5sZWdlbmQgPSBGLHNpemU9MSkrDQogIGNvb3JkX2ZsaXAoKSsNCiAgdGhlbWVfYncoKQ0KDQpgYGANCg0KVHJvbmcgUiBjw7MgbeG7mXQgcGFja2FnZSBraMOhYyBsw6Agc2pQbG90IGNobyBwaMOpcCBsw6BtIG5o4buvbmcgdmnhu4djIG5oxrAgdGjhur8gbsOgeSBjaOG7iSB24bubaSAxIGjDoG0gcGxvdF9tb2RlbCwgdHV5IG5oacOqbiBzalBsb3QgaG/DoG4gdG/DoG4gYuG6pXQgbOG7sWMgduG7m2kgbcO0IGjDrG5oIGdhbWxzcywgdsOgIGtoaSBsw6BtIHRo4bunIGPDtG5nIG3hu41pIGNodXnhu4duLCBi4bqhbiBz4bq9IGtp4buDbSBzb8OhdCB04buRdCBoxqFuIHThuqV0IGPhuqMgbeG7jWkgdGjhu6ksIHThu6sgdmnhu4djIHTDrW5oIE9SLCA5NSVDSSBjaG8gxJHhur9uIHTDuXkgY2jhu4luaCBtw6B1IHPhuq9jIGNobyBnZ3Bsb3Q7IHbDoCBsw6BtIHRo4bunIGPDtG5nIGtow7RuZyBraMOzIGzhuq9tIGtoaSBicm9vbSDEkcOjIGdpw7pwIGLhuqFuIGzDoG0gcGjhuqduIGtow7MgbmjhuqV0IGzDoCBjaHV54buDbiBtb2RlbCB0aMOgbmggZGF0YWZyYW1lLiBDw7MgZGF0YWZyYW1lIGzDoCBt4buNaSB2aeG7h2MgxJHhu4F1IGto4bqjIHRoaS4NCg0KVGnhur9wIHRoZW8sIHRhIHPhur0gdGjhu60gYm9vdHN0cmFwIG3DtCBow6xuaCBsb2dpc3RpYyB0csOqbiDEkcOieSB2w6AgbeG7mXQgbOG6p24gbuG7r2EsIHjDoWMgxJHhu4tuaCBs4bqhaSBraG/huqNuZyB0aW4gY+G6rXkgY2hvIE9SIGThu7FhIHbDoG8gMTAwIG3DtCBow6xuaCBib290c3RyYXA6DQoNCsSQ4buDIG7hu5FpIGjDoG0gYm9vdHN0cmFwIGPhu6dhIGJyb29tIHbDoG8gaMOgbSB0aWR5LCBjw7MgMiBjw6FjaCwgTmhpIGdp4bubaSB0aGnhu4d1IGPDoWNoIGThu4UgdHLGsOG7m2MsIMSRw7MgbMOgIGTDuW5nIGjDoG0gZG8gY+G7p2EgZHBseXIuIEzhu6NpIHRo4bq/IGPhu6dhIGjDoG0gYm9vdHN0cmFwIG7DoHkgxJHDsyBsw6A6DQoNCjEpIELhuqFuIGtow7RuZyBj4bqnbiBwaOG6o2kgZMO5bmcgcGFja2FnZSBib290LCBraMO0bmcgY+G6p24gdmnhur90IGjDoG0gZ8OsIGPhuqMNCjIpIELhuqFuIGPDsyB0aOG7gyBib290c3RyYXAgbeG7jWkgbcO0IGjDrG5oIGLhuqFuIG114buRbiwgdOG7qyBsbWU0IGNobyDEkeG6v24gc3Vydml2YWwuLi4NCjMpIEvhur90IHF14bqjIGJvb3RzdHJhcCDEkcaw4bujYyBow6BtIHRpZHkgxJHDs25nIGfDs2kgbHXDtG4sIHLhuqV0IGfhu41uIGfDoG5nIHbDoCBz4bq1biBzw6BuZyDEkeG7gyB24bq9IGJp4buDdSDEkeG7kw0KDQpL4bq/dCBxdeG6oyBib290c3RyYXAgbmjGsCBzYXU6DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KDQpkZjIkQ2xhc3M9YXMubnVtZXJpYyhkZjIkQ2xhc3MpDQoNCmJvb3Rsb2dpdDwtIGRmMiAlPiUNCiAgYm9vdHN0cmFwKDEwMCkgJT4lDQogIGRvKHRpZHkoZ2FtbHNzKC4kQ2xhc3N+LiRjbHVtcHRoaWNrbmVzcysNCiAgICAgICAgICAgICAgICAgICAuJFNoYXBlVW5pZm9ybWl0eSsNCiAgICAgICAgICAgICAgICAgICAuJE1hcmdpbl9hZGhlc2lvbisNCiAgICAgICAgICAgICAgICAgICAuJEJhcmVudWNsZWkrDQogICAgICAgICAgICAgICAgICAgLiRCbGFuZENocm9tYXRpbiwNCiAgICAgICAgICAgICAgICAgZmFtaWx5PUJJKCksDQogICAgICAgICAgICAgICAgIHRyYWNlPUYsDQogICAgICAgICAgICAgICAgIHBhcmFsbGVsPSJtdWx0aWNvcmUiLA0KICAgICAgICAgICAgICAgICBuY3B1cyA9IG5DKSkpDQoNCmJvb3Rsb2dpdDwtbXV0YXRlKGJvb3Rsb2dpdCxPUj1leHAoZXN0aW1hdGUpKQ0KDQpib290bG9naXQlPiVkcGx5cjo6c2VsZWN0KHRlcm0sT1IpJT4lDQogIGdncGxvdChhZXMoeT10ZXJtLA0KICAgICAgICAgICAgIHg9T1IpKSsNCiAgZ2VvbV9kZW5zaXR5X3JpZGdlcyhhZXMoZmlsbD10ZXJtKSxhbHBoYT0wLjUsc2NhbGU9MS41KSsNCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gYygxLDIpLGxpbmV0eXBlPTIsY29sPWMoImJsdWUiLCJyZWQiKSkrDQogIHRoZW1lX2J3KCkNCmBgYA0KDQpgYGB7cn0NCm11REY8LWJvb3Rsb2dpdCU+JQ0KICBkcGx5cjo6c2VsZWN0KHJlcGxpY2F0ZSx0ZXJtLGVzdGltYXRlKSU+JQ0KICBzcHJlYWQoa2V5PXRlcm0sdmFsdWU9ZXN0aW1hdGUpDQoNCm5hbWVzKG11REYpPWMoInJlcGxpY2F0ZSIsDQogICAgICAgICAgICAgICIoSW50ZXJjZXB0KSIsDQogICAgICAgICAgICAgICJCYXJlbnVjbGVpIiwNCiAgICAgICAgICAgICAgIkJsYW5kQ2hyb21hdGluIiwNCiAgICAgICAgICAgICAgImNsdW1wdGhpY2tuZXNzIiwNCiAgICAgICAgICAgICAgIk1hcmdpbl9hZGhlc2lvbiIsDQogICAgICAgICAgICAgICJTaGFwZVVuaWZvcm1pdHkiKQ0KYGBgDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KcHJlZGJvb3Q8LWRmJT4lbXV0YXRlKHByZWQ9TkEsSXRlcj1OQSkNCg0KZm9yKGkgaW4gMTpucm93KG11REYpKXsNCiAgYmV0YTA9bXVERiRgKEludGVyY2VwdClgW2ldDQogIGJldGExPW11REYkQmFyZW51Y2xlaVtpXQ0KICBiZXRhMj1tdURGJEJsYW5kQ2hyb21hdGluW2ldDQogIGJldGEzPW11REYkY2x1bXB0aGlja25lc3NbaV0NCiAgYmV0YTQ9bXVERiRNYXJnaW5fYWRoZXNpb25baV0NCiAgYmV0YTU9bXVERiRTaGFwZVVuaWZvcm1pdHlbaV0NCiAgdGVtcGRmPC1kZiU+JW11dGF0ZShwcmVkPXBsb2dpcyhiZXRhMCsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFNoYXBlVW5pZm9ybWl0eSpiZXRhNSsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIE1hcmdpbl9hZGhlc2lvbipiZXRhNCsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIEJhcmVudWNsZWkqYmV0YTErDQogICAgICAgICAgICAgICAgICAgICAgICAgICBCbGFuZENocm9tYXRpbipiZXRhMisNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsdW1wdGhpY2tuZXNzKmJldGEzKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBJdGVyPWkpDQogIHByZWRib290PXJiaW5kKHByZWRib290LHRlbXBkZiklPiVuYS5vbWl0KCkNCn0NCg0KDQpiaW5vbWlhbF9zbW9vdGggPC0gZnVuY3Rpb24oLi4uKSB7DQogIGdlb21fc21vb3RoKG1ldGhvZCA9ICJnbG0iLCBtZXRob2QuYXJncyA9IGxpc3QoZmFtaWx5ID0gImJpbm9taWFsIiksIHNlPVQsc2hvdy5sZWdlbmQgPSBGLC4uLikNCn0NCg0KbG9uZ2RmPXByZWRib290JT4lZHBseXI6OnNlbGVjdChJdGVyLA0KICAgICAgICAgICAgICAgICAgICAgICAgIFNoYXBlVW5pZm9ybWl0eSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBNYXJnaW5fYWRoZXNpb24sDQogICAgICAgICAgICAgICAgICAgICAgICAgQmFyZW51Y2xlaSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBCbGFuZENocm9tYXRpbiwNCiAgICAgICAgICAgICAgICAgICAgICAgICBjbHVtcHRoaWNrbmVzcywNCiAgICAgICAgICAgICAgICAgICAgICAgICBwcmVkLA0KICAgICAgICAgICAgICAgICAgICAgICAgIENsYXNzKSU+JQ0KICBnYXRoZXIoU2hhcGVVbmlmb3JtaXR5OmNsdW1wdGhpY2tuZXNzLGtleT0iRmVhdHVyZSIsdmFsdWU9IlNjb3JlIikNCg0KICBsb25nZGYlPiVnZ3Bsb3QoYWVzKHg9U2NvcmUsDQogICAgICAgICAgICAgICAgICAgICAgeT1wcmVkLA0KICAgICAgICAgICAgICAgICAgICAgIGdyb3VwPWFzLmZhY3RvcihJdGVyKSwNCiAgICAgICAgICAgICAgICAgICAgICBmaWxsPWFzLmZhY3RvcihJdGVyKSwNCiAgICAgICAgICAgICAgICAgICAgICBjb2w9YXMuZmFjdG9yKEl0ZXIpKSkrDQogIGJpbm9taWFsX3Ntb290aChhbHBoYT0wLjEpKw0KICB0aGVtZV9idygpKw0KICBmYWNldF93cmFwKH5GZWF0dXJlLHNjYWxlcz0iZnJlZSIsbmNvbD0zKSsNCiAgICBzY2FsZV9maWxsX2JyZXdlcigiUHVSZCIpKw0KICAgIHNjYWxlX2NvbG9yX2JyZXdlcigiUHVSZCIpDQpgYGANCg0KDQojIE3DtCBow6xuaCBQb2x5bm9taWFsIGLhuq1jIDUgZMO5bmcgR0xNDQoNClRow60gZOG7pSB0aeG6v3AgdGhlbywgTmhpIGThu7FuZyBt4buZdCBtw7QgaMOsbmggcG9seW5vbWlhbCBi4bqtYyA1IGto4bqjbyBzw6F0IHTGsMahbmcgcXVhbiBnaeG7r2EgbG9nKGNoaeG7gXUgY2FvKSB2w6AgdHXhu5VpIGPhu6dhIGPDoWMgYuG7h25oIG5oaSB0cm9uZyBkYXRhc2V0IExldWtlbWlhLg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCkxldWtlbWlhJT4lZ2dwbG90KGFlcyh4PWFnZSx5PWxvZyhoZWlnaHQpKSkrDQogIGdlb21fcG9pbnQoY29sPSJibHVlIixhbHBoYT0wLjMpKw0KICBnZW9tX3Ntb290aChtZXRob2Q9ImdsbSIsZm9ybXVsYSA9IHkgfiBwb2x5KHgsNSksY29sPSJyZWQiLGZpbGw9InJlZCIsYWxwaGE9MC41KSsNCiAgdGhlbWVfYncoKQ0KDQpgYGANCg0KTcO0IGjDrG5oIMSRxrDGoWMgZOG7sW5nIMSRxqFuIGdp4bqjbiBi4bqxbmcgaMOgbSBnbG0sIHbhu5tpIGZhbWlseSA9IGdhdXNzaWFuIHbDoCBsaW5rIGZ1bmN0aW9uID0gbG9nOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxtb2Q9Z2xtKExldWtlbWlhLGZvcm11bGE9aGVpZ2h0fnBvbHkoYWdlLDUpLGZhbWlseT1nYXVzc2lhbihsaW5rPSJsb2ciKSkNCg0Kc3VtbWFyeShsbW9kKQ0KDQpgYGANCg0KVGEgZMO5bmcgMiBow6BtIGdsYW5jZSB2w6AgdGlkeSBj4bunYSBicm9vbSBjaG8gbcO0IGjDrG5oIHRyw6puLCBuaMawIGLhuqFuIHRo4bqleSwgdGEgY8OzIDIgZGF0YWZyYW1lIHLhuqV0IGfhu41uIMSR4bq5cDoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQoNCmdsYW5jZShsbW9kKSU+JWtuaXRyOjprYWJsZSgpDQoNCnRpZHkobG1vZCklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCktoaSBjw7MgZGF0YWZyYW1lLCBi4bqhbiBz4bq9IG5naMSpIG5nYXkgxJHhur9uIGdncGxvdDIsIMSRw7puZyBuaMawIHbhuq15LCB0YSBjw7MgdGjhu4MgduG6vSBy4bqldCBuaGnhu4F1IGJp4buDdSDEkeG7kyB04burIGRhdGFmcmFtZSBtw6AgYnJvb20gY3VuZyBj4bqlcDoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpsbW9kJT4ldGlkeSgpJT4lDQogIGdncGxvdChhZXMoeD10ZXJtLA0KICAgICAgICAgICAgIHk9ZXN0aW1hdGUsDQogICAgICAgICAgICAgeW1pbj1lc3RpbWF0ZS0xLjk2KnN0ZC5lcnJvciwNCiAgICAgICAgICAgICB5bWF4PWVzdGltYXRlKzEuOTYqc3RkLmVycm9yLA0KICAgICAgICAgICAgIGNvbD10ZXJtKSkrDQogIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDAsbGluZXR5cGU9Mixjb2w9ImJsdWUiKSsNCiAgZ2VvbV9wb2ludHJhbmdlKHNob3cubGVnZW5kID0gRixzaXplPTEpKw0KICBjb29yZF9mbGlwKCkrDQogIHRoZW1lX2J3KCkNCg0KbG1vZCU+JXRpZHkoKSU+JQ0KICBnZ3Bsb3QoYWVzKHg9dGVybSwNCiAgICAgICAgICAgICB5PXAudmFsdWUsDQogICAgICAgICAgICAgY29sPXRlcm0pKSsNCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMC4wMDUsbGluZXR5cGU9Mixjb2w9ImJsdWUiKSsNCiAgZ2VvbV9wb2ludChzaG93LmxlZ2VuZCA9IEYsc2l6ZT01KSsNCiAgY29vcmRfZmxpcCgpKw0KICB0aGVtZV9idygpDQpgYGANCg0KSMOgbSBhdWdtZW50IGNobyBwaMOpcCB04bqhbyByYSAxIGRhdGFmcmFtZSBjaOG7k25nIGzDqm4gZGF0YWZyYW1lIGfhu5FjLCBjaOG7qWEgY8OhYyB0cuG7iyBz4buRIG5oxrAgZml0dGVkLCBzZS5maXQsIHJlc2lkdWFsIGVycm9yLCBjb29rc2QuLi4NCg0KYGBge3J9DQpkaWFnREY8LWF1Z21lbnQobG1vZCkNCg0KZGlhZ0RGJT4laGVhZCgpJT4la25pdHI6OmthYmxlKCkNCg0KZGlhZ0RGJT4lZ2dwbG90KGFlcyh4PS5maXR0ZWQseT0ucmVzaWQsY29sPS5yZXNpZCkpKw0KICBnZW9tX3BvaW50KGFscGhhPTAuMixzaG93LmxlZ2VuZCA9IEYpKw0KICBnZW9tX3Ntb290aChhbHBoYT0wLjIsc2hvdy5sZWdlbmQgPSBGLGZpbGw9ImJsdWUiLGNvbD0iYmx1ZTQiKSsNCiAgdGhlbWVfYncoKSsNCiAgc2NhbGVfY29sb3JfZ3JhZGllbnQyKGxvdz0icmVkIixoaWdoPSJyZWQiLG1pZD0iYmx1ZSIsbWlkcG9pbnQgPSAwKQ0KYGBgDQoNClRyb25nIG3hu5l0IHPhu5EgdHLGsOG7nW5nIGjhu6NwLCBi4bqhbiBtdeG7kW4gZOG7sW5nIG3DtCBow6xuaCByacOqbmcgY2hvIG3hu5l0IHPhu5EgcGjDom4gbmjDs20gdHJvbmcgZOG7ryBsaeG7h3UsIHRhIGPDsyB0aOG7gyBsw6BtIMSRaeG7gXUgbsOgeSBk4buFIGTDoG5nIGtoaSBr4bq/dCBo4bujcCBkcGx5ciB2w6AgYnJvb206IFRow60gZOG7pSB0YSBk4buxbmcgY8O5bmcgbcO0IGjDrG5oIHBvbHlub21pYWwgbsOzaSB0csOqbiByacOqbmcgY2hvIDMgcGjDom4gbmjDs20gxJFp4buBdSB0cuG7iyAxLDIsMzoNCg0KS+G6v3QgcXXhuqMgduG6q24gbMOgIDEgZGF0YWZyYW1lOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCm1vZGVscyA8LSBMZXVrZW1pYSAlPiUNCiAgZ3JvdXBfYnkodHJlYXRtZW50KSAlPiUNCiAgZG8obW9kID0gZ2xtKGRhdGE9LiwNCiAgICAgICAgICAgICAgIGZvcm11bGE9aGVpZ2h0fnBvbHkoYWdlLDUpLA0KICAgICAgICAgICAgICAgZmFtaWx5PWdhdXNzaWFuKGxpbms9ImxvZyIpKSkNCg0KbW9kZWxfc3VtIDwtIG1vZGVscyAlPiUgdGlkeShtb2QpDQoNCm1vZGVsX3N1bSU+JWtuaXRyOjprYWJsZSgpDQoNCmBgYA0KDQpUYSB24bq9IMSRxrDhu6NjIDMgYmnhu4N1IMSR4buTIHJpw6puZyBiaeG7h3QgdOG7qyBkYXRhZnJhbWUgbsOgeToNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQptb2RlbF9zdW0lPiUNCiAgZ2dwbG90KGFlcyh4PXRlcm0sDQogICAgICAgICAgICAgeT1lc3RpbWF0ZSwNCiAgICAgICAgICAgICB5bWluPWVzdGltYXRlLTEuOTYqc3RkLmVycm9yLA0KICAgICAgICAgICAgIHltYXg9ZXN0aW1hdGUrMS45NipzdGQuZXJyb3IsDQogICAgICAgICAgICAgY29sPXRlcm0pKSsNCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMCxsaW5ldHlwZT0yLGNvbD0iYmx1ZSIpKw0KICBnZW9tX3BvaW50cmFuZ2Uoc2hvdy5sZWdlbmQgPSBGKSsNCiAgY29vcmRfZmxpcCgpKw0KICB0aGVtZV9idygpK2ZhY2V0X3dyYXAofnRyZWF0bWVudCxuY29sPTEsc2NhbGVzPSJmcmVlIikNCmBgYA0KDQojIE3DtCBow6xuaCBnYW1sc3Mgbmhp4buBdSB0aGFtIHPhu5ENCg0KVGEgdHLhu58gbOG6oWkgcGFja2FnZSBnYW1sc3MsIGzhuqduIG7DoHkgTmhpIGThu7FuZyBt4buZdCBtw7QgaMOsbmggcGjhu6ljIHThuqFwIGjGoW4gY2hvIG3hu5l0IGRhdGFzZXQgxJHGoW4gZ2nhuqNuLiBCw6BpIHRvw6FuIOG7nyDEkcOieSBsw6AgxrDhu5tjIHTDrW5oIGNoaeG7gXUgZMOgaSBjYXRoZXJ0ZXIgdGjDtG5nIHRpbSBk4buxYSB2w6BvIGNoaeG7gXUgY2FvIGPhu6dhIGLhu4duaCBuaMOibi4gDQoNCk3hu6VjIHRpw6p1IGPhu6dhIG1pbmggaOG7jWEgbsOgeSBsw6AgxJHhu4MgY2hvIHRo4bqleSBicm9vbSBsw6BtIHZp4buHYyBoaeG7h3UgcXXhuqMgcmEgc2FvIGNobyBt4buZdCBtw7QgaMOsbmggZ2FtbHNzIHbhu5tpIHBow6JuIHBo4buRaSBzdHVkZW50IHQsIHbhu5tpIDMgbcO0IGjDrG5oIGNvbiBjaG8gMyB0aGFtIHPhu5EgTXUobGluaz1pZGVudGl0eSksIHNpZ21hIChsaW5rPWxvZykgdsOgIE51IChsaW5rPWxvZykuDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KZGF0YShoZWFydCwgcGFja2FnZT0icm9idXN0YmFzZSIpDQoNCmhlYXJ0JT4lZ2dwbG90KGFlcyh4PWhlaWdodCx5PWNsZW5ndGgpKSsNCiAgZ2VvbV9wb2ludChzaXplPTUsY29sPSJibHVlIixhbHBoYT0wLjMpKw0KICBnZW9tX3Ntb290aChtZXRob2Q9ImxtIixmaWxsPSJibHVlIixhbHBoYT0wLjIpKw0KICB0aGVtZV9idygpDQpgYGANCg0KVHLGsOG7m2MgdGnDqm4sIHRhIGThu7FuZyBtw7QgaMOsbmggdsOgIGTDuW5nIGJyb29tIMSR4buDIHRyw61jaCB4deG6pXQga+G6v3QgcXXhuqM6DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KZ21vZD1nYW1sc3MoZGF0YT1oZWFydCxmYW1pbHk9VEYoKSwNCiAgICAgICAgICAgIGZvcm11bGE9Y2xlbmd0aCB+IGhlaWdodCwNCiAgICAgICAgICAgIHRyYWNlPUZBTFNFLA0KICAgICAgICAgICAgcGFyYWxsZWw9Im11bHRpY29yZSIsDQogICAgICAgICAgICBuY3B1cyA9IG5DICAgICAgICAgICAgDQogICAgICAgICAgICApDQoNCg0Kc3VtbWFyeShnbW9kKQ0KYGBgDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KdGlkeShnbW9kKSU+JWtuaXRyOjprYWJsZSgpDQpgYGANCg0KVGnhur9wIHRoZW8sIE5oaSBz4bq9IGTDuW5nIGJyb29tIMSR4buDIGJvb3RzdHJhcCAxMDAgcGhpw6puIGLhuqNuIG3DtCBow6xuaCB0csOqbiDEkcOieToNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpib290Z21vZCA8LSBoZWFydCAlPiUNCiAgYm9vdHN0cmFwKDEwMCkgJT4lDQogIGRvKHRpZHkoZ2FtbHNzKGZhbWlseT1URigpLA0KICAgICAgICAgICAgICAgICBmb3JtdWxhPS4kY2xlbmd0aH4uJGhlaWdodCwNCiAgICAgICAgICAgICAgICAgdHJhY2U9RkFMU0UsDQogICAgICAgICAgICAgICAgIHBhcmFsbGVsPSJtdWx0aWNvcmUiLA0KICAgICAgICAgICAgICAgICBuY3B1cyA9IG5DKQ0KICApKQ0KYGBgDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KYm9vdGdtb2QlPiVoZWFkKCklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCkPDsyBt4buZdCBjw6FjaCBraMOhYyDEkeG7gyBkw7luZyBow6BtIGJvb3RzdHJhcCBj4bunYSBicm9vbSwgY8OhY2ggbsOgeSBwaOG7qWMgdOG6oXAgaMahbiAxIGNow7p0IHbDrCBwaOG6o2kgdGjDtG5nIHF1YSAyIGjDoG0gbmVzdCB2w6AgdW5uZXN0IGPhu6dhIHBhY2thZ2UgdGlkeSwgDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KYm9vdGdtb2QyIDwtIGhlYXJ0ICU+JQ0KICBtb2RlbHI6OmJvb3RzdHJhcCgxMDApICU+JQ0KICBtdXRhdGUobW9kID0gbWFwKHN0cmFwLCB+IGdhbWxzcyhkYXRhPS4sZmFtaWx5PVRGKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZvcm11bGE9Y2xlbmd0aCB+IGhlaWdodCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJhY2U9RkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFsbGVsPSJtdWx0aWNvcmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuY3B1cyA9IG5DKSkpJT4lDQogIHVubmVzdChtYXAobW9kLCB0aWR5KSkNCg0KYm9vdGdtb2QyJT4laGVhZCgpJT4la25pdHI6OmthYmxlKCkNCmBgYA0KDQpD4bqldSB0csO6YyAyIGRhdGFmcmFtZSBsw6AgdMawxqFuZyDEkcawxqFuZywgY2jhu4kga2jDoWMg4bufIGJp4bq/biAuaWQgY2hvIGPDoWNoIGzDoG0gdGjhu6kgMiB0aGF5IHbDrCBiaeG6v24gcmVwbGljYXRlIHbhu5tpIGjDoG0gZG8sIG5oxrBuZyDEkeG7lWkgbOG6oWkgdMOqbiBj4bunYSBjw6FjIHByZWRpY3RvciBj4bunYSBjw6FjaCAyIHLDtSByw6BuZyBoxqFuLg0KDQpUaeG6v3AgdGhlbywgZ2nhuqMgc+G7rSBOaGkgxJFhbmcgZMO5bmcgY8OhY2ggdGjhu6kgMiwgdsOgIG114buRbiB24bq9IGJp4buDdSDEkeG7kyAxMDAgbcO0IGjDrG5oIHRpw6puIGzGsOG7o25nIGNobyBNdS4gxJDhu4MgbMOgbSB2aeG7h2MgbsOgeSwgdHLGsOG7m2MgaOG6v3QgdGEgxJHGsGEgZGF0YWZyYW1lIGJvb3RnbW9kMiB2w6BvIGjDoG0gZmlsdGVyIMSR4buDIGzhu41jIHJpw6puZyBnacOhIHRy4buLIHBhcmFtZXRlcj1tdSwgc2F1IMSRw7MgZMO5bmcgaMOgbSBzcHJlYWQgxJHhu4MgdMOhY2ggSW50ZXJjZXB0IHbDoCBIZWlnaHQgcmEgdGjDoG5oIDIgYmnhur9uIChj4buZdCkuIFNhdSDEkcOzIHRhIGTDuW5nIDEgdsOybmcgbOG6t3AgxJHhu4MgdMOtbmggTXUgdOG7qyBtw7QgaMOsbmggdHV54bq/biB0w61uaCB24bubaSBJbnRlcmNlcHQgdsOgIEhlaWdodC4gS+G6v3QgcXXhuqMgdsOybmcgbOG6t3AgbMOgIDEgZGF0YWZyYW1lIHByZWRib290Lg0KDQpDdeG7kWkgY8O5bmcgdGEgduG6vSDEkcaw4bujYyBiaeG7g3UgxJHhu5MgbmjGsCBzYXU6DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KbXVERjwtYm9vdGdtb2QyJT4lZmlsdGVyKHBhcmFtZXRlcj09Im11IiklPiUNCiAgZHBseXI6OnNlbGVjdCguaWQ6ZXN0aW1hdGUpJT4lDQogIHNwcmVhZChrZXk9dGVybSx2YWx1ZT1lc3RpbWF0ZSkNCg0KcHJlZGJvb3Q8LWhlYXJ0JT4lbXV0YXRlKHByZWQ9TkEsSXRlcj1OQSkNCg0KZm9yKGkgaW4gMTpucm93KG11REYpKXsNCiAgaW50ZXJjZXB0PW11REYkYChJbnRlcmNlcHQpYFtpXQ0KICBiZXRhPW11REYkaGVpZ2h0W2ldDQogIHRlbXBkZjwtaGVhcnQlPiVtdXRhdGUocHJlZD1oZWFydCRoZWlnaHQqYmV0YStpbnRlcmNlcHQsDQogICAgICAgICAgICAgICAgICAgICAgICAgSXRlcj1pKQ0KICBwcmVkYm9vdD1yYmluZChwcmVkYm9vdCx0ZW1wZGYpJT4lbmEub21pdCgpDQp9DQoNCnByZWRib290JT4lDQogIGdncGxvdCgpKw0KICBnZW9tX3BvaW50KGFlcyh4PWhlaWdodCx5PWNsZW5ndGgpLGFscGhhPTAuMSxzaXplPTQsY29sPSJibHVlNCIpKw0KICBnZW9tX3BvaW50KGFlcyh4PWhlaWdodCx5PXByZWQsZ3JvdXA9SXRlcixjb2w9SXRlciksYWxwaGE9MC4wMixzaXplPTMsc2hvdy5sZWdlbmQgPSBGKSsNCiAgZ2VvbV9saW5lKGFlcyh4PWhlaWdodCx5PXByZWQsZ3JvdXA9SXRlcixjb2w9SXRlciksYWxwaGE9MC4xLHNob3cubGVnZW5kID0gRikrDQogIGdlb21fc21vb3RoKG1ldGhvZD0ibG0iLHNlPUYsYWVzKHg9aGVpZ2h0LHk9cHJlZCksY29sPSJibHVlNCIpKw0KICBzY2FsZV9jb2xvcl9kaXN0aWxsZXIocGFsZXR0ZT0iUHVSZCIpKw0KICB0aGVtZV9idygpDQpgYGANCg0KIyBNw7QgaMOsbmggQ294LVBIDQoNClRyb25nIHRow60gZOG7pSBjdeG7kWkgY8O5bmcgbsOgeSwgTmhpIHPhur0gZMO5bmcgYnJvb20gY2hvIG3hu5l0IG3DtCBow6xuaCBDb3gtUGggdHLDqm4gZGF0YXNldCBsdW5nLg0KDQpIw6BtIHRpZHkgY2hvIHJhIG3hu5l0IGRhdGFmcmFtZSBjb3hkZiB24bubaSBj4bqldSB0csO6YyBob8OgbiB0b8OgbiBraMOhYyB24bubaSBtw7QgaMOsbmggZ2xtLiBU4burIGRhdGFmcmFtZSBuw6B5IHRhIGPDsyB0aOG7gyB24bq9IMSRxrDhu6NjIGJp4buDdSDEkeG7kyBjaG8gbcO0IGjDrG5oIENveDoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHN1cnZpdmFsKQ0KDQpkYXRhKGx1bmcpDQoNCmNveG1vZCA8LSBzdXJ2Zml0KGNveHBoKFN1cnYodGltZSwgc3RhdHVzKSB+IGFnZSArIHNleCwgZGF0YT1sdW5nKSkNCg0KZ2xhbmNlKGNveG1vZCklPiVrbml0cjo6a2FibGUoKQ0KDQpjb3hkZj10aWR5KGNveG1vZCkNCg0KY294ZGYlPiVoZWFkKCklPiVrbml0cjo6a2FibGUoKQ0KDQpgYGANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpjb3htb2QlPiV0aWR5KCklPiUNCiAgZ2dwbG90KGFlcyh0aW1lLCBlc3RpbWF0ZSkpICsNCiAgZ2VvbV9saW5lKGNvbD0icmVkNCIpICsNCiAgZ2VvbV9yaWJib24oYWVzKHltaW4gPSBjb25mLmxvdywgDQogICAgICAgICAgICAgICAgICB5bWF4ID0gY29uZi5oaWdoKSwNCiAgICAgICAgICAgICAgZmlsbD0icmVkIiwNCiAgICAgICAgICAgICAgYWxwaGEgPSAwLjQpKw0KICB0aGVtZV9idygpDQoNCmBgYA0KDQpUYSBjw7MgdGjhu4MgYm9vdHN0cmFwIG3DtCBow6xuaCBDb3ggbsOgeSBk4buFIGTDoG5nIGLhurFuZyBicm9vbToNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpib290Y294PC0gbHVuZyAlPiUNCiAgYm9vdHN0cmFwKDIwMCkgJT4lDQogIGRvKHRpZHkoc3VydmZpdChjb3hwaChTdXJ2KHRpbWUsIHN0YXR1cykgfiBhZ2UgKyBzZXgsIC4pKSkpDQpgYGANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpib290Y294JT4laGVhZCU+JWtuaXRyOjprYWJsZSgpDQpgYGANCg0KxJDDonkgbMOgIGJp4buDdSDEkeG7kyBj4bunYSAyMDAgbcO0IGjDrG5oIENveCB04burIGJvb3RzdHJhcDoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpib290Y294JT4lDQogIGdncGxvdChhZXModGltZSwgZXN0aW1hdGUsZ3JvdXA9cmVwbGljYXRlLGNvbD1yZXBsaWNhdGUpKSArDQogIGdlb21fbGluZShhbHBoYT0wLjEsc2hvdy5sZWdlbmQgPSBGKSArDQogIHNjYWxlX2NvbG9yX2Rpc3RpbGxlcihwYWxldHRlPSJSZEJ1IikrDQogIHRoZW1lX2J3KCkNCmBgYA0KDQpIb+G6t2MgZ2nhuqNuIGThu4sgaMahbjogQ2jhu4kgIHRyw6xuaCBiw6B5IHRydW5nIHbhu4sgdsOgIDk3LjUlIENJIGPhu6dhIGdpw6EgdHLhu4sgZOG7sSBiw6FvIHThu6sgMjAwIG3DtCBow6xuaCBib290c3RyYXA7DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KYm9vdGNveHN1bSA8LSBib290Y294ICU+JQ0KICBncm91cF9ieSh0aW1lKSAlPiUNCiAgc3VtbWFyaXplKG1lZGlhbiA9IG1lZGlhbihlc3RpbWF0ZSksDQogICAgICAgICAgICBMTD0gcXVhbnRpbGUoZXN0aW1hdGUsIC4wMjUpLA0KICAgICAgICAgICAgVUwgPSBxdWFudGlsZShlc3RpbWF0ZSwgLjk3NSkpDQoNCmJvb3Rjb3hzdW0lPiVnZ3Bsb3QoKSsNCiAgZ2VvbV9yaWJib24oYWVzKHg9dGltZSwNCiAgICAgICAgICAgICAgICAgIHltaW4gPSBMTCwgDQogICAgICAgICAgICAgICAgICB5bWF4ID0gVUwpLA0KICAgICAgICAgICAgICBmaWxsPSJyZWQiLA0KICAgICAgICAgICAgICBhbHBoYSA9MC4xKSsNCiAgZ2VvbV9saW5lKGFlcyh0aW1lLCBtZWRpYW4pLGNvbD0icmVkNCIsc2l6ZT0xKSsNCiAgdGhlbWVfYncoKQ0KYGBgDQoNCg0KIyBU4buVbmcga+G6v3QNCg0KQsOgaSB0aOG7sWMgaMOgbmggxJHhur9uIMSRw6J5IGzDoCBjaOG6pW0gZOG7qXQuIFR1eSBOaGkgY2jhu4kgbeG7m2kga2jhuqNvIHPDoXQgcGFja2FnZSBicm9vbSB24bubaSAzIGxv4bqhaSBtw7QgaMOsbmg6IEdMTSwgR2FtbHNzIHbDoCBTdXJ2aXZhbDsgdHV5IG5oacOqbiBuaMawIMSRw6MgZ2nhu5tpIHRoaeG7h3UsIGJyb29tIGPDsm4gdMawxqFuZyB0aMOtY2ggduG7m2kgcuG6pXQgbmhp4buBdSBwYWNrYWdlIGjhu5NpIHF1eSBraMOhYywgYmFvIGfhu5NtIG5sbWUsIGxtZTQsIGdhbSwgbWdjdiwgcnN0YW5hcm0gdsOgIGJybXMuLi4gdsOgIHbhu5tpIG3hu5dpIHBhY2thZ2UgbsOzIMSR4buBdSBsw6BtIMSRxrDhu6NjIGNo4bupYyBuxINuZyDEkcOzbmcgZ8OzaSBtw7QgaMOsbmggYuG6sW5nIGjDoG0gdGlkeSB2w6AgdMOhaSBjaOG7jW4gbeG6q3UgYuG6sW5nIGjDoG0gYm9vdHN0cmFwLg0KDQpOaMawIHRhIHRo4bqleSwga2hpIHThuqNpIGJyb29tIMSR4buTbmcgdGjhu51pIHbhu5tpIHRpZHl2ZXJzZSBjaMO6bmcgdGEgxJHDoyBr4bq/dCBu4buRaSBuaOG7r25nIGPDtG5nIGThu6VuZyB0aeG7h24gbOG7o2kgY+G7p2EgZHBseXIsIGJyb29tIHbDoCBnZ3Bsb3QyIHRyb25nIG3hu5l0IHF1eSB0csOsbmgga2jDqXAga8OtbiB2w6AgcuG6pXQgdGnhu4duIGzhu6NpLk5oaSBraHV54bq/biBraMOtY2ggY8OhYyBi4bqhbiBz4butIGThu6VuZyBxdXkgdHLDrG5oIG7DoHkga2hpIHBow6JuIHTDrWNoIGThu68gbGnhu4d1IGLhurFuZyBtw7QgaMOsbmggaOG7k2kgcXV5LCBi4bqldCBr4buDIGLhuqFuIMSRYW5nIGTDuW5nIGFsZ29yaXRobSBuw6BvLg0KDQpU4bqhbSBiaeG7h3QgY8OhYyBi4bqhbiB2w6AgaOG6uW4gZ+G6t3AgbOG6oWkuDQo=