We are loading an xgboost (extreme gradient boosting) model - its a different type of tree model. It creates small trees (stumps) that correct for previous trees (compared to random forests, where each gradient-boosted tree is independent)
We will use the full mailing.csv set from your assignment.
load('./mailing_expected_benefit.rda')
mailing_xgb <- xgb.load('./mailing_response_model.xgb')
Individual-level profitability based on predicted probabilities.
Helps determine precisely who to mail.
Expected benefit of targeting somebody - we will look now at the probability of responding given all the data (p(R|x)):
Create \(P(R|\textbf{x})\)
o <- mailing %>%
dplyr::select(all_of(colnames(mailing_xs_train))) %>%
as.matrix()
mailing$prob_response <- predict(mailing_xgb, newdata = o)
# look at highest probability of donating
max(mailing$prob_response)
## [1] 0.228673
How much they donate: v(x)
The value of a response, \(v_R(\textbf{x})\), is equal to
gavr, the average gift size that they do give.
We’ll set the mailing cost to £1
#cost is £1 to post a request (what other costs are involved?)
mailing_cost <- 1
#gavr is average donation
mailing <- mailing %>%
mutate(response_value = gavr - mailing_cost) %>%
mutate(expected_benefit = prob_response * response_value) %>%
arrange(desc(expected_benefit))
#total possible amount of net benefit if everyone is targeted.
# Positive total indicates the campaign as a whole is financially beneficial.
sum(mailing$expected_benefit)
## [1] 105189
#how many below 0?
mailing %>% filter(expected_benefit < 0)
## # A tibble: 0 × 21
## # ℹ 21 variables: Income <dbl>, Firstdate <dbl>, Lastdate <dbl>, Amount <dbl>,
## # glast <dbl>, gavr <dbl>, class <fct>, rfaf2_1 <int>, rfaf2_4 <int>,
## # rfaf2_2 <int>, rfaf2_3 <int>, rfaa2_G <int>, rfaa2_E <int>, rfaa2_F <int>,
## # rfaa2_D <int>, pepstrfl_0 <int>, pepstrfl_X <int>, cv_part <int>,
## # prob_response <dbl>, response_value <dbl>, expected_benefit <dbl>
#plot histogram of expected benefit
ggplot(mailing, aes(x = expected_benefit)) +
geom_histogram(bins = 10, fill = "steelblue", color = "white") +
labs(title = "Histogram of Expected Benefit per Mailing",
x = "Expected Benefit (£)",
y = "Number of Individuals") +
theme_minimal()
There’s a net benefit to reaching to everyone.
But what if costs increase? For some it’s no longer worth targetting them.
mailing_cost <- 2
mailing_cost_change <- mailing %>%
mutate(response_value = gavr - mailing_cost) %>%
mutate(expected_benefit = prob_response * response_value) #%>%
# negative benefit count - the number of people it is not profitable to target
sum(mailing_cost_change$expected_benefit < 0)
## [1] 36
#total possible amount of net benefit if we target everyone
sum(mailing_cost_change$expected_benefit)
## [1] 95445.62
We can put all this inside a function (default mailing cost = 1) The effect of mailing costs on our campaign - after around £2.50 costs,
mailing_benefit <- function(mailing_cost = 1) {
o <- mailing %>%
dplyr::mutate(response_value = gavr - mailing_cost) %>% #response value
dplyr::mutate(expected_benefit = prob_response * response_value) %>% #expected beneift
dplyr::mutate(ben = ifelse(expected_benefit < 0, 0, 1)) %>% #binary change point
dplyr::count(ben) %>% #no benefit/benefit counts (count 1s and 0s)
dplyr::mutate(mailing_cost = mailing_cost) #mailing_cost may change as argument in function
return(o)
}
i <- seq(0, 10, length.out = 100) #apply for mailing costs 0-10 (increments up to 100)
d <- lapply(i, mailing_benefit) %>%
bind_rows() %>% # bind into a single dataset
mutate(n = ifelse(ben == 0, -n, n))
ggplot(d, aes(x = mailing_cost, y = n, fill = ben)) +
geom_col()
Let’s say that our profit margin is small: each offer costs £1 to make and market, and each accepted offer earns £18, for a profit of £17. We can produce a profit matrix where if we send a postcard, and they respond we get £17, and if we send a post-card and they fail to respond we lose the £1 we invested. And if we send nothing, we get nothing and lose nothing.
False positive - we thought they’d donate, but they didn’t.
False negative - we thought they wouldn’t donate, but they would
have
# should P-Ignored / Donated be -17? I think it should.
prof_matrix <- matrix(c(0, 0, -1, 17), byrow = T, nrow=2,
dimnames = list(c("P-Ignored", "P-Donated"), c("Ignored", "Donated")))
prof_matrix
## Ignored Donated
## P-Ignored 0 0
## P-Donated -1 17
We can change the thresholds using our mailing data using the XGBoost model OUr cost matrix assigns values to a FP, FN, TP, TN
p_thresh <- 0.50
the_model_results <- factor(ifelse(mailing$prob_response > p_thresh, 1, 0), levels = c(0, 1))
the_actual_data <- mailing$class
cm_50 <- caret::confusionMatrix(the_model_results, the_actual_data)
p_thresh <- 0.05
the_model_results <- factor(ifelse(mailing$prob_response > p_thresh, 1, 0), levels = c(0, 1))
the_actual_data <- mailing$class
cm_05 <- caret::confusionMatrix(the_model_results, the_actual_data)
cm_50$table
## Reference
## Prediction 0 1
## 0 182063 9716
## 1 0 0
cm_05$table
## Reference
## Prediction 0 1
## 0 109162 4010
## 1 72901 5706
Average profit per person at the 5% threshold is $0.13 / person, if we use the values defined
n_cases <- nrow(mailing)
sum((cm_05$table/n_cases) * prof_matrix)
## [1] 0.1256707
At the 50% threshold, we just don’t send anything. Should we have an opportunity cost though, as we lost £17 with our false negatives!?
n_cases <- nrow(mailing)
sum((cm_50$table/n_cases) * prof_matrix)
## [1] 0
Write a function that will compute this for any threshold (total profit):
prof_threshold <- function(p_thresh, prof_matrix) {
the_model_results <- factor(ifelse(mailing$prob_response > p_thresh, 1, 0),
levels = c(0, 1))
the_actual_data <- mailing$class
cm <- caret::confusionMatrix(the_model_results, the_actual_data)
sum(cm$table * prof_matrix)
}
prof_threshold(0.05, prof_matrix)
## [1] 24101
Plot to visualize and quantify the trade-offs between taking different actions based on our predictive model
i <- seq(0,0.5, length.out = 100)
profits <- sapply(i, prof_threshold, prof_matrix = prof_matrix)
d <- tibble(i, profits)
max_prof <- which.max(d$profits)
ggplot(d, aes(x = i, y = profits)) +
geom_line(linewidth = 1) +
geom_hline(yintercept = 0, color = 'grey50') +
theme_minimal() +
annotate(geom = "point", color = 'red',
x = d$i[max_prof], y = d$profits[max_prof]) +
annotate(geom = "text", color = 'grey40',
x = d$i[max_prof] + 0.01, y = d$profits[max_prof], label = d$profits[max_prof],
hjust = "left") +
annotate(geom = "segment", x = d$i[max_prof], y = d$profits[max_prof],
xend = d$i[max_prof], yend = 0, linetype = 2) +
xlab("Model p-threshold") +
ylab("Predicted profit")
What if we included opportunity costs? If we don’t target someone who would have donated it is a cost to us.
prof_matrix2 <- matrix(c(0, -17, -1, 17), byrow = T, nrow=2,
dimnames = list(c("P-Ignored", "P-Donated"), c("Ignored", "Donated")))
prof_matrix2
## Ignored Donated
## P-Ignored 0 -17
## P-Donated -1 17
d$profits2 <- sapply(i, prof_threshold, prof_matrix = prof_matrix2)
Opportunity cost maybe isn’t the best way to model this. So we need better data - so we can include opportunity cost. It seems the data isn’t sufficient to target people.
ggplot(d, aes(x = i, y = profits2)) +
geom_line(linewidth = 1) +
geom_hline(yintercept = 0, color = 'grey50') +
theme_minimal() +
xlab("Model p-threshold") +
ylab("Predicted profit")