R
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Data Structures
-
Section 4: Data Manipulation
-
Section 5: Visualization and Statistics
-
Section 6: Advanced R
-
Section 7: Practical Projects
-
Section 8: Interview Practice
-
Section 9: More Practice Exercises
-
Section 10: Shiny Apps in Depth
-
Section 11: More Data Wrangling
-
Section 12: Tidyverse Deep Dive
-
Section 13: Statistical Modeling Deep Dive
-
Section 14: Machine Learning in R
-
Section 15: R Visualization Deep Dive
-
Section 16: R Package Development Deep Dive
-
Section 17: R for Reproducible Research
-
Section 18: R and Databases
-
Section 19: R Performance Optimization
-
Section 20: Bioinformatics and Specialized R
-
Section 21: More Shiny Practice
-
Section 22: More Practice Exercises
-
Section 23: R Data Structures Deep Dive
-
Section 24: More Interview and Review
-
Section 25: R for Business Analytics
-
Section 26: R Text Mining and NLP
-
Section 27: R Spatial Data Analysis
-
Section 28: R Deep Learning
-
Section 29: Advanced Statistical Techniques
-
Section 30: R Object Systems Deep Dive
-
Section 31: R Environments and Metaprogramming
-
Section 32: R for Finance
-
Section 33: R for Clinical and Health Data
-
Section 34: More Shiny Advanced Practice
-
Section 35: R Data Cleaning Deep Dive
-
Section 36: R Reporting Automation
-
Section 37: More Practical Projects Round 2
-
Section 38: R Ecosystem and Career
-
Section 39: More Visualization Practice
-
Section 40: R for Experimentation
-
Section 41: R for Genomics and Bioinformatics Deep Dive
-
Section 42: R for Marketing Analytics
-
Section 43: R Data Import/Export Deep Dive
-
Section 44: R String Processing Deep Dive
-
Section 45: R for Actuarial and Insurance Analytics
-
Section 46: R Testing and Quality Assurance Deep Dive
-
Section 47: R Version Control and Collaboration
-
Section 48: R Advanced Functional Programming
-
Section 49: R for Supply Chain and Operations
-
Section 50: More Practice Exercises Round 3
-
Section 51: R Dashboards and BI Integration
-
Section 52: R Data Governance and Ethics
-
Section 53: More Modeling Practice
-
Section 54: R Final Capstone Projects
-
Section 55: R for Sports Analytics
-
Section 56: More Interview Practice Round 2
-
Section 57: R Networking and APIs
-
Section 58: R for Environmental Science
-
Section 59: R Notebook and Documentation Practices
-
Section 60: More Data Wrangling Mastery
-
Section 61: R for A/B Testing at Scale
-
Section 62: R Package Ecosystem Deep Dive
167: Marketing Mix Modeling in R Revisited
If you've ever been asked by a marketing manager to "figure out which channel is driving the most sales," your first instinct in R might be to reach for a simple linear model. It's clean, it's fast, and the coefficients give you a nice, neat number for "Return on Ad Spend." But if you've done this before, you know that a raw lm() call on marketing spend almost always lies to you. I've seen a lot of junior analysts present these results, only to have the CMO point out that the model suggests spending zero on a channel that they know for a fact is their primary growth engine.
The Seduction of the Simple Linear Model
Let's say we're modeling sales for a high-end espresso machine. We have weekly data on spend across three channels: Instagram, Google Search, and Email. The naive approach looks like this:
# The "Quick and Dirty" way
model_naive <- lm(sales ~ ig_spend + google_spend + email_spend, data = mkt_data)
summary(model_naive)
On the surface, this is great. You get a p-value and a coefficient. But this model assumes a "perfectly linear, instantaneous" world. It assumes that a dollar spent on Instagram today drives a sale today, and that spending $10,000 will produce exactly ten times the result of spending $1,000. In the real world, marketing doesn't work that way. I call this the "vacuum model" because it assumes your ads exist in a vacuum, devoid of time or human psychology.
Where the Linear Assumption Hits a Wall
The reason this approach breaks is twofold: adstock and saturation. First, there's the "carryover effect." If a customer sees a gorgeous ad for an espresso machine on Tuesday, they might not buy it until Friday after they've discussed it with their spouse. A linear model treats that Friday sale as "organic" or attributes it to whatever ad ran on Friday, completely ignoring the Tuesday trigger. This is adstock.
Then there's saturation. There is a finite number of people interested in espresso machines in a given week. Once you've reached the bulk of your audience, spending another $5,000 on Google Search doesn't give you a linear increase in sales; it just makes you pay more for the same clicks. Your response curve is concave, not a straight line. If you use lm(), you're trying to fit a straight ruler to a curved hill, and the resulting coefficients will be biased and misleading.
Accounting for Memory and Fatigue
The better way is to transform your independent variables before they hit the model. We need to account for the decay of the ad's impact over time and the diminishing returns of spend. Instead of feeding raw spend into the model, we create "transformed spend" variables.
For adstock, we use a decay function. A simple geometric decay looks like this in R:
# A simple function to calculate adstock
apply_adstock <- function(x, decay_rate = 0.5) {
return(stats::filter(x, filter = decay_rate, method = "recursive"))
}
mkt_data$ig_adstocked <- apply_adstock(mkt_data$ig_spend, 0.7)
Now, for saturation, we can use a Hill function or a simple log transformation if the curvature is mild. By transforming the data first, we are essentially telling the model: "The impact of this spend is delayed and it eventually plateaus."
The trade-off here is complexity. You no longer have a single coefficient you can point to; you now have hyperparameters (like the decay_rate) that you need to optimize. I usually handle this by iterating through a grid of possible decay rates and saturation parameters to find the combination that minimizes the root mean squared error (RMSE) of the model. It's more work, but it's the difference between giving your boss a guess and giving them a strategy.
📋 Practical Task
Implementing Geometric Adstock for the Coffee Machine Dataset
You have been provided with a dataframe mkt_data containing columns sales and tv_spend. Your current model lm(sales ~ tv_spend)` is underperforming because it ignores the carryover effect of TV advertising.
Write a script that does the following:
- Defines a function
calc_adstockthat takes a vector of spend and adecayvalue (between 0 and 1), returning the adstocked spend using a recursive filter. - Applies this function to
tv_spendusing a decay rate of0.6to create a new column calledtv_adstocked. - Fits a new linear model using
tv_adstockedinstead oftv_spend. - Prints the R-squared value of the new model to compare it against the original.
There are no comments for now.