# 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=