---
title: "Beyond churn: applying modelimpact to other use cases"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Beyond churn: applying modelimpact to other use cases}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 4.5,
  out.width = "100%"
)
has_ggplot2 <- requireNamespace("ggplot2", quietly = TRUE)
```

## The same engine, many problems

`modelimpact` was first written to reason about the business value of a
customer **churn** model, but nothing in the maths is churn-specific. Every
function does the same thing: it takes a column of predicted probabilities for
an event of interest and a column with the actual outcome, then translates the
model's behaviour into money using a handful of cost and value assumptions.

That makes the package directly usable for any **binary classification**
problem where acting on a prediction has a cost and being right (or wrong) has a
value, for example:

- **Fraud detection** — value of blocking a fraudulent transaction vs. the cost
  of reviewing a flagged one and the loss from fraud that slips through.
- **Credit default** — expected loss avoided vs. the cost of intervention.
- **Lead scoring and marketing response** — value of a converted lead vs. the
  cost of contacting it.
- **Upsell and cross-sell** — margin on an accepted offer vs. the cost of
  making it (use `prob_accept` to model take-up).
- **Predictive maintenance** — cost of an averted failure vs. the cost of an
  inspection.

The key is the `positive` argument, which every function accepts: it names the
value in your outcome column that identifies the event of interest. It defaults
to `"Yes"`, but it can be anything (`"fraud"`, `"default"`, `"converted"`, ...).

## A worked example: fraud detection

Suppose we have scored a batch of transactions with a fraud model. Each row is
a transaction with the model's predicted probability of fraud (`score`) and
whether it later turned out to be fraudulent (`fraud`). Fraud is rare, so the
classes are highly imbalanced.

```{r data}
library(modelimpact)
library(magrittr)  # for the %>% pipe

set.seed(42)
n <- 5000

# ~3% of transactions are actually fraudulent
is_fraud <- rbinom(n, size = 1, prob = 0.03)

# the model scores fraud higher than legitimate transactions, with overlap
score <- plogis(rnorm(n, mean = ifelse(is_fraud == 1, 1.5, -2.5), sd = 1.2))

transactions <- data.frame(
  score = score,
  fraud = ifelse(is_fraud == 1, "fraud", "legit")
)

head(transactions)
```

### The economics

We express the decision in monetary terms:

```{r econ}
review_cost  <- 8      # cost to manually review a flagged transaction
fraud_value  <- 400    # average value recovered when fraud is caught
missed_fraud <- -400   # average loss when fraud is not caught
```

### Cost and revenue as we review more transactions

Ranking transactions from most to least suspicious, how do the cumulative cost
of reviewing and the cumulative value recovered grow?

```{r cost-revenue, eval = has_ggplot2}
library(ggplot2)

transactions %>%
  cost_revenue(
    var_cost  = review_cost,
    tp_val    = fraud_value,
    prob_col  = score,
    truth_col = fraud,
    positive  = "fraud"
  ) %>%
  autoplot()
```

### How far down the ranked list should we review?

`profit()` shows the net value of reviewing the top *X %* of most suspicious
transactions, and `break_even()` / `impact_summary()` read off the key
operating points.

```{r profit, eval = has_ggplot2}
transactions %>%
  profit(
    var_cost  = review_cost,
    tp_val    = fraud_value,
    prob_col  = score,
    truth_col = fraud,
    positive  = "fraud"
  ) %>%
  autoplot()
```

```{r summary}
impact_summary(
  transactions,
  var_cost  = review_cost,
  tp_val    = fraud_value,
  prob_col  = score,
  truth_col = fraud,
  positive  = "fraud"
)
```

### Choosing a decision threshold

If the fraud model must instead produce an automatic block/allow decision,
`profit_thresholds()` sweeps every probability cutoff and scores the full
confusion matrix using a value for each cell. Here a missed fraud
(`fn_val`) is expensive, a false alarm (`fp_val`) merely costs a review, and
correct decisions on legitimate traffic are neutral.

```{r thresholds, eval = has_ggplot2}
transactions %>%
  profit_thresholds(
    var_cost  = review_cost,
    tp_val    = fraud_value,
    fp_val    = 0,
    tn_val    = 0,
    fn_val    = missed_fraud,
    prob_col  = score,
    truth_col = fraud,
    positive  = "fraud"
  ) %>%
  autoplot()
```

## Adapting to your own use case

To reuse any of these functions for a different problem, you only need to:

1. Point `prob_col` at your model's predicted-probability column and
   `truth_col` at the actual-outcome column.
2. Set `positive` to the label of the event you care about.
3. Translate your domain into the cost and value arguments (`var_cost`,
   `fixed_cost`, `tp_val`, and — for `profit_thresholds()` — `fp_val`,
   `tn_val`, `fn_val`), optionally using `prob_accept` when an action only
   incurs its cost if accepted.

The interpretation of every plot and summary then carries over unchanged.

## Regression models: targeting by predicted value

Not every model predicts a class. Sometimes the model predicts a *continuous
value* — expected customer lifetime value (CLV), next-year spend, claim size,
or expected loss — and we want to target the cases with the highest predicted
value. `value_gains()` and `value_profit()` are the regression counterparts of
`cumulative_gains()` and `profit()`: instead of a positive class they take the
model's prediction (`pred_col`) and the realised value (`value_col`).

Here we simulate customers whose predicted CLV is correlated with, but not
identical to, their realised CLV.

```{r reg-data}
set.seed(99)
n <- 2000

realised_clv  <- rgamma(n, shape = 2, scale = 300)          # actual value
predicted_clv <- realised_clv * runif(n, 0.4, 1.6)          # noisy model score

customers <- data.frame(
  predicted_clv = predicted_clv,
  realised_clv  = realised_clv
)

head(customers)
```

### How much value do we capture?

`value_gains()` ranks customers by predicted CLV and shows what share of the
*total realised value* is captured as we target more of them. The dashed
diagonal is random targeting; the further the curve bows above it, the better
the model concentrates value near the top. The **concentration (Gini)**
coefficient in the subtitle summarises this in one number (1 = as good as a
perfect ranking, 0 = no better than random).

```{r reg-gains, eval = has_ggplot2}
vg <- value_gains(customers, pred_col = predicted_clv, value_col = realised_clv)
autoplot(vg)

# the coefficient is also available programmatically
attr(vg, "gini")
```

### What is the profit of targeting by predicted value?

`value_profit()` accumulates the realised value of the customers we contact and
subtracts the cost of contacting them. Suppose each contact costs 50.

```{r reg-profit, eval = has_ggplot2}
customers %>%
  value_profit(
    var_cost  = 50,
    pred_col  = predicted_clv,
    value_col = realised_clv
  ) %>%
  autoplot()
```

Because `value_profit()` returns the same kind of object as `profit()`, the
plot reads exactly the same way: profit rises while the customers we add bring
in more value than they cost, peaks at the profit-maximising share (dashed
line), then falls once we start contacting low-value customers.

