Building a Simple Pricing Model in Python

python
modeling
tutorial
From demand estimation to price optimization — a hands-on walkthrough of building your first dynamic pricing model.
Author

Alek Racicot

Published

February 14, 2026

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:

  1. Generate (or collect) historical price-demand data
  2. Estimate a demand curve
  3. 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.