'setwd("/Users/dominator/Desktop/CSSA/DataMining&Visualization/Fundrasing Project/Fundrasing Project")'
fundrasing<-read.csv("Fundraising.csv")
future<-read.csv("FutureFundraising.csv")
set.seed(12345)
fundrasing<-fundrasing[c(-1,-2,-24)]
fundrasing$TARGET_B<-as.factor(fundrasing$TARGET_B)
with(fundrasing,table(fundrasing$TARGET_B))

Here we can notice that records of TARGET_B=0 and TARGET_B=1 are the same number. There are both 1560 records.

Firstly, Select Logistic Model as 1st classification model here

library(dplyr)
library(aod)
library(ggplot2)
library(car)
library(Hmisc)
library(rms)
library(ResourceSelection)
library(QuantPsyc)
library(InformationValue)
library(readxl)
library(corrplot)
res<-rcorr(as.matrix(fundrasing))
corrplot(res$P, type = "upper", order = "hclust", tl.col = "black", tl.srt = 45)

Consider rcorr of each input variables to TARGET_B, P-value for WEALTH and IC15 too large, drop these two variables

Initial model:Logistic Regression for Model w/No Predictors

logit0<-glm(TARGET_B~1,data=fundrasing,family='binomial')
summary(logit0)
wald.test(b = coef(logit0), Sigma = vcov(logit0), Terms = 1)
ClassLog(logit0, fundrasing$TARGET_B, cut = .5)

split train(60%) and valid(40%) dataset

train.index=sample(1:nrow(fundrasing),dim(fundrasing)[1]*0.6)
train=fundrasing[train.index,]
valid=fundrasing[-train.index,]

run a logistic full model without WALTH and IC15

logitfull<-glm(TARGET_B~zipconvert_2+zipconvert_3+zipconvert_4+zipconvert_5+homeowner.dummy+NUMCHLD
               +INCOME+gender.dummy+HV+Icmed+Icavg+NUMPROM+RAMNTALL+MAXRAMNT+LASTGIFT+totalmonths+
                 TIMELAG+AVGGIFT,data=train, family = "binomial")
summary(logitfull)

from P-value of all input variables, we shall drop ZIP for all of these dummy variables is not significant and hommeowner.dummy and RAMNTALL,MAXRAMNT will also be dropped for these P-value nearly 0.5

logit1<-glm(TARGET_B~NUMCHLD+INCOME+gender.dummy+HV+Icmed+Icavg+NUMPROM+LASTGIFT+totalmonths+
                 TIMELAG+AVGGIFT,data=train, family = "binomial")
summary(logit1)

drop the input variables:NUMCHLD, Icmed, Icavg,TIMELAG, AVGGIFT which above significance 0.1

logit2<-glm(TARGET_B~INCOME+gender.dummy+HV+NUMPROM+LASTGIFT+totalmonths,data=train, family = "binomial")
summary(logit2)

drop the input variables:HV which above significance 0.05

logit3<-glm(TARGET_B~INCOME+gender.dummy+NUMPROM+LASTGIFT+totalmonths,data=train, family = "binomial")
summary(logit3)

drop gender.dummy which significance is above 0.05

logit4<-glm(TARGET_B~INCOME+NUMPROM+LASTGIFT+totalmonths,data=train, family = "binomial")
summary(logit4)
anova(logit4,test = "Chisq")

Logit4 model can be our final optimal logist model that all input variables are significant. Then carry out Wald test for best version

wt<-matrix(nrow=length(coef(logit4)),ncol=3)
for( i in c(1:length(coef(logit4))) ){
  wttemp<-wald.test(b = coef(logit4), Sigma = vcov(logit4), Terms =i)
  wt[i,2]<-wttemp$result$chi2[3]
  wt[i,3]<-wttemp$result$chi2[1]
  wt[i,1]<-names(logit4$coefficients[i])
}
wt 

Since logit4 model is our final optimal logistic model, we shall use valid dataset to do prediction and compared with actual response

prob<-predict(logit4,valid,type='response')
pred_logit<-ifelse(prob>=0.5,1,0)
table(valid$TARGET_B,pred_logit)

Get an optimal Cutoff Point Drow and Draw ROC plot.

p<-plogis(predict(logit4,train))
optcutoff<-optimalCutoff(train$TARGET_B,p)[1]
optcutoff
ClassLog(logit4, train$TARGET_B, cut =optcutoff)
plotROC(train$TARGET_B,p)

As we can see the optimal cutoff point is 0.494912 and AUROC=0.5879

Second model: AdaBoost (adaptive boost) algorithm

library(adabag)
model <- boosting(TARGET_B~., data =train,mfinal = 20,boos = TRUE)

boos=TRUE means a bootstrap sample of the training set is drawn using the weights for each observation on that iteration.

mfinal=100 means the model will run 100 iterations

boost_pred=predict(model,valid)$class
table(boost_pred,valid$TARGET_B)
library(caret)
caret::confusionMatrix(factor(boost_pred),valid$TARGET_B)

The predict accuracy in Valid dataset is 0.5329

Part2 Calculate Net Profit

first create a dataframe in advance to save the net profit later.

Net_profit=data.frame('Model'=c('Logistic Model','AdaBoost'),'Net.Profit.train'=NA,'Net.Profit.valid'=NA)

Then we calculate the net profit, and we need to undo the weighted sampling

train_pred<-predict(model,train)$class
x<-as.numeric(train_pred)
Net_profit[2,2]<-sum(x)*(13-0.68)/9.8-(length(x)-sum(x))*0.68/0.53

valid_pred<-predict(model,valid)$class
x<-as.numeric(valid_pred)
Net_profit[2,3]<-sum(x)*(13-0.68)/9.8-(length(x)-sum(x))*0.68/0.53

train_pred2<-as.matrix(predict(logit4,train,type='response'))
x<-ifelse(train_pred2[,1]>optcutoff,1,0)
Net_profit[1,2]<-sum(x)*(13-0.68)/9.8-(length(x)-sum(x))*0.68/0.53

valid_pred2<-as.matrix(predict(logit4,valid,type='response'))
x<-ifelse(valid_pred2[,1]>optcutoff,1,0)
Net_profit[1,3]<-sum(x)*(13-0.68)/9.8-(length(x)-sum(x))*0.68/0.53
Net_profit

Draw Net Profit lift curves

donar <- (13 - 0.68)/9.8
non.donar <- (-0.68)/0.53
## for Logistic Regression
lift.table<-data.frame(cbind(as.numeric(as.character(valid$TARGET_B)),valid_pred2))
names(lift.table)=c('TARGET_B','pred')
lift.table=lift.table[order(-lift.table$pred),]
lift.table=lift.table%>%mutate(net.profit=ifelse(as.numeric(TARGET_B)==1,donar,non.donar))
lift.table$Cum.net.profit=lift.table$net.profit
for (i in 2:nrow(lift.table)){
  lift.table[i,4]<-lift.table[i-1,4]+lift.table[i,3]
}
g1=ggplot(lift.table,aes(y=Cum.net.profit,x=1:nrow(lift.table),color='red'))+geom_line()

valid_pred_p<-predict(model,valid)$prob
lift.table2=data.frame(cbind(as.numeric(as.character(valid$TARGET_B)),valid_pred_p[,2]))
names(lift.table2)=c('TARGET_B','pred')
lift.table2=lift.table2[order(-lift.table2$pred),]
lift.table2=lift.table2%>%mutate(net.profit=ifelse(as.numeric(TARGET_B==1),donar,non.donar))
lift.table2$Cum.net.profit=lift.table2$net.profit
for (i in 2:nrow(lift.table2)){
  lift.table2[i,4]<-lift.table2[i-1,4]+lift.table2[i,3]
}
g2=ggplot(lift.table2,aes(y=Cum.net.profit,x=1:nrow(lift.table2),color='red'))+geom_line()
lift=data.frame('logistic Model'=lift.table$Cum.net.profit,'AdaBoost'=lift.table2$Cum.net.profit)
lift$row=c(1:nrow(lift))
g=ggplot() + 
  geom_line(data = lift.table, aes(x = 1:nrow(lift.table), y = Cum.net.profit), color = "blue") +
  geom_line(data = lift.table2, aes(x = 1:nrow(lift.table2), y = Cum.net.profit), color = "red") +
  xlab('row') +
  ylab('Cum.net.profit')+ggtitle("Net Profit lift Curve: Blue-Logistic, Red-Adaboost")
g
lift.actual=data.frame(as.numeric(as.character(valid$TARGET_B)))
names(lift.actual)<-c('TARGET_B')
lift.actual=lift.actual%>%
  mutate(net.profit=ifelse(TARGET_B==1,donar,non.donar))
lift.actual=lift.actual[order(-lift.actual$TARGET_B),]
lift.actual$Cum.net.profit=lift.actual$net.profit
for (i in 2:nrow(lift.actual)){
  lift.actual[i,3]<-lift.actual[i-1,3]+lift.actual[i,2]
}
g_actual=ggplot(lift.actual,aes(x=1:nrow(lift.actual),y=Cum.net.profit),color='green')+geom_line()+xlab('row') +ylab('Cum.net.profit')+ggtitle("Actual Net Profit lift Curve")
library(ggpubr)
ggarrange(g,g_actual,ncol=1,nrow = 2)

From Lift curve, I prefer to select logistic model as our best model. The reasons are showed below:

1.Supposing all the predictors are correct then we sorted the probabilities in an decreasing order, the net profit lift curves should firstlt increase smoothly to the top peek point and then decease to the end as showed in above. Considering two cases, the blue one fits the actual lift curve chart well. Thatโ€™s why logistical model fits well 2. The predicting accuracy in valid datasets of logistic model is better.

future<-future[,-c(2)]
pred_future=as.matrix(predict(logit4,future,type='response'))
future$pred=pred_future
future=future %>% mutate(TARGET_B=ifelse(pred>optcutoff,1,0))
future=future[order(-future$pred),]
head(future[,c(2,22,24)],10)
table(future$TARGET_B)

Therefore, 997 mails can be donors.

LS0tCnRpdGxlOiAiRnVuZHJhc2luZyByZXBvcnQiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpCgpgYGAKCgpgYGB7ciByZWFkIGNzdn0KJ3NldHdkKCIvVXNlcnMvZG9taW5hdG9yL0Rlc2t0b3AvQ1NTQS9EYXRhTWluaW5nJlZpc3VhbGl6YXRpb24vRnVuZHJhc2luZyBQcm9qZWN0L0Z1bmRyYXNpbmcgUHJvamVjdCIpJwpmdW5kcmFzaW5nPC1yZWFkLmNzdigiRnVuZHJhaXNpbmcuY3N2IikKZnV0dXJlPC1yZWFkLmNzdigiRnV0dXJlRnVuZHJhaXNpbmcuY3N2IikKc2V0LnNlZWQoMTIzNDUpCmZ1bmRyYXNpbmc8LWZ1bmRyYXNpbmdbYygtMSwtMiwtMjQpXQpmdW5kcmFzaW5nJFRBUkdFVF9CPC1hcy5mYWN0b3IoZnVuZHJhc2luZyRUQVJHRVRfQikKd2l0aChmdW5kcmFzaW5nLHRhYmxlKGZ1bmRyYXNpbmckVEFSR0VUX0IpKQpgYGAKIyMgSGVyZSB3ZSBjYW4gbm90aWNlIHRoYXQgcmVjb3JkcyBvZiBUQVJHRVRfQj0wIGFuZCBUQVJHRVRfQj0xIGFyZSB0aGUgc2FtZSBudW1iZXIuIFRoZXJlIGFyZSBib3RoIDE1NjAgcmVjb3Jkcy4KIyMgRmlyc3RseSwgU2VsZWN0IExvZ2lzdGljIE1vZGVsIGFzIDFzdCBjbGFzc2lmaWNhdGlvbiBtb2RlbCBoZXJlCmBgYHtyfQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KGFvZCkKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGNhcikKbGlicmFyeShIbWlzYykKbGlicmFyeShybXMpCmxpYnJhcnkoUmVzb3VyY2VTZWxlY3Rpb24pCmxpYnJhcnkoUXVhbnRQc3ljKQpsaWJyYXJ5KEluZm9ybWF0aW9uVmFsdWUpCmxpYnJhcnkocmVhZHhsKQpsaWJyYXJ5KGNvcnJwbG90KQpgYGAKYGBge3J9CnJlczwtcmNvcnIoYXMubWF0cml4KGZ1bmRyYXNpbmcpKQpjb3JycGxvdChyZXMkUCwgdHlwZSA9ICJ1cHBlciIsIG9yZGVyID0gImhjbHVzdCIsIHRsLmNvbCA9ICJibGFjayIsIHRsLnNydCA9IDQ1KQpgYGAKI0NvbnNpZGVyIHJjb3JyIG9mIGVhY2ggaW5wdXQgdmFyaWFibGVzIHRvIFRBUkdFVF9CLCBQLXZhbHVlIGZvciBXRUFMVEggYW5kIElDMTUgdG9vIGxhcmdlLCBkcm9wIHRoZXNlIHR3byB2YXJpYWJsZXMKCiNJbml0aWFsIG1vZGVsOkxvZ2lzdGljIFJlZ3Jlc3Npb24gZm9yIE1vZGVsIHcvTm8gUHJlZGljdG9ycyMKYGBge3IgbG9naXN0aWMgbW9kZWx9CmxvZ2l0MDwtZ2xtKFRBUkdFVF9CfjEsZGF0YT1mdW5kcmFzaW5nLGZhbWlseT0nYmlub21pYWwnKQpzdW1tYXJ5KGxvZ2l0MCkKd2FsZC50ZXN0KGIgPSBjb2VmKGxvZ2l0MCksIFNpZ21hID0gdmNvdihsb2dpdDApLCBUZXJtcyA9IDEpCkNsYXNzTG9nKGxvZ2l0MCwgZnVuZHJhc2luZyRUQVJHRVRfQiwgY3V0ID0gLjUpCmBgYAojc3BsaXQgdHJhaW4oNjAlKSBhbmQgdmFsaWQoNDAlKSBkYXRhc2V0CmBgYHtyfQp0cmFpbi5pbmRleD1zYW1wbGUoMTpucm93KGZ1bmRyYXNpbmcpLGRpbShmdW5kcmFzaW5nKVsxXSowLjYpCnRyYWluPWZ1bmRyYXNpbmdbdHJhaW4uaW5kZXgsXQp2YWxpZD1mdW5kcmFzaW5nWy10cmFpbi5pbmRleCxdCmBgYAojI3J1biBhIGxvZ2lzdGljIGZ1bGwgbW9kZWwgd2l0aG91dCBXQUxUSCBhbmQgSUMxNQpgYGB7cn0KbG9naXRmdWxsPC1nbG0oVEFSR0VUX0J+emlwY29udmVydF8yK3ppcGNvbnZlcnRfMyt6aXBjb252ZXJ0XzQremlwY29udmVydF81K2hvbWVvd25lci5kdW1teStOVU1DSExECiAgICAgICAgICAgICAgICtJTkNPTUUrZ2VuZGVyLmR1bW15K0hWK0ljbWVkK0ljYXZnK05VTVBST00rUkFNTlRBTEwrTUFYUkFNTlQrTEFTVEdJRlQrdG90YWxtb250aHMrCiAgICAgICAgICAgICAgICAgVElNRUxBRytBVkdHSUZULGRhdGE9dHJhaW4sIGZhbWlseSA9ICJiaW5vbWlhbCIpCnN1bW1hcnkobG9naXRmdWxsKQpgYGAKIyMgZnJvbSBQLXZhbHVlIG9mIGFsbCBpbnB1dCB2YXJpYWJsZXMsIHdlIHNoYWxsIGRyb3AgWklQIGZvciBhbGwgb2YgdGhlc2UgZHVtbXkgdmFyaWFibGVzIGlzIG5vdCBzaWduaWZpY2FudCBhbmQgaG9tbWVvd25lci5kdW1teSBhbmQgUkFNTlRBTEwsTUFYUkFNTlQgd2lsbCBhbHNvIGJlIGRyb3BwZWQgZm9yIHRoZXNlIFAtdmFsdWUgbmVhcmx5IDAuNQoKYGBge3J9CmxvZ2l0MTwtZ2xtKFRBUkdFVF9Cfk5VTUNITEQrSU5DT01FK2dlbmRlci5kdW1teStIVitJY21lZCtJY2F2ZytOVU1QUk9NK0xBU1RHSUZUK3RvdGFsbW9udGhzKwogICAgICAgICAgICAgICAgIFRJTUVMQUcrQVZHR0lGVCxkYXRhPXRyYWluLCBmYW1pbHkgPSAiYmlub21pYWwiKQpzdW1tYXJ5KGxvZ2l0MSkKYGBgCiMjI2Ryb3AgdGhlIGlucHV0IHZhcmlhYmxlczpOVU1DSExELCBJY21lZCwgSWNhdmcsVElNRUxBRywgQVZHR0lGVCB3aGljaCBhYm92ZSBzaWduaWZpY2FuY2UgMC4xCmBgYHtyfQpsb2dpdDI8LWdsbShUQVJHRVRfQn5JTkNPTUUrZ2VuZGVyLmR1bW15K0hWK05VTVBST00rTEFTVEdJRlQrdG90YWxtb250aHMsZGF0YT10cmFpbiwgZmFtaWx5ID0gImJpbm9taWFsIikKc3VtbWFyeShsb2dpdDIpCmBgYAojIyNkcm9wIHRoZSBpbnB1dCB2YXJpYWJsZXM6SFYgd2hpY2ggYWJvdmUgc2lnbmlmaWNhbmNlIDAuMDUKYGBge3J9CmxvZ2l0MzwtZ2xtKFRBUkdFVF9CfklOQ09NRStnZW5kZXIuZHVtbXkrTlVNUFJPTStMQVNUR0lGVCt0b3RhbG1vbnRocyxkYXRhPXRyYWluLCBmYW1pbHkgPSAiYmlub21pYWwiKQpzdW1tYXJ5KGxvZ2l0MykKYGBgCiMjIGRyb3AgZ2VuZGVyLmR1bW15IHdoaWNoIHNpZ25pZmljYW5jZSBpcyBhYm92ZSAwLjA1CmBgYHtyfQpsb2dpdDQ8LWdsbShUQVJHRVRfQn5JTkNPTUUrTlVNUFJPTStMQVNUR0lGVCt0b3RhbG1vbnRocyxkYXRhPXRyYWluLCBmYW1pbHkgPSAiYmlub21pYWwiKQpzdW1tYXJ5KGxvZ2l0NCkKYW5vdmEobG9naXQ0LHRlc3QgPSAiQ2hpc3EiKQpgYGAKIyNMb2dpdDQgbW9kZWwgY2FuIGJlIG91ciBmaW5hbCBvcHRpbWFsIGxvZ2lzdCBtb2RlbCB0aGF0IGFsbCBpbnB1dCB2YXJpYWJsZXMgYXJlIHNpZ25pZmljYW50LiBUaGVuIGNhcnJ5IG91dCBXYWxkIHRlc3QgZm9yIGJlc3QgdmVyc2lvbgpgYGB7cn0Kd3Q8LW1hdHJpeChucm93PWxlbmd0aChjb2VmKGxvZ2l0NCkpLG5jb2w9MykKZm9yKCBpIGluIGMoMTpsZW5ndGgoY29lZihsb2dpdDQpKSkgKXsKICB3dHRlbXA8LXdhbGQudGVzdChiID0gY29lZihsb2dpdDQpLCBTaWdtYSA9IHZjb3YobG9naXQ0KSwgVGVybXMgPWkpCiAgd3RbaSwyXTwtd3R0ZW1wJHJlc3VsdCRjaGkyWzNdCiAgd3RbaSwzXTwtd3R0ZW1wJHJlc3VsdCRjaGkyWzFdCiAgd3RbaSwxXTwtbmFtZXMobG9naXQ0JGNvZWZmaWNpZW50c1tpXSkKfQp3dCAKYGBgCiMjIyBTaW5jZSBsb2dpdDQgbW9kZWwgaXMgb3VyIGZpbmFsIG9wdGltYWwgbG9naXN0aWMgbW9kZWwsIHdlIHNoYWxsIHVzZSB2YWxpZCBkYXRhc2V0IHRvIGRvIHByZWRpY3Rpb24gYW5kIGNvbXBhcmVkIHdpdGggYWN0dWFsIHJlc3BvbnNlCmBgYHtyfQpwcm9iPC1wcmVkaWN0KGxvZ2l0NCx2YWxpZCx0eXBlPSdyZXNwb25zZScpCnByZWRfbG9naXQ8LWlmZWxzZShwcm9iPj0wLjUsMSwwKQp0YWJsZSh2YWxpZCRUQVJHRVRfQixwcmVkX2xvZ2l0KQpgYGAKCiMjR2V0IGFuIG9wdGltYWwgQ3V0b2ZmIFBvaW50IERyb3cgYW5kIERyYXcgUk9DIHBsb3QuCmBgYHtyfQpwPC1wbG9naXMocHJlZGljdChsb2dpdDQsdHJhaW4pKQpvcHRjdXRvZmY8LW9wdGltYWxDdXRvZmYodHJhaW4kVEFSR0VUX0IscClbMV0Kb3B0Y3V0b2ZmCkNsYXNzTG9nKGxvZ2l0NCwgdHJhaW4kVEFSR0VUX0IsIGN1dCA9b3B0Y3V0b2ZmKQpwbG90Uk9DKHRyYWluJFRBUkdFVF9CLHApCmBgYAojI0FzIHdlIGNhbiBzZWUgdGhlIG9wdGltYWwgY3V0b2ZmIHBvaW50IGlzICAwLjQ5NDkxMiBhbmQgQVVST0M9MC41ODc5CgojIyMgU2Vjb25kIG1vZGVsOiBBZGFCb29zdCAoYWRhcHRpdmUgYm9vc3QpIGFsZ29yaXRobSAKYGBge3J9CmxpYnJhcnkoYWRhYmFnKQptb2RlbCA8LSBib29zdGluZyhUQVJHRVRfQn4uLCBkYXRhID10cmFpbixtZmluYWwgPSAyMCxib29zID0gVFJVRSkKYGBgCiMjYm9vcz1UUlVFIG1lYW5zIGEgYm9vdHN0cmFwIHNhbXBsZSBvZiB0aGUgdHJhaW5pbmcgc2V0IGlzIGRyYXduIHVzaW5nIHRoZSB3ZWlnaHRzIGZvciBlYWNoIG9ic2VydmF0aW9uIG9uIHRoYXQgaXRlcmF0aW9uLiAKbWZpbmFsPTEwMCBtZWFucyB0aGUgbW9kZWwgd2lsbCBydW4gMTAwIGl0ZXJhdGlvbnMKYGBge3J9CmJvb3N0X3ByZWQ9cHJlZGljdChtb2RlbCx2YWxpZCkkY2xhc3MKdGFibGUoYm9vc3RfcHJlZCx2YWxpZCRUQVJHRVRfQikKbGlicmFyeShjYXJldCkKY2FyZXQ6OmNvbmZ1c2lvbk1hdHJpeChmYWN0b3IoYm9vc3RfcHJlZCksdmFsaWQkVEFSR0VUX0IpCmBgYAojIyBUaGUgcHJlZGljdCBhY2N1cmFjeSBpbiBWYWxpZCBkYXRhc2V0IGlzIDAuNTMyOQoKIyNQYXJ0MiBDYWxjdWxhdGUgTmV0IFByb2ZpdAojI2ZpcnN0IGNyZWF0ZSBhIGRhdGFmcmFtZSBpbiBhZHZhbmNlIHRvIHNhdmUgdGhlIG5ldCBwcm9maXQgbGF0ZXIuCmBgYHtyfQpOZXRfcHJvZml0PWRhdGEuZnJhbWUoJ01vZGVsJz1jKCdMb2dpc3RpYyBNb2RlbCcsJ0FkYUJvb3N0JyksJ05ldC5Qcm9maXQudHJhaW4nPU5BLCdOZXQuUHJvZml0LnZhbGlkJz1OQSkKYGBgCiMjIFRoZW4gd2UgY2FsY3VsYXRlIHRoZSBuZXQgcHJvZml0LCBhbmQgd2UgbmVlZCB0byB1bmRvIHRoZSB3ZWlnaHRlZCBzYW1wbGluZyAKYGBge3J9CnRyYWluX3ByZWQ8LXByZWRpY3QobW9kZWwsdHJhaW4pJGNsYXNzCng8LWFzLm51bWVyaWModHJhaW5fcHJlZCkKTmV0X3Byb2ZpdFsyLDJdPC1zdW0oeCkqKDEzLTAuNjgpLzkuOC0obGVuZ3RoKHgpLXN1bSh4KSkqMC42OC8wLjUzCgp2YWxpZF9wcmVkPC1wcmVkaWN0KG1vZGVsLHZhbGlkKSRjbGFzcwp4PC1hcy5udW1lcmljKHZhbGlkX3ByZWQpCk5ldF9wcm9maXRbMiwzXTwtc3VtKHgpKigxMy0wLjY4KS85LjgtKGxlbmd0aCh4KS1zdW0oeCkpKjAuNjgvMC41MwoKdHJhaW5fcHJlZDI8LWFzLm1hdHJpeChwcmVkaWN0KGxvZ2l0NCx0cmFpbix0eXBlPSdyZXNwb25zZScpKQp4PC1pZmVsc2UodHJhaW5fcHJlZDJbLDFdPm9wdGN1dG9mZiwxLDApCk5ldF9wcm9maXRbMSwyXTwtc3VtKHgpKigxMy0wLjY4KS85LjgtKGxlbmd0aCh4KS1zdW0oeCkpKjAuNjgvMC41MwoKdmFsaWRfcHJlZDI8LWFzLm1hdHJpeChwcmVkaWN0KGxvZ2l0NCx2YWxpZCx0eXBlPSdyZXNwb25zZScpKQp4PC1pZmVsc2UodmFsaWRfcHJlZDJbLDFdPm9wdGN1dG9mZiwxLDApCk5ldF9wcm9maXRbMSwzXTwtc3VtKHgpKigxMy0wLjY4KS85LjgtKGxlbmd0aCh4KS1zdW0oeCkpKjAuNjgvMC41MwpOZXRfcHJvZml0CmBgYAojIyNEcmF3IE5ldCBQcm9maXQgbGlmdCBjdXJ2ZXMKYGBge3J9CmRvbmFyIDwtICgxMyAtIDAuNjgpLzkuOApub24uZG9uYXIgPC0gKC0wLjY4KS8wLjUzCiMjIGZvciBMb2dpc3RpYyBSZWdyZXNzaW9uCmxpZnQudGFibGU8LWRhdGEuZnJhbWUoY2JpbmQoYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIodmFsaWQkVEFSR0VUX0IpKSx2YWxpZF9wcmVkMikpCm5hbWVzKGxpZnQudGFibGUpPWMoJ1RBUkdFVF9CJywncHJlZCcpCmxpZnQudGFibGU9bGlmdC50YWJsZVtvcmRlcigtbGlmdC50YWJsZSRwcmVkKSxdCmxpZnQudGFibGU9bGlmdC50YWJsZSU+JW11dGF0ZShuZXQucHJvZml0PWlmZWxzZShhcy5udW1lcmljKFRBUkdFVF9CKT09MSxkb25hcixub24uZG9uYXIpKQpsaWZ0LnRhYmxlJEN1bS5uZXQucHJvZml0PWxpZnQudGFibGUkbmV0LnByb2ZpdApmb3IgKGkgaW4gMjpucm93KGxpZnQudGFibGUpKXsKICBsaWZ0LnRhYmxlW2ksNF08LWxpZnQudGFibGVbaS0xLDRdK2xpZnQudGFibGVbaSwzXQp9CmcxPWdncGxvdChsaWZ0LnRhYmxlLGFlcyh5PUN1bS5uZXQucHJvZml0LHg9MTpucm93KGxpZnQudGFibGUpLGNvbG9yPSdyZWQnKSkrZ2VvbV9saW5lKCkKCnZhbGlkX3ByZWRfcDwtcHJlZGljdChtb2RlbCx2YWxpZCkkcHJvYgpsaWZ0LnRhYmxlMj1kYXRhLmZyYW1lKGNiaW5kKGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKHZhbGlkJFRBUkdFVF9CKSksdmFsaWRfcHJlZF9wWywyXSkpCm5hbWVzKGxpZnQudGFibGUyKT1jKCdUQVJHRVRfQicsJ3ByZWQnKQpsaWZ0LnRhYmxlMj1saWZ0LnRhYmxlMltvcmRlcigtbGlmdC50YWJsZTIkcHJlZCksXQpsaWZ0LnRhYmxlMj1saWZ0LnRhYmxlMiU+JW11dGF0ZShuZXQucHJvZml0PWlmZWxzZShhcy5udW1lcmljKFRBUkdFVF9CPT0xKSxkb25hcixub24uZG9uYXIpKQpsaWZ0LnRhYmxlMiRDdW0ubmV0LnByb2ZpdD1saWZ0LnRhYmxlMiRuZXQucHJvZml0CmZvciAoaSBpbiAyOm5yb3cobGlmdC50YWJsZTIpKXsKICBsaWZ0LnRhYmxlMltpLDRdPC1saWZ0LnRhYmxlMltpLTEsNF0rbGlmdC50YWJsZTJbaSwzXQp9CmcyPWdncGxvdChsaWZ0LnRhYmxlMixhZXMoeT1DdW0ubmV0LnByb2ZpdCx4PTE6bnJvdyhsaWZ0LnRhYmxlMiksY29sb3I9J3JlZCcpKStnZW9tX2xpbmUoKQpsaWZ0PWRhdGEuZnJhbWUoJ2xvZ2lzdGljIE1vZGVsJz1saWZ0LnRhYmxlJEN1bS5uZXQucHJvZml0LCdBZGFCb29zdCc9bGlmdC50YWJsZTIkQ3VtLm5ldC5wcm9maXQpCmxpZnQkcm93PWMoMTpucm93KGxpZnQpKQpnPWdncGxvdCgpICsgCiAgZ2VvbV9saW5lKGRhdGEgPSBsaWZ0LnRhYmxlLCBhZXMoeCA9IDE6bnJvdyhsaWZ0LnRhYmxlKSwgeSA9IEN1bS5uZXQucHJvZml0KSwgY29sb3IgPSAiYmx1ZSIpICsKICBnZW9tX2xpbmUoZGF0YSA9IGxpZnQudGFibGUyLCBhZXMoeCA9IDE6bnJvdyhsaWZ0LnRhYmxlMiksIHkgPSBDdW0ubmV0LnByb2ZpdCksIGNvbG9yID0gInJlZCIpICsKICB4bGFiKCdyb3cnKSArCiAgeWxhYignQ3VtLm5ldC5wcm9maXQnKStnZ3RpdGxlKCJOZXQgUHJvZml0IGxpZnQgQ3VydmU6IEJsdWUtTG9naXN0aWMsIFJlZC1BZGFib29zdCIpCmcKbGlmdC5hY3R1YWw9ZGF0YS5mcmFtZShhcy5udW1lcmljKGFzLmNoYXJhY3Rlcih2YWxpZCRUQVJHRVRfQikpKQpuYW1lcyhsaWZ0LmFjdHVhbCk8LWMoJ1RBUkdFVF9CJykKbGlmdC5hY3R1YWw9bGlmdC5hY3R1YWwlPiUKICBtdXRhdGUobmV0LnByb2ZpdD1pZmVsc2UoVEFSR0VUX0I9PTEsZG9uYXIsbm9uLmRvbmFyKSkKbGlmdC5hY3R1YWw9bGlmdC5hY3R1YWxbb3JkZXIoLWxpZnQuYWN0dWFsJFRBUkdFVF9CKSxdCmxpZnQuYWN0dWFsJEN1bS5uZXQucHJvZml0PWxpZnQuYWN0dWFsJG5ldC5wcm9maXQKZm9yIChpIGluIDI6bnJvdyhsaWZ0LmFjdHVhbCkpewogIGxpZnQuYWN0dWFsW2ksM108LWxpZnQuYWN0dWFsW2ktMSwzXStsaWZ0LmFjdHVhbFtpLDJdCn0KZ19hY3R1YWw9Z2dwbG90KGxpZnQuYWN0dWFsLGFlcyh4PTE6bnJvdyhsaWZ0LmFjdHVhbCkseT1DdW0ubmV0LnByb2ZpdCksY29sb3I9J2dyZWVuJykrZ2VvbV9saW5lKCkreGxhYigncm93JykgK3lsYWIoJ0N1bS5uZXQucHJvZml0JykrZ2d0aXRsZSgiQWN0dWFsIE5ldCBQcm9maXQgbGlmdCBDdXJ2ZSIpCmxpYnJhcnkoZ2dwdWJyKQpnZ2FycmFuZ2UoZyxnX2FjdHVhbCxuY29sPTEsbnJvdyA9IDIpCmBgYAojI0Zyb20gTGlmdCBjdXJ2ZSwgSSBwcmVmZXIgdG8gc2VsZWN0IGxvZ2lzdGljIG1vZGVsIGFzIG91ciBiZXN0IG1vZGVsLiBUaGUgcmVhc29ucyBhcmUgc2hvd2VkIGJlbG93OgogIDEuU3VwcG9zaW5nIGFsbCB0aGUgcHJlZGljdG9ycyBhcmUgY29ycmVjdCB0aGVuIHdlIHNvcnRlZCB0aGUgcHJvYmFiaWxpdGllcyBpbiBhbiBkZWNyZWFzaW5nIG9yZGVyLCB0aGUgbmV0IHByb2ZpdCBsaWZ0IGN1cnZlcyBzaG91bGQgZmlyc3RsdCBpbmNyZWFzZSBzbW9vdGhseSB0byB0aGUgdG9wIHBlZWsgcG9pbnQgYW5kIHRoZW4gZGVjZWFzZSB0byB0aGUgZW5kIGFzIHNob3dlZCBpbiBhYm92ZS4gQ29uc2lkZXJpbmcgdHdvIGNhc2VzLCB0aGUgYmx1ZSBvbmUgZml0cyB0aGUgYWN0dWFsIGxpZnQgY3VydmUgY2hhcnQgd2VsbC4gVGhhdCdzIHdoeSBsb2dpc3RpY2FsIG1vZGVsIGZpdHMgd2VsbAogIDIuIFRoZSBwcmVkaWN0aW5nIGFjY3VyYWN5IGluIHZhbGlkIGRhdGFzZXRzIG9mIGxvZ2lzdGljIG1vZGVsIGlzIGJldHRlci4KYGBge3J9CmZ1dHVyZTwtZnV0dXJlWywtYygyKV0KcHJlZF9mdXR1cmU9YXMubWF0cml4KHByZWRpY3QobG9naXQ0LGZ1dHVyZSx0eXBlPSdyZXNwb25zZScpKQpmdXR1cmUkcHJlZD1wcmVkX2Z1dHVyZQpmdXR1cmU9ZnV0dXJlICU+JSBtdXRhdGUoVEFSR0VUX0I9aWZlbHNlKHByZWQ+b3B0Y3V0b2ZmLDEsMCkpCmZ1dHVyZT1mdXR1cmVbb3JkZXIoLWZ1dHVyZSRwcmVkKSxdCmhlYWQoZnV0dXJlWyxjKDIsMjIsMjQpXSwxMCkKdGFibGUoZnV0dXJlJFRBUkdFVF9CKQpgYGAKVGhlcmVmb3JlLCA5OTcgbWFpbHMgY2FuIGJlIGRvbm9ycy4=