'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)
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")
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=