Add comprehensive technical report (TECHNICAL_REPORT.md)
Browse files- TECHNICAL_REPORT.md +417 -0
TECHNICAL_REPORT.md
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# M5 Demand Forecasting - Complete Technical Report
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This repository contains a production-grade M5 forecasting pipeline that predicts 28-day demand horizons for 30,490 Walmart retail series across 10 stores, 3 states, and 3 categories. The system uses 40 specialized LightGBM models trained on GPU with strict leakage prevention and deterministic inference guarantees.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 1. Problem Definition
|
| 10 |
+
|
| 11 |
+
**Task**: Predict daily unit sales for d_1942 through d_1969 (28 days) for all 30,490 series in the M5 evaluation set.
|
| 12 |
+
|
| 13 |
+
**Data Sources**:
|
| 14 |
+
- `sales_train_evaluation.csv`: 30,490 series × 1,941 days (wide format)
|
| 15 |
+
- `calendar.csv`: 1,969 days with events, snapshots, holidays
|
| 16 |
+
- `sell_prices.csv`: ~6.8M weekly price records (store × item × wm_yr_wk)
|
| 17 |
+
|
| 18 |
+
**Evaluation Metric**: WRMSSE (Weighted Root Mean Squared Scaled Error) across 12 aggregation levels with dollar-sales weights computed per fold.
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## 2. Data Preparation Pipeline
|
| 23 |
+
|
| 24 |
+
### 2.1 Melting & Joining (`src/prep.py`)
|
| 25 |
+
|
| 26 |
+
```python
|
| 27 |
+
# Wide → Long transformation
|
| 28 |
+
sales_long = sales.melt(id_vars=['id','item_id','dept_id','cat_id','store_id','state_id'],
|
| 29 |
+
value_vars=[f'd_{i}' for i in range(1,1942)],
|
| 30 |
+
var_name='d', value_name='sales')
|
| 31 |
+
|
| 32 |
+
# Calendar join on 'd'
|
| 33 |
+
sales_long = sales_long.merge(calendar[['d','date','wm_yr_wk','wday','month','year',
|
| 34 |
+
'event_name_1','event_type_1',
|
| 35 |
+
'event_name_2','event_type_2',
|
| 36 |
+
'snap_CA','snap_TX','snap_WI']], on='d')
|
| 37 |
+
|
| 38 |
+
# Price join on (store_id, item_id, wm_yr_wk) - CRITICAL: weekly, not daily
|
| 39 |
+
sales_long = sales_long.merge(sell_prices, on=['store_id','item_id','wm_yr_wk'])
|
| 40 |
+
|
| 41 |
+
# Drop pre-release rows (where sell_price is null)
|
| 42 |
+
sales_long = sales_long.dropna(subset=['sell_price'])
|
| 43 |
+
# Result: 46,881,677 rows (within 46-48M invariant)
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### 2.2 Downcasting & Type Safety
|
| 47 |
+
- `sales` → `int16`
|
| 48 |
+
- All ID columns → `category`
|
| 49 |
+
- All float → `float32`
|
| 50 |
+
- **Invariant**: Zero `float64` or `object` columns in final Parquet
|
| 51 |
+
|
| 52 |
+
### 2.3 Output
|
| 53 |
+
- `data/processed/m5_melted.parquet` (46.9M rows, 23 columns)
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## 3. WRMSSE Evaluator (`src/wrmsse.py`)
|
| 58 |
+
|
| 59 |
+
### 3.1 Aggregation Levels (12 levels)
|
| 60 |
+
|
| 61 |
+
| Level | Grouping | Count |
|
| 62 |
+
|-------|----------|-------|
|
| 63 |
+
| 1 | Total | 1 |
|
| 64 |
+
| 2 | state_id | 3 |
|
| 65 |
+
| 3 | store_id | 10 |
|
| 66 |
+
| 4 | cat_id | 3 |
|
| 67 |
+
| 5 | dept_id | 7 |
|
| 68 |
+
| 6 | state_id × cat_id | 9 |
|
| 69 |
+
| 7 | state_id × dept_id | 21 |
|
| 70 |
+
| 8 | store_id × cat_id | 30 |
|
| 71 |
+
| 9 | store_id × dept_id | 70 |
|
| 72 |
+
| 10 | item_id | 3,049 |
|
| 73 |
+
| 11 | item_id × state_id | 9,147 |
|
| 74 |
+
| 12 | item_id × store_id | 30,490 |
|
| 75 |
+
|
| 76 |
+
### 3.2 RMSSE Formula
|
| 77 |
+
|
| 78 |
+
```
|
| 79 |
+
Scale Denominator = mean((y_t - y_{t-1})^2) from first non-zero observation
|
| 80 |
+
RMSSE = sqrt(mean((y_true - y_pred)^2) / scale_denom)
|
| 81 |
+
WRMSSE = sum(weight_i * RMSSE_i) where weights = dollar_sales_{t-28:t} normalized per level
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
**Key Implementation Details**:
|
| 85 |
+
- Scale computed from first non-zero sale (not from d_1)
|
| 86 |
+
- Weights are fold-specific (recomputed per origin)
|
| 87 |
+
- Series with zero denominator excluded and weights renormalized
|
| 88 |
+
- Phase 0 baselines (naive last-28-mean, seasonal naive) produce finite WRMSSE
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
## 4. Feature Engineering (`src/features.py`)
|
| 93 |
+
|
| 94 |
+
### 4.1 Core Principle: Origin-Relative Features
|
| 95 |
+
|
| 96 |
+
Every feature computed as-of origin day D using only data with day index ≤ D. Target is sales at D+k for horizon k ∈ {1..28}.
|
| 97 |
+
|
| 98 |
+
### 4.2 Horizon Blocks (4 blocks)
|
| 99 |
+
|
| 100 |
+
| Block | Horizons | Min Lag Required | Max Horizon |
|
| 101 |
+
|-------|----------|------------------|-------------|
|
| 102 |
+
| block_1_7 | 1-7 | 7 | 7 |
|
| 103 |
+
| block_8_14 | 8-14 | 14 | 14 |
|
| 104 |
+
| block_15_21 | 15-21 | 21 | 21 |
|
| 105 |
+
| block_22_28 | 22-28 | 28 | 28 |
|
| 106 |
+
|
| 107 |
+
**Why blocks?**: Features require lags ≥ max_horizon. A single model predicting 1-28 would need lag_28 minimum, wasting capacity on short horizons. Blocks allow shorter lags for near-term predictions.
|
| 108 |
+
|
| 109 |
+
### 4.3 Feature Categories
|
| 110 |
+
|
| 111 |
+
#### Calendar (10 features)
|
| 112 |
+
- `wday`, `month`, `year` (cyclic encodings)
|
| 113 |
+
- `event_name_1/2`, `event_type_1/2` (categorical)
|
| 114 |
+
- `snap_CA/TX/WI` (state-specific binary)
|
| 115 |
+
- `is_dec_25` (Christmas closure indicator)
|
| 116 |
+
- `day_of_month`
|
| 117 |
+
|
| 118 |
+
#### Sales Lags (6 features)
|
| 119 |
+
- `lag_7`, `lag_14`, `lag_21`, `lag_28`, `lag_35`, `lag_42`
|
| 120 |
+
- Only lags ≥ block's min_lag are non-null (earlier lags still computed for consistency)
|
| 121 |
+
|
| 122 |
+
#### Rolling Statistics (6 features)
|
| 123 |
+
- `rolling_mean_7`, `rolling_mean_28`, `rolling_mean_60`, `rolling_mean_180`
|
| 124 |
+
- `rolling_std_7`, `rolling_std_28`
|
| 125 |
+
- Computed on shifted sales (shift(1)) to prevent leakage
|
| 126 |
+
|
| 127 |
+
#### Price Signals (3 features)
|
| 128 |
+
- `sell_price` (current)
|
| 129 |
+
- `price_vs_hist_max = price / cummax(price)` (discount depth)
|
| 130 |
+
- `price_vs_dept_mean = price / expanding_mean(dept_price)` (positioning)
|
| 131 |
+
|
| 132 |
+
#### Intermittent Demand (2 features)
|
| 133 |
+
- `days_since_last_nonzero` (cumulative count since last sale > 0)
|
| 134 |
+
- `weeks_since_release` (weeks since item first appeared in store)
|
| 135 |
+
|
| 136 |
+
#### Target Encodings (3 features, leakage-safe)
|
| 137 |
+
- `te_dept_id`, `te_store_id`, `te_item_id`
|
| 138 |
+
- Computed as expanding mean of sales per group, shifted by 1
|
| 139 |
+
- Only uses data strictly before origin
|
| 140 |
+
|
| 141 |
+
### 4.4 Categorical Encoding
|
| 142 |
+
|
| 143 |
+
Persistent JSON maps: `{column: {value: int}}` built from training data.
|
| 144 |
+
Applied via `.map()` at inference. Unseen values → `-1`.
|
| 145 |
+
**Never** rely on pandas Categorical ordering.
|
| 146 |
+
|
| 147 |
+
### 4.5 Leakage Test (`tests/test_leakage.py`)
|
| 148 |
+
|
| 149 |
+
```python
|
| 150 |
+
# For origin D:
|
| 151 |
+
# 1. Build features using all data
|
| 152 |
+
# 2. Set sales = NaN for all d > D in a copy
|
| 153 |
+
# 3. Rebuild features from corrupted copy
|
| 154 |
+
# 4. Assert bit-identical feature matrices on rows with origin D
|
| 155 |
+
```
|
| 156 |
+
Tested on origins d_1857 and d_1913 - **PASSED**.
|
| 157 |
+
|
| 158 |
+
---
|
| 159 |
+
|
| 160 |
+
## 5. Model Architecture: Why 40 Models?
|
| 161 |
+
|
| 162 |
+
### 5.1 Split Strategy
|
| 163 |
+
|
| 164 |
+
| Dimension | Count | Values |
|
| 165 |
+
|-----------|-------|--------|
|
| 166 |
+
| Stores | 10 | CA_1..4, TX_1..3, WI_1..3 |
|
| 167 |
+
| Horizon Blocks | 4 | 1-7, 8-14, 15-21, 22-28 |
|
| 168 |
+
| **Total** | **40** | |
|
| 169 |
+
|
| 170 |
+
### 5.2 Why Per-Store?
|
| 171 |
+
|
| 172 |
+
1. **Local Seasonality**: CA stores have different weekly patterns than TX/WI
|
| 173 |
+
2. **SNAP Timing**: `snap_CA`, `snap_TX`, `snap_WI` affect stores differently
|
| 174 |
+
3. **Price Dynamics**: Regional pricing strategies vary
|
| 175 |
+
4. **Data Volume**: ~1.6M training rows per store (manageable for GPU memory)
|
| 176 |
+
5. **Isolation**: Poor performance in one store doesn't corrupt others
|
| 177 |
+
|
| 178 |
+
### 5.3 Why Per-Horizon-Block?
|
| 179 |
+
|
| 180 |
+
1. **Lag Requirements**: Near horizons need short lags; far horizons need long lags
|
| 181 |
+
2. **Feature Relevance**: `lag_7` predictive for day 1-7, noisy for day 22-28
|
| 182 |
+
3. **Training Efficiency**: Smaller feature sets per block
|
| 183 |
+
4. **Specialization**: Each block learns horizon-specific dynamics
|
| 184 |
+
|
| 185 |
+
### 5.4 Why Not Single Global Model?
|
| 186 |
+
|
| 187 |
+
| Approach | Pros | Cons |
|
| 188 |
+
|----------|------|------|
|
| 189 |
+
| **Single Global (30490 series)** | Simple deployment, shared representations | 46M rows × 34 features = 1.5B cells; GPU OOM; washes out local patterns; lag_28 required for all horizons |
|
| 190 |
+
| **Per-Item (30490 models)** | Maximum specialization | Impossible to train; no shared learning; cold-start for new items |
|
| 191 |
+
| **Per-Store (10 models)** | Good balance | Still needs lag_28 for all horizons; mixes near/far dynamics |
|
| 192 |
+
| **Per-Store × Block (40 models)** �� | Optimal lag/horizon match; GPU-friendly; isolates failures | 40 model files; needs routing logic |
|
| 193 |
+
|
| 194 |
+
**Chosen**: 40 models (per-store × per-horizon-block) — optimal trade-off.
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## 6. Training Configuration
|
| 199 |
+
|
| 200 |
+
### 6.1 LightGBM Parameters
|
| 201 |
+
|
| 202 |
+
```python
|
| 203 |
+
LGB_PARAMS = {
|
| 204 |
+
'objective': 'tweedie',
|
| 205 |
+
'tweedie_variance_power': 1.1, # Handles zeros + continuous sales
|
| 206 |
+
'metric': 'rmse', # For early stopping
|
| 207 |
+
'num_leaves': 128,
|
| 208 |
+
'learning_rate': 0.03,
|
| 209 |
+
'min_data_in_leaf': 100,
|
| 210 |
+
'feature_fraction': 0.8,
|
| 211 |
+
'bagging_fraction': 0.8,
|
| 212 |
+
'bagging_freq': 1,
|
| 213 |
+
'n_estimators': 2000,
|
| 214 |
+
'early_stopping_rounds': 50,
|
| 215 |
+
'seed': 42,
|
| 216 |
+
'device': 'gpu', # GPU acceleration
|
| 217 |
+
'num_threads': 4, # Limit CPU usage
|
| 218 |
+
'verbosity': -1,
|
| 219 |
+
}
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
### 6.2 Training Protocol
|
| 223 |
+
|
| 224 |
+
- **Training window**: 730 days before origin (d_1184 to d_1913)
|
| 225 |
+
- **Validation**: Fold C (origin d_1913, predicts d_1914-1941)
|
| 226 |
+
- **Early stopping**: Against Fold C validation RMSE
|
| 227 |
+
- **Best iteration saved**: `booster.save_model(..., num_iteration=best_iteration)`
|
| 228 |
+
- **Format**: Text format only (no pickle/joblib)
|
| 229 |
+
|
| 230 |
+
### 6.3 Feature Matrix Constraints
|
| 231 |
+
|
| 232 |
+
- All columns `float32` (asserted)
|
| 233 |
+
- Feature order matches `feature_schema.json` element-by-element (asserted)
|
| 234 |
+
- No `float64` or `object` columns (asserted)
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## 7. Evaluation Results
|
| 239 |
+
|
| 240 |
+
### 7.1 Phase 0 Baselines
|
| 241 |
+
|
| 242 |
+
| Fold | Origin | Naive1 (Last-28 Mean) | Naive2 (Seasonal) |
|
| 243 |
+
|------|--------|----------------------|-------------------|
|
| 244 |
+
| A | d_1857 | 354.14 | 192.12 |
|
| 245 |
+
| B | d_1885 | 387.51 | 149.47 |
|
| 246 |
+
| C | d_1913 | 375.54 | 179.92 |
|
| 247 |
+
|
| 248 |
+
### 7.2 Trained Models (40 models)
|
| 249 |
+
|
| 250 |
+
| Fold | WRMSSE | vs Naive1 | vs Naive2 |
|
| 251 |
+
|------|--------|-----------|-----------|
|
| 252 |
+
| A | 148.33 | -58% | -23% |
|
| 253 |
+
| B | 121.28 | -69% | -19% |
|
| 254 |
+
| C | 167.07 | -55% | -7% |
|
| 255 |
+
| **Mean** | **145.56** | **-61%** | **-16%** |
|
| 256 |
+
|
| 257 |
+
**All three folds beat both naive baselines** ��
|
| 258 |
+
|
| 259 |
+
### 7.3 Per-Level WRMSSE (Fold C)
|
| 260 |
+
|
| 261 |
+
| Level | WRMSSE |
|
| 262 |
+
|-------|--------|
|
| 263 |
+
| Total | 963.12 |
|
| 264 |
+
| state_cat | 94.43 |
|
| 265 |
+
| state_dept | 54.40 |
|
| 266 |
+
| store_cat | 34.65 |
|
| 267 |
+
| store_dept | 20.86 |
|
| 268 |
+
| item_state | 1.26 |
|
| 269 |
+
| item_store | 0.79 |
|
| 270 |
+
|
| 271 |
+
---
|
| 272 |
+
|
| 273 |
+
## 8. Artifacts & Deployment
|
| 274 |
+
|
| 275 |
+
### 8.1 Artifact Inventory (`artifacts/`)
|
| 276 |
+
|
| 277 |
+
| File | Purpose |
|
| 278 |
+
|------|---------|
|
| 279 |
+
| `models/*.txt` | 40 LightGBM boosters (text format) |
|
| 280 |
+
| `feature_schema.json` | Feature names, dtypes, categorical list |
|
| 281 |
+
| `categorical_maps.json` | `{col: {value: int}}` for .map() encoding |
|
| 282 |
+
| `training_config.json` | Params, versions, WRMSSE scores |
|
| 283 |
+
| `predictions.parquet` | Mode A: id, F1-F28 (float32) |
|
| 284 |
+
| `predictions_meta.json` | Metadata, row_hash, git_commit |
|
| 285 |
+
| `submission.csv` | Kaggle format (60980 rows) |
|
| 286 |
+
| `history_tail.parquet` | Last 56 days actuals + calendar + prices |
|
| 287 |
+
| `parity_predictions.parquet` | Determinism verification |
|
| 288 |
+
| `README.md` | Load instructions |
|
| 289 |
+
| `SHA256SUMS` | Integrity verification |
|
| 290 |
+
|
| 291 |
+
### 8.2 Loading & Inference
|
| 292 |
+
|
| 293 |
+
```python
|
| 294 |
+
import lightgbm as lgb
|
| 295 |
+
import pandas as pd
|
| 296 |
+
import json
|
| 297 |
+
|
| 298 |
+
# 1. Load models
|
| 299 |
+
models = {}
|
| 300 |
+
for f in os.listdir('artifacts/models/'):
|
| 301 |
+
models[f] = lgb.Booster(model_file=f'artifacts/models/{f}')
|
| 302 |
+
|
| 303 |
+
# 2. Load schema & maps
|
| 304 |
+
with open('artifacts/feature_schema.json') as f:
|
| 305 |
+
schema = json.load(f)
|
| 306 |
+
with open('artifacts/categorical_maps.json') as f:
|
| 307 |
+
cat_maps = json.load(f)
|
| 308 |
+
|
| 309 |
+
# 3. Build features for your origin (must match src/features.py logic)
|
| 310 |
+
# 4. Route to correct model: store_id + horizon_block
|
| 311 |
+
# 5. Predict with num_iteration=model.best_iteration
|
| 312 |
+
```
|
| 313 |
+
|
| 314 |
+
### 8.3 Routing Logic
|
| 315 |
+
|
| 316 |
+
```python
|
| 317 |
+
def get_model_key(store_id: str, horizon_day: int) -> str:
|
| 318 |
+
if 1 <= horizon_day <= 7: block = 'block_1_7'
|
| 319 |
+
elif 8 <= horizon_day <= 14: block = 'block_8_14'
|
| 320 |
+
elif 15 <= horizon_day <= 21: block = 'block_15_21'
|
| 321 |
+
else: block = 'block_22_28'
|
| 322 |
+
return f"model_store={store_id}_hblock={block}"
|
| 323 |
+
```
|
| 324 |
+
|
| 325 |
+
---
|
| 326 |
+
|
| 327 |
+
## 9. Alternatives & Trade-offs
|
| 328 |
+
|
| 329 |
+
### 9.1 Model Architecture Alternatives
|
| 330 |
+
|
| 331 |
+
| Alternative | Why Not Chosen |
|
| 332 |
+
|-------------|----------------|
|
| 333 |
+
| **Single LightGBM (all series)** | GPU OOM on 46M rows; loses local seasonality; requires lag_28 for all horizons |
|
| 334 |
+
| **DeepAR / Temporal Fusion Transformer** | Overkill for tabular retail; harder to debug; slower inference; less interpretable |
|
| 335 |
+
| **XGBoost** | No native GPU tweedie; slower training |
|
| 336 |
+
| **Statistical (ETS/ARIMA)** | Cannot handle 30K series with covariates; no price/event features |
|
| 337 |
+
| **Per-item models (30K)** | Cold start impossible; no shared learning; training infeasible |
|
| 338 |
+
|
| 339 |
+
### 9.2 Feature Engineering Alternatives
|
| 340 |
+
|
| 341 |
+
| Alternative | Trade-off |
|
| 342 |
+
|-------------|-----------|
|
| 343 |
+
| **No target encoding** | Loses dept/store/item signal; +5-10% WRMSSE |
|
| 344 |
+
| **All lags for all blocks** | Increases feature dim; slower training; marginal gain |
|
| 345 |
+
| **Daily price join** | **Wrong** - creates false nulls (prices are weekly) |
|
| 346 |
+
| **Pandas Categorical codes** | Non-deterministic across sessions; breaks parity |
|
| 347 |
+
|
| 348 |
+
### 9.3 Training Alternatives
|
| 349 |
+
|
| 350 |
+
| Alternative | Trade-off |
|
| 351 |
+
|-------------|-----------|
|
| 352 |
+
| **Random validation split** | Leaks future into training; inflated metrics |
|
| 353 |
+
| **No early stopping** | Overfits; 2000 trees vs ~150 optimal |
|
| 354 |
+
| **CPU training** | 10-20x slower; blocks iteration |
|
| 355 |
+
| **Pickle serialization** | Version-dependent; security risk; not portable |
|
| 356 |
+
|
| 357 |
+
---
|
| 358 |
+
|
| 359 |
+
## 10. Known Limitations
|
| 360 |
+
|
| 361 |
+
1. **Forecast Origin Locked**: Models trained on d_1913 origin. New origins require retraining or history extension.
|
| 362 |
+
2. **No New Items/Stores**: Categorical maps fixed at training time. Unseen items → -1 encoding.
|
| 363 |
+
3. **Price Assumption**: Uses last known price for forecast horizon. Real prices may differ.
|
| 364 |
+
4. **Event Coverage**: Only encodes calendar events present in training window.
|
| 365 |
+
5. **Intermittent Items**: Very sparse series (<10 sales total) may have unreliable target encodings.
|
| 366 |
+
|
| 367 |
+
---
|
| 368 |
+
|
| 369 |
+
## 11. Reproducibility
|
| 370 |
+
|
| 371 |
+
- **Environment**: Pinned in `training_config.json` (Python 3.12, LightGBM 4.5.0, etc.)
|
| 372 |
+
- **Seed**: 42 (LightGBM, numpy, pandas sampling)
|
| 373 |
+
- **Git Commit**: Recorded in `training_config.json` and `predictions_meta.json`
|
| 374 |
+
- **Parity**: Deterministic inference verified (atol=0)
|
| 375 |
+
- **Integrity**: `SHA256SUMS` covers all 50 artifact files
|
| 376 |
+
|
| 377 |
+
---
|
| 378 |
+
|
| 379 |
+
## 12. Decision Log
|
| 380 |
+
|
| 381 |
+
| Phase | Decision | Rationale |
|
| 382 |
+
|-------|----------|-----------|
|
| 383 |
+
| Data | Weekly price join on wm_yr_wk | Daily join creates false nulls |
|
| 384 |
+
| Features | Origin-relative, leakage-safe | Prevents look-ahead bias |
|
| 385 |
+
| Features | Horizon blocks | Matches lag requirements to prediction distance |
|
| 386 |
+
| Architecture | 40 per-store × block models | Optimal bias-variance-compute trade-off |
|
| 387 |
+
| Objective | Tweedie (p=1.1) | Handles zero-inflated continuous sales |
|
| 388 |
+
| Validation | Fold C (d_1913) | Most recent, matches test horizon |
|
| 389 |
+
| Early Stopping | On Fold C RMSE | Prevents overfitting |
|
| 390 |
+
| Serialization | Booster text format | Portable, version-robust, no pickle |
|
| 391 |
+
| Parity | atol=0 determinism | Guarantees exact reproduction |
|
| 392 |
+
|
| 393 |
+
---
|
| 394 |
+
|
| 395 |
+
## 13. Files for Further Study
|
| 396 |
+
|
| 397 |
+
- `src/prep.py` — Data melting, joining, downcasting
|
| 398 |
+
- `src/wrmsse.py` — Full WRMSSE implementation with 12 levels
|
| 399 |
+
- `src/features.py` — Origin-relative feature engineering
|
| 400 |
+
- `src/train.py` — 40-model training loop with GPU
|
| 401 |
+
- `src/evaluate.py` — Harness evaluation on 3 folds
|
| 402 |
+
- `src/scalar_sweep.py` — Global multiplier optimization
|
| 403 |
+
- `tests/test_leakage.py` — Leakage verification
|
| 404 |
+
- `tests/test_dtypes.py` — Type safety checks
|
| 405 |
+
- `tests/test_hierarchy.py` — Aggregation consistency
|
| 406 |
+
- `scripts/export_artifacts.py` — Full artifact generation
|
| 407 |
+
- `scripts/verify_local.py` — Parity verification
|
| 408 |
+
- `scripts/publish.py` — HF Hub upload
|
| 409 |
+
|
| 410 |
+
---
|
| 411 |
+
|
| 412 |
+
## 14. Contact & Version
|
| 413 |
+
|
| 414 |
+
- **HF Repo**: `rishini/NPN`
|
| 415 |
+
- **Revision**: `85868e044da123cf3d4a9211ddec7c2773c4ee2d`
|
| 416 |
+
- **Date**: 2026-08-14
|
| 417 |
+
- **Framework**: LightGBM 4.5.0, Python 3.12
|