E-Commerce Pricing¶
Problem Statement¶
Determine the optimal sales price for the items on an e-commerce platform.
Full example scenario
A backend engineer at an e-commerce company is tasked with optimizing pricing across a range of electronics products, taking into account supply, demand, customer behavior and shipping costs. What price should be charged for each product, in order to maximize total revenue?
The total quantity sold of each product cannot exceed the available stock.
Demand for each product decreases as its price increases, following a demand-elasticity relationship: \(Q = Q_0 \times (1 - E \times (P - P_0) / P_0)\), where \(Q\) is demand, \(Q_0\) the base demand, \(P_0\) the base price, \(P\) the new price and \(E\) the price elasticity.
Shipping costs are deducted from revenue and vary by product size and weight: \(SC = \text{Base Shipping Cost} + (2 \times \text{Size (kg)} + 1.5 \times \text{Weight (kg)}) \times \text{units sold}\).
Prices must be at least 80% of the base price, to ensure profitability.
The catalog data can be found in
e_commerce_pricing.csv, with the columnsProduct ID,Product Name,Stock Quantity,Base Demand,Base Price,Size (kg),Weight (kg),Base Shipping Cost,Price Elasticity.
What an optimal solution looks like
Optimal price, quantity sold and net revenue per product.
Letters match each product's initial (A = Smartphone A, B = Laptop B, ... J = Mouse J).
One product zoomed in: Smartphone A's demand and net revenue as price changes.
Base price $700 · base demand 300 units · elasticity 1.5 · shipping $5/unit.
Demand falls in a straight line as price rises. Net revenue — (price − shipping) × demand — is the parabola underneath: its peak, at $585.83, is the optimal price. The floor at $560 sits just left of that peak, so it never actually binds for this product.
Built with Modeler¶
The problem above is stated as a single shot: you hand over the whole problem and get a model back. Modeler takes the other route — you start with the business question you actually have, and it works with you until the problem is pinned down, the data is in, the code runs, and the answer holds up when the problem changes.
The tabs below walk the same pricing problem through that route, one Modeler capability at a time.
Instead of writing the whole prompt up front, you start with the business question you actually have. Modeler searches the knowledge base, asks one clarifying question at a time — always the most important one first — and tells you which parts of the model are still missing. The options it offers come from examples in the knowledge base and from how similar problems are usually modeled in your industry, so you can answer with a number instead of prose.
I run pricing for our electronics storefront. I want to set the prices on our product catalog to bring in as much revenue as possible. I know if we raise prices too much we’ll lose sales, and I don’t want to drop prices too far below what we currently charge either. Can you help me build a pricing model for this?
This is a pricing/revenue-optimization problem. Before anything else I need to know exactly what “as much revenue as possible” means — plain revenue, or revenue net of some cost — because that choice changes the objective function itself, not just a constraint.
Great use case — pricing models are a classic optimization application that can deliver real business value. Let me ask a few focused questions to nail down the details before we build the specification.
Question 1: What is your primary objective — maximize total revenue (price times units sold), or maximize total profit (revenue minus costs such as cost of goods or shipping)?
Revenue only — maximize the sum of price times units sold across all products, ignoring costs.
Gross profit — subtract the cost of goods (unit cost) from revenue so the model avoids pricing below cost.
Net profit — also account for shipping or fulfillment costs per unit sold.
Let’s go with option 3 — net revenue, subtracting the per-unit shipping cost for each unit sold, since we don’t track a separate cost-of-goods per product. One more requirement while we’re at it: whatever price the model lands on for a product, it should never go below 80% of that product’s current base price — we don’t want to protect margin at all costs, but we don’t want the model discounting so deep that it looks like a clearance sale.
That’s the objective settled (net revenue, shipping deducted per unit sold) plus a numeric floor, volunteered before I even asked about limits. Recording both. What’s still open is the shape of the demand side — nothing yet says how units sold respond to the price we choose, and that relationship is what turns this from a bounds problem into an actual optimization.
Good, that is saved. Here’s what the specification holds so far:
Objective: maximize total net revenue — sum of (price − shipping cost) × units sold, across all products.
Constraint recorded: price ≥ 80% of base price, per product.
Still open: how demand responds to the price you choose.
That last piece is the most important modeling decision left, so that’s the next question — how should demand change as price moves away from each product’s base price?
Modeler updates the specification after every answer, even while it is still incomplete — so you can always see what it has understood and what it is still waiting on. A few more short rounds like this one settled the rest: a price-elasticity demand curve, no explicit price ceiling beyond where demand hits zero, no cross-product constraints, and a skip-with-warning rule for any product whose data implies non-positive demand at its base price.
The remaining clarifying questions settled the demand model (price-elasticity
driven, no explicit price ceiling beyond where demand hits zero), confirmed
there are no cross-product constraints, and agreed on a skip-with-warning rule
for any product whose data implies non-positive demand at its base price. With
the shape of the problem pinned down, Modeler asked for the actual catalog as a
file rather than as ten rows typed into the chat. Attach e_commerce_pricing.csv
to the conversation and it reads the real shape of your data instead of guessing
at it — entity names, column headers, units and all.
Product ID,Product Name,Stock Quantity,Base Demand,Base Price,Size (kg),Weight (kg),Base Shipping Cost,Price Elasticity
P001,Smartphone A,500,300,700,0.2,0.5,5,1.5
P002,Laptop B,200,150,1200,1.5,2.5,10,1.2
P003,Tablet C,300,250,400,0.5,1.0,6,1.8
P004,Smartwatch D,100,100,150,0.1,0.2,3,2.0
P005,Camera E,100,80,500,0.7,1.2,7,1.6
P006,Headphones F,400,200,100,0.3,0.4,4,2.5
P007,Speaker G,250,120,250,1.0,1.5,8,1.4
P008,Monitor H,180,110,300,3.0,4.0,12,1.3
P009,Keyboard I,300,270,50,0.6,0.7,5,2.2
P010,Mouse J,450,320,30,0.4,0.5,3,2.0
e_commerce_pricing.csv attached
Stock Quantity, Base Demand and Base Price are per-unit figures, in USD for price; Size and Weight are in kg; Price Elasticity is unitless.
Ten rows, one per product, columns matching what we’ve already agreed on for base price, base demand, shipping cost and elasticity. The specification has no open items left after this, so there is nothing to ask before implementing — the fastest honest path is to build, run, and show the result rather than ask a question that has no real answer to wait on.
Got it — 10 products, columns matching base_price, base_demand, base_shipping_cost and price_elasticity exactly as discussed, so nothing needs rescaling. Stock Quantity, Size and Weight are also present in the file but weren’t part of what we’ve discussed modeling yet, so for now I’ve noted them as available-but-unused in the specification’s Input section.
That was the last open item — finalizing the specification, writing the implementation, and running the test suite now.
Where your files live
Everything in the conversation — the file you attached and every artifact Modeler generates from it — lands in the conversation workspace as an attachment. Each one opens in a side panel next to the chat, so you can read the specification while you keep answering questions, or scroll the code while you ask for a change. From that panel you can copy the contents or download the file.
By the end of this conversation the workspace holds six items:
Attachment |
What it is |
|---|---|
|
The product catalog you attached. |
|
The specification, rewritten after every answer. |
|
The |
|
The |
|
Output of the test runs, including the solver logs. |
|
The mathematical formulation, generated on request. |
Starting from files you already have
The same upload path runs in reverse. Attach the files of an existing
optimization project — a specification, a model script, a test file, or any
subset — and ask Modeler to check the project. It completes the
specification where it is thin (asking clarifying questions, as above), fills in
a missing implementation or test suite, reconciles code that has drifted from the
specification, and runs the tests. Point it at a model written for another solver
or another language and it recovers a specification from that file, asks you to
confirm it, then reimplements the model in gurobipy.
The specification is the artifact the questions are building toward, and the one everything else is generated from. Modeler rewrites it after every answer — even while it is still incomplete — so it doubles as the running record of what has been agreed and what is still open. It follows a fixed seven-section structure and is written entirely in business terms: no mathematics, so the person who owns the problem can read it and say whether it is right.
1. Overview
An electronics retailer wants to set optimal prices across its product catalog to maximize total net revenue — revenue minus per-unit shipping costs. Raising prices too high reduces unit sales through a demand-elasticity model, while prices must never fall below 80% of each product’s current base price, so the plan never reads as a clearance sale. Each product is priced independently; there are no cross-product constraints.
2. Objective
Maximize total net revenue across all active products, where net revenue per product is (optimized price − shipping cost) × demand at that price. Demand is a linear function of price, derived from each product’s price-elasticity coefficient: demand = base demand × (1 − elasticity × (price − base price) / base price).
3. Constraints
C1 — Minimum price floor. For each product, the optimized price must be at least 80% of the product’s base price. This prevents deep discounting that would damage brand perception.
C2 — Non-negative demand. For each product, the demand implied by the optimized price must be non-negative. This implicitly caps how high the price can be raised — beyond the point where demand hits zero, a further increase gains nothing.
C3 — Non-negative price. Each product’s price must be non-negative (subsumed in practice by C1, since the floor is always positive).
4. Input
Entity |
Fields |
Values |
|---|---|---|
Product catalog |
product_id, product_name, base_price ($), base_demand (units), base_shipping_cost ($), price_elasticity |
10 products, supplied in |
Products with non-positive base demand are automatically skipped before the model is built, and a warning is logged for each one. Stock Quantity, Size and Weight are present in the source file but are not used by this version of the model.
5. Output
The optimized price recommended for each product.
The forecasted demand at that price.
Net revenue per product, and the total net revenue across the catalog.
The list of any products skipped due to non-positive base demand.
6. Test scenarios
ID |
Scenario |
Acceptance criteria |
|---|---|---|
T1 |
Standard catalog optimization — the full 10-product catalog, all with positive base demand. |
Optimal. All 10 products priced, none skipped, every price at or above its 80% floor, every demand non-negative. |
T2 |
Price floor binding — a product with very high elasticity (10.0), so the unconstrained optimum would fall below the 80% floor. |
Optimal. Price pinned exactly at 80% of base price. |
T3 |
Skipped products with a warning — a catalog mixing one valid product with two that have zero or negative base demand. |
Optimal for the valid product; the two invalid ones are skipped with a logged warning and excluded from the output. |
T4 |
All products skipped — every product in the catalog has non-positive base demand. |
No model is built. The result explicitly signals no active products rather than crashing or returning a degenerate solve. |
T5 |
Zero shipping cost — a product with base_shipping_cost = 0. |
Optimal, net revenue equals price × demand exactly, no division or null-cost error. |
7. Out of scope
Cost of goods (COGS) per product — not tracked, and excluded from the revenue calculation.
Stock quantity constraints — available in the source data, but not modelled.
Cross-product pricing relationships, such as price ordering within a product family.
Category-level revenue floors or targets.
Demand estimation from historical transaction data — elasticity values are supplied as inputs, not fit by the model.
Shipping cost formulas based on size or weight —
base_shipping_costis used directly as a flat per-unit cost.
With the specification settled, Modeler writes the gurobipy implementation.
This is the version that came out of the run-and-fix loop in the Cloud solve
tab, after Modeler ran the tests against it.
"""
Electronics Storefront Pricing Optimizer
=========================================
Maximizes total net revenue across a product catalog by finding optimal prices,
subject to a minimum price floor (80% of base price) and non-negative demand.
Demand model (linear / price-elasticity):
demand(price) = base_demand * (1 - elasticity * (price - base_price) / base_price)
Net revenue per product:
(price - shipping_cost) * demand(price)
Substituting the demand formula into the objective yields a concave quadratic in
price alone, which Gurobi solves to global optimality as a convex QP.
Demand variables are retained only as auxiliary output variables (post-solve).
"""
import warnings
import gurobipy as gp
from gurobipy import GRB
import pandas as pd
# ---------------------------------------------------------------------------
# Optimizer class
# ---------------------------------------------------------------------------
class PricingOptimizer:
"""Optimize product prices to maximize total net revenue for an electronics storefront."""
# Minimum price as a fraction of base price (C1: price floor constraint)
MIN_PRICE_FRACTION = 0.8
def __init__(self, env: gp.Env):
"""Initialize the optimizer with a Gurobi environment."""
self.env = env
self.model: gp.Model = None
# Processed (active) product data
self._products: pd.DataFrame = None
# Gurobi decision variables (indexed by product_id)
self._price_vars: dict = {}
# Results
self._skipped_products: list = []
def __enter__(self):
"""Enter context manager."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context manager and dispose of the Gurobi model."""
if self.model is not None:
self.model.dispose()
return False
# ------------------------------------------------------------------
# Data loading
# ------------------------------------------------------------------
def set_data(self, catalog: pd.DataFrame) -> None:
"""
Load and validate the product catalog.
Expected columns:
product_id, product_name, base_price, base_demand,
base_shipping_cost, price_elasticity
Products with non-positive base_demand are skipped with a warning.
"""
required_cols = {
"product_id", "product_name", "base_price",
"base_demand", "base_shipping_cost", "price_elasticity"
}
missing = required_cols - set(catalog.columns)
if missing:
raise ValueError(f"Catalog is missing required columns: {missing}")
# Identify and skip products with non-positive base demand
invalid_mask = catalog["base_demand"] <= 0
for _, row in catalog[invalid_mask].iterrows():
warnings.warn(
f"Product '{row['product_id']}' ({row['product_name']}) has "
f"non-positive base demand ({row['base_demand']}). Skipping.",
UserWarning,
stacklevel=2,
)
self._skipped_products.append(row["product_id"])
self._products = catalog[~invalid_mask].copy().reset_index(drop=True)
# ------------------------------------------------------------------
# Model building
# ------------------------------------------------------------------
def build_model(self) -> None:
"""Build the Gurobi quadratic pricing model."""
self.model = gp.Model("ElectronicsStorefrontPricing", env=self.env)
self._create_variables()
self._add_constraints()
self._set_objective()
self.model.update()
def _create_variables(self) -> None:
"""
Create one price variable per active product.
price[p] >= MIN_PRICE_FRACTION * base_price[p] (C1: price floor)
price[p] >= 0 (C3: enforced by floor >= 0)
The natural upper bound where demand hits zero is:
price_max[p] = base_price[p] * (1 + 1 / elasticity[p])
Setting this as an explicit upper bound tightens the formulation.
"""
for _, row in self._products.iterrows():
pid = row["product_id"]
p0 = row["base_price"]
e = row["price_elasticity"]
floor = self.MIN_PRICE_FRACTION * p0
# Price at which demand = 0; beyond this revenue is zero or negative
price_max = p0 * (1.0 + 1.0 / e)
self._price_vars[pid] = self.model.addVar(
lb=floor,
ub=price_max,
name=f"price_{pid}"
)
def _add_constraints(self) -> None:
"""
C1 (minimum price floor): enforced via variable lower bound.
C2 (non-negative demand): enforced via variable upper bound (price <= price_max).
C3 (non-negative price): enforced via lower bound (floor >= 0).
No additional linear constraints are needed.
"""
pass # All constraints are captured as variable bounds above
def _set_objective(self) -> None:
"""
Maximize total net revenue.
For each product p, substituting the demand model into the net revenue:
net_rev(p) = (price - shipping) * demand(price)
= (price - s) * [d0 * (1 - e * (price - p0) / p0)]
where:
d0 = base_demand, p0 = base_price, e = elasticity, s = shipping_cost
Expanding:
Let A = d0 * (1 + e) (intercept of demand)
B = d0 * e / p0 (slope magnitude of demand w.r.t. price)
net_rev(p) = (price - s) * (A - B * price)
= A * price - B * price^2 - s * A + s * B * price
= -B * price^2 + (A + s*B) * price - s*A
The -B * price^2 term makes this a concave quadratic in price (B > 0),
so the problem is a convex QP and Gurobi solves it to global optimality.
"""
obj = gp.QuadExpr()
for _, row in self._products.iterrows():
pid = row["product_id"]
d0 = row["base_demand"]
p0 = row["base_price"]
e = row["price_elasticity"]
s = row["base_shipping_cost"]
p_var = self._price_vars[pid]
A = d0 * (1.0 + e) # demand intercept
B = d0 * e / p0 # demand slope (positive)
# -B * price^2 + (A + s*B) * price - s*A
obj += -B * p_var * p_var + (A + s * B) * p_var - s * A
self.model.setObjective(obj, GRB.MAXIMIZE)
# ------------------------------------------------------------------
# Solving
# ------------------------------------------------------------------
def solve(self) -> dict:
"""
Solve the model and return the solution dictionary.
Returns
-------
dict with keys:
status : int - Gurobi status code
is_optimal : bool - True if an optimal solution was found
objective_value : float or None
pricing : dict - per-product results keyed by product_id
skipped_products : list - product_ids skipped due to bad data
"""
# Handle empty catalog (all products skipped)
if self._products is None or self._products.empty:
return {
"status": GRB.LOADED,
"is_optimal": False,
"objective_value": None,
"pricing": {},
"skipped_products": self._skipped_products,
}
self.model.optimize()
status = self.model.Status
is_optimal = (status == GRB.OPTIMAL)
if not is_optimal:
return {
"status": status,
"is_optimal": False,
"objective_value": None,
"pricing": {},
"skipped_products": self._skipped_products,
}
# Extract per-product results
pricing = {}
for _, row in self._products.iterrows():
pid = row["product_id"]
d0 = row["base_demand"]
p0 = row["base_price"]
e = row["price_elasticity"]
ship = row["base_shipping_cost"]
opt_p = self._price_vars[pid].X
# Compute demand analytically from the optimized price
opt_d = d0 * (1.0 - e * (opt_p - p0) / p0)
pricing[pid] = {
"product_name": row["product_name"],
"base_price": p0,
"optimal_price": opt_p,
"optimal_demand": opt_d,
"net_revenue": (opt_p - ship) * opt_d,
}
return {
"status": status,
"is_optimal": True,
"objective_value": self.model.ObjVal,
"pricing": pricing,
"skipped_products": self._skipped_products,
}
# ------------------------------------------------------------------
# Display
# ------------------------------------------------------------------
def display_results(self, solution: dict) -> None:
"""Print a formatted summary of the pricing solution."""
if not solution["is_optimal"]:
print("No optimal solution found.")
if solution["skipped_products"]:
print(f"Skipped products: {solution['skipped_products']}")
return
print("=" * 72)
print("ELECTRONICS STOREFRONT - OPTIMAL PRICING SOLUTION")
print("=" * 72)
print(f"Total Net Revenue: ${solution['objective_value']:,.2f}")
print()
header = (
f"{'Product':<20} {'Base Price':>10} {'Opt. Price':>10} "
f"{'Demand':>8} {'Net Rev.':>12}"
)
print(header)
print("-" * 72)
for pid, info in solution["pricing"].items():
print(
f"{info['product_name']:<20} "
f"${info['base_price']:>9,.2f} "
f"${info['optimal_price']:>9,.2f} "
f"{info['optimal_demand']:>8.1f} "
f"${info['net_revenue']:>11,.2f}"
)
print("=" * 72)
if solution["skipped_products"]:
print(
f"Skipped (non-positive base demand): {solution['skipped_products']}"
)
# ---------------------------------------------------------------------------
# Main entry point — runs with the provided e_commerce_pricing.csv catalog
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import os
# Load catalog data
csv_path = os.path.join(os.path.dirname(__file__), "e_commerce_pricing.csv")
raw = pd.read_csv(csv_path)
# Rename CSV columns to the model's expected field names
catalog = raw.rename(columns={
"Product ID": "product_id",
"Product Name": "product_name",
"Base Price": "base_price",
"Base Demand": "base_demand",
"Base Shipping Cost": "base_shipping_cost",
"Price Elasticity": "price_elasticity",
})[["product_id", "product_name", "base_price", "base_demand",
"base_shipping_cost", "price_elasticity"]]
# Create Gurobi environment and run optimizer
with gp.Env(empty=True) as env:
env.setParam("OutputFlag", 1)
env.setParam("TimeLimit", 60)
env.start()
with PricingOptimizer(env) as optimizer:
optimizer.set_data(catalog)
optimizer.build_model()
solution = optimizer.solve()
optimizer.display_results(solution)
Alongside the implementation, Modeler writes a pytest suite with one test per
scenario in the specification. Each test references its scenario identifier, and
uses small hard-coded catalog data and a short solver time limit so the suite
runs in well under a second.
"""
Unit tests for the Electronics Storefront Pricing Optimizer.
Test scenarios correspond directly to those defined in specification.md:
T1 - Standard catalog optimization (10 products, all valid)
T2 - Price floor binding (very high elasticity forces price to floor)
T3 - Skipped products warning (some products have non-positive base demand)
T4 - All products skipped / empty catalog
T5 - Zero shipping cost edge case
"""
import time
import warnings
import pytest
import pandas as pd
import gurobipy as gp
from gurobipy import GRB
from e_commerce_pricing_modeler import PricingOptimizer
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def make_env() -> gp.Env:
"""Create a silent Gurobi environment with a 10-second time limit."""
env = gp.Env(empty=True)
env.setParam("OutputFlag", 0)
env.setParam("TimeLimit", 10)
env.start()
return env
def run_optimizer(catalog: pd.DataFrame) -> dict:
"""
Convenience wrapper: build, solve, and return the solution dict.
Also returns the optimizer instance so callers can inspect it.
"""
with make_env() as env:
with PricingOptimizer(env) as opt:
opt.set_data(catalog)
opt.build_model()
solution = opt.solve()
return solution
# ---------------------------------------------------------------------------
# Sample catalog fixtures
# ---------------------------------------------------------------------------
FULL_CATALOG = pd.DataFrame([
{"product_id": "P001", "product_name": "Smartphone A", "base_price": 700, "base_demand": 300, "base_shipping_cost": 5, "price_elasticity": 1.5},
{"product_id": "P002", "product_name": "Laptop B", "base_price": 1200, "base_demand": 150, "base_shipping_cost": 10, "price_elasticity": 1.2},
{"product_id": "P003", "product_name": "Tablet C", "base_price": 400, "base_demand": 250, "base_shipping_cost": 6, "price_elasticity": 1.8},
{"product_id": "P004", "product_name": "Smartwatch D", "base_price": 150, "base_demand": 100, "base_shipping_cost": 3, "price_elasticity": 2.0},
{"product_id": "P005", "product_name": "Camera E", "base_price": 500, "base_demand": 80, "base_shipping_cost": 7, "price_elasticity": 1.6},
{"product_id": "P006", "product_name": "Headphones F", "base_price": 100, "base_demand": 200, "base_shipping_cost": 4, "price_elasticity": 2.5},
{"product_id": "P007", "product_name": "Speaker G", "base_price": 250, "base_demand": 120, "base_shipping_cost": 8, "price_elasticity": 1.4},
{"product_id": "P008", "product_name": "Monitor H", "base_price": 300, "base_demand": 110, "base_shipping_cost": 12, "price_elasticity": 1.3},
{"product_id": "P009", "product_name": "Keyboard I", "base_price": 50, "base_demand": 270, "base_shipping_cost": 5, "price_elasticity": 2.2},
{"product_id": "P010", "product_name": "Mouse J", "base_price": 30, "base_demand": 320, "base_shipping_cost": 3, "price_elasticity": 2.0},
])
TOL = 1e-4 # numerical tolerance for floating-point checks
# ---------------------------------------------------------------------------
# T1: Standard catalog optimization
# ---------------------------------------------------------------------------
def test_T1_standard_optimization():
"""
T1: Full 10-product catalog. All products have positive base demand.
Expects an optimal solution with all products priced above the floor
and non-negative demand.
"""
start = time.time()
sol = run_optimizer(FULL_CATALOG)
elapsed = time.time() - start
assert elapsed < 10, f"Test exceeded time budget: {elapsed:.1f}s"
assert sol["is_optimal"] is True
assert sol["objective_value"] is not None
assert sol["objective_value"] > 0
# All 10 products must appear in results
assert len(sol["pricing"]) == 10
assert sol["skipped_products"] == []
for pid, info in sol["pricing"].items():
# C1: optimal_price >= 80% of base_price
assert info["optimal_price"] >= 0.8 * info["base_price"] - TOL, (
f"{pid}: price {info['optimal_price']:.4f} below floor "
f"{0.8 * info['base_price']:.4f}"
)
# C2: non-negative demand
assert info["optimal_demand"] >= -TOL, (
f"{pid}: demand {info['optimal_demand']:.4f} is negative"
)
# Objective must equal sum of per-product net revenues
total_check = sum(v["net_revenue"] for v in sol["pricing"].values())
assert abs(sol["objective_value"] - total_check) < TOL * max(abs(sol["objective_value"]), 1), (
f"Objective {sol['objective_value']:.4f} != sum of net revenues {total_check:.4f}"
)
# ---------------------------------------------------------------------------
# T2: Price floor binding
# ---------------------------------------------------------------------------
def test_T2_price_floor_binding():
"""
T2: Very high elasticity makes unconstrained optimum fall below 80% floor.
The constraint C1 must be active and price pinned exactly at the floor.
"""
catalog = pd.DataFrame([{
"product_id": "PX01",
"product_name": "High-Elastic Item",
"base_price": 100.0,
"base_demand": 200.0,
"base_shipping_cost": 5.0,
"price_elasticity": 10.0, # very high elasticity
}])
start = time.time()
sol = run_optimizer(catalog)
elapsed = time.time() - start
assert elapsed < 10, f"Test exceeded time budget: {elapsed:.1f}s"
assert sol["is_optimal"] is True
opt_price = sol["pricing"]["PX01"]["optimal_price"]
expected_floor = 0.8 * 100.0 # = 80.0
assert abs(opt_price - expected_floor) < TOL, (
f"Expected price floor 80.0, got {opt_price:.6f}"
)
# ---------------------------------------------------------------------------
# T3: Skipped products warning
# ---------------------------------------------------------------------------
def test_T3_skipped_products_warning():
"""
T3: Catalog contains one valid product and two with non-positive base demand.
The invalid products must be skipped with warnings; only the valid one is optimized.
"""
catalog = pd.DataFrame([
{"product_id": "PA01", "product_name": "Valid Product", "base_price": 200.0, "base_demand": 100.0, "base_shipping_cost": 5.0, "price_elasticity": 1.5},
{"product_id": "PA02", "product_name": "Zero Demand Product", "base_price": 150.0, "base_demand": 0.0, "base_shipping_cost": 4.0, "price_elasticity": 1.2},
{"product_id": "PA03", "product_name": "Negative Demand Product","base_price": 80.0, "base_demand": -10.0, "base_shipping_cost": 3.0, "price_elasticity": 1.8},
])
start = time.time()
with pytest.warns(UserWarning):
sol = run_optimizer(catalog)
elapsed = time.time() - start
assert elapsed < 10, f"Test exceeded time budget: {elapsed:.1f}s"
assert sol["is_optimal"] is True
# Only PA01 should be in pricing output
assert "PA01" in sol["pricing"]
assert "PA02" not in sol["pricing"]
assert "PA03" not in sol["pricing"]
# PA02 and PA03 must be in skipped list
assert "PA02" in sol["skipped_products"]
assert "PA03" in sol["skipped_products"]
# PA01 price must respect the 80% floor
opt_price = sol["pricing"]["PA01"]["optimal_price"]
assert opt_price >= 0.8 * 200.0 - TOL, (
f"PA01 price {opt_price:.4f} below floor {0.8 * 200.0:.4f}"
)
# ---------------------------------------------------------------------------
# T4: All products skipped / empty catalog
# ---------------------------------------------------------------------------
def test_T4_all_products_skipped():
"""
T4: All products have non-positive base demand. No model is built;
the response must signal no active products rather than crashing.
"""
catalog = pd.DataFrame([
{"product_id": "PB01", "product_name": "Dead Product 1", "base_price": 100.0, "base_demand": 0.0, "base_shipping_cost": 5.0, "price_elasticity": 1.5},
{"product_id": "PB02", "product_name": "Dead Product 2", "base_price": 200.0, "base_demand": -5.0, "base_shipping_cost": 8.0, "price_elasticity": 1.2},
])
start = time.time()
with pytest.warns(UserWarning):
sol = run_optimizer(catalog)
elapsed = time.time() - start
assert elapsed < 10, f"Test exceeded time budget: {elapsed:.1f}s"
assert sol["is_optimal"] is False
assert sol["objective_value"] is None
assert sol["pricing"] == {}
# Both products must appear in skipped list
assert "PB01" in sol["skipped_products"]
assert "PB02" in sol["skipped_products"]
# ---------------------------------------------------------------------------
# T5: Zero shipping cost edge case
# ---------------------------------------------------------------------------
def test_T5_zero_shipping_cost():
"""
T5: A product with zero shipping cost must be handled without errors.
Net revenue for this product equals price * demand.
"""
catalog = pd.DataFrame([{
"product_id": "PC01",
"product_name": "Free-Ship Item",
"base_price": 100.0,
"base_demand": 150.0,
"base_shipping_cost": 0.0,
"price_elasticity": 1.5,
}])
start = time.time()
sol = run_optimizer(catalog)
elapsed = time.time() - start
assert elapsed < 10, f"Test exceeded time budget: {elapsed:.1f}s"
assert sol["is_optimal"] is True
info = sol["pricing"]["PC01"]
# C1: price floor
assert info["optimal_price"] >= 0.8 * 100.0 - TOL, (
f"Price {info['optimal_price']:.4f} below floor 80.0"
)
# C2: non-negative demand
assert info["optimal_demand"] >= -TOL, (
f"Demand {info['optimal_demand']:.4f} is negative"
)
# Net revenue = price * demand (shipping = 0)
expected_rev = info["optimal_price"] * info["optimal_demand"]
assert abs(info["net_revenue"] - expected_rev) < TOL, (
f"Net revenue {info['net_revenue']:.4f} != price * demand {expected_rev:.4f}"
)
Generating code and tests is the easy half. Modeler also runs the suite in an isolated environment created fresh for this conversation, solving against Gurobi in the cloud — you do not need Gurobi installed locally, and you do not need a license of your own to see the model solve. It reads the output and fixes what it finds, so what you get back is a model that has been executed, not one that merely looks plausible.
The specification had no open items left by the time the catalog file arrived,
and attaching e_commerce_pricing.csv doubled as the go-ahead — the same
message that gave Modeler the data was read as authorization to finalize the
specification, implement it, write tests, run them, and report back.
Ten products, an objective that is a concave quadratic once the demand
formula is substituted in, and two bounds per price variable — nothing
here needs an integer variable or an extra linear constraint. Writing
the PricingOptimizer class and a pytest suite covering T1-T5, then
running the tests.
All 5 tests pass cleanly on the first run — no failures to chase down. Here is the run and the full summary of what was built.
$ pytest test_pricing_optimizer.py -q
.....
5 passed in 0.35s
Gurobi Optimizer version 13.0.2 build v13.0.2rc1 (armlinux64)
Optimize a model with 0 rows, 10 columns and 0 nonzeros
Model fingerprint: 0x8f2c11a4
Model has 10 quadratic objective terms
Coefficient statistics:
Matrix range [0e+00, 0e+00]
QObjective range [3e+00, 2e+01]
Objective range [2e+02, 2e+03]
Bounds range [2e+01, 1e+03]
RHS range [0e+00, 0e+00]
Presolve time: 0.00s
Presolved: 10 rows, 10 columns, 20 nonzeros
Presolved model has 10 quadratic objective terms
Ordering time: 0.00s
Barrier statistics:
AA' NZ : 0.000e+00
Factor NZ : 1.000e+01
Factor Ops : 1.000e+01 (less than 1 second per iteration)
Threads : 1
Objective Residual
Iter Primal Dual Primal Dual Compl Time
0 6.85471e+05 6.85471e+05 0.00e+00 0.00e+00 0.00e+00 0s
1 6.68941e+05 6.69017e+05 0.00e+00 0.00e+00 1.14e-06 0s
2 6.69000e+05 6.69000e+05 0.00e+00 0.00e+00 8.02e-10 0s
Barrier solved model in 2 iterations and 0.00 seconds (0.00 work units)
Optimal objective 6.69000000e+05
Here is a full summary of everything that was built:
Results
Optimal solution — total net revenue $669,000 across the 10-product catalog, none skipped:
Product |
Base price |
Optimal price |
Demand |
Net revenue |
|---|---|---|---|---|
Smartphone A |
$700.00 |
$585.83 |
373.4 |
$216,879 |
Laptop B |
$1,200.00 |
$1,105.00 |
164.2 |
$179,854 |
Tablet C |
$400.00 |
$320.00 |
340.0 |
$106,760 |
Smartwatch D |
$150.00 |
$120.00 |
140.0 |
$16,380 |
Camera E |
$500.00 |
$409.75 |
103.1 |
$41,525 |
Headphones F |
$100.00 |
$80.00 |
300.0 |
$22,800 |
Speaker G |
$250.00 |
$218.29 |
141.3 |
$29,716 |
Monitor H |
$300.00 |
$271.38 |
123.6 |
$32,070 |
Keyboard I |
$50.00 |
$40.00 |
388.8 |
$13,608 |
Mouse J |
$30.00 |
$24.00 |
448.0 |
$9,408 |
Five products — Tablet C, Smartwatch D, Headphones F, Keyboard I and Mouse J — land exactly at the 80% price floor; the other five settle above it, closer to their base price.
Files created
File |
Description |
|---|---|
|
Complete problem specification |
|
Input product catalog |
|
Gurobipy implementation ( |
|
Pytest suite covering scenarios T1-T5 |
Test results
Test |
Scenario |
Result |
|---|---|---|
T1 |
Standard catalog — 10 products priced, $669,000 total net revenue |
PASSED |
T2 |
Price floor binding — high-elasticity product pinned at 80% |
PASSED |
T3 |
Skipped products — two invalid products dropped with a warning |
PASSED |
T4 |
All products skipped — empty catalog handled without crashing |
PASSED |
T5 |
Zero shipping cost — net revenue equals price times demand exactly |
PASSED |
Would you like to adjust anything, add a constraint, or generate the mathematical formulation?
That log lands in the workspace as execution.log alongside the code, so the
solve is auditable rather than asserted.
The loop ends one of two ways. Either every test passes, as here — or Modeler concludes a failure reflects a genuine contradiction between the specification, the implementation and the tests. In that case it stops instead of guessing, tells you what it found, and asks which side is the one to correct.
What the cloud environment gives you, and what it does not
The environment is built for validating a model, not for running production
workloads. Python 3 is available with gurobipy, gurobipy-pandas,
numpy, pandas, scipy and pytest pre-installed. Within that:
Solves use a Gurobi trial license, capped at 2,000 variables and 2,000 constraints. This model, at 10 columns and 0 explicit rows, is nowhere near the cap — but a full-scale catalog with thousands of SKUs could be.
Execution is limited to 60 seconds per command, with tight CPU and memory limits. This is why the generated tests use small hard-coded data and a short solver time limit.
Additional packages cannot be installed, there is no internet access, and Compute Server and Instant Cloud cannot be used.
So treat a passing suite as evidence that the model is right, not as a performance measurement. Download the implementation and run it against your own license and your own full data set for that. See Solving limits for the details.
Business problems do not hold still. The specification above puts stock quantity constraints out of scope — a reasonable simplification, until someone points out that the catalog data already has a Stock Quantity column sitting right there, unused. Ask for the change in plain language and Modeler updates the specification first, then propagates it to the implementation and the tests, in that order.
One more thing — I’d like the model to also respect our stock limits. Demand for any product shouldn’t exceed what we currently have in stock for it; we already have a Stock Quantity column in the catalog data that isn’t being used yet.
This is a scope change, not a data correction: stock quantity
constraints are currently listed under “Out of scope”. Updating the
specification first — remove that bullet, add a stock_quantity field
to both the input and output tables, add constraint C4, and a new test
scenario for it. Then propagate to the implementation and tests, and
re-run the whole suite.
That moves stock quantity constraints from out of scope into the model. Here is what changed, drawn directly from the updated files:
Specification — added constraint C4: Stock Capacity: “For each product, the forecasted demand at the optimized price must not exceed the product’s available stock quantity. This prevents the model from implicitly planning to sell more units than are on hand.” Added
stock_quantityto the input and output tables. Updated T1-T5 to include a non-binding stock value. Added test scenario T6. Removed “Stock quantity constraints” from Out of Scope.Implementation — the entire body of
_add_constraintschanged from a singlepass(all constraints were bounds) to a loop adding one explicit linear constraint per product.Tests — all fixtures gained a
stock_quantitycolumn, and a newtest_T6_stock_capacity_bindingpins the constraint down directly.
All 6 tests pass. Would you like anything else, or is there something to adjust?
Specification, then code, then tests
The new constraint, quoted exactly as it now reads in specification.md:
C4: Stock Capacity
For each product, the forecasted demand at the optimized price must not
exceed the product's available stock quantity. This prevents the model
from implicitly planning to sell more units than are on hand.
The diff to _add_constraints:
def _add_constraints(self) -> None:
"""
C1 (minimum price floor): enforced via variable lower bound in _create_variables.
C2 (non-negative demand): enforced via variable upper bound (price <= price_max).
C3 (non-negative price): enforced via lower bound (floor >= 0).
-
- No additional linear constraints are needed.
+
+ C4 (stock capacity): demand(price) <= stock_quantity for each product.
+ Expanding the linear demand model:
+ d0 * (1 - e * (price - p0) / p0) <= stock_qty
+ This is a linear constraint in the price variable.
"""
- pass # All constraints are captured as variable bounds above
+ for _, row in self._products.iterrows():
+ pid = row["product_id"]
+ d0 = row["base_demand"]
+ p0 = row["base_price"]
+ e = row["price_elasticity"]
+ sq = row["stock_quantity"]
+ p_var = self._price_vars[pid]
+
+ # C4: demand(price) <= stock_quantity
+ self.model.addConstr(
+ d0 * (1 - e * (p_var - p0) / p0) <= sq,
+ name=f"stock_{pid}"
+ )
The new test, added alongside updated fixtures for T1-T5:
def test_T6_stock_capacity_binding():
"""
T6: A product with very limited stock (10 units) vs base demand (100 units).
The stock cap forces price upward until demand equals the stock limit.
With base_price=100, base_demand=100, elasticity=1.5, shipping=5, stock=10:
demand(price) = 100 * (1 - 1.5*(price-100)/100) = 250 - 1.5*price
demand = 10 => price = (250 - 10) / 1.5 = 160.0
"""
catalog = pd.DataFrame([{
"product_id": "PD01",
"product_name": "Low-Stock Item",
"base_price": 100.0,
"base_demand": 100.0,
"stock_quantity": 10.0,
"base_shipping_cost": 5.0,
"price_elasticity": 1.5,
}])
start = time.time()
sol = run_optimizer(catalog)
elapsed = time.time() - start
assert elapsed < 10, f"Test exceeded time budget: {elapsed:.1f}s"
assert sol["is_optimal"] is True
info = sol["pricing"]["PD01"]
assert info["optimal_demand"] <= 10.0 + TOL, (
f"Demand {info['optimal_demand']:.4f} exceeds stock limit 10"
)
assert info["optimal_demand"] >= 10.0 - TOL, (
f"Expected binding stock constraint: demand ~10, got {info['optimal_demand']:.4f}"
)
assert info["optimal_price"] >= 0.8 * 100.0 - TOL
Modeler re-runs the whole suite, not just the new test — the point of propagating in this order is that nothing is left stale behind the change.
Where it stops and asks
The mechanical part of the change was clean. But asking a follow-up question about how C4 interacts with the existing price floor surfaced a real gap that none of T1-T5 had ever exercised, because none of them needed to:
Quick question on that: for a product where even at the 80% price floor, demand would still be higher than available stock — does the model raise price above the floor to ration demand down to stock, or could that create a conflict with anything else we’ve set up?
The algebra works out cleanly for any positive stock quantity — C4’s implied minimum price stays below the zero-demand price, so the feasible range narrows but never closes. But walking through that algebra exposes the one case it does not cover: a stock quantity of zero or less, which the specification never anticipated because the field did not exist until this conversation. Worth surfacing before calling this settled.
The model raises price to ration demand down to stock, and there is no conflict there — at the 80% floor, demand is at its highest feasible value, so if stock is tighter than that, C4’s implied minimum price simply exceeds the C1 floor and becomes the binding lower bound instead. C2 (the zero-demand ceiling) stays clear of this too, since the stock-implied minimum is always below it as long as stock is positive.
The one genuine edge case: if stock_quantity <= 0 is present in
the data, the feasible price range for that product is empty — nothing
currently guards against it.
Reading |
Behavior for stock ≤ 0 |
Outcome |
|---|---|---|
Leave unguarded |
Model tries to build a variable with lb > ub |
Gurobi raises an error; the whole catalog fails to solve |
Skip with warning |
Same treatment as non-positive base demand |
That one product is excluded; the rest solve normally |
Would you like me to add a validation that skips products with non-positive stock quantity, with a warning, the same way we already handle non-positive base demand?
Yes, please add that guard — skip any product with non-positive stock quantity, with a warning, the same way we handle non-positive base demand. Please show me the updated numbers on our actual 10-product catalog plus confirm the tests still pass.
This is the payoff of a spec-first tool: a contradiction the new field could have introduced is surfaced and resolved explicitly, rather than silently producing a crash the first time real data happens to include a zero.
The answer flips
All 7 tests pass with the guard in place. On the real catalog — where every stock quantity is positive, so the guard itself never fires — the stock cap still changes the plan for four products, because their unconstrained demand already outran what’s on the shelf:
Product |
Stock |
Price before C4 |
Price after C4 |
Demand after C4 |
Change |
|---|---|---|---|---|---|
Tablet C |
300 |
$320.00 |
$355.56 |
300.0 |
C4 binding — price raised, demand rationed |
Smartwatch D |
100 |
$120.00 |
$150.00 |
100.0 |
C4 binding — price raised to base price |
Camera E |
100 |
$409.75 |
$421.88 |
100.0 |
C4 binding — price raised, demand rationed |
Keyboard I |
300 |
$40.00 |
$47.47 |
300.0 |
C4 binding — price raised, demand rationed |
Total net revenue drops from $669,000 to $664,524 — a $4,476 (0.7%) cost of respecting stock, against a plan that was, until now, quietly assuming inventory it didn’t have. The other six products are unaffected: their unconstrained optimal demand already fit within stock on hand.
The specification, the implementation and the tests never mention mathematics — that is deliberate, so the business owner can read them. But you may still need the formulation: to review the model, to document it, or to hand it to someone who thinks in constraints rather than in code. Ask at any point and Modeler derives one from the current specification and implementation, in Markdown with LaTeX, and saves it to the workspace with everything else.
Sets and indices
\(\mathcal{P}\): set of active products — positive base demand and positive stock quantity.
\(p \in \mathcal{P}\): index for a single product.
Parameters
For each product \(p \in \mathcal{P}\):
\(p^0_p\): base price (USD).
\(d^0_p\): base demand (units sold at base price).
\(e_p\): price elasticity coefficient (unitless, \(e_p > 0\)).
\(s_p\): per-unit shipping cost (USD).
\(q_p\): stock quantity (maximum units available).
\(\alpha\): minimum price fraction; \(\alpha = 0.80\).
Two derived parameters simplify the objective expansion:
Decision variables
\(x_p\): continuous — the price to charge for product \(p\) (USD).
Demand is not an independent decision — it follows directly from price:
with the price at which demand reaches zero given by \(x^{\max}_p = p^0_p \cdot \left(1 + \frac{1}{e_p}\right)\).
Objective function
Maximize total net revenue (price minus shipping, times units sold), expanding by substituting the demand function:
Since \(B_p > 0\), this is a strictly concave quadratic in each \(x_p\) — a convex QP, solved to certified global optimality by Gurobi’s barrier method.
Constraints
Complete model
Additional notes
Separability. No constraint links prices across products, so the QP fully decomposes into \(|\mathcal{P}|\) independent single-variable problems. The closed-form unconstrained optimum for each product is \(x^*_p = \frac{p^0_p \cdot (1 + e_p) + s_p \cdot e_p}{2 \cdot e_p}\). If this falls below the C1 floor, the price is pinned at \(\alpha \cdot p^0_p\); if it implies demand above \(q_p\), C4 becomes the binding lower bound and price is raised to ration demand to stock; otherwise \(x^*_p\) is the optimal price.
Implementation. C1, C2 and C3 are encoded as variable bounds (
lb/ub) in Gurobi; C4 is the only explicit linear constraint row, one per product.Feasibility precondition. C4 assumes \(q_p > 0\); products with non-positive stock are excluded from \(\mathcal{P}\) up front (the same treatment as non-positive base demand), rather than left to produce an empty feasible range.
Note that this formulates the current model, including the stock-capacity constraint (C4) added in the Changing the model tab. The version before that change is identical except for the absence of C4 and the \(q_p\) parameter.
### Sets and indices
- $\mathcal{P}$: set of active products — positive base demand and positive
stock quantity.
- $p \in \mathcal{P}$: index for a single product.
### Parameters
For each product $p \in \mathcal{P}$:
- $p^0_p$: base price (USD).
- $d^0_p$: base demand (units sold at base price).
- $e_p$: price elasticity coefficient (unitless, $e_p > 0$).
- $s_p$: per-unit shipping cost (USD).
- $q_p$: stock quantity (maximum units available).
- $\alpha$: minimum price fraction; $\alpha = 0.80$.
Two derived parameters simplify the objective expansion:
$$
A_p = d^0_p \cdot (1 + e_p) \qquad \text{(demand intercept)}
$$
$$
B_p = \frac{d^0_p \cdot e_p}{p^0_p} \qquad \text{(demand slope, } B_p > 0 \text{)}
$$
### Decision variables
- $x_p$: continuous — the price to charge for product $p$ (USD).
Demand is not an independent decision — it follows directly from price:
$$
\delta_p(x_p) = d^0_p \cdot \left(1 - e_p \cdot \frac{x_p - p^0_p}{p^0_p}\right) = A_p - B_p \cdot x_p \qquad \forall\, p \in \mathcal{P}
$$
with the price at which demand reaches zero given by
$x^{\max}_p = p^0_p \cdot \left(1 + \frac{1}{e_p}\right)$.
### Objective function
Maximize total net revenue (price minus shipping, times units sold), expanding
by substituting the demand function:
$$
\max \sum_{p \in \mathcal{P}} \left[ -B_p \cdot x_p^2 + (A_p + s_p \cdot B_p) \cdot x_p - s_p \cdot A_p \right]
$$
Since $B_p > 0$, this is a strictly concave quadratic in each $x_p$ — a convex
QP, solved to certified global optimality by Gurobi's barrier method.
### Constraints
$$
x_p \geq \alpha \cdot p^0_p \qquad \forall\, p \in \mathcal{P} \tag{C1: price floor}
$$
$$
x_p \leq p^0_p \cdot \left(1 + \frac{1}{e_p}\right) \qquad \forall\, p \in \mathcal{P} \tag{C2: non-negative demand}
$$
$$
x_p \geq 0 \qquad \forall\, p \in \mathcal{P} \tag{C3: non-negative price}
$$
$$
d^0_p \cdot \left(1 - e_p \cdot \frac{x_p - p^0_p}{p^0_p}\right) \leq q_p \qquad \forall\, p \in \mathcal{P} \tag{C4: stock capacity}
$$
### Complete model
$$
\begin{aligned}
\max_{x_p} \quad & \sum_{p \in \mathcal{P}} \left[ -B_p \cdot x_p^2 + (A_p + s_p \cdot B_p) \cdot x_p - s_p \cdot A_p \right] \\
\text{s.t.} \quad & \alpha \cdot p^0_p \;\leq\; x_p \;\leq\; p^0_p \cdot \left(1 + \tfrac{1}{e_p}\right), \quad x_p \geq 0 & \forall\, p \in \mathcal{P} \quad \text{(C1, C2, C3)} \\
& d^0_p \cdot \left(1 - e_p \cdot \frac{x_p - p^0_p}{p^0_p}\right) \leq q_p & \forall\, p \in \mathcal{P} \quad \text{(C4)}
\end{aligned}
$$
### Additional notes
- **Separability.** No constraint links prices across products, so the QP
fully decomposes into $|\mathcal{P}|$ independent single-variable problems.
The closed-form unconstrained optimum for each product is
$x^*_p = \frac{p^0_p \cdot (1 + e_p) + s_p \cdot e_p}{2 \cdot e_p}$. If this
falls below the C1 floor, the price is pinned at $\alpha \cdot p^0_p$; if it
implies demand above $q_p$, C4 becomes the binding lower bound and price is
raised to ration demand to stock; otherwise $x^*_p$ is the optimal price.
- **Implementation.** C1, C2 and C3 are encoded as variable bounds (`lb`/`ub`)
in Gurobi; C4 is the only explicit linear constraint row, one per product.
- **Feasibility precondition.** C4 assumes $q_p > 0$; products with
non-positive stock are excluded from $\mathcal{P}$ up front (the same
treatment as non-positive base demand), rather than left to produce an
empty feasible range.
Note that this formulates the current model, including the stock-capacity
constraint (C4) added in the **Changing the model** tab. The version before
that change is identical except for the absence of C4 and the $q_p$ parameter.