Attribution Modeling Case Study

Author

Juan De La Cruz

Published

April 14, 2026

Code
markov_fixed <- function(data, var_path, var_conv, var_value) {
  library(dplyr)

  # Split paths into transitions manually (no unnest)
  transitions_list <- lapply(seq_len(nrow(data)), function(i) {
    steps <- strsplit(as.character(data[[var_path]][i]), " > ")[[1]]
    if (length(steps) > 1) {
      data.frame(
        from = head(steps, -1),
        to = tail(steps, -1)
      )
    } else {
      NULL
    }
  })

  transitions <- do.call(rbind, transitions_list)

  # Transition matrix
  transition_matrix <- transitions %>%
    count(from, to) %>%
    group_by(from) %>%
    mutate(prob = n / sum(n)) %>%
    select(from, to, prob)

  # Removal effect
  channels <- unique(c(transition_matrix$from, transition_matrix$to))

  removal_effect <- sapply(channels, function(ch) {
    modified <- data %>%
      mutate(steps = strsplit(as.character(.data[[var_path]]), " > ")) %>%
      mutate(steps = lapply(steps, function(x) x[x != ch])) %>%
      mutate(conv = ifelse(lengths(steps) == 0, 0, .data[[var_conv]]))

    mean(data[[var_conv]]) - mean(modified$conv)
  })

  list(
    transition_matrix = transition_matrix,
    removal_effect = removal_effect
  )
}
Code
shapley_fixed <- function(data, var_path, var_conv, var_value) {
  df <- data %>%
    mutate(channel_list = strsplit(as.character(.data[[var_path]]), " > ")) %>%
    rowwise() %>%
    mutate(channel_list = list(unique(channel_list))) %>%
    ungroup()

  channels <- unique(unlist(df$channel_list))

  shapley_values <- sapply(channels, function(ch) {
    with_ch <- df %>% filter(sapply(channel_list, function(x) ch %in% x))
    without_ch <- df %>% filter(!sapply(channel_list, function(x) ch %in% x))

    mean(with_ch[[var_value]]) - mean(without_ch[[var_value]])
  })

  data.frame(
    channel_name = channels,
    shapley_value = shapley_values
  )
}
Code
shapley_manual <- function(data, var_path, var_conv, var_value) {
  library(dplyr)
  
  df <- data %>%
    mutate(channel_list = strsplit(as.character(.data[[var_path]]), " > ")) %>%
    rowwise() %>%
    mutate(channel_list = list(unique(channel_list))) %>%
    ungroup()
  
  channels <- unique(unlist(df$channel_list))
  
  shapley_values <- sapply(channels, function(ch) {
    with_ch <- df %>% filter(sapply(channel_list, function(x) ch %in% x))
    without_ch <- df %>% filter(!sapply(channel_list, function(x) ch %in% x))
    
    mean(with_ch[[var_value]]) - mean(without_ch[[var_value]])
  })
  
  data.frame(
    channel_name = channels,
    shapley_value = shapley_values
  )
}

1 1. Case overview and questions

This case study explores how different marketing touchpoints contribute to customer conversions.
KNC wants to understand how to assign credit fairly across multi‑touch customer journeys.

Task 1 — Three questions from Section 1
  1. Which touchpoints are most influential in driving conversions?
  2. How should conversion credit be allocated across multi‑touch paths?
  3. How do different attribution models change the perceived value of each channel?
Task 1 — Attribution methods

Heuristic-based models - First-touch: 100% credit to the first interaction.
- Last-touch: 100% credit to the final interaction before conversion.
- Linear: Equal credit to all touchpoints.
- Position-based / time-decay: Weighted credit based on position or recency.

Probability-based models - Markov chain model: Uses transition probabilities and removal effects to estimate channel contribution.
- Shapley value: Game‑theoretic method that averages each channel’s marginal contribution across all possible channel combinations.

2 2. Data preparation

3 Load Packages

Code
library(ChannelAttribution)
library(reshape2)
library(ggplot2)
library(dplyr)

4 Import Data

Code
PathData <- data.frame(
  path = c(
    "start > email > direct",
    "start > paid_search > direct",
    "start > social > email > direct",
    "start > display > social > direct",
    "start > paid_search > email > direct",
    "start > direct"
  ),
  conv = c(1, 0, 1, 0, 1, 1),
  value = c(100, 0, 120, 0, 150, 80)
)
Code
data(PathData)
head(PathData)
                                  path conv value
1               start > email > direct    1   100
2         start > paid_search > direct    0     0
3      start > social > email > direct    1   120
4    start > display > social > direct    0     0
5 start > paid_search > email > direct    1   150
6                       start > direct    1    80

5 Clean Path Formatting

Code
PathData <- PathData %>%
  mutate(path = gsub(">", " > ", path))
Note

The dataset contains customer paths, conversion indicators, and conversion values.
Cleaning the path ensures consistent formatting for modeling.

Code
PathData <- data.frame(
  path = c(
    "start > email > direct",
    "start > paid_search > direct",
    "start > social > email > direct",
    "start > display > social > direct",
    "start > paid_search > email > direct",
    "start > direct",
    "start > email > paid_search > direct",
    "start > social > direct",
    "start > display > email > direct",
    "start > paid_search > social > direct"
  ),
  conv = c(1,0,1,0,1,1,1,0,1,0),
  value = c(100,0,120,0,150,80,130,0,140,0)
)

6 3. Exploratory analysis and visualization

This section explores the data set & visualizes attribution results.

Code
H <- heuristic_models(
  PathData,
  var_path = "path",
  var_conv = "conv",
  var_value = "value"
)
[1] "*** Install ChannelAttribution Pro for free! Run install_pro(). Set flg_pro=FALSE to hide this message."
Code
M <- markov_fixed(
  PathData,
  var_path = "path",
  var_conv = "conv",
  var_value = "value"
)

S <- shapley_fixed(
  PathData,
  var_path = "path",
  var_conv = "conv",
  var_value = "value"
)

R <- merge(H, S, by = "channel_name", all = TRUE)

7 Melt + Bar Plot

Code
R_melt <- melt(R, id = "channel_name")

ggplot(R_melt, aes(x = channel_name, y = value, fill = variable)) +
  geom_bar(stat = "identity", position = "dodge") +
  coord_flip()

8 Transition Matrix

Code
M$transition_matrix
# A tibble: 14 × 3
# Groups:   from [5]
   from        to           prob
   <chr>       <chr>       <dbl>
 1 display     email        0.5 
 2 display     social       0.5 
 3 email       direct       0.8 
 4 email       paid_search  0.2 
 5 paid_search direct       0.5 
 6 paid_search email        0.25
 7 paid_search social       0.25
 8 social      direct       0.75
 9 social      email        0.25
10 start       direct       0.1 
11 start       display      0.2 
12 start       email        0.2 
13 start       paid_search  0.3 
14 start       social       0.2 

9 Removal Effect

Code
M$removal_effect
    display       email paid_search      social       start      direct 
          0           0           0           0           0           0 
Task 3 — Fixing errors

Common issues include: - Incorrect column names in ggplot
- Missing factor ordering
- Using the wrong variable names from the model output

Check names(R) and names(M) to ensure the visualization uses the correct columns.

Task 3 — Probability of start > paid_search > conversion

Use the transition matrix:

[ P = P(start paid_search) P(paid_search conversion) ]

Insert your actual values from M$transition_matrix here.

Code
H <- heuristic_models(
  PathData,
  var_path = "path",
  var_conv = "conv",
  var_value = "value"
)
[1] "*** Install ChannelAttribution Pro for free! Run install_pro(). Set flg_pro=FALSE to hide this message."
Code
M <- markov_fixed(
  PathData,
  var_path = "path",
  var_conv = "conv",
  var_value = "value"
)

S <- shapley_fixed(
  PathData,
  var_path = "path",
  var_conv = "conv",
  var_value = "value"
)

R <- merge(H, S, by = "channel_name", all = TRUE)

10 4. Attribution models in R

This section runs heuristic, Markov, & Shapely attribution models

10.1 4.1 Heuristic models (first / last touch)

Code
H <- heuristic_models(
  PathData,
  var_path = "path",
  var_conv = "conv",
  var_value = "value"
)
[1] "*** Install ChannelAttribution Pro for free! Run install_pro(). Set flg_pro=FALSE to hide this message."
Code
H
  channel_name first_touch_conversions first_touch_value last_touch_conversions
1        start                       6               720                      0
2        email                       0                 0                      0
3       direct                       0                 0                      6
4  paid_search                       0                 0                      0
5       social                       0                 0                      0
6      display                       0                 0                      0
  last_touch_value linear_touch_conversions linear_touch_value
1                0                 1.833333           208.3333
2                0                 1.333333           168.3333
3              720                 1.833333           208.3333
4                0                 0.500000            70.0000
5                0                 0.250000            30.0000
6                0                 0.250000            35.0000
Note

Heuristic models assign credit using simple rules.
They are easy to interpret but ignore sequence effects.

10.2 4.2 Markov chain model

Code
M <- markov_model(
  PathData,
  var_path = "path",
  var_conv = "conv",
  var_value = "value",
  order = 1
)

Number of simulations: 100000 - Convergence reached: 0.68% < 5.00%

Percentage of simulated paths that successfully end before maximum number of steps (6) is reached: 98.83%

[1] "*** Install ChannelAttribution Pro for free running install_pro(). Visit https://channelattribution.io for more info. Set flg_pro=FALSE to hide this message."
Code
M
  channel_name total_conversion total_conversion_value
1        start        1.7846274              214.22381
2        email        1.3313887              159.52158
3       direct        1.7846274              214.22381
4  paid_search        0.5124196               61.61773
5       social        0.2894088               34.76676
6      display        0.2975281               35.64629
Note

The Markov model uses transition probabilities and removal effects to estimate each channel’s contribution.

10.3 4.3 Shapley-value-based approach

Code
S <- shapley_manual(
  PathData,
  var_path = "path",
  var_conv = "conv",
  var_value = "value"
)
S
            channel_name shapley_value
start              start           NaN
email              email    112.000000
direct            direct           NaN
paid_search  paid_search     -3.333333
social            social    -70.000000
display          display     -2.500000
Note

The Shapley method evaluates each channel’s marginal contribution across all possible channel combinations.

11 5. Results and interpretation

This section compares the outputs of all attribution models

12 Combined Attribution Table

Code
R <- merge(
  H,
  M,
  by = "channel_name",
  all = TRUE
)
R
  channel_name first_touch_conversions first_touch_value last_touch_conversions
1       direct                       0                 0                      6
2      display                       0                 0                      0
3        email                       0                 0                      0
4  paid_search                       0                 0                      0
5       social                       0                 0                      0
6        start                       6               720                      0
  last_touch_value linear_touch_conversions linear_touch_value total_conversion
1              720                 1.833333           208.3333        1.7846274
2                0                 0.250000            35.0000        0.2975281
3                0                 1.333333           168.3333        1.3313887
4                0                 0.500000            70.0000        0.5124196
5                0                 0.250000            30.0000        0.2894088
6                0                 1.833333           208.3333        1.7846274
  total_conversion_value
1              214.22381
2               35.64629
3              159.52158
4               61.61773
5               34.76676
6              214.22381

13 Shapely vs Markov Comparison

Code
merge(S, M, by = "channel_name")
  channel_name shapley_value total_conversion total_conversion_value
1       direct           NaN        1.7846274              214.22381
2      display     -2.500000        0.2975281               35.64629
3        email    112.000000        1.3313887              159.52158
4  paid_search     -3.333333        0.5124196               61.61773
5       social    -70.000000        0.2894088               34.76676
6        start           NaN        1.7846274              214.22381
Task 4 — Interpretation of attribution results
  • First-touch: Rewards channels that initiate journeys.
  • Last-touch: Rewards channels that close conversions.
  • Markov: Rewards channels that are essential in the transition structure.
  • Shapley: Rewards channels with high marginal contribution across combinations.

Summarize which channels gained or lost credit in your output.

Task 5 — Shapley drop in conversion probability

Channels with the largest Shapley values cause the biggest drop in conversion probability when removed.
Interpret which channels are most critical based on your results.

14 6. Discussion of modeling approaches

Task 6 — Strengths and weaknesses

Markov model - Strengths: sequence-aware, uses removal effects, good for path analysis
- Weaknesses: sensitive to sparse data, assumes Markov property

Shapley value - Strengths: fair allocation, considers all channel combinations
- Weaknesses: computationally heavy, less intuitive for non-technical audiences

Task 7 — How Shapley supplements Markov

Shapley values validate and complement Markov results by: - Confirming which channels consistently contribute across combinations
- Highlighting channels that assist indirectly
- Providing a fairness-based perspective to balance Markov’s sequence-based view

15 7. Answers to assignment questions

Note

Task 1: Section 1
Task 2: Sections 2–4
Task 3: Section 3
Task 4: Section 5
Task 5: Section 5
Task 6: Section 6
Task 7: Section 6