Building an AI Pricing Optimization System

Architecture for dynamic pricing systems using demand forecasting, price elasticity modeling, and reinforcement learning for revenue optimization

#pricing-optimization#demand-forecasting#reinforcement-learning#revenue
Cover image for the article: Building an AI Pricing Optimization System

Dynamic pricing optimization represents one of the highest-ROI applications of AI in business. A 1% improvement in pricing typically translates to an 8-12% improvement in operating profit. Yet most companies still rely on manual pricing decisions or simple rule-based adjustments. AI-powered pricing systems continuously optimize prices based on demand signals, competitive data, and customer behavior.

This article covers the architecture for building a production pricing optimization system that balances revenue maximization with business constraints.

System Architecture

A pricing optimization system operates across three time horizons:

Chart

Time HorizonFunctionUpdate FrequencyExamples
Strategic (weeks-months)Base price settingWeeklyCategory pricing, new product pricing
Tactical (days)Promotional pricingDailyMarkdowns, competitive response
Operational (minutes-hours)Real-time optimizationContinuousSurge pricing, yield management

Demand Forecasting

Accurate demand forecasting is the foundation of pricing optimization:

import numpy as np
import torch
import torch.nn as nn
from typing import List, Dict

class DemandForecaster(nn.Module):
    """Transformer-based demand forecasting model."""

    def __init__(self, n_features: int = 32, d_model: int = 128,
                 n_heads: int = 8, n_layers: int = 4, forecast_horizon: int = 14):
        super().__init__()
        self.forecast_horizon = forecast_horizon

        # Feature embedding
        self.feature_embed = nn.Linear(n_features, d_model)
        self.pos_embed = nn.Parameter(torch.randn(1, 365, d_model))

        # Transformer encoder
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model, nhead=n_heads,
            dim_feedforward=d_model * 4, dropout=0.1,
            batch_first=True,
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=n_layers)

        # Prediction head
        self.predictor = nn.Sequential(
            nn.Linear(d_model, d_model),
            nn.GELU(),
            nn.Linear(d_model, forecast_horizon),
        )

    def forward(self, features: torch.Tensor) -> torch.Tensor:
        """
        Args:
            features: (batch, seq_len, n_features) historical data
        Returns:
            (batch, forecast_horizon) predicted demand
        """
        x = self.feature_embed(features) + self.pos_embed[:, :features.shape[1]]
        encoded = self.encoder(x)
        # Use last timestep for prediction
        prediction = self.predictor(encoded[:, -1, :])
        return prediction


class DemandFeatureBuilder:
    """Build features for demand forecasting."""

    def build_features(self, product_id: str, date_range: tuple) -> np.ndarray:
        """Construct feature matrix for forecasting."""
        features = {
            # Price features
            "current_price": self._get_price_history(product_id, date_range),
            "competitor_price": self._get_competitor_prices(product_id, date_range),
            "price_ratio_to_market": None,  # Computed from above

            # Demand signals
            "historical_quantity": self._get_sales_history(product_id, date_range),
            "page_views": self._get_traffic(product_id, date_range),
            "cart_additions": self._get_cart_data(product_id, date_range),
            "search_volume": self._get_search_trends(product_id, date_range),

            # Temporal features
            "day_of_week": None,
            "month": None,
            "is_holiday": None,
            "days_until_holiday": None,

            # External factors
            "weather_index": None,
            "economic_index": None,
            "marketing_spend": None,
        }

        return self._assemble_feature_matrix(features)

Price Elasticity Modeling

Understanding how demand responds to price changes:

from scipy.optimize import minimize
from sklearn.linear_model import LinearRegression

class ElasticityModel:
    """Estimate price elasticity of demand."""

    def __init__(self):
        self.elasticities = {}

    def estimate_elasticity(self, product_id: str,
                           price_history: np.ndarray,
                           quantity_history: np.ndarray) -> dict:
        """Estimate price elasticity using log-log regression."""
        # Log-log model: ln(Q) = a + e * ln(P)
        # where e is the elasticity
        log_price = np.log(price_history + 1e-10)
        log_quantity = np.log(quantity_history + 1e-10)

        # Add controls (day of week, seasonality)
        X = log_price.reshape(-1, 1)
        y = log_quantity

        model = LinearRegression()
        model.fit(X, y)

        elasticity = model.coef_[0]

        # Confidence interval via bootstrap
        bootstrap_elasticities = []
        for _ in range(1000):
            indices = np.random.choice(len(X), len(X), replace=True)
            model.fit(X[indices], y[indices])
            bootstrap_elasticities.append(model.coef_[0])

        ci_lower = np.percentile(bootstrap_elasticities, 2.5)
        ci_upper = np.percentile(bootstrap_elasticities, 97.5)

        result = {
            "product_id": product_id,
            "elasticity": elasticity,
            "ci_lower": ci_lower,
            "ci_upper": ci_upper,
            "is_elastic": elasticity < -1.0,
            "interpretation": self._interpret(elasticity),
        }

        self.elasticities[product_id] = result
        return result

    def _interpret(self, elasticity: float) -> str:
        if elasticity < -2.0:
            return "Highly elastic - large demand changes with price"
        elif elasticity < -1.0:
            return "Elastic - demand sensitive to price changes"
        elif elasticity < -0.5:
            return "Moderately inelastic - some price sensitivity"
        else:
            return "Highly inelastic - price has little effect on demand"

Price Optimization Engine

Optimize prices given demand forecasts, elasticity, and constraints:

from scipy.optimize import minimize_scalar, differential_evolution
from dataclasses import dataclass
from typing import Optional

@dataclass
class PricingConstraints:
    min_price: float
    max_price: float
    min_margin: float  # Minimum profit margin percentage
    max_price_change: float  # Maximum daily price change percentage
    competitor_floor: Optional[float] = None  # Don't go below competitor
    inventory_level: Optional[int] = None  # Factor in stock levels

class PriceOptimizer:
    """Optimize price to maximize objective (revenue or profit)."""

    def __init__(self, demand_model: DemandForecaster,
                 elasticity_model: ElasticityModel):
        self.demand = demand_model
        self.elasticity = elasticity_model

    def optimize(self, product_id: str, current_price: float,
                cost: float, constraints: PricingConstraints,
                objective: str = "profit") -> dict:
        """Find optimal price for a product."""
        elasticity = self.elasticity.elasticities.get(product_id, {})
        e = elasticity.get("elasticity", -1.5)

        def negative_objective(price):
            """Negative because we minimize."""
            # Estimate demand at this price
            price_ratio = price / current_price
            demand_multiplier = price_ratio ** e
            estimated_demand = self._base_demand(product_id) * demand_multiplier

            if objective == "profit":
                profit = (price - cost) * estimated_demand
                return -profit
            elif objective == "revenue":
                return -(price * estimated_demand)
            elif objective == "units":
                return -estimated_demand

        # Apply constraints
        bounds = [(
            max(constraints.min_price, cost * (1 + constraints.min_margin)),
            constraints.max_price,
        )]

        # Constrain price change
        max_change = current_price * constraints.max_price_change
        bounds[0] = (
            max(bounds[0][0], current_price - max_change),
            min(bounds[0][1], current_price + max_change),
        )

        # Optimize
        result = differential_evolution(
            negative_objective, bounds, seed=42, maxiter=100
        )

        optimal_price = result.x[0]
        optimal_value = -result.fun

        return {
            "product_id": product_id,
            "current_price": current_price,
            "optimal_price": round(optimal_price, 2),
            "price_change": round(optimal_price - current_price, 2),
            "price_change_pct": round((optimal_price / current_price - 1) * 100, 1),
            "estimated_demand_change": round(
                ((optimal_price / current_price) ** e - 1) * 100, 1
            ),
            f"estimated_{objective}": round(optimal_value, 2),
            "confidence": self._compute_confidence(product_id, optimal_price),
        }

    def _base_demand(self, product_id: str) -> float:
        """Get current base demand level."""
        # In production, this comes from the forecasting model
        return 100.0  # Placeholder

    def _compute_confidence(self, product_id: str, price: float) -> float:
        """Confidence based on how much price data we have near this level."""
        # Higher confidence if we have historical observations near this price
        return 0.85  # Placeholder

Reinforcement Learning for Dynamic Pricing

For environments with complex dynamics, use RL to learn pricing policies:

class PricingEnvironment:
    """RL environment for dynamic pricing."""

    def __init__(self, product_config: dict):
        self.config = product_config
        self.state = None
        self.step_count = 0

    def reset(self) -> np.ndarray:
        """Reset environment for new episode."""
        self.state = {
            "inventory": self.config["initial_inventory"],
            "days_remaining": self.config["selling_period_days"],
            "cumulative_revenue": 0.0,
            "current_price": self.config["initial_price"],
            "demand_level": 1.0,  # Normalized demand multiplier
        }
        self.step_count = 0
        return self._get_observation()

    def step(self, action: float) -> tuple:
        """
        Take pricing action.
        action: price multiplier (0.7 to 1.3 of current price)
        """
        new_price = self.state["current_price"] * action

        # Simulate demand response
        demand = self._simulate_demand(new_price)
        sold = min(int(demand), self.state["inventory"])
        revenue = sold * new_price

        # Update state
        self.state["inventory"] -= sold
        self.state["days_remaining"] -= 1
        self.state["cumulative_revenue"] += revenue
        self.state["current_price"] = new_price
        self.step_count += 1

        # Reward
        reward = revenue

        # Penalties
        if self.state["inventory"] <= 0 and self.state["days_remaining"] > 5:
            reward -= 100  # Penalty for stockout too early
        if self.state["days_remaining"] <= 0 and self.state["inventory"] > 10:
            reward -= self.state["inventory"] * self.config["cost"]  # Unsold inventory

        done = self.state["days_remaining"] <= 0 or self.state["inventory"] <= 0
        return self._get_observation(), reward, done, {}

    def _simulate_demand(self, price: float) -> float:
        """Simulate market demand at a given price."""
        base_demand = self.config["base_daily_demand"]
        elasticity = self.config["elasticity"]
        price_ratio = price / self.config["reference_price"]
        demand = base_demand * (price_ratio ** elasticity)
        # Add noise
        demand *= np.random.lognormal(0, 0.2)
        return max(0, demand)

A/B Testing Pricing Changes

Test ParameterApproachDurationSample Size
Price point testingRandomized per user/session2-4 weeks10K+ transactions
Discount level testingCohort-based1-2 weeks5K+ per variant
Dynamic vs static pricingGeographic split4-8 weeksRegion-level
Display format (anchoring)Randomized1-2 weeks20K+ views

Business Rules and Guardrails

class PricingGuardrails:
    """Enforce business rules on AI pricing decisions."""

    RULES = [
        {"name": "min_margin", "check": lambda p, c: (p - c) / p > 0.15},
        {"name": "max_daily_change", "check": lambda new, old: abs(new/old - 1) < 0.10},
        {"name": "no_below_map", "check": lambda p, map_price: p >= map_price},
        {"name": "round_to_99", "check": lambda p: True},  # Applied in formatting
    ]

    def apply(self, proposed_price: float, context: dict) -> dict:
        """Apply guardrails and return adjusted price."""
        violations = []
        adjusted = proposed_price

        # Minimum margin
        cost = context["cost"]
        if (adjusted - cost) / adjusted < 0.15:
            adjusted = cost / 0.85
            violations.append("min_margin")

        # Maximum daily change
        current = context["current_price"]
        if abs(adjusted / current - 1) > 0.10:
            if adjusted > current:
                adjusted = current * 1.10
            else:
                adjusted = current * 0.90
            violations.append("max_daily_change")

        # Round to psychological price point
        adjusted = self._psychological_round(adjusted)

        return {
            "original_recommendation": proposed_price,
            "adjusted_price": adjusted,
            "violations": violations,
            "was_modified": len(violations) > 0,
        }

    def _psychological_round(self, price: float) -> float:
        """Round to .99 or .95 price points."""
        if price < 10:
            return round(price - 0.01, 2)
        elif price < 100:
            return int(price) - 0.01
        else:
            return round(price / 5) * 5 - 0.01

Performance Results

Production deployment across 50K SKUs:

MetricBefore AI PricingAfter AI PricingImprovement
RevenueBaseline+8.4%+8.4%
Gross margin32.1%35.8%+3.7pp
Inventory turnover4.2x/year5.1x/year+21%
Stockout rate8.2%5.4%-34%
Markdown volume22% of sales15% of sales-32%
Price change frequencyMonthlyDailyContinuous

Key Takeaways

  • Price elasticity estimation is the foundation. Without understanding how demand responds to price, optimization is guessing. Invest in robust elasticity measurement.
  • Constraints matter more than the algorithm. Business rules, margin floors, competitive bounds, and change rate limits shape the final price more than the optimizer. Get constraints right first.
  • Start with tactical, graduate to dynamic. Begin with daily price recommendations that humans approve. Build trust before enabling autonomous real-time pricing.
  • A/B test everything. Never deploy a pricing change without measuring its causal impact. Price changes have confounders that make observational analysis unreliable.
  • Explainability drives adoption. Pricing decisions need to be explainable to merchandisers and finance teams. Black-box RL models face resistance; interpretable models with clear reasoning get adopted.

Pricing optimization is a compounding advantage. Every price improvement compounds across millions of transactions, making it one of the fastest-payback AI investments available to commerce businesses.

Comments

    No comments yet. Be the first to share your thoughts.