# Core ecosystem
library(tidyverse)
library(ISLR2) # data set
library(caret) # Unified interface for training and evaluation
library(DT) # fancy tables
library(modelr) # for model_matrix
# Tree-based model packages
library(rpart) # Single decision trees
library(rpart.plot) # Visualizing CART structures
library(randomForest)# Bagging & Random Forests
library(xgboost) # Gradient boosted decision trees
# Imbalanced data resampling
library(themis) # Tidymodels / recipe SMOTE methods
library(recipes) # Preprocessing pipelines
Load Data
# Read training dataset and future prediction dataset
train_raw<-read_csv("C:/Users/email/Documents/UTSA/STA 6543 Pred Mod/fundraising.csv")
future_raw <- read_csv("C:/Users/email/Documents/UTSA/STA 6543 Pred Mod/future_fundraising.csv")
# Clean target factor levels to be valid R variable names
y_train <- factor(
train_raw$target,
levels = c("No Donor", "Donor"),
labels = c("No_Donor", "Donor") # <--- Fixed space here for nb
)
# Separate predictors
train_x_raw <- train_raw %>% select(-target)
future_x_raw <- future_raw
Preprocessing & Dummy Encoding via model_matrix
# A. Apply model_matrix to convert factor/categorical predictors into dummy variables
# We use ~ . - 1 to omit the intercept column from model_matrix
mm_formula <- formula(~ . - 1)
# Generate design matrix for training set
X_train_mm <- model_matrix(mm_formula, data = train_x_raw) %>% as.data.frame()
# Generate design matrix for future scoring set
X_future_mm <- model_matrix(mm_formula, data = future_x_raw) %>% as.data.frame()
# Align columns to ensure future set matches training set design matrix exactly
X_future_mm <- X_future_mm %>% select(all_of(colnames(X_train_mm)))
# B. Caret Preprocessing Pipeline: Zero-Variance Removal, Median Imputation, Centering & Scaling
preproc_plan <- preProcess(
X_train_mm,
method = c("nzv", "medianImpute", "center", "scale")
)
# Transform predictors using preproc_plan
X_train_proc <- predict(preproc_plan, X_train_mm)
X_future_proc <- predict(preproc_plan, X_future_mm)
# Combine processed predictors with target for Caret formula modeling
train_processed <- cbind(X_train_proc, target = y_train)
Train Control with Cross-Validation & Class Probabilities
set.seed(123)
ctrl <- trainControl(
method = "cv",
number = 10,
classProbs = TRUE,
summaryFunction = twoClassSummary,
savePredictions = "final"
)
Model Training (Naive Bayes, Random Forest, XGBoost)
# A. Naive Bayes Model
set.seed(123)
fit_nb <- train(
target ~ .,
data = train_processed,
method = "nb",
trControl = ctrl,
metric = "ROC"
)
# B. Random Forest Model
set.seed(123)
fit_rf <- train(
target ~ .,
data = train_processed,
method = "rf",
trControl = ctrl,
metric = "ROC",
tuneLength = 10
)
# C. XGBoost (Gradient Boosted Trees) Model
set.seed(123)
# Define an explicit tuning grid for gbm
gbm_grid <- expand.grid(
interaction.depth = c(1, 3, 5),
n.trees = (1:3) * 50,
shrinkage = 0.1,
n.minobsinnode = 10
)
set.seed(123)
fit_xgb <- train(
target ~ .,
data = train_processed,
method = "gbm",
trControl = ctrl,
metric = "ROC",
tuneGrid = gbm_grid, # <--- Using an explicit grid avoids passing dots
verbose = FALSE
)
Model Comparison
resamples_list <- resamples(list(
NaiveBayes = fit_nb,
RandomForest = fit_rf,
XGBoost = fit_xgb
))
# Extract and average all CV metrics across folds automatically
model_metrics <- resamples_list$values %>%
pivot_longer(-Resample, names_to = "Metric", values_to = "Value") %>%
separate(Metric, into = c("Model", "Metric"), sep = "~") %>%
group_by(Model, Metric) %>%
summarize(Mean_Value = mean(Value, na.rm = TRUE), .groups = "drop") %>%
pivot_wider(names_from = Metric, values_from = Mean_Value) %>%
select(Model, ROC, Sens, Spec)
# Format into DT Datatable
datatable(
model_metrics,
colnames = c("Model Algorithm", "CV ROC AUC", "Sensitivity (Donor)", "Specificity"),
options = list(dom = 't', ordering = TRUE),
rownames = FALSE
) %>%
formatRound(columns = c("ROC", "Sens", "Spec"), digits = 3)
NA
# raw resamples output
summary(resamples_list)
Call:
summary.resamples(object = resamples_list)
Models: NaiveBayes, RandomForest, XGBoost
Number of resamples: 10
ROC
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
NaiveBayes 0.5142667 0.5547333 0.5676444 0.5665455 0.5850737 0.6224000 0
RandomForest 0.5080222 0.5376667 0.5582408 0.5560535 0.5757176 0.6013778 0
XGBoost 0.5160222 0.5508722 0.5792444 0.5785444 0.5983469 0.6687111 0
Sens
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
NaiveBayes 0.3866667 0.4216667 0.4533333 0.4543267 0.4966667 0.5266667 0
RandomForest 0.4666667 0.5083333 0.5333333 0.5336115 0.5533333 0.6000000 0
XGBoost 0.4666667 0.5316667 0.5600000 0.5609051 0.5733333 0.6466667 0
Spec
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
NaiveBayes 0.5600000 0.6116667 0.6433333 0.6397852 0.6750559 0.7000000 0
RandomForest 0.5000000 0.5216667 0.5300000 0.5323758 0.5483333 0.5637584 0
XGBoost 0.4066667 0.5250000 0.5485235 0.5297047 0.5600000 0.5733333 0
# Plot it
bwplot(resamples_list, metric = "ROC")

Cut-Off & Profit Optimization Analysis
# Extract out-of-fold cross-validation probability predictions from winning model (XGBoost)
xgb_preds <- fit_xgb$pred |> filter(obs != "")
# Financial Inputs
cost_per_mail <- 0.68
avg_donation <- 13.00
net_donation <- avg_donation - cost_per_mail
# Profit evaluation across candidate probability cutoffs
thresholds <- seq(0.1, 0.9, by = 0.01)
profit_results <- map_df(thresholds, function(cutoff) {
predicted_class <- ifelse(xgb_preds$Donor >= cutoff, "Donor", "No Donor")
# Confusion Matrix components
tp <- sum(predicted_class == "Donor" & xgb_preds$obs == "Donor")
fp <- sum(predicted_class == "Donor" & xgb_preds$obs == "No Donor")
# Total net profit = (True Donors * Net Donation) - (False Donors * Mailing Cost)
total_profit <- (tp * net_donation) - (fp * cost_per_mail)
data.frame(
Cutoff = cutoff,
Mailed_Count = tp + fp,
True_Donors = tp,
False_Donors = fp,
Total_Profit = total_profit
)
})
optimal_row <- profit_results %>%
filter(Total_Profit == max(Total_Profit)) %>%
filter(Cutoff == max(Cutoff)) #choose highest cutoff if profit ties exist
print(paste("Optimal Cutoff:", optimal_row$Cutoff))
[1] "Optimal Cutoff: 0.32"
print(paste("Max Profit:", round(optimal_row$Total_Profit, 2)))
[1] "Max Profit: 18467.68"
Score Future Fundraising Dataset & Write CSV Output
# Predict probabilities on future_fundraising.csv
future_probs <- predict(fit_xgb, newdata = X_future_proc, type = "prob")
# Apply optimal cutoff for decision recommendation
optimal_cutoff <- optimal_row$Cutoff[1]
future_results <- future_probs |>
mutate(
value = ifelse(future_probs$Donor >= optimal_cutoff, "Donor", "No Donor")
) |>
select(value)
# Output predictions to CSV file using write_csv
write_csv(future_results, "future_fundraising_predictions.csv")
cat("Successfully exported predictions to future_fundraising_predictions.csv\n")
Successfully exported predictions to future_fundraising_predictions.csv
LS0tDQp0aXRsZTogIkNvbXBhcmluZyBtb2RlbHIgYW5kIENBUkVUIg0Kb3V0cHV0OiANCiAgaHRtbF9ub3RlYm9vazoNCiAgICB0b2M6IHRydWUNCiAgICB0b2NfZmxvYXQ6IHRydWUNCiAgICB0b2MtZGVwdGg6IDMNCiAgICB0aGVtZTogY29zbW8NCiAgICBoaWdobGlnaHQtc3R5bGU6IHRoaXN0bGUNCi0tLQ0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayAkc2V0KHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFKQ0KYGBgDQoNCg0KYGBge3IgbGlicmFyaWVzfQ0KIyBDb3JlIGVjb3N5c3RlbQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KElTTFIyKSAgICAgICAjIGRhdGEgc2V0DQpsaWJyYXJ5KGNhcmV0KSAgICAgICAjIFVuaWZpZWQgaW50ZXJmYWNlIGZvciB0cmFpbmluZyBhbmQgZXZhbHVhdGlvbg0KbGlicmFyeShEVCkgICAgICAgICAgIyBmYW5jeSB0YWJsZXMNCmxpYnJhcnkobW9kZWxyKSAgICAgICMgZm9yIG1vZGVsX21hdHJpeA0KDQojIFRyZWUtYmFzZWQgbW9kZWwgcGFja2FnZXMNCmxpYnJhcnkocnBhcnQpICAgICAgICMgU2luZ2xlIGRlY2lzaW9uIHRyZWVzDQpsaWJyYXJ5KHJwYXJ0LnBsb3QpICAjIFZpc3VhbGl6aW5nIENBUlQgc3RydWN0dXJlcw0KbGlicmFyeShyYW5kb21Gb3Jlc3QpIyBCYWdnaW5nICYgUmFuZG9tIEZvcmVzdHMNCmxpYnJhcnkoeGdib29zdCkgICAgICMgR3JhZGllbnQgYm9vc3RlZCBkZWNpc2lvbiB0cmVlcw0KDQojIEltYmFsYW5jZWQgZGF0YSByZXNhbXBsaW5nDQpsaWJyYXJ5KHRoZW1pcykgICAgICAjIFRpZHltb2RlbHMgLyByZWNpcGUgU01PVEUgbWV0aG9kcw0KbGlicmFyeShyZWNpcGVzKSAgICAgIyBQcmVwcm9jZXNzaW5nIHBpcGVsaW5lcw0KYGBgDQoNCg0KDQojIExvYWQgRGF0YQ0KYGBge3J9DQojIFJlYWQgdHJhaW5pbmcgZGF0YXNldCBhbmQgZnV0dXJlIHByZWRpY3Rpb24gZGF0YXNldA0KdHJhaW5fcmF3PC1yZWFkX2NzdigiQzovVXNlcnMvZW1haWwvRG9jdW1lbnRzL1VUU0EvU1RBIDY1NDMgUHJlZCBNb2QvZnVuZHJhaXNpbmcuY3N2IikNCmZ1dHVyZV9yYXcgPC0gcmVhZF9jc3YoIkM6L1VzZXJzL2VtYWlsL0RvY3VtZW50cy9VVFNBL1NUQSA2NTQzIFByZWQgTW9kL2Z1dHVyZV9mdW5kcmFpc2luZy5jc3YiKQ0KDQojIENsZWFuIHRhcmdldCBmYWN0b3IgbGV2ZWxzIHRvIGJlIHZhbGlkIFIgdmFyaWFibGUgbmFtZXMNCnlfdHJhaW4gPC0gZmFjdG9yKA0KICB0cmFpbl9yYXckdGFyZ2V0LCANCiAgbGV2ZWxzID0gYygiTm8gRG9ub3IiLCAiRG9ub3IiKSwNCiAgbGFiZWxzID0gYygiTm9fRG9ub3IiLCAiRG9ub3IiKSAjIDwtLS0gRml4ZWQgc3BhY2UgaGVyZSBmb3IgbmINCikNCg0KIyBTZXBhcmF0ZSBwcmVkaWN0b3JzDQp0cmFpbl94X3JhdyA8LSB0cmFpbl9yYXcgJT4lIHNlbGVjdCgtdGFyZ2V0KQ0KZnV0dXJlX3hfcmF3IDwtIGZ1dHVyZV9yYXcNCg0KYGBgDQoNCiMgUHJlcHJvY2Vzc2luZyAmIER1bW15IEVuY29kaW5nIHZpYSBtb2RlbF9tYXRyaXgNCg0KYGBge3J9DQojIEEuIEFwcGx5IG1vZGVsX21hdHJpeCB0byBjb252ZXJ0IGZhY3Rvci9jYXRlZ29yaWNhbCBwcmVkaWN0b3JzIGludG8gZHVtbXkgdmFyaWFibGVzDQojIFdlIHVzZSB+IC4gLSAxIHRvIG9taXQgdGhlIGludGVyY2VwdCBjb2x1bW4gZnJvbSBtb2RlbF9tYXRyaXgNCm1tX2Zvcm11bGEgPC0gZm9ybXVsYSh+IC4gLSAxKQ0KDQojIEdlbmVyYXRlIGRlc2lnbiBtYXRyaXggZm9yIHRyYWluaW5nIHNldA0KWF90cmFpbl9tbSA8LSBtb2RlbF9tYXRyaXgobW1fZm9ybXVsYSwgZGF0YSA9IHRyYWluX3hfcmF3KSAlPiUgYXMuZGF0YS5mcmFtZSgpDQoNCiMgR2VuZXJhdGUgZGVzaWduIG1hdHJpeCBmb3IgZnV0dXJlIHNjb3Jpbmcgc2V0DQpYX2Z1dHVyZV9tbSA8LSBtb2RlbF9tYXRyaXgobW1fZm9ybXVsYSwgZGF0YSA9IGZ1dHVyZV94X3JhdykgJT4lIGFzLmRhdGEuZnJhbWUoKQ0KDQojIEFsaWduIGNvbHVtbnMgdG8gZW5zdXJlIGZ1dHVyZSBzZXQgbWF0Y2hlcyB0cmFpbmluZyBzZXQgZGVzaWduIG1hdHJpeCBleGFjdGx5DQpYX2Z1dHVyZV9tbSA8LSBYX2Z1dHVyZV9tbSAlPiUgc2VsZWN0KGFsbF9vZihjb2xuYW1lcyhYX3RyYWluX21tKSkpDQoNCiMgQi4gQ2FyZXQgUHJlcHJvY2Vzc2luZyBQaXBlbGluZTogWmVyby1WYXJpYW5jZSBSZW1vdmFsLCBNZWRpYW4gSW1wdXRhdGlvbiwgQ2VudGVyaW5nICYgU2NhbGluZw0KcHJlcHJvY19wbGFuIDwtIHByZVByb2Nlc3MoDQogIFhfdHJhaW5fbW0sDQogIG1ldGhvZCA9IGMoIm56diIsICJtZWRpYW5JbXB1dGUiLCAiY2VudGVyIiwgInNjYWxlIikNCikNCg0KIyBUcmFuc2Zvcm0gcHJlZGljdG9ycyB1c2luZyBwcmVwcm9jX3BsYW4NClhfdHJhaW5fcHJvYyA8LSBwcmVkaWN0KHByZXByb2NfcGxhbiwgWF90cmFpbl9tbSkNClhfZnV0dXJlX3Byb2MgPC0gcHJlZGljdChwcmVwcm9jX3BsYW4sIFhfZnV0dXJlX21tKQ0KDQojIENvbWJpbmUgcHJvY2Vzc2VkIHByZWRpY3RvcnMgd2l0aCB0YXJnZXQgZm9yIENhcmV0IGZvcm11bGEgbW9kZWxpbmcNCnRyYWluX3Byb2Nlc3NlZCA8LSBjYmluZChYX3RyYWluX3Byb2MsIHRhcmdldCA9IHlfdHJhaW4pDQoNCmBgYA0KDQoNCiMgVHJhaW4gQ29udHJvbCB3aXRoIENyb3NzLVZhbGlkYXRpb24gJiBDbGFzcyBQcm9iYWJpbGl0aWVzDQpgYGB7cn0NCnNldC5zZWVkKDEyMykNCmN0cmwgPC0gdHJhaW5Db250cm9sKA0KICBtZXRob2QgPSAiY3YiLA0KICBudW1iZXIgPSAxMCwNCiAgY2xhc3NQcm9icyA9IFRSVUUsDQogIHN1bW1hcnlGdW5jdGlvbiA9IHR3b0NsYXNzU3VtbWFyeSwNCiAgc2F2ZVByZWRpY3Rpb25zID0gImZpbmFsIg0KKQ0KYGBgDQoNCiMgTW9kZWwgVHJhaW5pbmcgKE5haXZlIEJheWVzLCBSYW5kb20gRm9yZXN0LCBYR0Jvb3N0KQ0KYGBge3IsIHdhcm5pbmc9U1VQUkVTUyB9DQojIEEuIE5haXZlIEJheWVzIE1vZGVsDQpzZXQuc2VlZCgxMjMpDQpmaXRfbmIgPC0gdHJhaW4oDQogIHRhcmdldCB+IC4sIA0KICBkYXRhID0gdHJhaW5fcHJvY2Vzc2VkLA0KICBtZXRob2QgPSAibmIiLA0KICB0ckNvbnRyb2wgPSBjdHJsLA0KICBtZXRyaWMgPSAiUk9DIg0KKQ0KDQojIEIuIFJhbmRvbSBGb3Jlc3QgTW9kZWwNCnNldC5zZWVkKDEyMykNCmZpdF9yZiA8LSB0cmFpbigNCiAgdGFyZ2V0IH4gLiwgDQogIGRhdGEgPSB0cmFpbl9wcm9jZXNzZWQsDQogIG1ldGhvZCA9ICJyZiIsDQogIHRyQ29udHJvbCA9IGN0cmwsDQogIG1ldHJpYyA9ICJST0MiLA0KICB0dW5lTGVuZ3RoID0gMTANCikNCg0KIyBDLiBYR0Jvb3N0IChHcmFkaWVudCBCb29zdGVkIFRyZWVzKSBNb2RlbA0Kc2V0LnNlZWQoMTIzKQ0KIyBEZWZpbmUgYW4gZXhwbGljaXQgdHVuaW5nIGdyaWQgZm9yIGdibQ0KZ2JtX2dyaWQgPC0gZXhwYW5kLmdyaWQoDQogIGludGVyYWN0aW9uLmRlcHRoID0gYygxLCAzLCA1KSwNCiAgbi50cmVlcyA9ICgxOjMpICogNTAsDQogIHNocmlua2FnZSA9IDAuMSwNCiAgbi5taW5vYnNpbm5vZGUgPSAxMA0KKQ0KDQpzZXQuc2VlZCgxMjMpDQpmaXRfeGdiIDwtIHRyYWluKA0KICB0YXJnZXQgfiAuLCANCiAgZGF0YSA9IHRyYWluX3Byb2Nlc3NlZCwNCiAgbWV0aG9kID0gImdibSIsDQogIHRyQ29udHJvbCA9IGN0cmwsDQogIG1ldHJpYyA9ICJST0MiLA0KICB0dW5lR3JpZCA9IGdibV9ncmlkLCAjIDwtLS0gVXNpbmcgYW4gZXhwbGljaXQgZ3JpZCBhdm9pZHMgcGFzc2luZyBkb3RzDQogIHZlcmJvc2UgPSBGQUxTRQ0KKQ0KYGBgDQoNCiMgTW9kZWwgQ29tcGFyaXNvbg0KYGBge3J9DQpyZXNhbXBsZXNfbGlzdCA8LSByZXNhbXBsZXMobGlzdCgNCiAgTmFpdmVCYXllcyA9IGZpdF9uYiwNCiAgUmFuZG9tRm9yZXN0ID0gZml0X3JmLA0KICBYR0Jvb3N0ID0gZml0X3hnYg0KKSkNCg0KIyBFeHRyYWN0IGFuZCBhdmVyYWdlIGFsbCBDViBtZXRyaWNzIGFjcm9zcyBmb2xkcyBhdXRvbWF0aWNhbGx5DQptb2RlbF9tZXRyaWNzIDwtIHJlc2FtcGxlc19saXN0JHZhbHVlcyAlPiUNCiAgcGl2b3RfbG9uZ2VyKC1SZXNhbXBsZSwgbmFtZXNfdG8gPSAiTWV0cmljIiwgdmFsdWVzX3RvID0gIlZhbHVlIikgJT4lDQogIHNlcGFyYXRlKE1ldHJpYywgaW50byA9IGMoIk1vZGVsIiwgIk1ldHJpYyIpLCBzZXAgPSAifiIpICU+JQ0KICBncm91cF9ieShNb2RlbCwgTWV0cmljKSAlPiUNCiAgc3VtbWFyaXplKE1lYW5fVmFsdWUgPSBtZWFuKFZhbHVlLCBuYS5ybSA9IFRSVUUpLCAuZ3JvdXBzID0gImRyb3AiKSAlPiUNCiAgcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IE1ldHJpYywgdmFsdWVzX2Zyb20gPSBNZWFuX1ZhbHVlKSAlPiUNCiAgc2VsZWN0KE1vZGVsLCBST0MsIFNlbnMsIFNwZWMpDQoNCiMgRm9ybWF0IGludG8gRFQgRGF0YXRhYmxlDQpkYXRhdGFibGUoDQogIG1vZGVsX21ldHJpY3MsDQogIGNvbG5hbWVzID0gYygiTW9kZWwgQWxnb3JpdGhtIiwgIkNWIFJPQyBBVUMiLCAiU2Vuc2l0aXZpdHkgKERvbm9yKSIsICJTcGVjaWZpY2l0eSIpLA0KICBvcHRpb25zID0gbGlzdChkb20gPSAndCcsIG9yZGVyaW5nID0gVFJVRSksDQogIHJvd25hbWVzID0gRkFMU0UNCikgJT4lDQogIGZvcm1hdFJvdW5kKGNvbHVtbnMgPSBjKCJST0MiLCAiU2VucyIsICJTcGVjIiksIGRpZ2l0cyA9IDMpDQoNCmBgYA0KDQpgYGB7cn0NCiMgcmF3IHJlc2FtcGxlcyBvdXRwdXQNCnN1bW1hcnkocmVzYW1wbGVzX2xpc3QpDQpgYGANCmBgYHtyfQ0KIyBQbG90IGl0DQpid3Bsb3QocmVzYW1wbGVzX2xpc3QsIG1ldHJpYyA9ICJST0MiKQ0KYGBgDQoNCiMgQ3V0LU9mZiAmIFByb2ZpdCBPcHRpbWl6YXRpb24gQW5hbHlzaXMNCmBgYHtyfQ0KIyBFeHRyYWN0IG91dC1vZi1mb2xkIGNyb3NzLXZhbGlkYXRpb24gcHJvYmFiaWxpdHkgcHJlZGljdGlvbnMgZnJvbSB3aW5uaW5nIG1vZGVsIChYR0Jvb3N0KQ0KeGdiX3ByZWRzIDwtIGZpdF94Z2IkcHJlZCB8PiBmaWx0ZXIob2JzICE9ICIiKQ0KDQojIEZpbmFuY2lhbCBJbnB1dHMNCmNvc3RfcGVyX21haWwgPC0gMC42OA0KYXZnX2RvbmF0aW9uIDwtIDEzLjAwDQpuZXRfZG9uYXRpb24gPC0gYXZnX2RvbmF0aW9uIC0gY29zdF9wZXJfbWFpbA0KDQojIFByb2ZpdCBldmFsdWF0aW9uIGFjcm9zcyBjYW5kaWRhdGUgcHJvYmFiaWxpdHkgY3V0b2Zmcw0KdGhyZXNob2xkcyA8LSBzZXEoMC4xLCAwLjksIGJ5ID0gMC4wMSkNCnByb2ZpdF9yZXN1bHRzIDwtIG1hcF9kZih0aHJlc2hvbGRzLCBmdW5jdGlvbihjdXRvZmYpIHsNCiAgcHJlZGljdGVkX2NsYXNzIDwtIGlmZWxzZSh4Z2JfcHJlZHMkRG9ub3IgPj0gY3V0b2ZmLCAiRG9ub3IiLCAiTm8gRG9ub3IiKQ0KICANCiAgIyBDb25mdXNpb24gTWF0cml4IGNvbXBvbmVudHMNCiAgdHAgPC0gc3VtKHByZWRpY3RlZF9jbGFzcyA9PSAiRG9ub3IiICYgeGdiX3ByZWRzJG9icyA9PSAiRG9ub3IiKQ0KICBmcCA8LSBzdW0ocHJlZGljdGVkX2NsYXNzID09ICJEb25vciIgJiB4Z2JfcHJlZHMkb2JzID09ICJObyBEb25vciIpDQogIA0KICAjIFRvdGFsIG5ldCBwcm9maXQgPSAoVHJ1ZSBEb25vcnMgKiBOZXQgRG9uYXRpb24pIC0gKEZhbHNlIERvbm9ycyAqIE1haWxpbmcgQ29zdCkNCiAgdG90YWxfcHJvZml0IDwtICh0cCAqIG5ldF9kb25hdGlvbikgLSAoZnAgKiBjb3N0X3Blcl9tYWlsKQ0KICANCiAgZGF0YS5mcmFtZSgNCiAgICBDdXRvZmYgPSBjdXRvZmYsDQogICAgTWFpbGVkX0NvdW50ID0gdHAgKyBmcCwNCiAgICBUcnVlX0Rvbm9ycyA9IHRwLA0KICAgIEZhbHNlX0Rvbm9ycyA9IGZwLA0KICAgIFRvdGFsX1Byb2ZpdCA9IHRvdGFsX3Byb2ZpdA0KICApDQp9KQ0KDQpvcHRpbWFsX3JvdyA8LSBwcm9maXRfcmVzdWx0cyAlPiUgDQogIGZpbHRlcihUb3RhbF9Qcm9maXQgPT0gbWF4KFRvdGFsX1Byb2ZpdCkpICU+JSANCiAgZmlsdGVyKEN1dG9mZiA9PSBtYXgoQ3V0b2ZmKSkgI2Nob29zZSBoaWdoZXN0IGN1dG9mZiBpZiBwcm9maXQgdGllcyBleGlzdA0KcHJpbnQocGFzdGUoIk9wdGltYWwgQ3V0b2ZmOiIsIG9wdGltYWxfcm93JEN1dG9mZikpDQpwcmludChwYXN0ZSgiTWF4IFByb2ZpdDoiLCByb3VuZChvcHRpbWFsX3JvdyRUb3RhbF9Qcm9maXQsIDIpKSkNCmBgYA0KDQojIFNjb3JlIEZ1dHVyZSBGdW5kcmFpc2luZyBEYXRhc2V0ICYgV3JpdGUgQ1NWIE91dHB1dA0KYGBge3J9DQojIFByZWRpY3QgcHJvYmFiaWxpdGllcyBvbiBmdXR1cmVfZnVuZHJhaXNpbmcuY3N2DQpmdXR1cmVfcHJvYnMgPC0gcHJlZGljdChmaXRfeGdiLCBuZXdkYXRhID0gWF9mdXR1cmVfcHJvYywgdHlwZSA9ICJwcm9iIikNCg0KIyBBcHBseSBvcHRpbWFsIGN1dG9mZiBmb3IgZGVjaXNpb24gcmVjb21tZW5kYXRpb24NCm9wdGltYWxfY3V0b2ZmIDwtIG9wdGltYWxfcm93JEN1dG9mZlsxXQ0KDQpmdXR1cmVfcmVzdWx0cyA8LSBmdXR1cmVfcHJvYnMgfD4NCiAgbXV0YXRlKA0KICAgIHZhbHVlID0gaWZlbHNlKGZ1dHVyZV9wcm9icyREb25vciA+PSBvcHRpbWFsX2N1dG9mZiwgIkRvbm9yIiwgIk5vIERvbm9yIikNCiAgKSB8Pg0KICBzZWxlY3QodmFsdWUpDQojIE91dHB1dCBwcmVkaWN0aW9ucyB0byBDU1YgZmlsZSB1c2luZyB3cml0ZV9jc3YNCndyaXRlX2NzdihmdXR1cmVfcmVzdWx0cywgImZ1dHVyZV9mdW5kcmFpc2luZ19wcmVkaWN0aW9ucy5jc3YiKQ0KY2F0KCJTdWNjZXNzZnVsbHkgZXhwb3J0ZWQgcHJlZGljdGlvbnMgdG8gZnV0dXJlX2Z1bmRyYWlzaW5nX3ByZWRpY3Rpb25zLmNzdlxuIikNCmBgYA0KDQo=