← Back to Curriculum
Module 05

The Ceiling Effect

Feature Engineering Part II: Modeling Diminishing Returns because you cannot spend your way to infinity.

If you spend $100 on Google Search, you might get 10 clicks. If you spend $10,000, you will not get 1,000 clicks. You might get 600.

Why? because there are only so many people searching for "Best Running Shoes" in a given week. Eventually, you exhaust the audience. The next dollar you spend becomes less efficient than the first dollar.

If we use a standard Linear Regression (y = mx + c), the model assumes a straight line that goes up forever. This is dangerous. It tells the CFO: "If we spend $1 Billion, we will make $5 Billion." We need to bend that line.

1. The Math: The Hill Function

While there are many saturation curves (Logistic, Power, Negative Exponential), the industry standard for modern MMM (used by Facebook's Robyn and Google's LightweightMMM) is the Hill Function. It is flexible enough to model S-curves and C-curves.

Effect =
SpendS SpendS + KS

This looks intimidating, but it only has two levers you need to understand:

Slope (S)

Controls the shape of the curve.
S > 1: It's an "S-Curve". Spend doesn't work at first, then accelerates, then flattens.
S < 1: It's a "C-Curve". Returns diminish immediately (typical for Search).

Half-Saturation (K)

This is the inflection point. K is the amount of spend where you achieve exactly 50% of your maximum possible return.

2. Python Implementation

We do not need to invent this math. We can define a simple Python function to transform our Adstocked data into Saturated data.

import numpy as np

def hill_saturation(x_adstocked, alpha, gamma):
    """
    x_adstocked: The media data AFTER adstock transformation
    alpha: The Slope parameter (S)
    gamma: The Half-Saturation parameter (K)
    """
    
    # 1. Calculate the inflection point (K) based on gamma range
    # Gamma is usually a % of the max spend range (e.g., 0.3 to 0.8)
    inflection = np.max(x_adstocked) * gamma
    
    # 2. Apply the Hill Equation
    return (x_adstocked**alpha) / (x_adstocked**alpha + inflection**alpha)

3. Visualizing The "Curve"

When you run this transformation, your linear spend data (which goes from 0 to infinity) is squashed into a range between 0 and 1.
- 0 means $0 Spend.
- 1 means "Total Saturation" (spending more yields 0 extra results).

4. Chaining Transformations

It is critical to remember the Order of Operations in MMM. You cannot saturate first, then adstock. It must represent the physical reality:

  1. Spend Occurs: Raw Data.
  2. Memory Happens (Adstock): The impact spreads over days.
  3. Saturation Hits (Hill): The accumulated pressure hits a ceiling.
# The Full Pipeline for one channel
step_1_adstock = geometric_adstock(df['fb_spend'], alpha=0.5)
step_2_final_feature = hill_saturation(step_1_adstock, alpha=1.2, gamma=0.6)

Just like Adstock, we do not guess the Alpha and Gamma values. We will let the machine learn them during the Modeling phase.

Previous Module ← The Memory Effect