2 weeks ago
After nearly 15 years working in enterprise supply chain and data engineering — most recently as a Staff Engineer at a large national grocery retailer — I have seen firsthand how retail organizations struggle with one persistent problem: store-level demand forecasting at scale.
The consequences of getting it wrong are real. Overstock leads to waste, especially in perishable categories like meat, dairy, and produce. Understock leads to empty shelves, missed sales, and frustrated customers. Most enterprise retailers have forecasting systems, but they are often built on legacy infrastructure that is slow to adapt, difficult to extend, and opaque to the business users who rely on them.
I wanted to explore whether modern cloud-native tooling — specifically Databricks — could make it meaningfully easier to build a forecasting system that was not just technically sound, but operationally useful: one that translates ML forecasts directly into order recommendations, flags waste and stockout risks, and gives buyers a natural language interface to ask questions without writing SQL.
So I built one from scratch. This article documents what I built, how I built it, what broke along the way, and what I learned.
Build a fully automated prototype retail order forecasting pipeline on Databricks that:
Scope: 10 stores, 20 products, 52 weeks of synthetic data — small enough to build fast, structured enough to reflect real enterprise patterns.
The system follows the Medallion Architecture — an industry-standard data engineering pattern that organizes data into progressively refined layers — extended with an ML layer, a decision engine, and operational outputs.
Raw CSV Files
↓
Bronze Layer → Raw Delta tables (never modified)
↓
Silver Layer → Cleaned, validated, quality-flagged data
↓
Gold Layer → Feature-engineered ML-ready + business-ready tables
↓
ML Models → LightGBM per category, tracked with MLflow
↓
Decision Engine → Order recommendations, risk labels, financial estimates
↓
Dashboard + Genie Space + Workflow AutomationEach layer has a distinct contract: Bronze preserves raw data exactly as received. Silver fixes quality issues visibly without silently dropping records. Gold produces model-ready and business-consumable tables. This separation makes the pipeline auditable, debuggable, and extensible.
Since this was a prototype built on a trial Databricks environment, I generated all input data synthetically using Python and Pandas — no proprietary or real-world data was used.
Seven CSV datasets were generated and stored in a Databricks Volume:
| sales.csv | 10,400 rows — 10 stores × 20 SKUs × 52 weeks |
| inventory.csv | Perpetual stock on hand plus in-transit quantities |
| persistent_stock.csv | Physical counts every 4 weeks |
| deliveries.csv | Supplier delivery records with dates and quantities |
| stores.csv | Store reference data |
| products.csv | Product catalogue with shelf life and supplier lead times |
| promotions.csv | Promotional calendar |
One important consistency challenge: the stock_on_hand value in inventory.csv and the perpetual_stock value in persistent_stock.csv needed to align across datasets. I solved this by using a shared lookup dictionary during generation — a small but critical detail that would cause downstream join failures if ignored.
First lesson learned: Data consistency between input files is not automatic. Even in synthetic data generation, you need to explicitly enforce referential integrity across datasets.
The Bronze layer reads each CSV using Apache Spark with schema inference and saves each file as a Delta table in the bronze schema.
Two design decisions here that matter in production contexts:
1. Added an ingestion timestamp at write time:
df.withColumn("_ingested_at", current_timestamp())This creates a simple audit trail — you always know when data arrived, which is essential for debugging pipeline failures.
2. Used schema-safe overwrite:
.mode("overwrite").option("overwriteSchema", "true")During iterative development, schema changes between runs are common. Without overwriteSchema, Databricks will throw a DELTA_METADATA_MISMATCH error every time the schema evolves. This single option makes the notebook safely re-runnable.
Key concept: Delta tables are not just storage — they support versioning, ACID transactions, and time travel. Bronze equals raw data: ingest it, timestamp it, never modify it.
The Silver layer is where data quality work happens. The critical design principle here: never silently drop bad records. Always flag them.
For each source table, I applied targeted cleaning:
Rather than dropping rows with quality issues, each table gets a _quality_flag column marking records as clean, warning, or error. This means downstream teams can always trace data quality issues back to the source — an essential capability in any production data pipeline.
Key concept: The Silver rule is auditability. If something goes wrong in ML training or order recommendations, you need to be able to trace it back to the raw data. Silent drops make that impossible.
The Gold layer is where raw cleaned data becomes model-ready features. This is the most complex and highest-value layer in the pipeline.
The primary ML training table — gold.weekly_sales_features — contains 35+ features per store, SKU, and week, including:
In addition to the ML training table, the Gold layer produces two business-consumption tables:
Key concept: Window functions are essential here. Rolling averages, volatility calculations, and trend features all require looking back at prior rows — window functions let you do this entirely in SQL without leaving the table. Gold = model-ready and business-readable data in one layer.
With 35+ features per store/SKU/week available in the Gold layer, I trained one LightGBM model per product category.
Why LightGBM? It handles tabular data well, trains fast, and works naturally with the feature set I had — including categorical columns (store, SKU, region) encoded with LabelEncoder.
The most important decision: time-based train/test split.
# First 80% of weeks for training, last 20% for evaluation
cutoff_week = sorted_weeks[int(len(sorted_weeks) * 0.8)]
train = df[df['week'] <= cutoff_week]
test = df[df['week'] > cutoff_week]In forecasting, a random train/test split causes data leakage — the model sees future data during training. Always split by time.
Model results across 8 category models:
| Meat | 10.5% |
| Snacks | 10.8% |
| Produce | 11.6% |
| Dairy | 14.8% |
| Average | 12.3% |
A 12.3% average MAPE on synthetic data is a reasonable baseline. In production retail forecasting, MAPE targets typically range from 15–25% for perishable categories, so this prototype is in a credible range.
Quantile forecasting for uncertainty: Rather than a single point forecast, I generated p10/p50/p90 forecasts using rolling standard deviation as the uncertainty measure. This is important for order recommendations — you want to know not just the expected demand, but the range of outcomes.
All models were registered in Unity Catalog Model Registry with MLflow signatures. MLflow tracks every experiment run so you can compare models, roll back to prior versions, and audit exactly which model version produced which recommendations.
Key concept: Never use random splits in time-series forecasting. And always track experiments with MLflow — without it, you cannot reproduce or audit your results.
A forecast has no business value until it drives a decision. The Decision Engine translates ML forecasts into actionable order recommendations using a standard inventory replenishment formula:
Order Qty = Demand over lead time
+ Safety Stock
- Stock on Hand
- In TransitKey design decisions:
The output table gold.order_recommendations contains order quantity, waste risk score, stockout risk score, and financial estimates (unit cost × order quantity) for each store/SKU combination.
Key concept: The decision engine is where supply chain domain knowledge meets ML output. The formula, service levels, and risk labels all encode business rules — and these rules matter as much as model accuracy.
I built a 9-tile operational dashboard using Databricks AI/BI Dashboards:
The dashboard is connected directly to the Gold layer tables — it refreshes automatically when the daily workflow runs.
One of the most practically useful additions was the Databricks Genie Space — an AI-powered natural language interface over the Gold tables.
I connected 5 Gold tables and added detailed context instructions explaining business terminology, table descriptions, and example questions. This enables buyers and operations managers to ask questions like:
Without Genie, answering these questions requires either a SQL analyst or pre-built dashboard tiles for every possible question. Genie makes the data accessible to non-technical users directly.
Key concept: Curated Gold tables with good context instructions are the foundation of useful AI analytics. Garbage in, garbage out — Genie is only as good as the data and context you give it.
The final stage ties everything together into an automated daily pipeline using Databricks Workflows:
01_bronze → 02_silver → 03_gold → 04_ml_model → 05_decision
5 tasks chained in dependency order, scheduled at 06:00 AM daily, with email notifications on success and failure. A manual test run confirmed all 5 tasks passed end to end.
This is the difference between a notebook experiment and an operational system. Automation creates a repeatable pipeline that runs without human intervention — and fails loudly when something goes wrong.
These are real errors from building the prototype — worth documenting because they will likely affect anyone following a similar path:
| DELTA_METADATA_MISMATCH | Schema changed between runs | Add option("overwriteSchema", "true") |
| ModuleNotFoundError: lightgbm | Not pre-installed on serverless compute | Add %pip install lightgbm at top of notebook |
| ValueError: pandas dtypes | Non-numeric columns in LightGBM features | Use pd.to_numeric(...).astype(float) |
| MlflowException: signature required | Unity Catalog requires model schema | Call infer_signature() before log_model() |
| RestException: directory not found | MLflow experiment parent path missing | Use WorkspaceClient().workspace.mkdirs() |
| Window function in WHERE clause | SQL restriction | Move window function into a CTE first |
| timedelta numpy int error | NumPy vs Python int type mismatch | Wrap with int() |
This prototype was built on synthetic data in a trial environment. Taking it to production at enterprise scale would require:
| Real data | Connect cloud data warehouse (e.g., GCP BigQuery) using Databricks connector |
| Scale | Support 2,000+ stores — the pipeline architecture scales without code changes |
| Governance | Configure Unity Catalog row- and column-level security |
| Model monitoring | Add Lakehouse Monitoring for forecast drift detection |
| ERP integration | Push order_recommendations to procurement systems via REST API |
| CI/CD | Set up Databricks Asset Bundles for dev/staging/production promotion |
The Medallion Architecture and modular notebook design mean that each of these steps can be added incrementally without restructuring the pipeline.
Building this prototype in a single focused session on Databricks confirmed something I had suspected from years of enterprise supply chain work: the hardest problems in retail forecasting are not the ML algorithms — they are the data engineering foundations that make those algorithms trustworthy and operationally useful.
The Medallion Architecture, MLflow tracking, and the Decision Engine layer are what separate a notebook experiment from a system that a business can actually rely on. Getting those right matters more than squeezing another percentage point of MAPE.
If you are building something similar — whether for retail, logistics, or any demand-driven supply chain — I hope the architecture, the errors, and the lessons documented here save you some of the discovery time I spent.
a week ago
Really solid project—especially the focus on turning ML forecasts into practical order recommendations. The data quality and decision-engine approach makes it genuinely useful.