Building a Simple Pricing Model in Python
From theory to code
In the previous posts, we covered the intuition behind dynamic pricing and the math of demand curves. Now let’s put it together into a working model.
The workflow is straightforward:
- Generate (or collect) historical price-demand data
- Estimate a demand curve
- Optimize price to maximize revenue (or profit)
Step 1: Simulating data
In practice, you’d pull this from your transaction database. Here we’ll simulate realistic-looking data:
import numpy as np
import pandas as pd
from scipy.optimize import minimize_scalar
np.random.seed(42)
# True demand parameters (unknown in practice)
TRUE_A = 200
TRUE_B = 3.5
n_obs = 500
prices = np.random.uniform(10, 50, n_obs)
noise = np.random.normal(0, 8, n_obs)
quantities = np.maximum(TRUE_A - TRUE_B * prices + noise, 0)
data = pd.DataFrame({"price": prices, "quantity": quantities})
data.head()| price | quantity |
|---|---|
| 24.87 | 117.96 |
| 38.59 | 60.43 |
| 15.60 | 152.11 |
| 41.22 | 51.87 |
| 29.75 | 96.54 |
Step 2: Estimating the demand curve
A simple linear regression gives us \(\hat{a}\) and \(\hat{b}\):
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(data[["price"]], data["quantity"])
a_hat = model.intercept_
b_hat = -model.coef_[0] # negate because Q = a - b*p
print(f"Estimated demand: Q = {a_hat:.1f} - {b_hat:.2f} * p")
print(f"True demand: Q = {TRUE_A} - {TRUE_B} * p")With 500 observations and moderate noise, the estimates should be close to the true values.
Step 3: Optimizing price
Given our estimated demand curve, the revenue function is:
\[ R(p) = p \cdot \hat{Q}(p) = p \cdot (\hat{a} - \hat{b} \cdot p) \]
We can solve this analytically or numerically:
# Analytical solution
p_star_analytical = a_hat / (2 * b_hat)
# Numerical (useful for more complex demand curves)
result = minimize_scalar(
lambda p: -(p * (a_hat - b_hat * p)), # negative because we minimize
bounds=(0, a_hat / b_hat),
method="bounded",
)
p_star_numerical = result.x
print(f"Optimal price (analytical): ${p_star_analytical:.2f}")
print(f"Optimal price (numerical): ${p_star_numerical:.2f}")
print(f"Expected revenue: ${p_star_analytical * (a_hat - b_hat * p_star_analytical):.2f}")Adding cost structure
Revenue maximization ignores costs. If we have a per-unit cost \(c\), we want to maximize profit:
\[ \Pi(p) = (p - c) \cdot (\hat{a} - \hat{b} \cdot p) \]
unit_cost = 12 # $ per unit
p_star_profit = (a_hat + b_hat * unit_cost) / (2 * b_hat)
expected_profit = (p_star_profit - unit_cost) * (a_hat - b_hat * p_star_profit)
print(f"Profit-maximizing price: ${p_star_profit:.2f}")
print(f"Expected profit: ${expected_profit:.2f}")The profit-maximizing price is always higher than the revenue-maximizing price (assuming positive costs). This makes intuitive sense — when you account for costs, you want to sell fewer units at a higher margin.
Adding inventory constraints
What if you have limited inventory? You need to ensure \(Q(p) \leq S\) where \(S\) is your stock:
inventory = 80
# Minimum price to not exceed inventory
p_min_inventory = (a_hat - inventory) / b_hat
# Constrained optimal price
p_star_constrained = max(p_star_profit, p_min_inventory)
print(f"Unconstrained optimal: ${p_star_profit:.2f}")
print(f"Min price for inventory: ${p_min_inventory:.2f}")
print(f"Constrained optimal: ${p_star_constrained:.2f}")Putting it all together
Here’s a clean function that wraps the full pipeline:
def optimize_price(
data: pd.DataFrame,
unit_cost: float = 0,
inventory: float | None = None,
price_col: str = "price",
quantity_col: str = "quantity",
) -> dict:
"""Estimate demand and find optimal price."""
model = LinearRegression()
model.fit(data[[price_col]], data[quantity_col])
a = model.intercept_
b = -model.coef_[0]
# Profit-maximizing price
p_star = (a + b * unit_cost) / (2 * b)
# Apply inventory constraint
if inventory is not None:
p_min = (a - inventory) / b
p_star = max(p_star, p_min)
q_star = a - b * p_star
revenue = p_star * q_star
profit = (p_star - unit_cost) * q_star
return {
"optimal_price": round(p_star, 2),
"expected_quantity": round(q_star, 1),
"expected_revenue": round(revenue, 2),
"expected_profit": round(profit, 2),
"demand_intercept": round(a, 2),
"demand_slope": round(b, 4),
}
result = optimize_price(data, unit_cost=12, inventory=80)
for k, v in result.items():
print(f" {k}: {v}")Limitations
This model is intentionally simple. Real-world pricing systems need to handle:
- Non-linear demand — the linear assumption breaks at extreme prices
- Competitor pricing — your demand depends on what others charge
- Time dynamics — demand patterns shift over hours, days, and seasons
- Multiple products — pricing one product affects demand for others
We’ll tackle these in upcoming posts. But even this simple model captures the core logic: estimate demand, define an objective, optimize.