Exploring Full-Funnel Advertising Strategies with Markov Chains and Python SymPy
In the world of digital advertising, understanding how users move through the marketing funnel is key to designing effective ad campaigns. Here we are exploring full-funnel advertising, which integrates brand awareness and performance-driven ads to maximize conversions. To model this process, we treat the funnel as a Markov chain, where users transition between stages (Not Aware → Aware → Purchase) with certain probabilities. In this post, we’ll walk through a simulation model based on Markov chains that helps analyze different ad strategies using Python and SymPy.
The Funnel Model
We model user behavior with a three-stage funnel:
- Not Aware – Users who have not yet seen the brand.
- Aware – Users who know the brand but haven’t purchased.
- Purchase – Users who have purchased the brand (terminal stage).
We assume:
- Users arrive with a fraction
μ
in Not Aware and1-μ
in Aware. - Brand ads move users from Not Aware → Aware with probability
p_ba
. - Performance ads move users from Aware → Purchase with probability
p_pa
. - Users progress linearly through the funnel (no skipping stages or moving backward).
Setting Up the Model Symbolically
We start by defining symbolic variables and transition matrices with sympy
:
import sympy as sp
from IPython.display import display, Math
import numpy as np
import matplotlib.pyplot as plt
mu, p_ba, p_pa = sp.symbols('mu p_ba p_pa')
display(mu, p_ba, p_pa)
# Initial distribution
x0 = sp.Matrix([mu, 1 - mu, 0])
x0
B = sp.Matrix([
[1- p_ba, 0, 0 ],
[p_ba, 1, 0 ],
[0, 0, 1 ]])
B
P = sp.Matrix([
[1, 0, 0],
[0, 1 - p_pa, 0],
[0, p_pa, 1]
])
P
F = sp.Matrix([
[1-p_ba, 0, 0],
[p_ba, 1-p_pa, 0],
[0, p_pa,1]
])
F
Here:
B
represents Brand adsP
represents Performance adsF
represents Full-funnel ads (stage-dependent)
Simulating One-Step Progression
We can compute the distribution after one or two exposures:
x1_B = (B*x0)
x2_B = (B*x1_B)
display(x1_B, x2_B)
x1_P = (P*x0)
x2_P = (P*x1_P)
display(x1_P, x2_P)
x1_BP = (B*x0)
x2_BP = (P*x1_BP)
display(x1_BP, x2_BP)
x1_F = (F*x0)
x2_F = (F*x1_F)
display(x1_F, x2_F)
This allows us to symbolically track user fractions as they move through the funnel for each strategy:
- Brand-Ads-Only
- Performance-Ads-Only
- Brand-Plus-Performance
- Full-Funnel
Moving to Numerical Simulation
Now we assign numerical values for simulation:
# Parameters
mu = 0.6 # fraction initially Not Aware
p_ba = 0.2 # Brand ad conversion
p_pa = 0.3 # Performance ad conversion
steps = 20 # Number of time steps
# Initial distribution
x0 = np.array([mu, 1-mu, 0])
# Transition matrices
B = np.array([
[1-p_ba, 0, 0],
[p_ba, 1, 0],
[0, 0, 1]
])
P_mat = np.array([
[1, 0, 0],
[0, 1-p_pa, 0],
[0, p_pa, 1]
])
F = np.array([
[1-p_ba, 0, 0],
[p_ba, 1-p_pa, 0],
[0, p_pa, 1]
])
Simulating Multiple Steps
We define a function to apply the transition matrices repeatedly:
# Function to simulate repeated application of matrices
def simulate_steps(x0, matrices, steps):
traj = [x0.copy()]
x = x0.copy()
n_mats = len(matrices)
for i in range(steps):
# Cycle through matrices (strategy can have multiple types)
M = matrices[i % n_mats]
x = M @ x
traj.append(x.copy())
return np.array(traj)
Defining Strategies
We simulate four strategies:
# Define strategies
strategies = {
"Brand-Ads-Only": [B],
"Performance-Ads-Only": [P_mat],
"Brand-Plus-Performance": [B, P_mat],
"Full-Funnel": [F]
}
# Run simulations
results = {}
for name, mats in strategies.items():
results[name] = simulate_steps(x0, mats, steps)
Visualizing the Funnel Dynamics
Finally, we can plot the fraction of users who have purchased over time:
# Plot results
plt.figure(figsize=(12, 8))
for name, traj in results.items():
plt.plot(traj[:,2], label=f'{name} - Purchase') # focus on Purchase fraction
plt.xlabel("Step")
plt.ylabel("Fraction of users in Purchase")
plt.title("Progression to Purchase over time for different strategies")
plt.legend()
plt.grid(True)
plt.show()
This plot reveals which advertising strategy converts users most efficiently over multiple exposures. You can easily extend it to plot Not Aware or Aware stages as well.
Conclusion
Using symbolic matrices and numerical simulations, we can model and compare advertising strategies in a full-funnel context:
- Brand-Ads-Only increases awareness but may leave many users unconverted.
- Performance-Ads-Only targets already aware users but ignores those who need awareness.
- Brand-Plus-Performance combines both sequentially.
- Full-Funnel dynamically chooses the best ad for each stage, often leading to faster conversions.
This approach provides a transparent, data-driven way to evaluate ad campaigns before spending on real-world ads.
Perfect! I understand — you want the symbolic equations in a neat, step-by-step matrix multiplication style, similar to a textbook. Let’s create an appendix with all four strategies in that style. I’ll keep it in LaTeX-style equations for clarity.
Appendix: Symbolic Step-By-Step Funnel Calculations
We define: \(\begin{aligned} x_0 = \begin{bmatrix} \mu \\ 1-\mu \\ 0 \end{bmatrix}, \quad B = \begin{bmatrix} 1-p_{ba} & 0 & 0 \\ p_{ba} & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}, \quad P = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1-p_{pa} & 0 \\ 0 & p_{pa} & 1 \end{bmatrix}, \quad F = \begin{bmatrix} 1-p_{ba} & 0 & 0 \\ p_{ba} & 1-p_{pa} & 0 \\ 0 & p_{pa} & 1 \end{bmatrix} \end{aligned}\) —
1. Brand Ads Only
Step 1:
\(x_1 = B x_0 = \begin{bmatrix} 1-p_{ba} & 0 & 0 \\ p_{ba} & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} \mu \\ 1-\mu \\ 0 \end{bmatrix}\) \(= \begin{bmatrix} \mu (1-p_{ba}) \\ (1-\mu) + \mu p_{ba} \\ 0 \end{bmatrix}\)
Step 2:
\(x_2 = B x_1 = \begin{bmatrix} 1-p_{ba} & 0 & 0 \\ p_{ba} & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} \mu (1-p_{ba}) \\ (1-\mu) + \mu p_{ba} \\ 0 \end{bmatrix}\) \(= \begin{bmatrix} \mu (1-p_{ba})^2 \\ (1-\mu) + \mu p_{ba} (2-p_{ba}) \\ 0 \end{bmatrix}\)
2. Performance Ads Only
Step 1:
\(x_1 = P x_0 = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1-p_{pa} & 0 \\ 0 & p_{pa} & 1 \end{bmatrix} \begin{bmatrix} \mu \\ 1-\mu \\ 0 \end{bmatrix}\) \(= \begin{bmatrix} \mu \\ (1-\mu)(1-p_{pa}) \\ (1-\mu)p_{pa} \end{bmatrix}\)
Step 2:
\(x_2 = P x_1 = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1-p_{pa} & 0 \\ 0 & p_{pa} & 1 \end{bmatrix} \begin{bmatrix} \mu \\ (1-\mu)(1-p_{pa}) \\ (1-\mu)p_{pa} \end{bmatrix}\) \(= \begin{bmatrix} \mu \\ (1-\mu)(1-p_{pa})^2 \\ (1-\mu)(1-(1-p_{pa})^2) \end{bmatrix}\)
3. Brand-Plus-Performance
Step 1 (Brand):
\(x_1 = B x_0 = \begin{bmatrix} 1-p_{ba} & 0 & 0 \\ p_{ba} & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} \mu \\ 1-\mu \\ 0 \end{bmatrix}\) \(= \begin{bmatrix} \mu (1-p_{ba}) \\ (1-\mu) + \mu p_{ba} \\ 0 \end{bmatrix}\)
Step 2 (Performance):
\(x_2 = P x_1 = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1-p_{pa} & 0 \\ 0 & p_{pa} & 1 \end{bmatrix} \begin{bmatrix} \mu (1-p_{ba}) \\ (1-\mu) + \mu p_{ba} \\ 0 \end{bmatrix}\) \(= \begin{bmatrix} \mu (1-p_{ba}) \\ ((1-\mu)+\mu p_{ba})(1-p_{pa}) \\ ((1-\mu)+\mu p_{ba})p_{pa} \end{bmatrix}\)
4. Full-Funnel
Step 1:
\(x_1 = F x_0 = \begin{bmatrix} 1-p_{ba} & 0 & 0 \\ p_{ba} & 1-p_{pa} & 0 \\ 0 & p_{pa} & 1 \end{bmatrix} \begin{bmatrix} \mu \\ 1-\mu \\ 0 \end{bmatrix}\) \(= \begin{bmatrix} \mu (1-p_{ba}) \\ \mu p_{ba} + (1-\mu)(1-p_{pa}) \\ (1-\mu)p_{pa} \end{bmatrix}\)
Step 2:
\(x_2 = F x_1 = \begin{bmatrix} 1-p_{ba} & 0 & 0 \\ p_{ba} & 1-p_{pa} & 0 \\ 0 & p_{pa} & 1 \end{bmatrix} \begin{bmatrix} \mu (1-p_{ba}) \\ \mu p_{ba} + (1-\mu)(1-p_{pa}) \\ (1-\mu)p_{pa} \end{bmatrix}\) \(= \begin{bmatrix} \mu(1-p_{ba})^2 \\ \mu p_{ba}(1-p_{ba}) + (1-p_{pa})(\mu p_{ba} + (1-\mu)(1-p_{pa})) \\ p_{pa}(1-\mu) + p_{pa}(\mu p_{ba} + (1-\mu)(1-p_{pa})) \end{bmatrix}\)
Appendix: How B, P, and F Matrices Were Derived
The matrices represent stage-to-stage probabilities in the marketing funnel. The funnel has three stages:
- Not Aware (N)
- Aware (A)
- Purchase (P)
We assume:
- Users progress linearly through the funnel: N → A → P
- Users cannot skip stages or go backward
- Purchase is a terminal stage
1. Brand Ads Matrix (B)
Brand ads only affect users who are Not Aware, moving them to Aware with probability p_ba
.
- If a user is Not Aware, they either stay Not Aware (
1-p_ba
) or become Aware (p_ba
). - Aware users are not affected by brand ads, so they stay Aware.
- Purchase is terminal.
Thus, the matrix is:
B = sp.Matrix([
[1- p_ba, 0, 0],
[p_ba, 1, 0],
[0, 0, 1]
])
From \ To | Not Aware | Aware | Purchase |
---|---|---|---|
Not Aware | 1-p_ba | p_ba | 0 |
Aware | 0 | 1 | 0 |
Purchase | 0 | 0 | 1 |
Explanation: Only Not Aware → Aware is possible; others remain unchanged.
2. Performance Ads Matrix (P)
Performance ads only affect users who are Aware, moving them to Purchase with probability p_pa
.
- Not Aware users are unaffected.
- Aware users either stay Aware (
1-p_pa
) or Purchase (p_pa
). - Purchase is terminal.
Matrix:
P = sp.Matrix([
[1, 0, 0],
[0, 1 - p_pa, 0],
[0, p_pa, 1]
])
From \ To | Not Aware | Aware | Purchase |
---|---|---|---|
Not Aware | 1 | 0 | 0 |
Aware | 0 | 1-p_pa | p_pa |
Purchase | 0 | 0 | 1 |
Explanation: Only Aware → Purchase is possible; others remain unchanged.
3. Full-Funnel Matrix (F)
The Full-Funnel strategy targets the right ad based on the user’s stage:
- Not Aware → Brand ad → moves to Aware with
p_ba
- Aware → Performance ad → moves to Purchase with
p_pa
- Purchase is terminal
This combines the effects of both B and P in one matrix:
F = sp.Matrix([
[1-p_ba, 0, 0],
[p_ba, 1-p_pa, 0],
[0, p_pa, 1]
])
From \ To | Not Aware | Aware | Purchase |
---|---|---|---|
Not Aware | 1-p_ba | 0 | 0 |
Aware | p_ba | 1-p_pa | 0 |
Purchase | 0 | p_pa | 1 |
Explanation:
- Row 1: Not Aware users → either stay Not Aware or move to Aware via brand ad
- Row 2: Aware users → stay Aware or move to Purchase via performance ad
- Row 3: Purchase is terminal
This matrix dynamically applies the correct ad per user stage, making it the most realistic full-funnel model.
Summary of Derivation Logic
- Identify which funnel stage the ad affects
- Assign probabilities for progressing to the next stage (
p_ba
orp_pa
) - Ensure no backward movement
- Terminal stage (Purchase) always remains
1