ML-driven dynamic pricing for e-commerce using XGBoost, price elasticity modelling, and a validated A/B test simulation.
| Metric | Value |
|---|---|
| Dataset size | 500K+ transactions (bootstrap-augmented from ~173K real Kaggle rows) |
| Elasticity model R² (price ratio + seasonality/lag controls) | 0.6011 |
| Elasticity range across segments | −0.73 to −0.79 (all 8 segments inelastic) |
| XGBoost MAPE (holdout, chronological split) | 6.39 % |
| XGBoost R² | 0.9903 |
| vs Static baseline MAPE | 140.70 % → 95.5 % improvement |
| A/B test revenue uplift | +10.8 % (95% CI: 9.6–11.9 %, P(uplift>0) = 1.000) |
On the A/B test design: treatment price is not an XGBoost prediction — that model reconstructs prices already historically charged, so it can't actually optimize anything. Treatment price is the profit-maximizing price from the elasticity model, searched per product within a bounded ±15% of that product's base price. The bound exists because every segment is inelastic (|ε| < 1): profit is mathematically increasing in price with no interior optimum, so an unconstrained search always walks to the edge of whatever range it's given — the bound mirrors real-world price-change guardrails and keeps predictions inside the range the model was actually fit on. Full reasoning in
src/elasticity.pyandsrc/ab_test.py.This is a model-based counterfactual simulation, not a live experiment — it shows what the fitted elasticity model predicts, not confirmed real-world customer behavior.
dynamic-pricing-engine/
├── data/
│ ├── raw/ # Kaggle dataset (gitignored)
│ └── processed/
│ ├── features.csv # Engineered feature matrix
│ └── synthetic_500k.csv # Augmented dataset
├── notebooks/
│ ├── 01_eda.ipynb # Exploratory data analysis
│ ├── 02_feature_engineering.ipynb
│ ├── 03_elasticity_modeling.ipynb
│ ├── 04_xgboost_pricing.ipynb
│ └── 05_ab_test_simulation.ipynb
├── src/
│ ├── config.py # Paths, hyperparameters, constants
│ ├── data_loader.py # Kaggle download + synthetic augmentation
│ ├── features.py # Feature engineering pipeline
│ ├── elasticity.py # Log-log OLS elasticity model + bounded profit optimizer
│ ├── model.py # XGBoost pricing model
│ └── ab_test.py # A/B test simulation
├── streamlit_app/
│ └── app.py # Interactive dashboard
├── models/ # Saved model artefacts (gitignored)
├── reports/ # Generated plots
├── requirements.txt
└── README.md
git clone https://github.com/dswithmohit/dynamic-pricing-engine
cd dynamic-pricing-engine
pip install -r requirements.txt# Place kaggle.json in ~/.kaggle/
chmod 600 ~/.kaggle/kaggle.jsonDataset: noopurbhatt/retail-pricing-and-demand-signals-dataset
01_eda.ipynb → downloads data, builds 500K synthetic dataset
02_feature_engineering.ipynb → builds feature matrix
03_elasticity_modeling.ipynb → fits elasticity model (R² = 0.6011, ε ≈ −0.73 to −0.79)
04_xgboost_pricing.ipynb → trains XGBoost (MAPE = 6.39%, R² = 0.9903)
05_ab_test_simulation.ipynb → simulates A/B test (+10.8% revenue uplift, 95% CI 9.6–11.9%)
streamlit run streamlit_app/app.pyEngineered features from raw transaction data:
| Feature | Description |
|---|---|
price_ratio_to_base |
unit_price / base_price — isolates within-product price movement (promotions/discounts) from cross-product price-level differences; this is what the elasticity model regresses on |
competitor_price_ratio |
unit_price / competitor_price. Note: competitor_price is a simulated proxy (unit_price ± 10% noise), not real market data |
demand_lag_7 |
units_sold lagged 7 rows — short-term momentum |
demand_lag_14 |
units_sold lagged 14 rows — medium-term trend |
rolling_demand_28 |
28-row rolling mean demand |
seasonality_index |
Sinusoidal month signal ∈ [0, 1] — peaks in June, troughs in December |
inventory_level |
Stock on hand — supply constraint signal, decays with cumulative demand |
price_to_avg_category |
Item price vs daily category average |
price_ratio_to_base, competitor_price_ratio, and price_to_avg_category are all computed from unit_price and are excluded from the XGBoost feature set (see config.PRICE_LEAKAGE_COLS) — they're built for the elasticity model and reporting, not as price-prediction inputs.
Per-segment log-log OLS regression:
ln(Q) = α + ε · ln(P_ratio) + controls
where P_ratio = unit_price / base_price, and controls are seasonality_index and demand_lag_7.
- Overall R² = 0.6011 across product segments
- Elasticity consistently −0.73 to −0.79 across all 8 segments — every segment is inelastic, meaning demand falls slower than price rises within a realistic adjustment range
- Because every segment is inelastic, unconstrained profit maximization has no interior optimum —
optimal_price_per_row()searches within a bounded ±15% band instead, which is what drives the A/B test's treatment pricing
XGBoost regressor predicting unit_price from engineered features, after excluding columns that leak the target (see config.PRICE_LEAKAGE_COLS). Time-aware (chronological) train/test split.
- MAPE: 6.39% on holdout, vs. a 140.70% static-baseline MAPE (95.5% improvement)
- R²: 0.9903
- Feature importance is dominated by
base_price(~82%) andpromotion_type(~11%) —unit_pricerarely deviates more than ~15–20% frombase_pricein this dataset, so the model mainly learns a promotion-driven adjustment on a strong anchor price rather than reacting primarily to demand/inventory signals
Compares static (historical mean) pricing against elasticity-optimized pricing, with both arms scored through the same fitted demand curve for a fair comparison.
- +10.8% revenue uplift over static pricing
- 95% CI: [9.6%, 11.9%] — excludes zero; P(uplift > 0) = 1.000
- Treatment price = profit-maximizing price per product, bounded to ±15% of base price, assuming a 40% gross margin (
COST_MARGIN_FACTORinconfig.py, since the dataset has no real cost column)
Python · XGBoost · scikit-learn · pandas · NumPy · Streamlit · Plotly · Matplotlib