rishini commited on
Commit
7131718
·
verified ·
1 Parent(s): 85868e0

Add comprehensive technical report (TECHNICAL_REPORT.md)

Browse files
Files changed (1) hide show
  1. 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