The business problem
McKinsey research shows that a 1% improvement in pricing yields 8-11% improvement in operating profit, more than any other business lever. Yet most companies set prices using cost-plus rules or simple competitive matching. They miss the cross-product dynamics: raising the price of one product shifts demand to substitutes and affects the demand for complements.
Optimal pricing requires understanding the entire product demand network simultaneously. When Amazon changes the price of a laptop, it affects demand for laptop bags, mice, monitors, and competing laptops. These cross-product effects make pricing a graph problem.
Why flat ML fails
- Independent optimization: Flat models optimize each product's price independently. Raising Product A's price might maximize A's margin but cannibalize B's demand, reducing total portfolio margin.
- No cross-elasticity: The cross-price elasticity between substitutes is the most important pricing signal, and flat models cannot capture it without extensive feature engineering.
- Competitive blindness: Competitor price changes affect your demand. The graph connects your products to competitor products, propagating competitive signals automatically.
- Bundle effects: Complementary products have positive cross-elasticity: reducing the price of printers increases ink cartridge demand. These effects require graph-level reasoning.
The relational schema
Node types:
Product (id, cost, current_price, category, inventory)
Competitor (id, brand, market_share)
Segment (id, price_sensitivity, size, growth_rate)
Edge types:
Product --[substitute_of]--> Product (cross_elasticity)
Product --[complement_of]--> Product (bundle_lift)
Product --[competes_with]--> Competitor (price_gap)
Product --[sold_to]--> Segment (volume, price_paid)Substitute and complement edges encode cross-product demand effects. Competitor edges capture competitive dynamics.
PyG architecture: SAGEConv demand model
import torch
import torch.nn.functional as F
from torch_geometric.nn import SAGEConv, Linear
class PricingGNN(torch.nn.Module):
def __init__(self, node_dim, hidden_dim=128):
super().__init__()
self.node_lin = Linear(node_dim, hidden_dim)
# Aggregate substitute, complement, competitor signals
self.conv1 = SAGEConv(hidden_dim, hidden_dim)
self.conv2 = SAGEConv(hidden_dim, hidden_dim)
# Demand prediction head: given price, predict units sold
self.demand_head = torch.nn.Sequential(
Linear(hidden_dim + 1, 64), # +1 for candidate price
torch.nn.ReLU(),
Linear(64, 1),
)
def encode(self, x, edge_index):
x = F.relu(self.node_lin(x))
x = F.relu(self.conv1(x, edge_index))
x = self.conv2(x, edge_index)
return x
def predict_demand(self, node_emb, candidate_price):
# Concatenate embedding with candidate price
h = torch.cat([node_emb, candidate_price.unsqueeze(-1)], dim=-1)
return F.softplus(self.demand_head(h)) # demand >= 0
def forward(self, x, edge_index, candidate_prices):
z = self.encode(x, edge_index)
demand = self.predict_demand(z, candidate_prices)
return demand
# Portfolio optimization (post-GNN)
# For each product, evaluate demand at multiple price points
# Optimize: max Σ (price_i - cost_i) * demand_i(price_i)
# Subject to: inventory, competitor, and business constraintsThe GNN predicts demand as a function of price and network context. Portfolio optimization runs on top, maximizing total margin across all products simultaneously.
Expected performance
Demand prediction for pricing is a regression task. The right metric is MAPE on held-out demand predictions:
- Cost-plus pricing: Baseline (no demand model)
- LightGBM (independent demand): ~15% MAPE
- GNN (cross-product demand): ~10-12% MAPE
- KumoRFM (zero-shot): ~10% MAPE
Or use KumoRFM in one line
PREDICT units_sold FOR product
USING product, sales, competitor, segmentOne PQL query generates the demand model. Feed predictions at candidate price points into your optimization layer for portfolio-level pricing.